使用Play 2.6和akka流的Websocket代理

时间:2017-04-12 09:22:53

标签: scala playframework akka akka-http akka-stream

我正在尝试使用Play和akka流为Websocket连接创建一个简单的代理。 交通流量是这样的:

(Client) request  ->         -> request (Server)
                      Proxy 
(Client) response <-         <- response (Server)

按照一些例子后,我想出了以下代码:

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

val uuid = UUID.randomUUID().toString

// wsOut - actor that deals with incoming websocket frame from the Client
// wsIn - publisher of the frame for the Server
val (wsOut: ActorRef, wsIn: Publisher[String]) = {
  val source: Source[String, ActorRef] = Source.actorRef[String](10, OverflowStrategy.dropTail)
  val sink: Sink[String, Publisher[String]] = Sink.asPublisher(fanout = false)
  source.toMat(sink)(Keep.both).run()
}

// sink that deals with the incoming messages from the Server
val serverIncoming: Sink[Message, Future[Done]] =
  Sink.foreach[Message] {
    case message: TextMessage.Strict =>
      println("The server has sent: " + message.text)
  }

// source for sending a message over the WebSocket
val serverOutgoing = Source.fromPublisher(wsIn).map(TextMessage(_))

// flow to use (note: not re-usable!)
val webSocketFlow = Http().webSocketClientFlow(WebSocketRequest("ws://0.0.0.0:6000"))

// the materialized value is a tuple with
// upgradeResponse is a Future[WebSocketUpgradeResponse] that
// completes or fails when the connection succeeds or fails
// and closed is a Future[Done] with the stream completion from the incoming sink
val (upgradeResponse, closed) =
serverOutgoing
  .viaMat(webSocketFlow)(Keep.right) // keep the materialized Future[WebSocketUpgradeResponse]
  .toMat(serverIncoming)(Keep.both) // also keep the Future[Done]
  .run()

// just like a regular http request we can access response status which is available via upgrade.response.status
// status code 101 (Switching Protocols) indicates that server support WebSockets
val connected = upgradeResponse.flatMap { upgrade =>
  if (upgrade.response.status == StatusCodes.SwitchingProtocols) {
    Future.successful(Done)
  } else {
    throw new RuntimeException(s"Connection failed: ${upgrade.response.status}")
  }
}

// in a real application you would not side effect here
connected.onComplete(println)
closed.foreach(_ => println("closed"))

val actor = system.actorOf(WebSocketProxyActor.props(wsOut, uuid))
val finalFlow = {
  val sink = Sink.actorRef(actor, akka.actor.Status.Success(()))
  val source = Source.maybe[String] // what the client receives. How to connect with the serverIncoming sink ???
  Flow.fromSinkAndSource(sink, source)
}

finalFlow

使用此代码,流量从客户端到代理服务器再到服务器,再返回到代理服务器就是这样。它没有进一步向客户提供。我怎样才能解决这个问题 ? 我想我需要以某种方式将serverIncoming接收器连接到source中的finalFlow,但我无法弄清楚如何做到这一点......

或者我对这种方法完全错了?使用BidiflowGraph会更好吗?我是akka流的新手,仍在努力解决问题。

4 个答案:

答案 0 :(得分:3)

以下似乎有效。注意:我在同一个控制器中实现了服务器套接字和代理套接字,但您可以拆分它们或在不同的实例上部署相同的控制器。 ws url to the&#39; upper&#39;在这两种情况下都需要更新服务。

package controllers

import javax.inject._

import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.ws.{Message, TextMessage, WebSocketRequest, WebSocketUpgradeResponse}
import akka.stream.Materializer
import akka.stream.scaladsl.Flow
import play.api.libs.streams.ActorFlow
import play.api.mvc._

import scala.concurrent.{ExecutionContext, Future}
import scala.language.postfixOps

@Singleton
class SomeController @Inject()(implicit exec: ExecutionContext,
                                actorSystem: ActorSystem,
                                materializer: Materializer) extends Controller {

  /*--- proxy ---*/
  def websocketFlow: Flow[Message, Message, Future[WebSocketUpgradeResponse]] =
    Http().webSocketClientFlow(WebSocketRequest("ws://localhost:9000/upper-socket"))

  def proxySocket: WebSocket = WebSocket.accept[String, String] { _ =>
    Flow[String].map(s => TextMessage(s))
      .via(websocketFlow)
      .map(_.asTextMessage.getStrictText)
  }

  /*--- server ---*/
  class UpperService(socket: ActorRef) extends Actor {
    override def receive: Receive = {
      case s: String => socket ! s.toUpperCase()
      case _ =>
    }
  }

  object UpperService {
    def props(socket: ActorRef): Props = Props(new UpperService(socket))
  }

  def upperSocket: WebSocket = WebSocket.accept[String, String] { _ =>
    ActorFlow.actorRef(out => UpperService.props(out))
  }   
}

您需要设置如下路线:

GET /upper-socket controllers.SomeController.upperSocket
GET /proxy-socket controllers.SomeController.proxySocket

您可以通过向ws:// localhost:9000 / proxy-socket发送字符串来进行测试。答案是大写的字符串。

但是在1分钟不活动后会有超时:

akka.stream.scaladsl.TcpIdleTimeoutException: TCP idle-timeout encountered on connection to [localhost:9000], no bytes passed in the last 1 minute

但请参阅:http://doc.akka.io/docs/akka-http/current/scala/http/common/timeouts.html了解如何配置此内容。

答案 1 :(得分:3)

首先,您需要一些akka次导入:

import akka.actor.ActorSystem
import akka.stream.ActorMaterializer
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.ws.WebSocketRequest
import akka.http.scaladsl.model.ws.Message
import akka.http.scaladsl.model.HttpRequest
import akka.http.scaladsl.model.HttpResponse
import akka.stream.scaladsl.Flow
import akka.http.scaladsl.server.Directives.{ extractUpgradeToWebSocket, complete }

这是一个示例App,用于创建WebSocket代理,绑定在端口0.0.0.080,代理ws://echo.websocket.org

object WebSocketProxy extends App {
  implicit val system = ActorSystem()
  implicit val materializer = ActorMaterializer()

  private[this] def manipulateFlow: Flow[Message, Message, akka.NotUsed] = ???

  private[this] def webSocketFlow =
    Http().webSocketClientFlow(WebSocketRequest("ws://echo.websocket.org"))

  private[this] val route: Flow[HttpRequest, HttpResponse, Any] = 
    extractUpgradeToWebSocket { upgrade =>
      val webSocketFlowProxy = manipulateFlow via webSocketFlow
      val handleWebSocketProxy = upgrade.handleMessages(webSocketFlowProxy)
      complete(handleWebSocketProxy)
    }

  private[this] val proxyBindingFuture =
    Http().bindAndHandle(route, "0.0.0.0", 80)

  println(s"Server online\nPress RETURN to stop...")
  Console.readLine()
}

您必须根据play和您的应用程序结构进行调整。

注意:

  • 请记住取消绑定proxyBindingFuture并终止生产中的system;
  • 只有在您想要操纵消息时才需要manipulateFlow

答案 2 :(得分:2)

代理需要提供两个流(代理流A / B):

(Client) request  ->  Proxy Flow A -> request (Server)

(Client) response <-  Proxy Flow B <- response (Server)

实现此类代理流的一个选项是使用ActorSubscriber和SourceQueue:

class Subscriber[T](proxy: ActorRef) extends ActorSubscriber {
  private var queue = Option.empty[SourceQueueWithComplete[T]] 
  def receive = {
    case Attach(sourceQueue) => queue = Some(sourceQueue)
    case msg: T => // wait until queue attached and pass forward all msgs to queue and the proxy actor
  }
}

def proxyFlow[T](proxy: ActorRef): Flow[T, ActorRef] = {
  val sink = Sink.actorSubscriber(Props(new Subscriber[T](proxy)))
  val source = Source.queue[T](...)
  Flow.fromSinkAndSourceMat(sink, source){ (ref, queue) =>
    ref ! Attach(queue)
    ref
  }
}

然后您可以组装客户端流程,如:

val proxy = actorOf(...)
val requestFlow = proxyFlow[Request](proxy)
val responseFlow = proxyFlow[Response](proxy)
val finalFlow: Flow[Request, Response] = 
    requestFlow.via(webSocketFlow).via(responseFlow)

答案 3 :(得分:1)

作为Federico非常好的解决方案的扩展-此代码可用于代理转发网关服务中,在该服务中,您将连接到将Websockets“管道”到微服务的代理。下面的代码使用Akka Http 10.2.0,并且该代码中有一些规定可以在始发者Websocket客户端断开连接时处理上游流失败-即,通过在Websocket客户端流中添加可投例的方式进行恢复。

import akka.actor.typed.ActorSystem
import akka.actor.typed.scaladsl.Behaviors
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.ws._
import akka.http.scaladsl.model.{HttpRequest, HttpResponse}
import akka.http.scaladsl.server.Directives.{complete, extractWebSocketUpgrade}
import akka.stream.scaladsl._

import scala.io.StdIn
import scala.util.{Failure, Success}

object Main {

  def main(args: Array[String]) {

    implicit val system = ActorSystem(Behaviors.empty, "webtest")
    implicit val executionContext = system.executionContext

    def webSocketFlow =
      Http().webSocketClientFlow(WebSocketRequest("ws://localhost:8000/ws"))
        .recover {
          case throwable: Throwable =>
            try {
              throw new RuntimeException(s"Websocket Upstream Flow failed... Message: ${throwable.getMessage}")
            } catch {
              case t: Throwable => system.log.info(t.getMessage)           //catching all Throwable exceptions
            }
            TextMessage("Websocket Upstream Flow failed...")
        }

    def routeFlow: Flow[HttpRequest, HttpResponse, Any] = extractWebSocketUpgrade { upgrade =>
      val handleWebSocketProxy = upgrade.handleMessages(webSocketFlow)
      complete(handleWebSocketProxy)
    }

    Http().newServerAt("0.0.0.0", 8080).bindFlow(routeFlow)
      .onComplete {
        case Success(_) =>
          system.log.info("Server online at http://0.0.0.0:8080")
        case Failure(ex) =>
          system.log.error("Failed to bind HTTP endpoint, terminating system", ex)
          system.terminate()
      }

    system.log.info("Press RETURN to stop...")
    StdIn.readLine()
    system.terminate()
  }

}

此处用户/发起者以代理身份连接到0.0.0.0:8080,并“管道”(转发)到localhost:8000。