I'm using akka-streams to set up a client web socket. I'm trying to encapsulate the setup in a method with the following signature:
def createConnectedWebSocket(url: String): Flow[Message, Message, _]
It is clear how to create the web socket flow but it is not connected yet:
val webSocketFlow: Flow[Message, Message, Future[WebSocketUpgradeResponse]] =
Http().webSocketClientFlow(WebSocketRequest(url))
I first want to Await
the upgrade response future and then return the socket flow. However, in order to get the future, I have to materialize the flow and for that I have to connect a Source
and a Sink
. But this should be the responsibility of some other adapter class, for instance one that serializes and deserializes json objects and exposes a Flow[JsValue, JsValue, _]
. It should not have to worry about connecting and maybe reconnecting when the connection is lost (this behaviour will be part of a more elaborate version of my method once I manage to write it). It should only have to deal with a simple Flow
.
I managed to achieve part of what I want by using hubs:
val mergeHubSource = MergeHub.source[Message](perProducerBufferSize = 16)
val broadcastHubSink = BroadcastHub.sink[Message](bufferSize = 16)
val ((messageSink, upgradeResponse), messageSource) =
mergeHubSource
.viaMat(webSocketFlow)(Keep.both)
.toMat(broadcastHubSink)(Keep.both)
.run()
So now I have a Source
and a Sink
that I can combine to a Flow
and return it. The problem is, that I am not interested in the hub functionality. When I connect a Source
to the resulting Flow
and close it, this should be propagated to the socket, i.e., the socket should close. When using a MergeHub
it remains open in order to be able to accept new sources.
Is this possible? I think I could bridge the gap with custom actors but it feels like I'm reinventing something here that is likely already implemented in another form.
I found a solution using SourceRef
and SinkRef
. Although they are meant to be used to bridge the gap between two machines, they can be used here as well.
val webSocketFlow: Flow[Message, Message, Future[WebSocketUpgradeResponse]] =
Http().webSocketClientFlow(WebSocketRequest(someUrl))
val (sinkRefFuture, sourceRefFuture) =
StreamRefs.sinkRef[In]()
.viaMat(f)(Keep.left)
.toMat(StreamRefs.sourceRef[Out]())(Keep.both)
.run()
val flow = Flow.fromSinkAndSource(await(sinkRefFuture), await(sourceRefFuture))
with await()
being defined for instance like this:
def await[T, F <: T](f: Future[F]): T = Await.result(f, 3.seconds)
That being said, I figured that it is actually better, at least in my case, not to materialize the socket in advance. This way whoever uses it can also take care of reconnecting. I'm now passing around a flow factory that creates new instances of the web socket Flow
(may only me materialized once) on demand.
来源:https://stackoverflow.com/questions/50188029/waiting-for-a-client-websocket-flow-to-connect-before-connecting-source-and-sink