简体   繁体   English

如何在单个 Akka-http 流中读取多个 Web 套接字?

[英]How can I read several web-sockets within a single Akka-http Flow?

I am currently practicing Akka-http by attempting to establish multiple websockets connections.我目前正在通过尝试建立多个 websockets 连接来练习 Akka-http。 My code for creating the websockets client flow (snippet) looks like:我用于创建 websockets 客户端流(片段)的代码如下所示:

val webSocketFlow =
  Http().webSocketClientFlow(WebSocketRequest(url), settings = customSettings)

val (upgradeResponse, closed) =
  outgoing
    .viaMat(webSocketFlow)(Keep.right)
    .viaMat(decoder)(Keep.left)
    .toMat(sink)(Keep.both)
    .run()

This currently works great if I have one url.如果我有一个 url,这目前效果很好。 I am curious about how can I scale this to connect to multiple urls.我很好奇如何扩展它以连接到多个 url。 So for example, if I have an indefinite list of websockets endpoints List("ws://localhost:8080/foo", "ws://localhost:8080/bar", "ws://localhost:8080/baz") .例如,如果我有一个不确定的 websockets 端点List("ws://localhost:8080/foo", "ws://localhost:8080/bar", "ws://localhost:8080/baz") .

I have considered adding a new flow for each URL, but what if I have a long list of websockets endpoints/urls.我已经考虑为每个 URL 添加一个新流程,但是如果我有一长串 websockets 端点/url 怎么办。 Then that becomes cumbersome and overtly-manual.然后,这变得繁琐且过于手动。 I have also considered wrapping this into a function and calling for each URL in a given iterable.我还考虑将其包装到 function 中,并在给定的可迭代中调用每个 URL。 But that also felt-over-kill.但这也让人感觉过度杀戮。

Is there a way to have a pool of connections all sourced into one Flow (or something like this)?有没有办法让一个连接池全部来源于一个流(或类似的东西)? Further readings are also welcome.也欢迎进一步阅读。 As a "nice-to-have", is there also a way to tag the incoming messages to signal with url they are coming from?作为一个“不错的选择”,是否还有一种方法可以标记传入的消息以使用它们来自的 url 发出信号?

Update: for clarification, I am only reading from websockets (only client side) and not sending any messages back.更新:为澄清起见,我只是从 websockets 读取(仅客户端),而不是发回任何消息。

This should work (code is being written in the textbox...):这应该可以工作(代码正在文本框中编写......):

def taggedWebsocketForUrl(url: String, tag: Int): Source[(Int, Message), Future[WebSocketUpgradeResponse]] =
  outgoing.viaMat(Http().webSocketClientFlow(WebSocketRequest(url), settings = customSettings))(Keep.right).map(tag -> _)

val websocketMergedSource: Source[(Int, Message), Seq[Future[WebSocketUpgradeResponse]]] = {
  // You could replace this with a mess of headOptions etc., but...
  if (websocketUrls.isEmpty) Source.empty[(Int, Message)].mapMaterializedValue(_ => Seq(Future.failed(new NoSuchElementException("no websocket URLs"))))
  else {
    val first: Source[(Int, Message), List[Future[WebSocketUpgradeResponse]]] =
      taggedWebsocketForUrl(websocketUrls.head, 0).mapMaterializedValue(List(_))
    if (websocketUrls.tail.isEmpty) first
    else {
      websocketUrls.tail.foldLeft(first -> 1) {
        (acc, url) =>
          val newSource = acc._1.mergeMat(taggedWebsocketForUrl(url, acc._2)) {
            (futs: List[Future[WebSocketUpgradeResponse]], fut: Future[WebSocketUpgradeResponse]) =>
              fut :: futs // Will reverse at the end...
          }
          newSource -> (acc._2 + 1)
      }._1.mapMaterializedValue(_.reverse)
    }
  }
}

With this, you'll have many upgrade responses (you could mapMaterializedValue(Future.sequence _) to combine them into a Future[Seq[WebsocketUpgradeResponse]] which will fail if any fail).有了这个,你会有很多升级响应(你可以mapMaterializedValue(Future.sequence _)将它们组合成一个Future[Seq[WebsocketUpgradeResponse]]如果任何失败都会失败)。 The messages from the n th url in the list will be tagged with n .来自列表中第n个 url 的消息将被标记为n

Note that websocketUrls being a List guides to building up as a fold: if there are n urls, the messages from the first url will go through n -1 merge stages and the last url will go through only 1 merge stage, so you'd want to put the urls you expect to generate more traffic towards the end of the list. Note that websocketUrls being a List guides to building up as a fold: if there are n urls, the messages from the first url will go through n -1 merge stages and the last url will go through only 1 merge stage, so you'd想要将您期望产生更多流量的网址放在列表末尾。

An alternative, more efficient approach would be if using an IndexedSeq like Vector or Array to divide and conquer to build up a tree of merge s.另一种更有效的方法是,如果使用像VectorArray这样的IndexedSeq来分而治之,以建立一个merge树。

Using the Akka Streams GraphDSL would also give you a lot of control, but I would tend to use that only as a last resort.使用 Akka Streams GraphDSL也可以为您提供很多控制,但我倾向于仅将其用作最后的手段。

What you need is some way to merge the various WebSocket flows so that you can process the incoming messages as if they came from a single Source.您需要某种方法来合并各种 WebSocket 流,以便您可以处理传入消息,就好像它们来自单个源一样。

Since you do not require to send any data but only receiving the implementation is straightforward.由于您不需要发送任何数据,而只需要接收实现就很简单了。

Let's start creating a function that will create a WebSocket Source for a given uri:让我们开始创建一个 function ,它将为给定的 uri 创建一个 WebSocket 源:

def webSocketSource(uri: Uri): Source[Message, Future[WebSocketUpgradeResponse]] = {
  Source.empty.viaMat(Http().webSocketClientFlow(uri))(Keep.right)
}

Since you do not care about sending data, the function immediately close the out channel by providing an empty Source.由于您不关心发送数据,因此 function 通过提供一个空的 Source 立即关闭输出通道。 The result is a Source containing the message read from the WebSocket.结果是包含从 WebSocket 读取的消息的 Source。

At this point we can use this function to create a dedicated source for each uri:此时我们可以使用这个 function 为每个 uri 创建一个专用源:

val wsSources: List[Source[Message, NotUsed]] = uris.map { uri =>
  webSocketSource(uri).mapMaterializedValue { respFuture =>
    respFuture.map {
      case _: ValidUpgrade => log.debug(s"Websocket upgrade for [${uri}] successful")
      case err: InvalidUpgradeResponse => log.error(s"Websocket upgrade for [${uri}] failed: ${err.cause}")
    }

    NotUsed
  }
}

Here we need to somehow take care of the materialized values since it's not possible (or at least not easy) to combine them given we do not know how many they are.在这里,我们需要以某种方式处理物化值,因为不可能(或至少不容易)将它们组合起来,因为我们不知道它们有多少。 So here we go with the simplest approach of just logging.所以在这里我们用最简单的方法来记录 go。

Now that we have our sources ready we can proceed to merge them:现在我们已经准备好源代码,我们可以继续合并它们:

val mergedSource: Source[Message, NotUsed] = wsSources match {
  case s1 :: s2 :: rest => Source.combine(s1, s2, rest: _*)(Merge(_))
  case s1 :: Nil => s1
  case Nil => Source.empty[Message]
}

Here the idea is that in case we have 2 or more uris, we actually do a merge operation, otherwise if we have a single one we just use it without any modification.这里的想法是,如果我们有 2 个或更多 uri,我们实际上会执行合并操作,否则如果我们有一个,我们只需使用它而不做任何修改。 Lastly we also cover the case where we do not have any uri at all by providing an empty Source which will simply terminate the stream without errors.最后,我们还通过提供一个空 Source 来涵盖根本没有任何 uri 的情况,该 Source 将简单地终止 stream 而不会出现错误。

At this point we can combine this source to the flows and sink you already have and run it:此时,我们可以将此源与您已经拥有的流和接收器结合起来并运行它:

val done: Future[Done] = mergedSource.via(decoder).toMat(sink)(Keep.right).run

Which gives us back a single future which will be completed when all connection are completed or failed as soon as one connection fails.这给了我们一个单一的未来,它将在所有连接完成时完成,或者在一个连接失败时立即失败。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM