5

This is rather a basic question but I couldn't find a satisfying answer after googling for hours. From the example in here, the way to make web socket is something like this:

Controller code:

import play.api.mvc._
import play.api.libs.streams.ActorFlow
import javax.inject.Inject
import akka.actor.ActorSystem
import akka.stream.Materializer

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)
    }
  }
}

Actor code:

import akka.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)
  }
}

But how exactly do I send message from controller to the actor via web socket? Let's say in the controller code, I have an action code that handles when a button is pressed, it will send a block of string to the actor. How do I send this string to the actor above from the controller code?

Algorithman
  • 1,309
  • 1
  • 16
  • 39

4 Answers4

0

I can provide you with some examples of websockets in Play. Essentially they use a Flow (akka-streams) to handle a websocket connection.

There is an official Play Websocket example: Lightbend's Websocket example

Based on that, I have several projects that use websockets, for example:

play-wsocket-scalajs

This is an example application showing how you can integrate a Play project with a Scala.js, Binding.scala project - using Web Sockets.

It is quite involved, so the easiest way is to check HomeController, UserParentActor, UserActor and AdapterActor how they work together.

scala-adapters

Is a framework that is based on the example above - that also shows how to register websocket clients.

pme
  • 14,156
  • 3
  • 52
  • 95
0

Let's first understand what is already created, and what we need to add. The type of socket, is a WebSocket.

This WebSocket, reveals a single apply method:

def apply(request: RequestHeader): Future[Either[Result, Flow[Message, Message, _]]]

Therefore, as long as you did not send a message, the flow has not yet been created. Now, once a message is sent we can create the flow, and send it a meesage:

def index = Action.async(parse.json) { request =>
  socket(request).map {
    case Left(result) =>
      Ok("Done: Left: " + result.body)
    case Right(value) =>
      Source.single(TextMessage(Json.stringify(request.body))).via(value).to(Sink.ignore).run()
      Ok("Done: Right: ")
  }
}
Tomer Shetah
  • 8,413
  • 7
  • 27
  • 35
0

This sample app and related discussion might be helpful. Here's some code clipped/summarized from the linked sample app:

Flow.futureFlow(futureUserActor.map { userActor =>
  val incomingMessages: Sink[Message, NotUsed] =
    Flow[Message]
      .map(...)
      .to(...)

  val outgoingMessages: Source[Message, NotUsed] =
    ActorSource
      .actorRef[User.OutgoingMessage](...)
      .mapMaterializedValue { outActor =>
        // give the user actor a way to send messages out
        userActor ! User.Connected(outActor)
        NotUsed
      }
      .map(...)

  // then combine both to a flow
  Flow.fromSinkAndSourceCoupled(incomingMessages, outgoingMessages)
})
Ryan Hoffman
  • 126
  • 4
0

There are at least two ways to approach this:

  1. Customize play's ActorFlow.actorRef method to return the underlying actor. There was a similar discussion before, here's a gist. If you put the underlying actor into a (user, websocket) map, make sure to use a thread-safe implementation like the TrieMap.
  2. What you're trying to do could be solved by creating an event bus & subscribing to it from within the actor. Then you could filter the events you're interested in & react accordingly. This solution is better, in that it actually scales - you can have more than one replica of your web app (the 1st approach wouldn't work in that case, because a replica that doesn't hold the reference to a user's WS actor could receive button clicked event). In pseudo code to illustrate the idea:
sealed trait AppEvent
final case class ButtonClicked(user: User.ID) extends AppEvent

// inside an action
system.eventStream.publish(ButtonClicked(request.identity.id))

// inside your actor
override def preStart =
  context.system.eventStream.subscribe(self, classOf[AppEvent])

Please note that the idea of event bus is abstract. What I've demonstrated above is the most basic approach using akka's classic event bus, which works locally. For this approach to scale, you would need an actual message queue behind the scenes.

Maciej Kozik
  • 176
  • 2
  • 8