WebSocket is a protocol that provides a bi-directional channel between browser and webserver usually run over an upgraded HTTP(S) connection. Data is exchanged in messages whereby a message can either be binary data or unicode text.
Akka HTTP provides a stream-based implementation of the WebSocket protocol that hides the low-level details of the underlying binary framing wire-protocol and provides a simple API to implement services using WebSocket.
2.4.1 Model
The basic unit of data exchange in the WebSocket protocol is a message. A message can either be binary message, i.e. a sequence of octets or a text message, i.e. a sequence of unicode code points.
In the data model the two kinds of messages, binary and text messages, are represented by the two classes
BinaryMessage and TextMessage deriving from a common superclass Message. The superclass
MessagecontainsisTextandisBinarymethods to distinguish a message andasBinaryMessageand
asTextMessagemethods to cast a message.
The subclassesBinaryMessageandTextMessagecontain methods to access the data. Take the API of
TextMessageas an example (BinaryMessageis very similar withStringreplaced byByteString): abstract class TextMessage extends Message {
/**
* Returns a source of the text message data. */
def getStreamedText: Source[String, _]
/** Is this message a strict one? */ def isStrict: Boolean
/**
* Returns the strict message text if this message is strict, throws otherwise. */
def getStrictText: String }
The data of a message is provided as a stream because WebSocket messages do not have a predefined size and could (in theory) be infinitely long. However, only one message can be open per direction of the WebSocket connection, so that many application level protocols will want to make use of the delineation into (small) messages to transport single application-level data units like “one event” or “one chat message”.
Many messages are small enough to be sent or received in one go. As an opportunity for optimization, the model provides the notion of a “strict” message to represent cases where a whole message was received in one
go. IfTextMessage.isStrictreturns true, the complete data is already available and can be accessed with
TextMessage.getStrictText(analogously forBinaryMessage).
When receiving data from the network connection the WebSocket implementation tries to create a strict message whenever possible, i.e. when the complete data was received in one chunk. However, the actual chunking of messages over a network connection and through the various streaming abstraction layers is not deterministic from the perspective of the application. Therefore, application code must be able to handle both streaming and strict messages and not expect certain messages to be strict. (Particularly, note that tests againstlocalhostwill behave differently than tests against remote peers where data is received over a physical network connection.) For sending data, you can use the static TextMessage.create(String) method to cre- ate a strict message if the complete message has already been assembled. Otherwise, use
TextMessage.create(Source<String, ?>) to create a streaming message from an Akka Stream source.
2.4.2 Server API
The entrypoint for the Websocket API is the synthetic UpgradeToWebsocket header which is added to a request if Akka HTTP encounters a Websocket upgrade request.
The Websocket specification mandates that details of the Websocket connection are negotiated by placing special- purpose HTTP-headers into request and response of the HTTP upgrade. In Akka HTTP these HTTP-level details of the WebSocket handshake are hidden from the application and don’t need to be managed manually.
Instead, the synthetic UpgradeToWebsocket represents a valid Websocket upgrade request. An appli- cation can detect a Websocket upgrade request by looking for the UpgradeToWebsocket header. It can choose to accept the upgrade and start a Websocket connection by responding to that request with an HttpResponse generated by one of the UpgradeToWebsocket.handleMessagesWith meth- ods. In its most general form this method expects two arguments: first, a handler Flow<Message, Message, ?> that will be used to handle Websocket messages on this connection. Second, the ap- plication can optionally choose one of the proposed application-level sub-protocols by inspecting the val- ues of UpgradeToWebsocket.getRequestedProtocols and pass the chosen protocol value to
handleMessagesWith. Handling Messages
A message handler is expected to be implemented as aFlow<Message, Message, ?>. For typical request- response scenarios this fits very well and such a Flow can be constructed from a simple function by using
Flow.<Message>create().maporFlow.<Message>create().mapAsync.
There are other use-cases, e.g. in a server-push model, where a server message is sent spontaneously, or in a true bi- directional scenario where input and output aren’t logically connected. Providing the handler as aFlowin these cases may not fit. An overload ofUpgradeToWebsocket.handleMessagesWithis provided, instead, which allows to pass an output-generatingSource<Message, ?>and an input-receivingSink<Message, ?>independently.
Note that a handler is required to consume the data stream of each message to make place for new messages. Otherwise, subsequent messages may be stuck and message traffic in this direction will stall.
Example
Let’s look at anexample.
Websocket requests come in like any other requests. In the example, requests to/greeterare expected to be Websocket requests:
public static HttpResponse handleRequest(HttpRequest request) { System.out.println("Handling request to " + request.getUri());
return Websocket.handleWebsocketRequestWith(request, greeter()); else
return HttpResponse.create().withStatus(404); }
It uses a helper methodakka.http.javadsl.model.ws.Websocket.handleWebsocketRequestWith
which can be used if only Websocket requests are expected. The method looks for theUpgradeToWebsocket
header and returns a response that will install the passed Websocket handler if the header is found. If the request is no Websocket request it will return a400 Bad Requesterror response.
In the example, the passed handler expects text messages where each message is expected to contain (a person’s) name and then responds with another text message that contains a greeting:
/**
* A handler that treats incoming messages as a name, * and responds with a greeting to that name
*/
public static Flow<Message, Message, BoxedUnit> greeter() { return
Flow.<Message>create()
.collect(new JavaPartialFunction<Message, Message>() { @Override
public Message apply(Message msg, boolean isCheck) throws Exception { if (isCheck)
if (msg.isText()) return null; else throw noMatch();
else
return handleTextMessage(msg.asTextMessage()); }
}); }
public static TextMessage handleTextMessage(TextMessage msg) {
if (msg.isStrict()) // optimization that directly creates a simple response... return TextMessage.create("Hello "+msg.getStrictText());
else // ... this would suffice to handle all text messages in a streaming fashion return TextMessage.create(Source.single("Hello ").concat(msg.getStreamedText())); }
2.4.3 Routing support
The routing DSL provides the handleWebsocketMessages directive to install a WebSocket handler if a request is a WebSocket request. Otherwise, the directive rejects the request.
Let’s look at how the above example can be rewritten using the high-level routing DSL.
Instead of writing the request handler manually, the routing behavior of the app is defined by a route that uses the
handleWebsocketRequestsdirective in place of theWebsocket.handleWebsocketRequestWith: @Override
public Route createRoute() { return
path("greeter").route(
handleWebsocketMessages(greeter()) );
}
The handling code itself will be the same as with using the low-level API. See thefull routing example.