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.

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-interceptor.

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 once the request has been upgraded to a WebSocket request.

WebSocket Options

The behavior of the endpoint is defined in terms of a websocket options which provides configuration for the WebSocket, as well as callbacks for specific events in the WebSocket lifecyle.

Table 1. Callbacks
Key Signature Description

:on-open

(WebSocketChannel, Map) → Object

Invoked when the client first opens a connection. Passed the channel and the request map, returns the process object.

:on-close

(WebSocketChannel, Object, Keyword) → nil

Invoked when the socket is closed, allowing any resources to be freed. The callback is passed a keyword indicating why the channel was closed.

:on-text

(WebSocketChannel, Object, String) → nil

Callback: passed a received text message from the client, as a single String.

:on-binary

(WebSocketChannel, Object, ByteBuffer) → nil

Callback: passed a received binary message, as a single ByteBuffer.

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.

The :on-text and :on-binary callbacks are invoked when a text or binary message from the client is received.

Whatever value is returned from the :on-open callback is retained, and passed as the second 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.

When the container passes a ByteBuffer to the :on-binary callback it is important that any reference to the ByteBuffer ends when the callback returns.

The container will likely clear and reuse the ByteBuffer after executing your callback.

Get the necessary data out of the ByteBuffer, or make a deep copy of it, if your code needs the ByteBuffer’s content past the end of the callback.

Don’t rely on .asReadOnlyBuffer; that makes a shallow copy — this can lead to truly awful bugs, because the underlying byte array or direct memory is shared and mutable.

Transmitting Messages

The WebSocketChannel that is passed to the :on-open callback contains further methods that are used to transmit text or binary 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 WebSocketChannel and an options map, and sets up a transmission loop, driven by a core.async channel, which is returned.

The transmission loop receives values from the returned 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 underlying web socket connection.

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.

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.

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.