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.16-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.
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.
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 MethodHandle
s 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 OutOfMemoryError
s, 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);
}
}
|
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:
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 OutOfMemoryError
s.
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 Remember that trying to control the number of outgoing frames is very difficult and tricky; you may set |
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
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 |