Quarkus WebSockets Next

1. Introduction

In this article, we’re going to look at the quarkus-websockets-next extension for the Quarkus framework. This extension is a new, experimental extension to support WebSockets within our applications.

Quarkus WebSockets Next is a new extension that’s intended to replace the older Quarkus WebSockets extension. This will be easier to use and more efficient than the older extension.

However, unusually for Quarkus, it doesn’t support the Jakarta WebSockets API and instead offers a simplified and more modern API for working with WebSockets. It uses its own annotated classes and methods, providing greater flexibility in functionality while also offering built-in features like JSON support.

At the same time, Quarkus WebSockets Next still builds on top of the standard Quarkus core. This means we get all of the expected performance and scalability we expect, while also benefiting from the development experience that Quarkus offers us.

2. Dependencies

If we’re starting a brand new project, we can use Maven to create our structure with the websockets-next extension already installed:

$ mvn io.quarkus.platform:quarkus-maven-plugin:3.16.4:create \
    -DprojectGroupId=com.baeldung.quarkus \
    -DprojectArtifactId=quarkus-websockets-next \
    -Dextensions="websockets-next"

Note that we need to use io.quarkus.platform:quarkus-maven-plugin for this since the extension is still experimental.

Alternatively, if we’re working with an existing project, we can simply add the appropriate dependency to our pom.xml file:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-websockets-next</artifactId>
</dependency>

3. Server Endpoints

Once we’ve got our application ready and have the websockets-next extension installed, we’re ready to start using WebSockets.

Server endpoints are created in Quarkus by creating a new class annotated with @WebSocket:

@WebSocket(path = "/echo")
public class EchoWebsocket {
    // WebSocket code here.
}

This will create an endpoint listening on the provided path. As always with Quarkus, we can use path parameters within this if we need, as well as fixed paths.

3.1. Message Callbacks

For our WebSocket endpoint to be useful, we need to be able to handle messages.

WebSocket connections support two types of messages – text and binary. We can handle these in our server endpoint by using methods annotated with either @OnTextMessage or @OnBinaryMessage:

@OnTextMessage
public String onMessage(String message) {
    return message;
}

In this case, we receive the message payload as a method parameter and send the method’s return value to the client. As such, this example is all we need for an echo server – we send back every received message exactly as-is.

If necessary, we can also receive binary payloads instead of text ones. This is done by using the @OnBinaryMessage annotation instead:

@OnBinaryMessage
public Buffer onMessage(Buffer message) {
    return message;
}

In this case, we accept and return an io.vertx.core.buffer.Buffer instance. This will contain the raw bytes of the message received.

3.2. Method Parameters and Returns

As well as our handlers receiving the raw payload of the incoming message, there are many other things that Quarkus is able to pass into our callback messages.

All message handlers must have exactly one parameter that represents the payload of the incoming message. The exact type of this parameter determines how we can access it.

As we saw earlier, if we use Buffer or byte[], we’ll be provided with the exact bytes of the incoming message. If we use a String, these bytes will be decoded into a string first.

We can also use richer objects, though. If we use JsonObject or JsonArray, the incoming message will be treated as JSON and decoded as appropriate. Alternatively, if we use any other type that Quarkus doesn’t support, Quarkus will attempt to deserialize the incoming message as JSON into that type:

@OnTextMessage
public Message onTextMessage(Message message) {
    return message;
}
record Message(String message) {}

We can use all of these same types as return values as well, in which case Quarkus will serialize the messages exactly as expected when sending them back to the client. In addition, a message handler can have a void return to indicate that there will be nothing sent back as a response.

In addition to these, there are some other method parameters that we can accept.

As we’ve seen, an unannotated String parameter will be provided with the message payload. However, we can also have a String parameter annotated with @PathParam to receive the value of this parameter in the incoming URL:

@WebSocket(path = "/chat/:user")
public class ChatWebsocket {
    @OnTextMessage(broadcast = true)
    public String onTextMessage(String message, @PathParam("user") String user) {
        return user + ": " + message;
    }
}

We can also accept a parameter of type WebSocketConnection, which will represent this exact connection between the client and our server:

@OnTextMessage
public Map<String, String> onTextMessage(String message, WebSocketConnection connection) {
    return Map.of(
        "message", message,
        "connection", connection.toString()
    );
}

Using this allows us to access details of the network connection between the client and server – including a unique ID for the connection, when the connection was established, and the path parameters from the URL that was used to connect.

We can also use it to interact with the connection more directly, by sending messages down it or even forcibly closing it:

@OnTextMessage
public void onTextMessage(String message, WebSocketConnection connection) {
    if ("close".equals(message)) {
        connection.sendTextAndAwait("Goodbye");
        connection.closeAndAwait();
    }
}

3.3. OnOpen and OnClose Callbacks

In addition to handlers for receiving messages, we can also register handlers for when a new connection is first opened – using @OnOpen, and for when a connection is closed – using @OnClose:

@OnOpen
public void onOpen() {
    LOG.info("Connection opened");
}
@OnClose
public void onClose() {
    LOG.info("Connection closed");
}

These callback handlers can’t receive any message payloads but can receive any other method parameters as described earlier.

In addition, the @OnOpen handler can have a return value that will be serialized and sent to the client. This is useful for sending a message immediately on connection without waiting for the client to send something first. D0ing this follows all the same rules as return values from message handlers:

@OnOpen
public String onOpen(WebSocketConnection connection) {
    return "Hello, " + connection.id();
}

3.4. Accessing Connections

We’ve already seen that we can have the current connection injected into our callback handlers. However, this isn’t the only way we can access connection details.

Quarkus lets us inject a WebSocketConnection object as a CDI session-scoped bean, using @Inject. This can be injected into any other bean in our system, and we can access the current connection from there:

@ApplicationScoped
public class CdiConnectionService {
    @Inject
    WebSocketConnection connection;
}

However, this only works when called from within the context of a WebSocket handler. If we try to access this from any other context, including a regular HTTP call, then we’ll instead get a jakarta.enterprise.context.ContextNotActiveException thrown.

We can also access all currently open WebSocket connections, by injecting an object of type OpenConnections:

@Inject
OpenConnections connections;

We can then use this not only to query all currently open connections but also to send messages to them:

public void sendToAll(String message) {
    connections.forEach(connection -> connection.sendTextAndAwait(message));
}

Unlike injecting a single WebSocketConnection, this will work correctly from any context. This allows us to access WebSocket connections from any other context if needed.

3.5. Error Handling

In some cases, things can go wrong when we’re processing WebSocket callbacks. Quarkus allows us to handle any exceptions thrown out of these methods by writing exception handler methods. These are any methods annotated with @OnError:

@OnError
public String onError(RuntimeException e) {
    return e.toString();
}

These follow the same rules as other callback handlers regarding the parameters they can receive and their return values. In addition, they must have a parameter representing the exception to handle.

We can write as many of these error handlers as we need, as long as they’re all for different exception classes. If we have any that overlap – in other words, if one is a subclass of another – then the most specific one will be called:

@OnError
public String onIoException(IOException e) {
    // Handles IOException and all subclasses.
}
@OnError
public String onException(Exception e) {
    // Handles Exception and all subclasses except for IOException.
}

4. Client API

As well as allowing us to write server endpoints, Quarkus also allows us to write WebSocket clients that can communicate with other servers.

4.1. Basic Connector

The most basic way to write a WebSocket client is to use the BasicWebSocketConnector. This lets us open a connection and send and receive raw messages.

To start with, we need to inject a BasicWebSocketConnector into our code:

@Inject
BasicWebSocketConnector connector;

We can then use this to connect to a remote service:

WebSocketClientConnection connection = connector
  .baseUri(serverUrl)
  .executionModel(BasicWebSocketConnector.ExecutionModel.NON_BLOCKING)
  .onTextMessage((c, m) -> {
      // Handle incoming messages.
  })
  .connectAndAwait();

As part of this connection, we register a lambda to handle any incoming messages received from the server. This is necessary because of the asynchronous, full-duplex nature of WebSockets. We can’t just treat it as a standard HTTP connection with request and response pairs.

Once we’ve opened our connection, we can use it to send messages to the server as well:

connection.sendTextAndAwait("Hello, World!");

We can do this both from inside our callback handler and from outside it. However, remember that the connection isn’t thread-safe, so we need to ensure that we’re never writing to it by multiple threads at the same time.

In addition to the onTextMessage callback, we can also register callbacks for other lifecycle events, including onOpen() and onClose().

4.2. Rich Client Beans

The basic connector works well enough for simple connections, but sometimes, we need something more flexible than that. Quarkus also allows us to write much richer clients similarly to how we wrote the server endpoints.

To do this, we need to write a new class annotated with @WebSocketClient:

@WebSocketClient(path = "/json")
class RichWebsocketClient {
    // Client code here.
}

Within this class, we then write methods annotated in the same way as for our server endpoints, using annotations such as @OnTextMessage, @OnOpen, and so on:

@OnTextMessage
void onMessage(String message, WebSocketClientConnection connection) {
    // Process message here
}

This follows all of the same rules as for server endpoints regarding method parameters and return values, except that we use WebSocketClientConnection instead of WebSocketConnection if we want to access the connection details.

Once we’ve written our client class, we can use it via an injected WebSocketConnector<T> instance:

@Inject
WebSocketConnector<RichWebsocketClient> connector;

Using this, we create our connection similarly to before, only we don’t need to provide any callbacks since our client instance will handle all of that for us:

WebSocketClientConnection connection = connector
  .baseUri(serverUrl)
  .connectAndAwait();

At this point, we have a WebSocketClientConnection that we can use exactly as before.

If we need to get access to our client instance, we do so by injecting an Instance<T> for the appropriate class:

@Inject
Instance<RichWebsocketClient> clients;

However, we need to remember that this client instance will only be available from the correct contexts, and because it’s all asynchronous, we need to make sure that certain events have already happened.

5. Conclusion

In this article, we’ve seen a brief introduction to WebSockets in Quarkus using the websockets-next extension. We’ve seen how to write both server and client components using this. Here, we’ve only discussed the basics of this library, but it’s capable of handling many more advanced scenarios.

As usual, all of the examples from this article are available over on GitHub.

The post Quarkus WebSockets Next first appeared on Baeldung.

      

Leave a Reply

Your email address will not be published. Required fields are marked *