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>websocket-jetty-client</artifactId>
  <version>11.0.21-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 may 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.
HTTP2Client http2Client = new HTTP2Client();
HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(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(HttpRequest request, HttpResponse 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 connect event. This event is emitted when the WebSocket communication has been successfully established. Applications interested in the connect 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 message event. The message event is emitted when a WebSocket message is received. Only one thread at a time will be delivering a message event to the onMessage method; the next message event will not be delivered until the previous call to the onMessage method has exited. Endpoints will always be notified of message events in the same order they were received over the network. The message event can be of two types:

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

    • Binary message event. Applications interested in this type of messages receive a byte[] representing the raw bytes received.

Listener Endpoints

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

public class ListenerEndPoint implements WebSocketListener (1)
{
    private Session session;

    @Override
    public void onWebSocketConnect(Session session)
    {
        // The WebSocket connection is established.

        // 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.getRemote().sendString("connected", WriteCallback.NOOP);
    }

    @Override
    public void onWebSocketClose(int statusCode, String reason)
    {
        // The WebSocket connection is closed.

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

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

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

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

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

        // You may echo it back if it matches certain criteria.
        if (message.startsWith("echo:"))
            session.getRemote().sendString(message.substring("echo:".length()), WriteCallback.NOOP);
    }

    @Override
    public void onWebSocketBinary(byte[] payload, int offset, int length)
    {
        // A WebSocket binary message is received.

        // Save only PNG images.
        byte[] pngBytes = new byte[]{(byte)0x89, 'P', 'N', 'G'};
        for (int i = 0; i < pngBytes.length; ++i)
        {
            if (pngBytes[i] != payload[offset + i])
                return;
        }
        savePNGImage(payload, offset, length);
    }
}
1 Your listener class implements WebSocketListener.

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 must implement interface org.eclipse.jetty.websocket.api.WebSocketPartialListener instead of WebSocketListener.

Interface WebSocketPartialListener exposes one method for textual messages, and one method to binary messages that 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 WebSocketPartialListener
{
    private Path textPath;

    @Override
    public void onWebSocketPartialText(String payload, boolean fin)
    {
        // Forward chunks to external REST service.
        forwardToREST(payload, fin);
    }

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

Annotated Endpoints

A WebSocket endpoint may annotate methods with org.eclipse.jetty.websocket.api.annotations.* annotations to receive WebSocket events. Each annotated method may take an optional Session argument as its first parameter:

@WebSocket (1)
public class AnnotatedEndPoint
{
    private Session session;

    @OnWebSocketConnect (2)
    public void onConnect(Session session)
    {
        // The WebSocket connection is established.

        // 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.getRemote().sendString("connected", WriteCallback.NOOP);
    }

    @OnWebSocketClose (3)
    public void onClose(int statusCode, String reason)
    {
        // The WebSocket connection is closed.

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

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

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

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

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

        // You may echo it back if it matches certain criteria.
        if (message.startsWith("echo:"))
            session.getRemote().sendString(message.substring("echo:".length()), WriteCallback.NOOP);
    }

    @OnWebSocketMessage (5)
    public void onBinaryMessage(byte[] payload, int offset, int length)
    {
        // A WebSocket binary message is received.

        // Save only PNG images.
        byte[] pngBytes = new byte[]{(byte)0x89, 'P', 'N', 'G'};
        for (int i = 0; i < pngBytes.length; ++i)
        {
            if (pngBytes[i] != payload[offset + i])
                return;
        }
        savePNGImage(payload, offset, length);
    }
}
1 Use the @WebSocket annotation at the class level to make it a WebSocket endpoint.
2 Use the @OnWebSocketConnect annotation for the connect event. As this is the first event notified to the endpoint, you can configure the Session object.
3 Use the @OnWebSocketClose annotation for the close event. The method may take an optional Session as first parameter.
4 Use the @OnWebSocketError annotation for the error event. The method may take an optional Session as first parameter.
5 Use the @OnWebSocketMessage annotation for the message event, both for textual and binary messages. The method may take an optional Session as first parameter.

For binary messages, you may declare the annotated method with either or these two signatures:

@OnWebSocketMessage
public void methodName(byte[] bytes, int offset, int length) { ... }

or

@OnWebSocketMessage
public void methodName(ByteBuffer buffer) { ... }

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, respectively a Reader and an InputStream:

@WebSocket
public class StreamingAnnotatedEndpoint
{
    @OnWebSocketMessage
    public void onTextMessage(Reader reader)
    {
        // Read chunks and forward.
        forwardToREST(reader);
    }

    @OnWebSocketMessage
    public void onBinaryMessage(InputStream stream)
    {
        // Save chunks to file.
        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.

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 connect event handler:

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

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

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 need to obtain the RemoteEndpoint object from the Session, and then use its API to send data.

RemoteEndpoint offers two styles of APIs to send data:

  • Blocking APIs, where the call returns when the data has been sent, or throws an IOException if the data cannot be sent.

  • Non-blocking APIs, where a callback object is notified when the data has been sent, or when there was a failure sending the data.

Blocking APIs

RemoteEndpoint blocking APIs throw IOException:

@WebSocket
public class BlockingSendEndpoint
{
    @OnWebSocketMessage
    public void onText(Session session, String text)
    {
        // Obtain the RemoteEndpoint APIs.
        RemoteEndpoint remote = session.getRemote();

        try
        {
            // Send textual data to the remote peer.
            remote.sendString("data");

            // Send binary data to the remote peer.
            ByteBuffer bytes = readImageFromFile();
            remote.sendBytes(bytes);

            // Send a PING frame to the remote peer.
            remote.sendPing(ByteBuffer.allocate(8).putLong(NanoTime.now()).flip());
        }
        catch (IOException x)
        {
            // No need to rethrow or close the session.
            System.getLogger("websocket").log(System.Logger.Level.WARNING, "could not send data", x);
        }
    }
}

Blocking APIs are simpler to use since they can be invoked one after the other sequentially.

Sending large messages to the remote peer may cause the sending thread to block, possibly exhausting the thread pool. Consider using non-blocking APIs for large messages.

Non-Blocking APIs

RemoteEndpoint non-blocking APIs have an additional callback parameter:

@WebSocket
public class NonBlockingSendEndpoint
{
    @OnWebSocketMessage
    public void onText(Session session, String text)
    {
        // Obtain the RemoteEndpoint APIs.
        RemoteEndpoint remote = session.getRemote();

        // Send textual data to the remote peer.
        remote.sendString("data", new WriteCallback() (1)
        {
            @Override
            public void writeSuccess()
            {
                // Send binary data to the remote peer.
                ByteBuffer bytes = readImageFromFile();
                remote.sendBytes(bytes, new WriteCallback() (2)
                {
                    @Override
                    public void writeSuccess()
                    {
                        // Both sends succeeded.
                    }

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

            @Override
            public void writeFailed(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", WriteCallback.NOOP); // May throw WritePendingException! (3)
    }
}
1 Non-blocking APIs require a WriteCallback 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 a binary send, until the previous send has completed.

Furthermore, if you have initiated a non-blocking send, you cannot initiate a blocking 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 buffered messages 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.

Both blocking and non-blocking 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 byte[] in memory to send it.

Streaming sends using blocking APIs is quite simple:

@WebSocket
public class StreamSendBlockingEndpoint
{
    @OnWebSocketMessage
    public void onText(Session session, String text)
    {
        try
        {
            RemoteEndpoint remote = session.getRemote();
            while (true)
            {
                ByteBuffer chunk = readChunkToSend();
                if (chunk == null)
                {
                    // No more bytes, finish the WebSocket message.
                    remote.sendPartialBytes(ByteBuffer.allocate(0), true);
                    break;
                }
                else
                {
                    // Send the chunk.
                    remote.sendPartialBytes(chunk, false);
                }
            }
        }
        catch (IOException x)
        {
            x.printStackTrace();
        }
    }
}

Streaming sends using non-blocking APIs is more complicated, as you should wait (without blocking!) for the callbacks to complete.

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

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

    private class Sender extends IteratingCallback implements WriteCallback (1)
    {
        private final RemoteEndpoint remote;
        private boolean finished;

        private Sender(RemoteEndpoint remote)
        {
            this.remote = remote;
        }

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

            ByteBuffer chunk = readChunkToSend();
            if (chunk == null)
            {
                // No more bytes, finish the WebSocket message.
                remote.sendPartialBytes(ByteBuffer.allocate(0), true, this); (3)
                finished = true;
                return Action.SCHEDULED;
            }
            else
            {
                // Send the chunk.
                remote.sendPartialBytes(ByteBuffer.allocate(0), false, this); (3)
                return Action.SCHEDULED;
            }
        }

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

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

        @Override
        protected void onCompleteFailure(Throwable x)
        {
            x.printStackTrace();
        }
    }
}
1 Implementing WriteCallback allows to pass this to sendPartialBytes(…​).
2 The process() method is called iteratively when each sendPartialBytes(…​) is completed.
3 Send the message chunks.

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 interface org.eclipse.jetty.websocket.api.WebSocketPingPongListener.

Ping/Pong events are not supported when using annotations.

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 WebSocketPingPongListener (1)
{
    @Override
    public void onWebSocketConnect(Session session)
    {
        // Send to the remote peer the local nanoTime.
        ByteBuffer buffer = ByteBuffer.allocate(8).putLong(NanoTime.now()).flip();
        session.getRemote().sendPing(buffer, WriteCallback.NOOP);
    }

    @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);
    }
}
1 The WebSocket endpoint class must implement WebSocketPingPongListener

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");
    }
}

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.