WebSockets-Next extension reference guide

The websockets-next extension is experimental. The proposal API may change in future releases.

1. The WebSocket protocol

The WebSocket protocol, documented in the RFC6455, establishes a standardized method for creating a bidirectional communication channel between a client and a server through a single TCP connection. Unlike HTTP, WebSocket operates as a distinct TCP protocol but is designed to function seamlessly alongside HTTP. For example, it reuses the same ports and is compatible with the same security mechanisms.

The interaction using WebSocket initiates with an HTTP request employing the 'Upgrade' header to transition to the WebSocket protocol. Instead of a 200 OK response, the server replies with a 101 Switching Protocols response to upgrade the HTTP connection to a WebSocket connection. Following this successful handshake, the TCP socket utilized in the initial HTTP upgrade request remains open, allowing both client and server to exchange messages in both direction continually.

2. HTTP and WebSocket architecture styles

Despite WebSocket’s compatibility with HTTP and its initiation through an HTTP request, it’s crucial to recognize that the two protocols lead to distinctly different architectures and programming models.

With HTTP/REST, applications are structured around resources/endpoints that handle various HTTP methods and paths. Client interaction occurs through emitting HTTP requests with appropriate methods and paths, following a request-response pattern. The server routes incoming requests to corresponding handlers based on path, method, and headers and then replies with a well-defined response.

Conversely, WebSocket typically involves a single endpoint for the initial HTTP connection, after which all messages utilize the same TCP connection. It introduces an entirely different interaction model: asynchronous and message-driven.

WebSocket is a low-level transport protocol, in contrast to HTTP. Message formats, routing, or processing require prior agreement between the client and server regarding message semantics.

For WebSocket clients and servers, the Sec-WebSocket-Protocol header in the HTTP handshake request allows negotiation of a higher-level messaging protocol. In its absence, the server and client must establish their own conventions.

3. Quarkus WebSockets vs. Quarkus WebSockets Next

This guide utilizes the quarkus-websockets-next extension, an implementation of the WebSocket API boasting enhanced efficiency and usability compared to the legacy quarkus-websockets extension. The original quarkus-websockets extension remains accessible, will receive ongoing support, but it’s unlikely to receive to feature development.

Unlike quarkus-websockets, the quarkus-websockets-next extension does not implement the Jakarta WebSocket specification. Instead, it introduces a modern API, prioritizing simplicity of use. Additionally, it’s tailored to integrate with Quarkus' reactive architecture and networking layer seamlessly.

The annotations utilized by the Quarkus WebSockets next extension differ from those in JSR 356 despite, sometimes, sharing the same name. The JSR annotations carry a semantic that the Quarkus WebSockets Next extension does not follow.

4. Use the WebSockets Next extension

To use the websockets-next extension, you need to add the io.quarkus.quarkus-websockets-next extension to your project. In your pom.xml file, add:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-websockets-next</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-websockets-next")

5. Configure the WebSocket server

The WebSocket handling reuses the main HTTP server.

Thus, the configuration of the WebSocket server is done in the quarkus.http. configuration section.

WebSocket paths configured within the application are concatenated with the root path defined by quarkus.http.root (which defaults to /). This concatenation ensures that WebSocket endpoints are appropriately positioned within the application’s URL structure.

Refer to the HTTP guide for more details.

6. Declare WebSocket endpoints

To declare web socket endpoints, you need to create a class annotated with @io.quarkus.websockets.next.WebSocket and define the path of the WebSocket endpoint:

package org.acme.websockets;

import io.quarkus.websockets.next.WebSocket;
import jakarta.inject.Inject;

@WebSocket(path = "/chat/{username}")
public class ChatWebSocket {

}

Thus, client can connect to this web socket endpoint using ws://localhost:8080/chat/your-name. If TLS is used, the URL is wss://localhost:8443/chat/your-name.

6.1. Path parameters

The path of the WebSocket endpoint can contain path parameters. The syntax is the same as for JAX-RS resources: {parameterName}.

Access to the path parameter values is done through the io.quarkus.websockets.next.WebSocketConnection session object:

@Inject io.quarkus.websockets.next.WebSocketConnection session;
// ...
String value = session.pathParam("parameterName");

Path parameter values are always strings. If the path parameter is not present in the path, the pathParam method returns null.

Query parameters are not supported. However, you can access the query using session.handshakeRequest().query()

6.2. Sub-websockets endpoints

A class annotated with @WebSocket can encapsulate static nested classes, which are also annotated with @WebSocket and represent sub-web sockets. The resulting path of these sub-web sockets concatenates the path from the enclosing class and the nested class. The resulting path is normalized, following the HTTP URL rules.

Sub-web sockets inherit access to the path parameters declared in the @WebSocket annotation of both the enclosing and nested classes. The consumePrimary method within the enclosing class can access the version parameter in the following example. Meanwhile, the consumeNested method within the nested class can access both version and id parameters:

@WebSocket("/ws/v{version}")
public class MyPrimaryWebSocket {

    @OnTextMessage
    void consumePrimary(String s)    { ... }

    @WebSocket("/products/{id}")
    public static class MyNestedWebSocket {

      @OnTextMessage
      void consumeNested(String s)    { ... }

    }

}

6.3. CDI Scopes for WebSocket Endpoints

Classes annotated with @WebSocket are managed as CDI beans, allowing for flexible scope management within the application. By default, WebSocket endpoints are considered in the singleton pseudo-scope. However, developers can specify alternative scopes to suit their specific requirements:

@WebSocket("/ws")
public class MyWebSocket {
    // Singleton scoped bean
}

@WebSocket("/ws")
@ApplicationScoped
public class MyRequestScopedWebSocket {
  // Application scoped.
}

Furthermore, each WebSocket connection is associated with its own session scope. When the @OnOpen method is invoked, a session scope corresponding to the WebSocket connection is established. Subsequent calls to @On[Text|Binary]Message or @OnClose methods utilize this same session scope. The session scope remains active until the @OnClose method completes execution, at which point it is terminated.

The WebSocketConnection object, which represents the connection itself, is also a session-scoped bean, allowing developers to access and manage WebSocket-specific data within the context of the session.

In cases where a WebSocket endpoint does not declare an @OnOpen method, the session scope is still created. It remains active until the connection terminates, regardless of the presence of an @OnClose method.

Methods annotated with @OnTextMessage, @OnBinaryMessage, @OnOpen, and @OnClose also have the request scoped activated for the duration of the method execution (until it produced its result).

6.4. WebSocket endpoint methods

A WebSocket endpoint comprises the following components:

  • Path: This is the URL path where the WebSocket connection is established (e.g., ws://localhost:8080/).

  • At most one @OnTextMessage method: Handles the connected client’s text messages.

  • At most one @OnBinaryMessage method: Handles the binary messages the connected client sends.

  • At most one @OnOpen method: Invoked when a client connects to the WebSocket.

  • At most one @OnClose method: Executed upon the client disconnecting from the WebSocket.

Only some endpoints need to include all methods. However, it must contain at least @On[Text|Binary]Message or @OnOpen.

An error is thrown at build time if any endpoint violates these rules. The static nested classes representing sub-websockets adhere to the same guidelines.

Any methods annotated with @OnTextMessage, @OnBinaryMessage, @OnOpen, and @OnClose outside a WebSocket endpoint are considered erroneous and will result in the build failing with an appropriate error message.

7. Processing messages

Method receiving messages from the client are annotated with @OnTextMessage or @OnBinaryMessage.

OnTextMessage are invoked for every text message received from the client. OnBinaryMessage are invoked for every binary message the client receives.

7.1. Invocation Rules

When invoking these annotated methods, the session scope linked to the WebSocket connection remains active. In addition, the request scope is active until the completion of the method (or until it produces its result for async and reactive methods).

Quarkus WebSocket Next supports blocking and non-blocking logic, akin to Quarkus REST, determined by the method signature and additional annotations such as @Blocking and @NonBlocking.

Here are the rules governing execution:

  • Non-blocking methods must execute on the connection’s event loop.

  • Methods annotated with @RunOnVirtualThread are considered blocking and should execute on a virtual thread.

  • Blocking methods must execute on a worker thread if not annotated with @RunOnVirtualThread.

  • When @RunOnVirtualThread is employed, each invocation spawns a new virtual thread.

  • Methods returning CompletionStage and Uni are considered non-blocking

  • Methods returning Multi are considered non-blocking and must be subscribed to, except if they return their own Multi.

  • Methods returning void or plain objects are considered blocking.

7.2. Parameters

These methods can accept parameters in two formats:

  • The message object (of any type).

  • A Multi<X> with X as the message type.

  • Any other parameters should be flagged as errors.

The message object represents the data sent and can be accessed as either raw content (String, JsonObject, JsonArray, Buffer or byte[]) or deserialized high-level objects, which is the recommended approach.

When receiving a Multi, the method is invoked once per connection, and the provided Multi receives the items transmitted by this connection. The method must subscribe to the Multi to receive these items (or return a Multi). Cancelling this subscription closes the associated connection.

7.3. Allowed Returned Types

Methods annotated with @OnTextMessage or @OnBinaryMessage can return various types to handle WebSocket communication efficiently:

  • void: Indicates a blocking method where no explicit response is sent back to the client.

  • Uni<Void>: Denotes a non-blocking method where the completion of the returned Uni signifies the end of processing. No explicit response is sent back to the client.

  • An object of type X represents a blocking method in which the returned object is serialized and sent back to the client as a response.

  • Uni<X>: Specifies a non-blocking method where the item emitted by the non-null Uni is sent to the client as a response.

  • Multi<X>: Indicates a non-blocking method where the items emitted by the non-null Multi are sequentially sent to the client until completion or cancellation.

Here are some examples of these methods:

@OnTextMessage
void consume(Message m) {
// Process the incoming message. The method is called on an executor thread for each incoming message.
}

@OnTextMessage
Uni<Void> consumeAsync(Message m) {
// Process the incoming message. The method is called on an event loop thread for each incoming message.
// The method completes when the returned Uni emits its item.
}

@OnTextMessage
ReponseMessage process(Message m) {
// Process the incoming message and send a response to the client.
// The method is called for each incoming message.
// Note that if the method returns `null`, no response will be sent to the client.
}

@OnTextMessage
Uni<ResponseMessage> processAsync(Message m) {
// Process the incoming message and send a response to the client.
// The method is called for each incoming message.
// Note that if the method returns `null`, no response will be sent to the client. The method completes when the returned Uni emits its item.
}

OnTextMessage
Multi<ResponseMessage> stream(Message m) {
// Process the incoming message and send multiple responses to the client.
// The method is called for each incoming message.
// The method completes when the returned Multi emits its completion signal.
// The method cannot return `null` (but an empty multi if no response must be sent)
}

When returning a Multi, Quarkus subscribes to the returned Multi automatically and writes the emitted items until completion, failure, or cancellation. Failure or cancellation terminates the connection.

7.4. Streams

In addition to individual messages, WebSocket endpoints can handle streams of messages. In this case, the method receives a Multi<X> as a parameter. Each instance of X is deserialized using the same rules listed above.

The method receiving the Multi can either return another Multi or void. If the method returns a Multi, it does not have to subscribe to the incoming multi:

@OnTextMessage
public Multi<ChatMessage> stream(Multi<ChatMessage> incoming) {
    return incoming.log();
}

This approach allows bi-directional streaming.

When the method returns void, it must subscribe to the incoming Multi:

@OnTextMessage
public void stream(Multi<ChatMessage> incoming) {
    incoming.subscribe().with(item -> log(item));
}

7.5. Skipping reply

When a method is intended to produce a message written to the client, it can emit null. Emitting null signifies no response to be sent to the client, allowing for skipping a response when needed.

7.6. JsonObject and JsonArray

Vert.x JSONObject and JSONArray instances bypass the serialization and deserialization mechanisms. Messages are sent as text messages.

7.7. Broadcasting

By default, responses produced by @On[Text|Binary]Message methods are sent back to the connected client. However, using the broadcast parameter, responses can be broadcasted to all connected clients.

@OnTextMessage(broadcast=true)
String emitToAll(String message) {
    // Send the response to all connected clients.
}

The same principle applies to methods returning instances of Multi or Uni.

8. OnOpen and OnClose methods

The WebSocket endpoint can also be notified when a client connects or disconnects.

This is done by annotating a method with @OnOpen or @OnClose:

@OnOpen(broadcast = true)
public ChatMessage onOpen() {
    return new ChatMessage(MessageType.USER_JOINED, connection.pathParam("username"), null);
}

@Inject WebSocketConnection connection;

@OnClose
public void onClose() {
    ChatMessage departure = new ChatMessage(MessageType.USER_LEFT, connection.pathParam("username"), null);
    connection.broadcast().sendTextAndAwait(departure);
}

@OnOpen is triggered upon client connection, while @OnClose is invoked upon disconnection.

These methods have access to the session-scoped WebSocketConnection bean.

8.1. Parameters

Methods annotated with @OnOpen and @OnClose do not accept any parameters. If such methods declare parameters, they will be flagged as errors and reported at build time.

8.2. Allowed Returned Types

@OnOpen and @OnClose methods support different returned types.

For @OnOpen methods, the same rules as @On[Text|Binary]Message apply. Thus, a method annotated with @OnOpen can send messages to the client immediately after connecting. The supported return types for @OnOpen methods are:

  • void: Indicates a blocking method where no explicit message is sent back to the connected client.

  • Uni<Void>: Denotes a non-blocking method where the completion of the returned Uni signifies the end of processing. No message is sent back to the client.

  • An object of type X: Represents a blocking method where the returned object is serialized and sent back to the client.

  • Uni<X>: Specifies a non-blocking method where the item emitted by the non-null Uni is sent to the client.

  • Multi<X>: Indicates a non-blocking method where the items emitted by the non-null Multi are sequentially sent to the client until completion or cancellation.

Items sent to the client are serialized except for the String, JsonObject, JsonArray, Buffer, and byte[] types. In the case of Multi, Quarkus subscribes to the returned Multi and writes the items to the WebSocket as they are emitted. String, JsonObject and JsonArray are sent as text messages. Buffers and byte arrays are sent as binary messages.

For @OnClose methods, the allowed return types are:

  • void: The method is considered blocking.

  • Uni<Void>: The method is considered non-blocking.

@OnClose methods cannot send items to the connection client by returning objects. They can only send messages to the other client by using the WebSocketConnection object.

8.3. Server-side Streaming

Methods annotated with @OnOpen can utilize server-side streaming by returning a Multi<X>:

@WebSocket("/foo")
@OnOpen
public Multi<Integer> streaming() {
    return Multi.createFrom().ticks().every(Duration.ofSecond(1))
        .onOverflow().ignore();
}

8.4. Broadcasting with @OnOpen

Similar to @On[Text|Binary]Message, items sent to the client from a method annotated with @OnOpen can be broadcasted to all clients instead of just the connecting client:

@OnOpen(broadcast=true)
String onOpen() {
    return "We have a new member!";
}

9. Access to the WebSocketConnection

The io.quarkus.websockets.next.WebSocketConnection object represents the WebSocket connection. It’s session-scoped and is valid for the whole duration of the connection.

Methods annotated with @OnOpen, @OnTextMessage, @OnBinaryMessage, and @OnClose can access the WebSocketConnection object:

@Inject WebSocketConnection connection;

Note that outside of these methos, the WebSocketConnection object is not available.

The connection can be used to send messages to the client, access the path parameters, and broadcast messages to all connected clients.

// Send a message:
connection.sendTextAndAwait("Hello!");

// Broadcast messages:
connection.broadcast().sendTextAndAwait(departure);

// Access path parameters:
String param = connection.pathParam("foo");

The WebSocketConnection provides both a blocking and a non-blocking method to send messages:

  • sendTextAndAwait(String message): Sends a text message to the client and waits for the message to be sent. It’s blocking and should only be called from an executor thread.

  • sendText(String message): Sends a text message to the client. It returns a Uni. It’s non-blocking, but you must subscribe to it.

10. Serialization and Deserialization

The WebSocket Next extension supports automatic serialization and deserialization of messages.

Objects of type String, JsonObject, JsonArray, Buffer, and byte[] are sent as-is and by-pass the serialization and deserialization. When no codec is provided, the serialization and deserialization uses JSON (Jackson) automatically.

When you need to customize the serialization and deserialization, you can provide a custom codec.

10.1. Custom codec

To implement a custom codec, you must provides a CDI bean implementing:

  • io.quarkus.websockets.next.BinaryMessageCodec for binary messages

  • io.quarkus.websockets.next.TextMessageCodec for text messages

The following example shows how to implement a custom codec for a Item class:

@Singleton
    public static class ItemBinaryMessageCodec implements BinaryMessageCodec<Item> {

        @Override
        public boolean supports(Type type) {
            // Allows selecting the right codec for the right type
            return type.equals(Item.class);
        }

        @Override
        public Buffer encode(Item value) {
            // Serialization
            return Buffer.buffer(value.toString());
        }

        @Override
        public Item decode(Type type, Buffer value) {
            return new Item(value.toString());
        }

    }

OnTextMessage and OnBinaryMessage methods can also specify which codec need to be used explicitly:

@OnTextMessage(codec = MyInputCodec.class) (1)
Item find(Item item) {
        //....
}
  1. Specify the codec to use for both the deserialization and serialization of the message

When the serialization and deserialization must use a different codec, you can specify the codec to use for the serialization and deserialization separately:

@OnTextMessage(
        codec = MyInputCodec.class, (1)
        outputCodec = MyOutputCodec.class (2)
Item find(Item item) {
        //....
}
  1. Specify the codec to use for both the deserialization of the incoming message

  2. Specify the codec to use for the serialization of the outgoing message

11. Handle Pong message

The @OnPongMessage annotation is used to consume pong messages. A websocket endpoint must declare at most one method annotated with @OnPongMessage.

The method must accept a single parameter of type Buffer:

@OnPongMessage
void pong(Buffer data) {
    // ....
}

12. Configuration reference

Configuration property fixed at build time - All other configuration properties are overridable at runtime

Configuration property

Type

Default

TODO Not implemented yet. The default timeout to complete processing of a message.

Environment variable: QUARKUS_WEBSOCKETS_NEXT_TIMEOUT

Show more

Duration

About the Duration format

To write duration values, use the standard java.time.Duration format. See the Duration#parse() Java API documentation for more information.

You can also use a simplified format, starting with a number:

  • If the value is only a number, it represents time in seconds.

  • If the value is a number followed by ms, it represents time in milliseconds.

In other cases, the simplified format is translated to the java.time.Duration format for parsing:

  • If the value is a number followed by h, m, or s, it is prefixed with PT.

  • If the value is a number followed by d, it is prefixed with P.

Related content