WebSockets in Play Framework

Datetime:2016-08-22 22:30:56          Topic: WebSocket           Share

Part 1. WebSockets in Play Framework: theoretical basis

First of all let's sort out what WebSocket is. WebSocket protocol completely changes the approach to client/server interaction. Instead of the old customary synchronous model of communication "request-response" with its division of the roles into "client" and "server", WebSocket uses an asynchronous model of communication. In such a model, the client and the server are equal sides of the "dialogue", that communicate independently from each other. After sending a request the client or the server doesn't wait for a response, they continue to work.

Unlike HTTP, where each request triggers a new TCP connection that you need to close when you receive a response, WebSocket opens only one TCP connection for communication between the client and the server. After a lightweight stage of exchanging service data for establishing a connection, a channel gets opened and starts transferring data both ways. Such an approach allows you to considerably reduce expenses and the amount of data that carries service information.

Now we can move to the next stage and examine how WebSocket works in the rapidly developing Play framework. Play Framework is a fullstack framework and allows working with databases, routing, page rendering with the use of a template engine and many other useful tools. One of the cool "out of the box" possibilities of Play is working with web sockets.

HTTP-controller in Play is a method that returns objects of Action type. Such a controller can return different types of results -XML, JSON, text. Here's, for example, a controller that returns the result in the form of a usual string:

def user(user: User): Action[AnyContent] = Action {
 Ok(user.name)
}

And here's how you can return the result after rendering a page by means of Play built-in engine.

def clientsList: Action[AnyContent] = Action {
val clients = userService.listClients()
Ok(views.html.admin.clientsList(clients))
}

Now let's take a look at the possibilities of Play concerning web socket controllers. There are two ways to create such a controller:

  • asynchronous collections Iteratee/Enumerator ,
  • actors from Akka library.

Both ways allow asynchronously receive and send messages through web sockets, however, they differ greatly in terms of the architecture of building these message handlers. Let's examine these differences.

Web socket controller on Iteratee/Enumerator

This approach suits better when you need to transfer a steady string of data, for example, long texts or files in the form of a byte array. Iteratee is used as a data receiver from the client, and Enumerator is used as a response generator:

def socket: WebSocket[String, String] = WebSocket.using[String] { request =>


  val out = Enumerator("Hello!")

  val in = Iteratee.foreach[String] { msg =>
    println(msg)
    out >>> Enumerator(msg)
  } map { _ =>
    println("Disconnected.")
  }

  (in, out)
}

You can see from the example, as a result, web socket controller returns WebSocket , not Action . WebSocket itself is based on a tuple of two elements – an input and output channel. When the connection is established, the client will be sent a "Hello" message. When a message from the client is received through the input channel, it will be displayed in the console and sent back to the client through the output channel. When the connection is stopped, the message "Disconnected" will be displayed in the console.

Web socket controller on actors

The second option is to handle a web socket with the help of an actor. This way suits better when messages come as separate requests and are not a part of a string. For example, JSON and XML messages. As a processor for such sockets a specimen of actor class from Akka library is used.

object WebSocketActor {
 def props(out: ActorRef) = Props(new WebSocketActor(out))
}

class WebSocketActor(out: ActorRef) extends Actor {

 out ! "Hello"

 override def receive: Receive = {
   case msg: String =>
     println(msg)
     out ! msg
 }

 override def postStop() {
   println("Disconnected.")
 }

}

def actorSocket = WebSocket.acceptWithActor[String, String] { request => out =>
 WebSocketActor.props(out)
}

AcceptWithActor method has two generic parameters – types of outgoing and incoming messages. WebSocketActor is a successor of actor class and contains the realization of one abstract receive method, into which all the client's messages come. In the given example the type of incoming and outgoing messages is a string . The link to the actor out is the output channel. All messages sent to this actor will be passed to the client. Like in the previous example, the processor displays the received message in the console and sends it back to the client. To handle the disconnection you can redefine the postStop method, which will be launched after the actor is terminated.

Apart from handling messages in the form of a string, Play can also work with a byte array and JsValue objects:

def actorSocket = WebSocket.acceptWithActor [JsValue, JsValue] { request => out =>
 WebSocketActor.props(out)
}

This method is more convenient if messages are not just strings, but are presented in JSON format.

Part 2. Practical application

Setting the task

The practical side of setting the task looked in the following way:

  1. Implement the possibility of establishing a connection through a web socket between the client (Android device) and the server.
  2. The message exchange between the client and the server is in JSON format.
  3. The message exchange is done in request-response mode: the client sends a request to the server and waits for its response.
  4. A mechanism of client authentication must be provided.
  5. It is necessary to provide the following development of a protocol with asynchronous requests from the server to the connected client. Taking into consideration the requirements to data format and the discrete nature of sending/receiving messages, we decided to use actors for implementing the server part of web sockets.

The mechanism of message exchange looks as follows

Apart from request/response data, each JSON message necessarily contains message identifier and type. In order to simplify the work with messages in services, several of wrapper classes were written for every type of message. A controller receiving messages from the client looks like this:

def socket = WebSocket.acceptWithActor[JsValue, JsValue] { request => out =>
 ClientWebSocketActor.props(out)
}

The handling actor looks something like this:

class ClientWebSocketActor(out: ActorRef) extends Actor {

 def handleMessage(msg: ClientMessage): JsValue = {
   lazy val responseTimestamp = currentTime
   msg match {
     case msg: ClientMessage if !isAuthenticated() => ErrorMessage(responseTimestamp, "Client not logged in")
     case msg: MessageA => handleMessageA(msg)
     case msg: MessageB => handleMessageB(msg)
     case _ => ErrorMessage(responseTimestamp, "Unsupported message type")
   }
 }

 def receive = {
   case request: JsValue =>
     val response = handleMessage(request)
     out ! response
 }
 
 def handleMessageA(msg: MessageA): JsValue = {
   // Message handling
   ResponseMessageA(...)
 }
 def handleMessageB(msg: MessageB): JsValue = {
   // Message handling
   ResponseMessageB(...)
 }
}

There is some magic in the code shown above. Pay attention to the call of the handleMessage method and the returned result of the methods handleMessageA and handleMessageB . There happens a transformation of messages in these lines from JsValue into ClientMessage and vice versa. Such an approach makes it possible to hide low level handling of JSON string field "under the hood" and to make the processor's code cleaner. Let's take a look under this hood and see what happens there.

An abstract parent class with the field that holds the message type.

abstract class MessageObjectTypeAware(val MSG_TYPE: String)

A companion object for the case class message.

object MessageA extends MessageObjectTypeAware("message_a") {
 implicit val format = Json.format[MessageA]
}

The message case class itself.

case class MessageA(timestamp: Long, data: String, messageType: String = ErrorMessage.MSG_TYPE) extends ClientMessage

The object that performs implicit transformations.

object ClientMessage {

 implicit def jsValue2ClientMessage(jsValue: JsValue): ClientMessage = {
   (jsValue \ "messageType").as[String] match {
     case ErrorMessage.MSG_TYPE => jsValue.as[ErrorMessage]
     case MessageA.MSG_TYPE => jsValue.as[MessageA]
     case MessageB.MSG_TYPE => jsValue.as[MessageB]
     case messageType => ErrorMessage(currentTime, ErrorCodes.UNKNOWN_MESSAGE_TYPE)
   }
 }

 implicit def clientMessage2jsValue(clientMessage: clientMessage): JsValue = {
   clientMessage match {
     case error: ErrorMessage => Json.toJson(error)
     case msgA: MessageA => Json.toJson(msgA)
     case msgB: MessageB => Json.toJson(msgB)
   }
 }

}

The operation scheme of the code given above:

  1. Each message is a case class that was inherited from ClientMessage . The message contains the type, time mark and associated data.
  2. There is a companion object for each message, which contains the type of the message and a formatter for converting the message into JSON and back.
  3. MSG_TYPE field of companion objects is put into a general abstract parent class MessageObjectTypeAware .
  4. The object ClientMessage contains implicit transformations, thanks to which automatic conversion between JsValue and ClientMessage takes place in the client code.

In practice there was a problem found in this solution – when new types of messages were added, the object ClientMessage began to grow because it contains a case branch for each type of messages. Unfortunately, it wasn't possible to solve this problem effectively and at the same time to meet the project deadline. That's why a decision was made to divide the messages into groups according to their meaning. It allowed breaking a big converter object into a few smaller objects – each for its own group of messages.

The following authentication mechanism was developed in the project

With the first message the user sends his unique uuid (for example, a unique uuid of a mobile device). In the method of handling this message (handleUuidSignIn) the user is saved with this uuid if there is no such a user yet, otherwise a previously saved user is returned from the database. In case the previous operation is performed successfully, we save the user into the variable signedInUser of the actor class, generate authorization token, which we return in our response to the client for further authorization with the help of the token.

private def handleUuidSignIn(responseTimestamp: => Long, uuid: String): JsValue = {
...
    // getting existent user or create new one

    user match {
      case Success(u) =>
        signedInUser = Some(u)// store user to variable
        val token = userTokenService.getOrCreate(u)// token generating
        SignInResponseMessage(responseTimestamp, token.securityToken)// return response

      case Failure(e) => ErrorMessage(responseTimestamp, e.getMessage)
    }
  }

When you sign in with the help of a token, it is passed in the message. In the method of handling the message, a user with such token is getting and saving it into the variable signedInUser of the actor class.

When you sign in with the help of a login and a password, the same actions as in the authorization involving a token are carried out, but the user is searched for in the database according to the login and a check up of the password correctness takes place.

The variable signedInUser is used in order to collect information about the authorized user, which is used in further requests as well as for eliminating repeated authorization attempts.

class ClientWebSocketActor(out: ActorRef) extends Actor {
  private var signedInUser: Option[User] = None
  private def userUuid = signedInUser.get.uuid

 def handleMessage(msg: ClientMessage): JsValue = {
   lazy val responseTimestamp = currentTime
   msg match {
     case msg: case _@(UuidSignInMessage(_, _, _)| TokenSignInMessage(_, _, _)) if signedInUser.isDefined =>
        ErrorMessage(responseTimestamp, "Already autorized!")
...
     case msg: MessageA => handleMessageA(msg)
...
     case _ => ErrorMessage(responseTimestamp, "Unsupported message type")
   }

 def handleMessageA(msg: MessageA): JsValue = {
...
   ResponseMessageA(...)
 }
}

Bottom line

In this article we described the work with WebSocket protocol in the context of using Play Framework. In the first part of the article we provided theoretical information about the ways of creating web socket controllers in Play. In the second part of the article we described the practical implementation of the technology in a real project. There were given examples of realizing message exchange between the client and the server as well as the authentication mechanism used in the project.





About List