WebSockets
WebSockets are an asynchronous and bidirectional connection between a client and a server. Once a Websocket connection is established, either party may transmit messages to the other party - this unleashes truly unbounded possibilities for creating dynamic, real-time, and asynchronous applications.
WebSocket Lifecycle
A WebSocket connection starts with the client (the web browser) sending an HTTP or HTTPS request; this request includes a special header to indicate that it is intended to upgrade the request to a WebSocket connection; it is always a GET request.
In the handling of this kind of request, there’s a point where the request is upgraded to a WebSocket connection. There is no response to this request. Instead, both the client and the server may begin transmitting and receiving text and binary messages until the connection is closed by either party.
WebSockets in Java
In Java, WebSocket endpoints are POJO objects with a @ClientEndpoint class annotation.
Such endpoints will annotate specific methods to mark them as callbacks for asynchronous events. For example, to handle initial setup when a connection is first opened, the @OnOpen annotation can be applied.
The Session
object passed to the @OnOpen
callback can be retained by the application code, and later used to transmit text and binary messages
to the client.
Other annotations cover receiving text or binary messages from the client.
WebSockets in Pedestal
POJOs and annotations make sense in Java, but do not fit well with Clojure, so Pedestal redefines this in terms of maps and Clojure functions as callbacks [1].
Under Pedestal, a WebSocket request is routed like any other request, but the final interceptor
must invoke
upgrade-request-to-websocket
instead of attaching a response.
In many cases, the final interceptor is created via websocket-upgrade
.
A WebSocket upgrade request has no response. If your application includes interceptors that examine or modify the :response key of the context map (in the :leave or :error phase), they may need to be adjusted for an entirely missing :response key. If some interceptor does attach a response key, it will be ignored by the Servlet Interceptor. |
WebSocket Endpoint Map
The behavior of the endpoint is defined in terms of a websocket endpoint map which provides configuration for the WebSocket, as well as callbacks for specific events in the WebSocket lifecyle.
Key | Signature | Description |
---|---|---|
:on-open |
(Session, EndpointConfig) → Object |
Invoked when the client first opens a connection. |
:on-close |
(Object, Session, CloseReason) → nil |
Invoked when the socket is closed, allowing any resources to be freed. |
:on-error |
(Object, Session, Throwable) → nil |
Passed any unexpected exception. |
:on-text |
(Object, String) → nil |
Passed a text message from the client, as a single String. |
:on-binary |
(Object, ByteBuffer) → nil |
Passed a binary message, as a single ByteBuffer. |
:configure |
(ServerEndpointConfig$Builder) → nil |
Allows additional configuration using an instance of ClientEndpointConfig.Builder. This configuration occurs before the request is upgraded to a connection, and provides access to other configuration options, such as encoders and decoders, that are not yet exposed by Pedestal’s API. |
Key | Type | Description |
---|---|---|
:subprotocols |
vector of String |
Optional, specifies sub-protocols for the websocket connection |
:idle-timeout-ms |
long |
Sets the max idle timeout for the websocket to a fixed value. |
Essentially, the :on-open callback is invoked when the client initiates the connection.
It is intended that, when the client connects, some form of server-side process will be initiated capable of transmitting messages to the client asynchronously. It is the responsibility of the :on-open callback to create such a process, and to shut it down from an :on-close callback.
Whatever value is returned from the :on-open callback is retained, and passed as the first argument to the :on-close, :on-error, :on-text, and :on-binary callbacks. This can be a single value, such as a core.async channel, or a map, or whatever your application needs it to be. If no :on-open callback is provided, the value will be nil, and it will not be possible to transmit messages to the client, only receive via the :on-text and :on-binary callbacks. |
The :on-text and :on-binary callbacks are invoked when a text or binary message from the client is received.
Limitiations
The Jakarta WebSocket API supports receiving partial messages, useful when streaming very large objects. The :on-text and :on-binary callbacks only support whole objects.
Likewise, the underlying APIs do provide support for streaming transmissions to the client, but the built-in approach to transmitting messages does not.
Transmitting Messages
The Session passed to the :on-open callback contains further objects that are used to transmit messages to the client.
Pedestal provides utility functions to make it easier to transmit messages, in the form of a core.async channel.
The function start-ws-connection
is passed the Session and options, and sets up a transmission loop, driven
by a channel, which is returns.
The transmission loop receives values from the channel, and transmits those values to the client.
-
A String is sent as a text message
-
A ByteBuffer is sent as a binary message
-
A vector is used to monitor the result, asynchronously.
-
The first element is the value to transmit (String or ByteBuffer)
-
The second element is a channel that will convey the transmission result
-
The result is either :success, or an Exception (if unable to transmit the message)
-
Closing the channel will shut down the transmission loop, and close the WebSocket session. |
As currently implemented, the transmission of messages is not fully asynchronous: the processing loop waits for each transmission to complete before it advances to the next value in the channel.
The function on-open-start-ws-connection
wraps around start-ws-connection
;
it is passed options used to create the transmission loop, and returns a function that can be included in the websocket map as the :on-open callback.
WebSocketSendAsync
The WebSocketSendAsync
protocol does the actual work of transmitting a value (String
or ByteBuffer) asynchronously. This protocol could be extended to, for example,
convert EDN data to JSON before transmitting it to the client as a text message.
Upgrading from Pedestal 0.7
In Pedestal 0.7, WebSockets are specified using the :io.pedestal.http/websockets key of the service map. This approach is supported in Pedestal 0.8, but is deprecated, and may be removed in a later release entirely.
WebSocket requests are routed entirely outside of the interceptor chain, so they do not benefit from logging, exception handling, telemetry, or any other application-specific behaviors provided by the interceptor chain.
In the service map, the :io.pedestal.http/websockets key maps string routes to endpoint maps. There is no facility for using path parameters in these requests.
FnEndpont
, as the bridge between the Jakarta APIs and Clojure