scalawebsocketplayframeworkpekko

Send data from a Post Request to the output of a Websocket in Play Framework (Scala)


I need some help with the Play Framework. I have this Websocket that immediately responds with a message to a client. My API has to be able to receive messages from another API, that sends them via POST request.

It has to follow this logic:

  1. I receive a message from a POST request from an already existing API
  2. It passes that message to the WebSocket function, or the Flow, or whatever I need to do.
  3. The WebSocket outputs that message to the client
  4. The client (In this case, an Angular APP) receives the message from the WebSocket

Is there any way I could pass the data from the Post Request to the Websocket, so it sends it to the client?

I've only tried with the same web socket, but I couldn't make it too far. I left it the same way it is written in the documentation. As for now, it only responds with "I've received your message" to the client who sent that message.

I'll leave here the code in question, the post function has a comment that says "// connect the WebSocket". That's the place where I need to send that message to the Output of the WebSocket, so it reaches the client in real-time. I've read the documentation a lot of times but it's not really that helpful. It lacks a lot of things.

(I don't think this is the way it should work, but that's what my boss ordered... I haven't tried sending to another client via the same WebSocket due to that reason)


package controllers

import org.apache.pekko.actor._
import org.apache.pekko.stream.Materializer
import play.api.libs.streams.ActorFlow

import javax.inject._
import play.api.mvc._


@Singleton
class HomeController @Inject()(val controllerComponents: ControllerComponents) (implicit system: ActorSystem, mat: Materializer) extends BaseController {

    def index(): Action[AnyContent] = Action { implicit request: Request[AnyContent] =>
        Ok(views.html.index())
    }

    def getMessage: Action[AnyContent] = Action { request: Request[AnyContent] =>
        println(request.body.toString)

        //connect the websocket here
        
        Ok("It works!")
    }

    def socket: WebSocket = WebSocket.accept[String, String] { request =>
        ActorFlow.actorRef { out => MyWebSocketActor.props(out) }
    }
}

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

class MyWebSocketActor(out: ActorRef) extends Actor {
    def receive: Receive = {
        case msg: String =>
            out ! ("I received your message: " + msg)
    }
}

Solution

  • I just extracted part of the code from MarkCLewis - Play-Videos. There is also a youtube playlist Play Framework using Scala which teaches you how to build a chat room using websockets.

    This is just a basic and really dummy example. It will not work in a real project with lot of traffic, but I hope it helps you.

    Not sure how well you are familiarized with the actor model and akka/pekko. Play Framework is built on top of play akka or play pekko depends on the version of Play.

    You can think of an actor as a message queue that will process each message sequentially. You can not create an actor as a new instance of an object, you need an ActorSystem that let you spawns actors. Each time you create a new one using the ActorSystem, you get a reference to it. The only way to send messages to an actor is through that reference

    val dummyActor = system.actorOf(Props[DummyActor], "unique-name-of-actor")
    dummyActor ! SomeMessage(param1, param2, param3)
    

    WebSocket is an actor that can receive message from a client and send a message to the client that opened the websocket connection. In the example is called ChatActor. We will need a second actor (named ChatManager) that let us send messages to the ChatActor using its reference

    routes

    Not much to explain. Two endpoints

    GET  /    HelloController.index
    GET  /ws  HelloController.socket
    

    Controller

    Just a basic controller that has two methods and one field

    import org.apache.pekko.actor.{ActorSystem, Props}
    import org.apache.pekko.stream.Materializer
    import play.api.libs.streams.ActorFlow
    import play.api.mvc._
    import javax.inject.Inject
    
    class HelloController @Inject() (cc: ControllerComponents)(implicit
        system: ActorSystem,
        mat: Materializer
    ) extends AbstractController(cc) {
    
      private val manager = system.actorOf(Props[ChatManager], "Manager")
    
      def index: Action[AnyContent] =
        Action { _ =>
          // this is the place where we sent a message to the manager.
          // Similar to what you need. You receive a POST (in this example
          // is a GET) and you send a message to the client connected to the
          // websocket
          manager ! ChatManager.Message("a hello message sent from index")
          Ok("hello")
        }
    
      def socket = WebSocket.accept[String, String] { _ =>
        ActorFlow.actorRef { out =>
          ChatActor.props(out, manager)
        }
      }
    }
    

    ChatActor

    As you can see, once the actor is created, in the first line we sent a message to the manager saying that we have a new chatter. In the receive method we consider two cases

    import org.apache.pekko.actor.{Actor, ActorRef, Props}
    
    class ChatActor(out: ActorRef, manager: ActorRef) extends Actor {
      manager ! ChatManager.NewChatter(self)
    
      import ChatActor._
      def receive = {
        case s: String        => manager ! ChatManager.Message(s)
        case SendMessage(msg) => out ! msg
      }
    }
    
    object ChatActor {
      def props(out: ActorRef, manager: ActorRef) = Props(
        new ChatActor(out, manager)
      )
    
      case class SendMessage(msg: String)
    }
    

    ChatManager

    We have a mutable list named chatters that will contain all the open webscoket connections. Then we consider two cases

    import org.apache.pekko.actor.{Actor, ActorRef}
    import scala.collection.mutable.ListBuffer
    
    class ChatManager extends Actor {
      private val chatters = ListBuffer.empty[ActorRef]
    
      import ChatManager._
      def receive = {
        case NewChatter(chatter) => chatters += chatter
        case Message(msg)        => for (c <- chatters) c ! ChatActor.SendMessage(msg)
      }
    }
    
    object ChatManager {
      case class NewChatter(chatter: ActorRef)
      case class Message(msg: String)
    }