§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 Pekko 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. Pekko 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 Pekko Streams and 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 a pekko.actor.Props
object that describes the actor that Play should create when it receives the WebSocket connection:
import javax.inject.Inject
import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.stream.Materializer
import play.api.libs.streams.ActorFlow
import play.api.mvc._
class Application @Inject() (cc: ControllerComponents)(implicit system: ActorSystem, mat: Materializer)
extends AbstractController(cc) {
def socket = WebSocket.accept[String, String] { request =>
ActorFlow.actorRef { out => MyWebSocketActor.props(out) }
}
}
Note that ActorFlow.actorRef(...)
can be replaced with any Pekko Streams Flow[In, Out, _]
, but actors are generally the most straightforward way to do it.
The actor that we’re sending to here in this case looks like this:
import org.apache.pekko.actor._
object MyWebSocketActor {
def props(out: ActorRef) = Props(new MyWebSocketActor(out))
}
class MyWebSocketActor(out: ActorRef) extends Actor {
def receive = {
case msg: String =>
out ! ("I received your message: " + msg)
}
}
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:
override def postStop() = {
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:
import org.apache.pekko.actor.PoisonPill
self ! PoisonPill
§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 acceptOrResult
to address this, allowing you to return either a result (such as forbidden, or not found), or the actor to handle the WebSocket with:
import javax.inject.Inject
import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.stream.Materializer
import play.api.libs.streams.ActorFlow
import play.api.mvc._
class Application @Inject() (cc: ControllerComponents)(implicit system: ActorSystem, mat: Materializer)
extends AbstractController(cc) {
def socket = WebSocket.acceptOrResult[String, String] { request =>
Future.successful(request.session.get("user") match {
case None => Left(Forbidden)
case Some(_) =>
Right(ActorFlow.actorRef { out => MyWebSocketActor.props(out) })
})
}
}
}
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.
§Handling different types of messages
So far we have only seen handling String
frames. Play also has built in handlers for Array[Byte]
frames, and JsValue
messages parsed from String
frames. You can pass these as the type parameters to the WebSocket creation method, for example:
import javax.inject.Inject
import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.stream.Materializer
import play.api.libs.json._
import play.api.libs.streams.ActorFlow
import play.api.mvc._
class Application @Inject() (cc: ControllerComponents)(implicit system: ActorSystem, mat: Materializer)
extends AbstractController(cc) {
def socket = WebSocket.accept[JsValue, JsValue] { request =>
ActorFlow.actorRef { out => MyWebSocketActor.props(out) }
}
}
You may have noticed that there are two type parameters, this allows us to handle differently typed messages coming in to messages going out. This is typically not useful with the lower level frame types, but can be useful if you parse the messages into a higher level type.
For example, let’s say we want to receive JSON messages, and we want to parse incoming messages as InEvent
and format outgoing messages as OutEvent
. The first thing we want to do is create JSON formats for out InEvent
and OutEvent
types:
import play.api.libs.json._
implicit val inEventFormat: Format[InEvent] = Json.format[InEvent]
implicit val outEventFormat: Format[OutEvent] = Json.format[OutEvent]
Now we can create a WebSocket MessageFlowTransformer
for these types:
import play.api.mvc.WebSocket.MessageFlowTransformer
implicit val messageFlowTransformer: MessageFlowTransformer[InEvent, OutEvent] =
MessageFlowTransformer.jsonMessageFlowTransformer[InEvent, OutEvent]
And finally, we can use these in our WebSocket:
import javax.inject.Inject
import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.stream.Materializer
import play.api.libs.streams.ActorFlow
import play.api.mvc._
class Application @Inject() (cc: ControllerComponents)(implicit system: ActorSystem, mat: Materializer)
extends AbstractController(cc) {
def socket = WebSocket.accept[InEvent, OutEvent] { request =>
ActorFlow.actorRef { out => MyWebSocketActor.props(out) }
}
}
Now in our actor, we will receive messages of type InEvent
, and we can send messages of type OutEvent
.
§Handling WebSockets with Pekko streams directly
Actors are not always the right abstraction for handling WebSockets, particularly if the WebSocket behaves more like a stream.
import org.apache.pekko.stream.scaladsl._
import play.api.mvc._
def socket = WebSocket.accept[String, String] { request =>
// Log events to the console
val in = Sink.foreach[String](println)
// Send a single 'Hello!' message and then leave the socket open
val out = Source.single("Hello!").concat(Source.maybe)
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.
In 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:
import org.apache.pekko.stream.scaladsl._
import play.api.mvc._
def socket = WebSocket.accept[String, String] { request =>
// Just ignore the input
val in = Sink.ignore
// Send a single 'Hello!' message and close
val out = Source.single("Hello!")
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:
import org.apache.pekko.stream.scaladsl._
import play.api.mvc._
def socket = WebSocket.accept[String, String] { request =>
// log the message to stdout and send response back to client
Flow[String].map { msg =>
println(msg)
"I received your message: " + msg
}
}
§Accessing a WebSocket
To send data or access a websocket you need to add a route for your websocket in your routes file. For Example
GET /ws controllers.Application.socket
§Configuring WebSocket Frame Length
You can configure the max length for WebSocket data frames using play.server.websocket.frame.maxLength
or passing -Dwebsocket.frame.maxLength
system property when running your application. For example:
sbt -Dwebsocket.frame.maxLength=64k run
This configuration gives you more control of WebSocket frame length and can be adjusted to your application requirements. It may also reduce denial of service attacks using long data frames.
§Configuring keep-alive Frames
First of all, if a client sends a ping
Frame to the Play backend server, it automatically answers with a pong
frame. This is a requirement according to RFC 6455 Section 5.5.2, therefore this is hardcoded within Play, you don’t need to set up or configure anything.
Related to that, be aware, that when using web browsers as clients, that they do not send periodically ping
frames nor do they support JavaScript APIs to do so (Only Firefox has a network.websocket.timeout.ping.request
config that can be manually set in about:config
, but that does not really help).
By default, the Play backend server will not send periodically ping
frames to the client. That means, if neither the server nor the client send periodically pings or pongs, an idle WebSocket connection will be closed by Play after play.server.http[s].idleTimeout
has been reached.
To avoid that, you can make the Play backend server ping the client after an idle timeout (within the server did not hear anything from the client) has been reached:
play.server.websocket.periodic-keep-alive-max-idle = 10 seconds
Play will then send an empty ping
frame to the client. Usually that means that an active client will answer with a pong
frame that you can handle in your application if desired.
Instead of using bi-directional ping/pong keep-alive heartbeating by sending a ping
frame, you can make Play send an empty pong
frame for uni-directional pong keep-alive heartbeating, which means the client is not supposed to answer:
play.server.websocket.periodic-keep-alive-mode = "pong"
Note: Be aware that these configs are not picked up in dev mode (when using
sbt run
) if you solely set them in yourapplication.conf
. Because these are backend server configs you have to set them viaPlayKeys.devSettings
in yourbuild.sbt
to make them work in dev mode. More details why and how can be found here.
For development, to test these keep-alive frames, we recommend using Wireshark for monitoring (e.g. using a display filter like (http or websocket)
) and websocat to send frames to the server, e.g. with:
# Add --ping-interval 5 if you want to ping the server every 5 seconds
websocat -vv --close-status-code 1000 --close-reason "bye bye" ws://127.0.0.1:9000/websocket
If clients send close status codes other than the default 1000 to your Play app, make sure they use the ones that are defined and valid according to RFC 6455 Section 7.4.1 to avoid any problems. For example web browsers usually throw exceptions when trying to use such status codes and some server implementations (e.g. Netty) fail with exceptions if they receive them (and close the connection).
Note: The pekko-http specific configs
pekko.http.server.websocket.periodic-keep-alive-max-idle
andpekko.http.server.websocket.periodic-keep-alive-mode
do not affect Play. To be backend server agnostic, Play uses its own low-level WebSocket implementation and therefore handles frames itself.