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>10.0.24-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.
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.
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 theonMessage
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:
or
|
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);
}
}
|
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 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 buffered messages 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.
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
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 |