WebSocket Client

Jetty’s WebSocketClient is a more powerful alternative to the WebSocket client provided by the standard JSR 356 javax.websocket APIs.

Similarly to Jetty’s HttpClient, the WebSocketClient is non-blocking and asynchronous, making it very efficient in resource utilization. A synchronous, blocking, API is also offered for simpler cases.

Since the first step of establishing a WebSocket communication is an HTTP request, WebSocketClient makes use of HttpClient and therefore depends on it.

The Maven artifact coordinates are the following:

<dependency>
  <groupId>org.eclipse.jetty.websocket</groupId>
  <artifactId>jetty-websocket-jetty-client</artifactId>
  <version>12.0.10-SNAPSHOT</version>
</dependency>

Starting WebSocketClient

The main class is org.eclipse.jetty.websocket.client.WebSocketClient; you instantiate it, configure it, and then start it like many other Jetty components. This is a minimal example:

// Instantiate WebSocketClient.
WebSocketClient webSocketClient = new WebSocketClient();

// Configure WebSocketClient, for example:
webSocketClient.setMaxTextMessageSize(8 * 1024);

// Start WebSocketClient.
webSocketClient.start();

However, it is recommended that you explicitly pass an HttpClient instance to WebSocketClient so that you can have control over the HTTP configuration as well:

// Instantiate and configure HttpClient.
HttpClient httpClient = new HttpClient();
// For example, configure a proxy.
httpClient.getProxyConfiguration().addProxy(new HttpProxy("localhost", 8888));

// Instantiate WebSocketClient, passing HttpClient to the constructor.
WebSocketClient webSocketClient = new WebSocketClient(httpClient);
// Configure WebSocketClient, for example:
webSocketClient.setMaxTextMessageSize(8 * 1024);

// Start WebSocketClient; this implicitly starts also HttpClient.
webSocketClient.start();

You may create multiple instances of WebSocketClient, but typically one instance is enough for most applications. Creating multiple instances may be necessary for example when you need to specify different configuration parameters for different instances. For example, you may need different instances when you need to configure the HttpClient differently: different transports, different proxies, different cookie stores, different authentications, etc.

The configuration that is not WebSocket specific (such as idle timeout, etc.) should be directly configured on the associated HttpClient instance.

The WebSocket specific configuration can be configured directly on the WebSocketClient instance. Configuring the WebSocketClient allows to give default values to various parameters, whose values may be overridden more specifically, as described in this section.

Refer to the WebSocketClient javadocs for the setter methods available to customize the WebSocket specific configuration.

Stopping WebSocketClient

It is recommended that when your application stops, you also stop the WebSocketClient instance (or instances) that you are using.

Similarly to stopping HttpClient, you want to stop WebSocketClient from a thread that is not owned by WebSocketClient itself, for example:

// Stop WebSocketClient.
// Use LifeCycle.stop(...) to rethrow checked exceptions as unchecked.
new Thread(() -> LifeCycle.stop(webSocketClient)).start();

Connecting to a Remote Host

A WebSocket client may initiate the communication with the server either using HTTP/1.1 or using HTTP/2. The two mechanism are quite different and detailed in the following sections.

Using HTTP/1.1

Initiating a WebSocket communication with a server using HTTP/1.1 is detailed in RFC 6455.

A WebSocket client first establishes a TCP connection to the server, then sends an HTTP/1.1 upgrade request.

If the server supports upgrading to WebSocket, it responds with HTTP status code 101, and then switches the communication over that connection, either incoming or outgoing, to happen using the WebSocket protocol.

When the client receives the HTTP status code 101, it switches the communication over that connection, either incoming or outgoing, to happen using the WebSocket protocol.

Diagram

In code:

// Use a standard, HTTP/1.1, HttpClient.
HttpClient httpClient = new HttpClient();

// Create and start WebSocketClient.
WebSocketClient webSocketClient = new WebSocketClient(httpClient);
webSocketClient.start();

// The client-side WebSocket EndPoint that
// receives WebSocket messages from the server.
ClientEndPoint clientEndPoint = new ClientEndPoint();
// The server URI to connect to.
URI serverURI = URI.create("ws://domain.com/path");

// Connect the client EndPoint to the server.
CompletableFuture<Session> clientSessionPromise = webSocketClient.connect(clientEndPoint, serverURI);

WebSocketClient.connect() links the client-side WebSocket endpoint to a specific server URI, and returns a CompletableFuture of an org.eclipse.jetty.websocket.api.Session.

The endpoint offers APIs to receive WebSocket data (or errors) from the server, while the session offers APIs to send WebSocket data to the server.

Using HTTP/2

Initiating a WebSocket communication with a server using HTTP/1.1 is detailed in RFC 8441.

A WebSocket client establishes a TCP connection to the server or reuses an existing one currently used for HTTP/2, then sends an HTTP/2 CONNECT request over an HTTP/2 stream.

If the server supports upgrading to WebSocket, it responds with HTTP status code 200, then switches the communication over that stream, either incoming or outgoing, to happen using HTTP/2 DATA frames wrapping WebSocket frames.

When the client receives the HTTP status code 200, it switches the communication over that stream, either incoming or outgoing, to happen using HTTP/2 DATA frames wrapping WebSocket frames.

From an external point of view, it will look like client is sending chunks of an infinite HTTP/2 request upload, and the server is sending chunks of an infinite HTTP/2 response download, as they will exchange HTTP/2 DATA frames; but the HTTP/2 DATA frames will contain each one or more WebSocket frames that both client and server know how to deliver to the respective WebSocket endpoints.

When either WebSocket endpoint decides to terminate the communication, the HTTP/2 stream will be closed as well.

Diagram

In code:

// Use the HTTP/2 transport for HttpClient.
HTTP2Client http2Client = new HTTP2Client();
HttpClient httpClient = new HttpClient(new HttpClientTransportOverHTTP2(http2Client));

// Create and start WebSocketClient.
WebSocketClient webSocketClient = new WebSocketClient(httpClient);
webSocketClient.start();

// The client-side WebSocket EndPoint that
// receives WebSocket messages from the server.
ClientEndPoint clientEndPoint = new ClientEndPoint();
// The server URI to connect to.
URI serverURI = URI.create("wss://domain.com/path");

// Connect the client EndPoint to the server.
CompletableFuture<Session> clientSessionPromise = webSocketClient.connect(clientEndPoint, serverURI);

Alternatively, you can use the dynamic HttpClient transport:

// Use the dynamic HTTP/2 transport for HttpClient.
ClientConnector clientConnector = new ClientConnector();
HTTP2Client http2Client = new HTTP2Client(clientConnector);
HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(clientConnector, new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client)));

// Create and start WebSocketClient.
WebSocketClient webSocketClient = new WebSocketClient(httpClient);
webSocketClient.start();

ClientEndPoint clientEndPoint = new ClientEndPoint();
URI serverURI = URI.create("wss://domain.com/path");

// Connect the client EndPoint to the server.
CompletableFuture<Session> clientSessionPromise = webSocketClient.connect(clientEndPoint, serverURI);

Customizing the Initial HTTP Request

Sometimes you need to add custom cookies, or other HTTP headers, or specify a WebSocket sub-protocol to the HTTP request that initiates the WebSocket communication.

You can do this by using overloaded versions of the WebSocketClient.connect(…​) method:

ClientEndPoint clientEndPoint = new ClientEndPoint();
URI serverURI = URI.create("ws://domain.com/path");

// Create a custom HTTP request.
ClientUpgradeRequest customRequest = new ClientUpgradeRequest();
// Specify a cookie.
customRequest.getCookies().add(new HttpCookie("name", "value"));
// Specify a custom header.
customRequest.setHeader("X-Token", "0123456789ABCDEF");
// Specify a custom sub-protocol.
customRequest.setSubProtocols("chat");

// Connect the client EndPoint to the server with a custom HTTP request.
CompletableFuture<Session> clientSessionPromise = webSocketClient.connect(clientEndPoint, serverURI, customRequest);

Inspecting the Initial HTTP Response

If you want to inspect the HTTP response returned by the server as a reply to the HTTP request that initiates the WebSocket communication, you may provide a JettyUpgradeListener:

ClientEndPoint clientEndPoint = new ClientEndPoint();
URI serverURI = URI.create("ws://domain.com/path");

// The listener to inspect the HTTP response.
JettyUpgradeListener listener = new JettyUpgradeListener()
{
    @Override
    public void onHandshakeResponse(Request request, Response response)
    {
        // Inspect the HTTP response here.
    }
};

// Connect the client EndPoint to the server with a custom HTTP request.
CompletableFuture<Session> clientSessionPromise = webSocketClient.connect(clientEndPoint, serverURI, null, listener);

Jetty WebSocket Architecture

The Jetty WebSocket architecture is organized around the concept of a logical connection between the client and the server.

The connection may be physical, when connecting to the server using HTTP/1.1, as the WebSocket bytes are carried directly by the TCP connection.

The connection may be virtual, when connecting to the server using HTTP/2, as the WebSocket bytes are wrapped into HTTP/2 DATA frames of an HTTP/2 stream. In this case, a single TCP connection may carry several WebSocket virtual connections, each wrapped in its own HTTP/2 stream.

Each side of a WebSocket connection, either client or server, is made of two entities:

  • A WebSocket endpoint, the entity that receives WebSocket events.

  • A WebSocket session, the entity that offers an API to send WebSocket data (and to close the WebSocket connection), as well as to configure WebSocket connection parameters.

WebSocket Endpoints

A WebSocket endpoint is the entity that receives WebSocket events.

The WebSocket events are the following:

  • The open event. This event is emitted when the WebSocket communication has been successfully established. Applications interested in the open event receive the WebSocket session so that they can use it to send data to the remote peer.

  • The close event. This event is emitted when the WebSocket communication has been closed. Applications interested in the close event receive a WebSocket status code and an optional close reason message.

  • The error event. This event is emitted when the WebSocket communication encounters a fatal error, such as an I/O error (for example, the network connection has been broken), or a protocol error (for example, the remote peer sends an invalid WebSocket frame). Applications interested in the error event receive a Throwable that represent the error.

  • The frame events. The frame events are emitted when a WebSocket frame is received, either a control frame such as PING, PONG or CLOSE, or a data frame such as BINARY or TEXT. One or more data frames of the same type define a message.

  • The message events. The message event are emitted when a WebSocket message is received. The message event can be of two types:

    • TEXT. Applications interested in this type of messages receive a String representing the UTF-8 bytes received.

    • BINARY. Applications interested in this type of messages receive a ByteBuffer representing the raw bytes received.

Listener endpoints are notified of events by invoking the correspondent method defined by the org.eclipse.jetty.websocket.api.Session.Listener interface.

Annotated endpoints are notified of events by invoking the correspondent method annotated with the correspondent annotation from the org.eclipse.jetty.websocket.api.annotations.* package.

Jetty uses MethodHandles to instantiate WebSocket endpoints and invoke WebSocket event methods, so WebSocket endpoint classes and WebSocket event methods must be public.

When using JPMS, your classes must be public and must be exported using the exports directive in your module-info.java. It is not recommended to use the opens directive in your module-info.java for your classes, as it would expose your classes to deep reflection, which is unnecessary, as the exports directive is sufficient.

This guarantees that WebSocket endpoints can be accessed by the Jetty implementation without additional configuration, no matter whether you are using only the class-path, or the module-path.

For both types of WebSocket endpoints, only one thread at a time will be delivering frame or message events to the corresponding methods; the next frame or message event will not be delivered until the previous call to the corresponding method has exited, and if there is demand for it. Endpoints will always be notified of message events in the same order they were received over the network.

WebSocket Events Demand

In order to receive WebSocket events, you must demand for them; the only exception is the open event, because it is the initial event that applications can interact with.

When a WebSocket event is received by an endpoint, the demand for WebSocket events (for that endpoint) is reset, so that no more WebSocket events will be received by the endpoint. It is responsibility of the endpoint to demand to receive more WebSocket events.

For simple cases, you can just annotate your WebSocket endpoint with @WebSocket(autoDemand = true), or implement Session.Listener.AutoDemanding. In these two cases, when a method that receives a WebSocket event returns, the Jetty implementation automatically demands for another WebSocket event.

For example:

// Attribute autoDemand is true by default.
@WebSocket(autoDemand = true)
public class AutoDemandAnnotatedEndPoint
{
    @OnWebSocketOpen
    public void onOpen(Session session)
    {
        // No need to demand here, because this endpoint is auto-demanding.
    }

    @OnWebSocketMessage
    public void onText(String message)
    {
        System.getLogger("ws.message").log(INFO, message);
        // No need to demand here, because this endpoint is auto-demanding.
    }
}

public class AutoDemandListenerEndPoint implements Session.Listener.AutoDemanding
{
    private Session session;

    @Override
    public void onWebSocketOpen(Session session)
    {
        this.session = session;
        // No need to demand here, because this endpoint is auto-demanding.
    }

    @Override
    public void onWebSocketText(String message)
    {
        System.getLogger("ws.message").log(INFO, message);
        // No need to demand here, because this endpoint is auto-demanding.
    }
}

While auto-demand works for simple cases, it may not work in all cases, especially those where the method that receives the WebSocket event performs asynchronous operations.

The following example shows the problem:

public class WrongAutoDemandListenerEndPoint implements Session.Listener.AutoDemanding
{
    private Session session;

    @Override
    public void onWebSocketOpen(Session session)
    {
        this.session = session;
        // No need to demand here, because this endpoint is auto-demanding.
    }

    @Override
    public void onWebSocketText(String message)
    {
        // Perform an asynchronous operation, such as invoking
        // a third party service or just echoing the message back.
        session.sendText(message, Callback.NOOP);

        // Returning from this method will automatically demand,
        // so this method may be entered again before sendText()
        // has been completed, causing a WritePendingException.
    }
}

Note how, in the example above, auto-demanding has the problem that receiving WebSocket text messages may happen faster than echoing them back, because the call to sendText(...) may return almost immediately but be slow to complete because it is asynchronous.

In the example above, if another WebSocket text message arrives, and the sendText(...) operation is not complete, a WritePendingException will be thrown.

In other cases, this may lead to infinite buffering of data, eventually causing OutOfMemoryErrors, and in general excessive resource consumption that may be difficult to diagnose and troubleshoot.

For more information, see also the section about sending data.

Always be careful when using auto-demand.

Analyze the operations that your endpoint performs and make sure they complete synchronously within the method.

To solve the problem outlined above, you must explicitly demand for the next WebSocket event, only when the processing of the previous events is complete.

For example:

public class ExplicitDemandListenerEndPoint implements Session.Listener
{
    private Session session;

    @Override
    public void onWebSocketOpen(Session session)
    {
        this.session = session;

        // Explicitly demand here, otherwise no other event is received.
        session.demand();
    }

    @Override
    public void onWebSocketText(String message)
    {
        // Perform an asynchronous operation, such as invoking
        // a third party service or just echoing the message back.

        // We want to demand only when sendText() has completed,
        // which is notified to the callback passed to sendText().
        session.sendText(message, Callback.from(session::demand, failure ->
        {
            // Handle the failure, in this case just closing the session.
            session.close(StatusCode.SERVER_ERROR, "failure", Callback.NOOP);
        }));

        // Return from the method without demanding yet,
        // waiting for the completion of sendText() to demand.
    }
}

Note how it is necessary to invoke Session.demand() from the open event, in order to receive message events.

Furthermore, note how every time a text message is received, a possibly slow asynchronous operation is initiated (which returns almost immediately, although it may not be completed yet) and then the method returns.

Because there is no demand when the method returns (because the asynchronous operation is not completed yet), the implementation will not notify any other WebSocket event (not even frame, close or error events).

When the asynchronous operation completes successfully the callback is notified; this, in turn, invokes Session.demand(), and the implementation may notify another WebSocket event (if any) to the WebSocket endpoint.

Listener Endpoints

A WebSocket endpoint may implement the org.eclipse.jetty.websocket.api.Session.Listener interface to receive WebSocket events:

public class ListenerEndPoint implements Session.Listener
{
    private Session session;

    @Override
    public void onWebSocketOpen(Session session)
    {
        // The WebSocket endpoint has been opened.

        // Store the session to be able to send data to the remote peer.
        this.session = session;

        // You may configure the session.
        session.setMaxTextMessageSize(16 * 1024);

        // You may immediately send a message to the remote peer.
        session.sendText("connected", Callback.from(session::demand, Throwable::printStackTrace));
    }

    @Override
    public void onWebSocketText(String message)
    {
        // A WebSocket text message is received.

        // You may echo it back if it matches certain criteria.
        if (message.startsWith("echo:"))
        {
            // Only demand for more events when sendText() is completed successfully.
            session.sendText(message.substring("echo:".length()), Callback.from(session::demand, Throwable::printStackTrace));
        }
        else
        {
            // Discard the message, and demand for more events.
            session.demand();
        }
    }

    @Override
    public void onWebSocketBinary(ByteBuffer payload, Callback callback)
    {
        // A WebSocket binary message is received.

        // Save only PNG images.
        boolean isPNG = true;
        byte[] pngBytes = new byte[]{(byte)0x89, 'P', 'N', 'G'};
        for (int i = 0; i < pngBytes.length; ++i)
        {
            if (pngBytes[i] != payload.get(i))
            {
                // Not a PNG image.
                isPNG = false;
                break;
            }
        }

        if (isPNG)
            savePNGImage(payload);

        // Complete the callback to release the payload ByteBuffer.
        callback.succeed();

        // Demand for more events.
        session.demand();
    }

    @Override
    public void onWebSocketError(Throwable cause)
    {
        // The WebSocket endpoint failed.

        // You may log the error.
        cause.printStackTrace();

        // You may dispose resources.
        disposeResources();
    }

    @Override
    public void onWebSocketClose(int statusCode, String reason)
    {
        // The WebSocket endpoint has been closed.

        // You may dispose resources.
        disposeResources();
    }
}

Message Streaming Reads

If you need to deal with large WebSocket messages, you may reduce the memory usage by streaming the message content. For large WebSocket messages, the memory usage may be large due to the fact that the text or the bytes must be accumulated until the message is complete before delivering the message event.

To stream textual or binary messages, you override either org.eclipse.jetty.websocket.api.Session.Listener.onWebSocketPartialText(...) or org.eclipse.jetty.websocket.api.Session.Listener.onWebSocketPartialBinary(...).

These methods receive chunks of, respectively, text and bytes that form the whole WebSocket message.

You may accumulate the chunks yourself, or process each chunk as it arrives, or stream the chunks elsewhere, for example:

public class StreamingListenerEndpoint implements Session.Listener
{
    private Session session;

    @Override
    public void onWebSocketOpen(Session session)
    {
        this.session = session;
        session.demand();
    }

    @Override
    public void onWebSocketPartialText(String payload, boolean fin)
    {
        // Forward chunks to external REST service, asynchronously.
        // Only demand when the forwarding completed successfully.
        CompletableFuture<Void> result = forwardToREST(payload, fin);
        result.whenComplete((ignored, failure) ->
        {
            if (failure == null)
                session.demand();
            else
                failure.printStackTrace();
        });
    }

    @Override
    public void onWebSocketPartialBinary(ByteBuffer payload, boolean fin, Callback callback)
    {
        // Save chunks to file.
        appendToFile(payload, fin);

        // Complete the callback to release the payload ByteBuffer.
        callback.succeed();

        // Demand for more events.
        session.demand();
    }
}

Annotated Endpoints

A WebSocket endpoint may annotate methods with org.eclipse.jetty.websocket.api.annotations.* annotations to receive WebSocket events.

Each annotated event method may take an optional Session argument as its first parameter:

@WebSocket(autoDemand = false) (1)
public class AnnotatedEndPoint
{
    @OnWebSocketOpen (2)
    public void onOpen(Session session)
    {
        // The WebSocket endpoint has been opened.

        // You may configure the session.
        session.setMaxTextMessageSize(16 * 1024);

        // You may immediately send a message to the remote peer.
        session.sendText("connected", Callback.from(session::demand, Throwable::printStackTrace));
    }

    @OnWebSocketMessage (3)
    public void onTextMessage(Session session, String message)
    {
        // A WebSocket textual message is received.

        // You may echo it back if it matches certain criteria.
        if (message.startsWith("echo:"))
        {
            // Only demand for more events when sendText() is completed successfully.
            session.sendText(message.substring("echo:".length()), Callback.from(session::demand, Throwable::printStackTrace));
        }
        else
        {
            // Discard the message, and demand for more events.
            session.demand();
        }
    }

    @OnWebSocketMessage (3)
    public void onBinaryMessage(Session session, ByteBuffer payload, Callback callback)
    {
        // A WebSocket binary message is received.

        // Save only PNG images.
        boolean isPNG = true;
        byte[] pngBytes = new byte[]{(byte)0x89, 'P', 'N', 'G'};
        for (int i = 0; i < pngBytes.length; ++i)
        {
            if (pngBytes[i] != payload.get(i))
            {
                // Not a PNG image.
                isPNG = false;
                break;
            }
        }

        if (isPNG)
            savePNGImage(payload);

        // Complete the callback to release the payload ByteBuffer.
        callback.succeed();

        // Demand for more events.
        session.demand();
    }

    @OnWebSocketError (4)
    public void onError(Throwable cause)
    {
        // The WebSocket endpoint failed.

        // You may log the error.
        cause.printStackTrace();

        // You may dispose resources.
        disposeResources();
    }

    @OnWebSocketClose (5)
    public void onClose(int statusCode, String reason)
    {
        // The WebSocket endpoint has been closed.

        // You may dispose resources.
        disposeResources();
    }
}
1 Use the @WebSocket annotation at the class level to make it a WebSocket endpoint, and disable auto-demand.
2 Use the @OnWebSocketOpen annotation for the open event. As this is the first event notified to the endpoint, you can configure the Session object.
3 Use the @OnWebSocketMessage annotation for the message event, both for textual and binary messages.
4 Use the @OnWebSocketError annotation for the error event.
5 Use the @OnWebSocketClose annotation for the close event.

Message Streaming Reads

If you need to deal with large WebSocket messages, you may reduce the memory usage by streaming the message content.

To stream textual or binary messages, you still use the @OnWebSocketMessage annotation, but you change the signature of the method to take an additional boolean parameter:

@WebSocket(autoDemand = false)
public class PartialAnnotatedEndpoint
{
    @OnWebSocketMessage
    public void onTextMessage(Session session, String partialText, boolean fin)
    {
        // Forward the partial text.
        // Demand only when the forward completed.
        CompletableFuture<Void> result = forwardToREST(partialText, fin);
        result.whenComplete((ignored, failure) ->
        {
            if (failure == null)
                session.demand();
            else
                failure.printStackTrace();
        });
    }

    @OnWebSocketMessage
    public void onBinaryMessage(Session session, ByteBuffer partialPayload, boolean fin, Callback callback)
    {
        // Save partial payloads to file.
        appendToFile(partialPayload, fin);
        // Complete the callback to release the payload ByteBuffer.
        callback.succeed();
        // Demand for more events.
        session.demand();
    }
}

Alternatively, but less efficiently, you can use the @OnWebSocketMessage annotation, but you change the signature of the method to take, respectively, a Reader and an InputStream:

@WebSocket
public class StreamingAnnotatedEndpoint
{
    @OnWebSocketMessage
    public void onTextMessage(Reader reader)
    {
        // Read from the Reader and forward.
        // Caution: blocking APIs.
        forwardToREST(reader);
    }

    @OnWebSocketMessage
    public void onBinaryMessage(InputStream stream)
    {
        // Read from the InputStream and save to file.
        // Caution: blocking APIs.
        appendToFile(stream);
    }
}

Reader or InputStream only offer blocking APIs, so if the remote peers are slow in sending the large WebSocket messages, reading threads may be blocked in Reader.read(char[]) or InputStream.read(byte[]), possibly exhausting the thread pool.

Note that when you use blocking APIs, the invocations to Session.demand() are now performed by the Reader or InputStream implementations (as well as the ByteBuffer lifecycle management). You indirectly control the demand by deciding when to read from Reader or InputStream.

WebSocket Session

A WebSocket session is the entity that offers an API to send data to the remote peer, to close the WebSocket connection, and to configure WebSocket connection parameters.

Configuring the Session

You may configure the WebSocket session behavior using the org.eclipse.jetty.websocket.api.Session APIs. You want to do this as soon as you have access to the Session object, typically from the open event handler:

public class ConfigureEndpoint implements Session.Listener
{
    @Override
    public void onWebSocketOpen(Session session)
    {
        // Configure the max length of incoming messages.
        session.setMaxTextMessageSize(16 * 1024);

        // Configure the idle timeout.
        session.setIdleTimeout(Duration.ofSeconds(30));

        // Demand for more events.
        session.demand();
    }
}

The settings that can be configured include:

maxBinaryMessageSize

the maximum size in bytes of a binary message (which may be composed of multiple frames) that can be received.

maxTextMessageSize

the maximum size in bytes of a text message (which may be composed of multiple frames) that can be received.

maxFrameSize

the maximum payload size in bytes of any WebSocket frame that can be received.

inputBufferSize

the input (read from network/transport layer) buffer size in bytes; it has no relationship with the WebSocket frame size or message size.

outputBufferSize

the output (write to network/transport layer) buffer size in bytes; it has no relationship to the WebSocket frame size or message size.

autoFragment

whether WebSocket frames are automatically fragmented to respect the maximum frame size.

idleTimeout

the duration that a WebSocket connection may remain idle (that is, there is no network traffic, neither in read nor in write) before being closed by the implementation.

Please refer to the Session javadocs for the complete list of configuration APIs.

Sending Data

To send data to the remote peer, you can use the non-blocking APIs offered by Session.

@WebSocket
public class NonBlockingSendEndpoint
{
    @OnWebSocketMessage
    public void onText(Session session, String text)
    {
        // Send textual data to the remote peer.
        session.sendText("data", new Callback() (1)
        {
            @Override
            public void succeed()
            {
                // Send binary data to the remote peer.
                ByteBuffer bytes = readImageFromFile();
                session.sendBinary(bytes, new Callback() (2)
                {
                    @Override
                    public void succeed()
                    {
                        // Both sends succeeded.
                    }

                    @Override
                    public void fail(Throwable x)
                    {
                        System.getLogger("websocket").log(System.Logger.Level.WARNING, "could not send binary data", x);
                    }
                });
            }

            @Override
            public void fail(Throwable x)
            {
                // No need to rethrow or close the session.
                System.getLogger("websocket").log(System.Logger.Level.WARNING, "could not send textual data", x);
            }
        });

        // remote.sendString("wrong", Callback.NOOP); // May throw WritePendingException! (3)
    }
}
1 Non-blocking APIs require a Callback parameter.
2 Note how the second send must be performed from inside the callback.
3 Sequential sends may throw WritePendingException.

Non-blocking APIs are more difficult to use since you are required to meet the following condition:

  • You cannot initiate another send of any kind until the previous send is completed.

For example, if you have initiated a text send, you cannot initiate another text or binary send, until the previous send has completed.

This requirement is necessary to avoid unbounded buffering that could lead to OutOfMemoryErrors.

We strongly recommend that you follow the condition above.

However, there may be cases where you want to explicitly control the number of outgoing buffered messages using RemoteEndpoint.setMaxOutgoingFrames(int).

Remember that trying to control the number of outgoing frames is very difficult and tricky; you may set maxOutgoingFrames=4 and have a situation where 6 threads try to concurrently send messages: threads 1 to 4 will be able to successfully buffer their messages, thread 5 may fail, but thread 6 may succeed because one of the previous threads completed its send. At this point you have an out-of-order message delivery that could be unexpected and very difficult to troubleshoot because it will happen non-deterministically.

While non-blocking APIs are more difficult to use, they don’t block the sender thread and therefore use less resources, which in turn typically allows for greater scalability under load: with respect to blocking APIs, non-blocking APIs need less resources to cope with the same load.

Streaming Send APIs

If you need to send large WebSocket messages, you may reduce the memory usage by streaming the message content.

The Jetty WebSocket APIs offer sendPartial*(...) methods that allow you to send a chunk of the whole message at a time, therefore reducing the memory usage since it is not necessary to have the whole message String or ByteBuffer in memory to send it.

The Jetty WebSocket APIs for streaming the message content are non-blocking and therefore you should wait (without blocking!) for the callbacks to complete.

Fortunately, Jetty provides the IteratingCallback utility class (described in more details in this section) which greatly simplify the use of non-blocking APIs:

@WebSocket(autoDemand = false)
public class StreamSendNonBlockingEndpoint
{
    @OnWebSocketMessage
    public void onText(Session session, String text)
    {
        new Sender(session).iterate();
    }

    private class Sender extends IteratingCallback implements Callback (1)
    {
        private final ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
        private final Session session;
        private boolean finished;

        private Sender(Session session)
        {
            this.session = session;
        }

        @Override
        protected Action process() throws Throwable (2)
        {
            if (finished)
                return Action.SUCCEEDED; (4)

            int read = readChunkToSendInto(byteBuffer);
            if (read < 0)
            {
                // No more bytes to send, finish the WebSocket message.
                session.sendPartialBinary(byteBuffer, true, this); (3)
                finished = true;
                return Action.SCHEDULED;
            }
            else
            {
                // Send the chunk.
                session.sendPartialBinary(byteBuffer, false, this); (3)
                return Action.SCHEDULED;
            }
        }

        @Override
        public void succeed()
        {
            // When the send succeeds, succeed this IteratingCallback.
            succeeded();
        }

        @Override
        public void fail(Throwable x)
        {
            // When the send fails, fail this IteratingCallback.
            failed(x);
        }

        @Override
        protected void onCompleteSuccess()
        {
            session.demand(); (5)
        }

        @Override
        protected void onCompleteFailure(Throwable x)
        {
            x.printStackTrace();
        }
    }
}
1 Implementing Callback allows to pass this to sendPartialBinary(...).
2 The process() method is called iteratively when each sendPartialBinary(...) is completed.
3 Sends the message chunks.
4 When the last chunk as been sent, complete successfully the IteratingCallback.
5 Only when the IteratingCallback is completed successfully, demand for more WebSocket events.

Sending Ping/Pong

The WebSocket protocol defines two special frame, named PING and PONG that may be interesting to applications for these use cases:

  • Calculate the round-trip time with the remote peer.

  • Keep the connection from being closed due to idle timeout — a heartbeat-like mechanism.

To handle PING/PONG events, you may implement methods Session.Listener.onWebSocketPing(ByteBuffer) and/or Session.Listener.onWebSocketPong(ByteBuffer).

PING/PONG events are also supported when using annotations via the OnWebSocketFrame annotation.

PING frames may contain opaque application bytes, and the WebSocket implementation replies to them with a PONG frame containing the same bytes:

public class RoundTripListenerEndpoint implements Session.Listener
{
    private Session session;

    @Override
    public void onWebSocketOpen(Session session)
    {
        this.session = session;
        // Send to the remote peer the local nanoTime.
        ByteBuffer buffer = ByteBuffer.allocate(8).putLong(NanoTime.now()).flip();
        session.sendPing(buffer, Callback.NOOP);
        // Demand for more events.
        session.demand();
    }

    @Override
    public void onWebSocketPong(ByteBuffer payload)
    {
        // The remote peer echoed back the local nanoTime.
        long start = payload.getLong();

        // Calculate the round-trip time.
        long roundTrip = NanoTime.since(start);

        // Demand for more events.
        session.demand();
    }
}

Closing the Session

When you want to terminate the communication with the remote peer, you close the Session:

@WebSocket
public class CloseEndpoint
{
    @OnWebSocketMessage
    public void onText(Session session, String text)
    {
        if ("close".equalsIgnoreCase(text))
            session.close(StatusCode.NORMAL, "bye", Callback.NOOP);
    }
}

Closing a WebSocket Session carries a status code and a reason message that the remote peer can inspect in the close event handler (see this section).

The reason message is optional, and may be truncated to fit into the WebSocket frame sent to the client. It is best to use short tokens such as "shutdown", or "idle_timeout", etc. or even application specific codes such as "0001" or "00AF" that can be converted by the application into more meaningful messages.