§WebSockets
WebSockets are sockets that can be used from a web browser based on a protocol that allows two way full duplex communication. The client can send messages and the server can receive messages at any time, as long as there is an active WebSocket connection between the server and the client.
Modern HTML5 compliant web browsers natively support WebSockets via a JavaScript WebSocket API. However WebSockets are not limited in just being used by WebBrowsers, there are many WebSocket client libraries available, allowing for example servers to talk to each other, and also native mobile apps to use WebSockets. Using WebSockets in these contexts has the advantage of being able to reuse the existing TCP port that a Play server uses.
Tip: Check caniuse.com to see more about which browsers supports WebSockets, known issues and more information.
§Handling WebSockets
Until now, we were using Action
instances to handle standard HTTP requests and send back standard HTTP responses. WebSockets are a totally different beast and can’t be handled via standard Action
.
Play’s WebSocket handling mechanism is built around Akka streams. A WebSocket is modelled as a Flow
, incoming WebSocket messages are fed into the flow, and messages produced by the flow are sent out to the client.
Note that while conceptually, a flow is often viewed as something that receives messages, does some processing to them, and then produces the processed messages - there is no reason why this has to be the case, the input of the flow may be completely disconnected from the output of the flow. Akka streams provides a constructor, Flow.fromSinkAndSource
, exactly for this purpose, and often when handling WebSockets, the input and output will not be connected at all.
Play provides some factory methods for constructing WebSockets in WebSocket.
§Handling WebSockets with actors
To handle a WebSocket with an actor, we can use a Play utility, ActorFlow to convert an ActorRef
to a flow. This utility takes a function that converts the ActorRef
to send messages to to a akka.actor.Props
object that describes the actor that Play should create when it receives the WebSocket connection:
import play.libs.streams.ActorFlow;
import play.mvc.*;
import akka.actor.*;
import akka.stream.*;
import javax.inject.Inject;
public class HomeController extends Controller {
private final ActorSystem actorSystem;
private final Materializer materializer;
@Inject
public HomeController(ActorSystem actorSystem, Materializer materializer) {
this.actorSystem = actorSystem;
this.materializer = materializer;
}
public WebSocket socket() {
return WebSocket.Text.accept(request ->
ActorFlow.actorRef(MyWebSocketActor::props,
actorSystem, materializer
)
);
}
}
The actor that we’re sending to here in this case looks like this:
import akka.actor.*;
public class MyWebSocketActor extends UntypedActor {
public static Props props(ActorRef out) {
return Props.create(MyWebSocketActor.class, out);
}
private final ActorRef out;
public MyWebSocketActor(ActorRef out) {
this.out = out;
}
public void onReceive(Object message) throws Exception {
if (message instanceof String) {
out.tell("I received your message: " + message, self());
}
}
}
Any messages received from the client will be sent to the actor, and any messages sent to the actor supplied by Play will be sent to the client. The actor above simply sends every message received from the client back with I received your message:
prepended to it.
§Detecting when a WebSocket has closed
When the WebSocket has closed, Play will automatically stop the actor. This means you can handle this situation by implementing the actors postStop
method, to clean up any resources the WebSocket might have consumed. For example:
public void postStop() throws Exception {
someResource.close();
}
§Closing a WebSocket
Play will automatically close the WebSocket when your actor that handles the WebSocket terminates. So, to close the WebSocket, send a PoisonPill
to your own actor:
self().tell(PoisonPill.getInstance(), self());
§Rejecting a WebSocket
Sometimes you may wish to reject a WebSocket request, for example, if the user must be authenticated to connect to the WebSocket, or if the WebSocket is associated with some resource, whose id is passed in the path, but no resource with that id exists. Play provides a acceptOrResult
WebSocket builder for this purpose:
public WebSocket socket() {
return WebSocket.Text.acceptOrResult(request -> {
if (session().get("user") != null) {
return CompletableFuture.completedFuture(
F.Either.Right(ActorFlow.actorRef(MyWebSocketActor::props,
actorSystem, materializer)));
} else {
return CompletableFuture.completedFuture(F.Either.Left(forbidden()));
}
});
}
Note: the WebSocket protocol does not implement Same Origin Policy, and so does not protect against Cross-Site WebSocket Hijacking. To secure a websocket against hijacking, the
Origin
header in the request must be checked against the server’s origin, and manual authentication (including CSRF tokens) should be implemented. If a WebSocket request does not pass the security checks, thenacceptOrResult
should reject the request by returning a Forbidden result.
§Accepting a WebSocket asynchronously
You may need to do some asynchronous processing before you are ready to create an actor or reject the WebSocket, if that’s the case, you can simply return CompletionStage<WebSocket<A>>
instead of WebSocket<A>
.
§Handling different types of messages
So far we have only seen handling String
frames, using the Text
builder. Play also has built in handlers for ByteString
frames using the Binary
builder, and JSONNode
messages parsed from String
frames using the Json
builder. Here’s an example of using the Json
builder:
public WebSocket socket() {
return WebSocket.Json.accept(request ->
ActorFlow.actorRef(MyWebSocketActor::props,
actorSystem, materializer));
}
Play also provides built in support for translating JSONNode
messages to and from a higher level object. If you had a class, InEvent
, representing input events, and another class, OutEvent
, representing output events, you could use it like this:
public WebSocket socket() {
return WebSocket.json(InEvent.class).accept(request ->
ActorFlow.actorRef(MyWebSocketActor::props,
actorSystem, materializer));
}
§Handling WebSockets using Akka streams directly
Actors are not always the right abstraction for handling WebSockets, particularly if the WebSocket behaves more like a stream.
Instead, you can use Akka streams directly to handle WebSockets. To use Akka streams, first import the Akka streams javadsl:
import akka.stream.javadsl.*;
Now you can use it like so.
public WebSocket socket() {
return WebSocket.Text.accept(request -> {
// Log events to the console
Sink<String, ?> in = Sink.foreach(System.out::println);
// Send a single 'Hello!' message and then leave the socket open
Source<String, ?> out = Source.single("Hello!").concat(Source.maybe());
return Flow.fromSinkAndSource(in, out);
});
}
A WebSocket
has access to the request headers (from the HTTP request that initiates the WebSocket connection), allowing you to retrieve standard headers and session data. However, it doesn’t have access to a request body, nor to the HTTP response.
It this example we are creating a simple sink that prints each message to console. To send messages, we create a simple source that will send a single Hello! message. We also need to concatenate a source that will never send anything, otherwise our single source will terminate the flow, and thus the connection.
Tip: You can test WebSockets on https://www.websocket.org/echo.html. Just set the location to
ws://localhost:9000
.
Let’s write another example that discards the input data and closes the socket just after sending the Hello! message:
public WebSocket socket() {
return WebSocket.Text.accept(request -> {
// Just ignore the input
Sink<String, ?> in = Sink.ignore();
// Send a single 'Hello!' message and close
Source<String, ?> out = Source.single("Hello!");
return Flow.fromSinkAndSource(in, out);
});
}
Here is another example in which the input data is logged to standard out and then sent back to the client using a mapped flow:
public WebSocket socket() {
return WebSocket.Text.accept(request -> {
// log the message to stdout and send response back to client
return Flow.<String>create().map(msg -> {
System.out.println(msg);
return "I received your message: " + msg;
});
});
}