Download - Akka HTTP
Akka HTTP安田裕介
@TanUkkii0072016/3/4
Akka Stream & HTTPリリースおめでとう!
ノンブロッキングで背圧制御に満ちたエコシステム形成の幕開けになることを期待します
Akka HTTP とは
• akka IO と Akka Stream を使って、 NIO かつ背圧制御に基づいた HTTP モジュール• 低レベル API の akka-http-core と高レベル API の
akka-http-experimental がある• Akka Stream がシステム内の調和を保つとしたら、 Akka HTTP はシステム間の調和を実現する
マイクロサービスの難しさ
“every single one of your peer teams suddenly becomes a potential DOS attacker”
周りの全ての同僚チームが、突如 DOS アタッカーになりうるようになった和訳原文
Stevey の “ Google プラットフォームぶっちゃけ話”にでてくるAmazon のマイクロサービス化による AWS 誕生のくだり
システム間のインターフェースとしてもっとも使われる HTTP に背圧制御をもたらす必要がある
アジェンダ
• Akka Stream の復習• Akka HTTP API
• HTTP レイヤーと TCP レイヤーとの接合• TCP レイヤーの内部構造
Akka Stream の復習 val intSource: Source[Int, NotUsed] = Source(List(1, 2, 3)) //Source は1つの出力をもつ。 Source などをグラフという。 val toStringFlow: Flow[Int, String, NotUsed] = Flow[Int].map(_.toString) //Flow は1つの入力と1つの出力をもつ。 map などをステージという。 val seqSink: Sink[String, Future[Seq[String]]] = Sink.seq[String] //Sink は1つの入力をもつ。第2型引数が Materialized Value 。 /** * +-----------------------------------------------------------------------------------+ * | runnableGraph | * | | * | +------------+ +--------------+ +---------+ | * | | | | | | | | * | | intSource ~ Int ~> ~ Int ~> toStringFlow ~ String ~> ~ String ~> seqSink | | * | | | | | | | | * | +------------+ +--------------+ +---------+ | * +-----------------------------------------------------------------------------------+ */ // すべてのポートが閉じたグラフは RunnableGraph[Mat] になり、マテリアライズ化が可能になる val runnableGraph: RunnableGraph[Future[Seq[String]]] = intSource.via(toStringFlow).toMat(seqSink)((sourceMatValue, sinkMatValue) => sinkMatValue)
// RunnableGraph の run を呼んでマテリアライズ化する。ここからストリームが動き出す。マテリアライズ化の結果として Materialized Value が返る。 val materializedValue: Future[Seq[String]] = runnableGraph.run()(ActorMaterializer())
// Materialized Value からストリームの結果を受け取れる場合がある val streamResult: Seq[String] = Await.result(materializedValue, 10 seconds) //Seq("1", "2", "3")
Akka Stream の復習intSource.via(toStringFlow).toMat(seqSink)((sourceMatValue, sinkMatValue) => sinkMatValue).run()
すべては下流から始まる
Source Flow Sinkpull(in)
pull(in)
onPullonPull
push(out, 1)onPush
push(out, “1”)
pull(in)pull(in)
onPull
push(out, 2) onPush
※ アクターモデルとの最大の違い
onPush
onPull
Akka HTTP
クライアントサイド API
val connectionFlow: Flow[HttpRequest, HttpResponse, Future[OutgoingConnection]] = Http().outgoingConnection(host, port) // 単一の HTTP コネクションストリームval responseFuture1: Future[HttpResponse] = Source.single(HttpRequest(uri = "/")) .via(connectionFlow) .runWith(Sink.head)
val poolClientFlow: Flow[(HttpRequest, Int), (Try[HttpResponse], Int), HostConnectionPool] = Http().cachedHostConnectionPool[Int](host, port) // ホスト単位でコネクションプールをもつストリームval responseFuture2: Future[(Try[HttpResponse], Int)] = Source.single(HttpRequest(uri = "/") -> 1) .via(poolClientFlow) .runWith(Sink.head)
サーバーサイド API
val serverSource: Source[Http.IncomingConnection, Future[Http.ServerBinding]] = Http().bind(host, port) //bind でコネクションの Source を得るval bindingFuture: Future[ServerBinding] = serverSource.to(Sink.foreach { connection => val connFlow: Flow[HttpResponse, HttpRequest, NotUsed] = connection.flow // それぞれのコネクションのデータの送受信を表すフロー val requestHandler: Flow[HttpRequest, HttpResponse, NotUsed] = Flow[HttpRequest].map { case HttpRequest(GET, Uri.Path("/"), _, _, _) => HttpResponse(entity = HttpEntity(ContentTypes.`text/html(UTF-8)`, "<html><body>Hello world!</body></html>")) }
/** * +----------+ +----------------+ * | | ~HttpRequest~> | | * | connFlow | | requestHandler | * | | <~HttpResponse~ | | * +----------+ +----------------+ */
connFlow.joinMat(requestHandler)(Keep.right).run() // コネクションのフローにリクエストハンドラーを接続してリクエストを消費する}).run()
HttpRequest, HttpResponsefinal case class HttpRequest(method: HttpMethod, uri: Uri, headers: immutable.Seq[HttpHeader], entity: RequestEntity, protocol: HttpProtocol)
sealed trait RequestEntity extends HttpEntity
final case class HttpResponse(status: StatusCode, headers: immutable.Seq[HttpHeader], entity: ResponseEntity, protocol: HttpProtocol)
sealed trait ResponseEntity extends HttpEntity
sealed trait HttpEntity {
def dataBytes: Source[ByteString, Any]
}
なんと HttpEntity の中身は Source[ByteString, Any] だこれが効率的なデータの送受信と
Websoket や SSE などの異なるプロトコルを統一的に扱うことを可能にしている
• Future なのでコネクションプールの数を超えて並列に呼ぶことができる• IO の方が CPU より遅いのでコネクションが足りなくなる• NIO なのでスレッドプールのスレッド数 = コネクション数にしても解決しない
リソース間調整import akka.io.IOimport spray.can.Httpimport spray.client.pipelining._
trait RequestProvider { this: Actor => import context.system import context.dispatcher
lazy val pipeline = { sendReceive(IO(Http)(system)) ~> unmarshal[String] }
def request(path: String): Future[String] = pipeline(Get(s"$requestUrl/$path"))}
スレッドプールとコネクションプール
val safeRequest: Flow[String, String, NotUsed] = Flow[String].mapAsync(maxConnection)(request)
Akka Stream の mapAsync(parallelism)(asyncFunction) を使えばparallelism 以上に asyncFunction が呼ばれることはない
Akka HTTPでもコネクションプールをもつクライアントはmapAsyncで制御している
Spray client の例
HTTP レイヤーと TCP レイヤーとの接合
TCP コネクションに HTTP のセマンティクスをのせる
val transportFlow: Flow[ByteString, ByteString, Future[Tcp.OutgoingConnection]] = Tcp().outgoingConnection(new InetSocketAddress(host, port)) //TCP コネクションのステージval tlsStage: BidiFlow[SslTlsOutbound, ByteString, ByteString, SessionBytes, NotUsed] = TLSPlacebo() //TLS のプラシーボ効果ステージ。 ByteString を TLS の型にラップしているだけで何もしていない。 HTTP スキーム用。val outgoingTlsConnectionLayer: Flow[SslTlsOutbound, SessionBytes, Future[Http.OutgoingConnection]] = tlsStage.joinMat(transportFlow) { (_, tcpConnFuture: Future[Tcp.OutgoingConnection]) => tcpConnFuture map { tcpConn => Http.OutgoingConnection(tcpConn.localAddress, tcpConn.remoteAddress) } //TCP コネクションステージに TLS ステージを接続する。 Materialized Value を Tcp.OutgoingConnection からHttp.OutgoingConnection に変換する。}
val clientLayer: BidiFlow[HttpRequest, SslTlsOutbound, SslTlsInbound, HttpResponse, NotUsed] = Http().clientLayer(Host(host, port)) //HttpRequest -> SslTlsOutbound 、 SslTlsInbound -> HttpResponse への変換ステージval outgoingConnection: Flow[HttpRequest, HttpResponse, Future[Http.OutgoingConnection]] = clientLayer.joinMat(outgoingTlsConnectionLayer)(Keep.right) //TCP/TLS ステージと HTTP ステージを接続
クライアント編
val transportFlow: Flow[ByteString, ByteString, Future[Tcp.OutgoingConnection]] = Tcp().outgoingConnection(new InetSocketAddress(host, port)) //TCP コネクションのステージval tlsStage: BidiFlow[SslTlsOutbound, ByteString, ByteString, SessionBytes, NotUsed] = TLSPlacebo() //TLS のプラシーボ効果ステージ。 ByteString を TLS の型にラップしているだけで何もしていない。 HTTP スキーム用。/** * +------------------------------------------------+ * | outgoingTlsConnectionLayer | * | | * | +----------+ +---------------+ | * SslTlsOutbound ~~> | ~ByteString~> | | | * | | tlsStage | | transportFlow | | * SessionBytes <~~ | <~ByteString~ | | | * | +----------+ +---------------+ | * +------------------------------------------------+ */
val outgoingTlsConnectionLayer: Flow[SslTlsOutbound, SessionBytes, Future[Http.OutgoingConnection]] = tlsStage.joinMat(transportFlow) { (_, tcpConnFuture: Future[Tcp.OutgoingConnection]) => tcpConnFuture map { tcpConn => Http.OutgoingConnection(tcpConn.localAddress, tcpConn.remoteAddress) } //TCP コネクションステージに TLS ステージを接続する。 Materialized Value を Tcp.OutgoingConnection からHttp.OutgoingConnection に変換する。}
TCP コネクションに HTTP のセマンティクスをのせる① TCP コネクションに TLS の解釈を接続する
val clientLayer: BidiFlow[HttpRequest, SslTlsOutbound, SslTlsInbound, HttpResponse, NotUsed] = Http().clientLayer(Host(host, port)) //HttpRequest -> SslTlsOutbound 、 SslTlsInbound -> HttpResponse への変換ステージ/** * +-------------------------------------------------------------------+ * | outgoingConnection | * | | * | +-------------+ +----------------------------+ | * HttpRequest ~~> | ~SslTlsOutbound~> | | | * | | clientLayer | | outgoingTlsConnectionLayer | | * HttpResponse <~~ | <~SslTlsInbound~ | | | * | +-------------+ +----------------------------+ | * +-------------------------------------------------------------------+ */
val outgoingConnection: Flow[HttpRequest, HttpResponse, Future[Http.OutgoingConnection]] = clientLayer.joinMat(outgoingTlsConnectionLayer)(Keep.right) //TCP/TLS ステージと HTTP ステージを接続
TCP コネクションに HTTP のセマンティクスをのせる② HTTP のセマンティクスを解釈する
val outgoingConnection: Flow[HttpRequest, HttpResponse, Future[Http.OutgoingConnection]] = clientLayer.joinMat(outgoingTlsConnectionLayer)(Keep.right) //TCP/TLS ステージと HTTP ステージを接続Source.single(httpRequest).via(outgoingConnection).toMat(Sink.head)(Keep.right).run()
Source.single(httpRequest).via(Http().outgoingConnection(host, port)).toMat(Sink.head)(Keep.right).run()
これら一連の処理はHttp().outgoingConnection(host, port)と等価です
TCP コネクションに HTTP のセマンティクスをのせる動かしてみる
TCP コネクションに HTTP のセマンティクスをのせるval connections: Source[Tcp.IncomingConnection, Future[Tcp.ServerBinding]] = Tcp().bind(host, port) //TCP の bind
val tlsStage: BidiFlow[SslTlsOutbound, ByteString, ByteString, SessionBytes, NotUsed] = TLSPlacebo()//TLS のプラシーボ効果ステージ。 ByteString を TLS の型にラップしているだけで何もしていない。 HTTP スキーム用。val serverLayer: BidiFlow[HttpResponse, SslTlsOutbound, SslTlsInbound, HttpRequest, NotUsed] = Http().serverLayer()//HttpResponse -> SslTlsOutbound 、 SslTlsInbound -> HttpRequest への変換ステージval serverSource: Source[IncomingConnection, Future[ServerBinding]] = connections.map { case Tcp.IncomingConnection(localAddress, remoteAddress, flow) => /** * +----------------------------------------------------------------------------+ * | IncomingConnection.flow | * | | * | +-------------+ +----------+ +----------+ | * HttpResponse ~~> | ~SslTlsOutbound~> | | ~ByteString~> | | | * | | serverLayer | | tlsStage | | identity | | * HttpRequest <~~ | <~SslTlsInbound~ | | <~ByteString~ | | | * | +-------------+ +----------+ +----------+ | * +----------------------------------------------------------------------------+ */ // TCP/TLS ステージと HTTP ステージを接続して、 Tcp.IncomingConnection から Http.IncomingConnection に変換 Http.IncomingConnection(localAddress, remoteAddress, serverLayer atop tlsStage join Flow[ByteString].map(identity) )}.mapMaterializedValue { bindFuture: Future[Tcp.ServerBinding] => bindFuture.map(tcpBinding => Http.ServerBinding(tcpBinding.localAddress)(unbindAction = () => tcpBinding.unbind())) // Tcp.ServerBinding から Http.ServerBinding に変換}
サーバー編
val bindingFuture: Future[ServerBinding] = serverSource.to(Sink.foreach { connection => val connFlow: Flow[HttpResponse, HttpRequest, NotUsed] = connection.flow
val requestHandler: Flow[HttpRequest, HttpResponse, NotUsed] = Flow[HttpRequest].map { case HttpRequest(GET, Uri.Path("/"), _, _, _) => HttpResponse(entity = HttpEntity(ContentTypes.`text/html(UTF-8)`, "<html><body>Hello world!</body></html>")) } connFlow.joinMat(requestHandler)(Keep.right).run()}).run()
val serverSource: Source[Http.IncomingConnection, Future[Http.ServerBinding]] = Http().bind(host, port)
これら一連の処理はHttp().bind(host, port)と等価です
TCP コネクションに HTTP のセマンティクスをのせる動かしてみる
TCP レイヤーの内部構造
TCP レイヤーの内部構造クライアント編
akka.io.Tcp の復習case object Ack extends Tcp.Event
class PullModeClient(remote: InetSocketAddress) extends Actor with ActorLogging { import context.system
override def preStart: Unit = { val manager: ActorRef = IO(Tcp) //IO エクステンションから TCP マネージャーを取得 manager ! Tcp.Connect(remote, pullMode = true) //TCP マネージャーに接続要求を送る } def receive: Receive = connecting
def connecting: Receive = { case Tcp.Connected(remote, local) => { //TCP 接続の完了 val connection = sender() //sender がコネクション Worker アクター context.become(connected(connection)) connection ! Tcp.Register(self) // 自分自身をコネクション Worker アクターに登録 connection ! Tcp.ResumeReading // サーバーからの受信を再開する } }
def connected(connection: ActorRef): Receive = { case Tcp.Received(data) => log.info("received {}", data) // サーバーからデータを受信 case data: ByteString => connection ! Tcp.Write(data, Ack) // ユーザーからデータの送信要求がきたら書き込む。成功したら Ack を返してもらう。 Ack が返るまで受け付けてはいけません!! case Ack => connection ! Tcp.ResumeReading // データの送信が完了したら受信を再開する }}
Pull Mode を使う読み込み側の背圧制御のために
akka.io.Tcp の復習case object Ack extends Tcp.Event
class PullModeClient(remote: InetSocketAddress) extends Actor with ActorLogging { import context.system
override def preStart: Unit = { val manager: ActorRef = IO(Tcp) //IO エクステンションから TCP マネージャーを取得 manager ! Tcp.Connect(remote, pullMode = true) //TCP マネージャーに接続要求を送る } def receive: Receive = connecting
def connecting: Receive = { case Tcp.Connected(remote, local) => { //TCP 接続の完了 val connection = sender() //sender がコネクション Worker アクター context.become(connected(connection)) connection ! Tcp.Register(self) // 自分自身をコネクション Worker アクターに登録 connection ! Tcp.ResumeReading // サーバーからの受信を再開する } }
def connected(connection: ActorRef): Receive = { case Tcp.Received(data) => log.info("received {}", data) // サーバーからデータを受信 case data: ByteString => connection ! Tcp.Write(data, Ack) // ユーザーからデータの送信要求がきたら書き込む。成功したら Ack を返してもらう。 Ack が返るまで受け付けてはいけません!! case Ack => connection ! Tcp.ResumeReading // データの送信が完了したら受信を再開する }}
Pull Mode を使う読み込み側の背圧制御のために
_人人人人人人人人人人人人人人人人_> 書き込み側にも背圧制御欲しい < ̄ Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y  ̄そこで Akka Stream だ
Akka Stream のステージfinal case class Map[In, Out](f: In ⇒ Out) extends GraphStage[FlowShape[In, Out]] { val in = Inlet[In]("Map.in") // 入力ポート val out = Outlet[Out]("Map.out") // 出力ポート override def shape: FlowShape[In, Out] = FlowShape.of(in, out)
override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = new GraphStageLogic(shape) { setHandler(in, new InHandler { // 入力ポート `in` のイベントハンドラ override def onPush(): Unit = { //PUSH された時のフック val v = f(grab(in)) //push された要素を取得して f を適用 push(out, v) //`out` に push } }) setHandler(out, new OutHandler { // 出力ポート `out` のイベントハンドラ override def onPull(): Unit = { //PULL された時のフック pull(in) //`in` から pull } }) }}// 使い方def mapFlow[In, Out](f: In => Out): Flow[In, Out, NotUsed] = Flow.fromGraph(Map(f))
ストリームのステージの実装例として Map を見てみよう
Source.map() や Flow.map() はこのように実装されている
class TcpStreamLogic(val shape: FlowShape[ByteString, ByteString], val role: TcpRole) extends GraphStageLogic(shape) { implicit def self: ActorRef = stageActor.ref
private def bytesIn = shape.in // 読み込み用のポート private def bytesOut = shape.out // 書き込み用のポート private var connection: ActorRef = _ //TCP コネクションの worker アクター override def preStart(): Unit = { role match { case ob @ Outbound(manager, cmd: akka.io.Tcp.Connect, _, _) ⇒ getStageActor(connecting(ob)) // このステージのアクターの receive を connecting で初期化 manager ! cmd // manager は IO(Tcp) と同じ。 Tcp.Connect コマンドを送る。 } }
private def connecting(ob: Outbound)(evt: (ActorRef, Any)): Unit = { val (sender, msg) = evt msg match { case c: akka.io.Tcp.Connected => role.asInstanceOf[Outbound].localAddressPromise.success(c.localAddress) //Materialized Value の Promise に localAddress を書き込む connection = sender // sender が TCP コネクションの worker アクター setHandler(bytesOut, readHandler) //bytesOut のイベントハンドラを readHandler に設定 stageActor.become(connected) // このステージのアクターの receive を connected にする( context.become(connected) と同じ) connection ! akka.io.Tcp.Register(self) if (isAvailable(bytesOut)) connection ! ResumeReading pull(bytesIn) //bytesIn から pull して要素を要求 } }
private def connected(evt: (ActorRef, Any)): Unit = { val (sender, msg) = evt msg match { case akka.io.Tcp.Received(data) => push(bytesOut, data) // データを受信したら bytesOut に push する case WriteAck => pull(bytesIn) // 書き込み成功の Ack が返ってきたら bytesIn に pull して要素を要求 } }
setHandler(bytesOut, new OutHandler { override def onPull(): Unit = () })
val readHandler = new OutHandler { override def onPull(): Unit = { //pull されたときに呼ばれるイベントハンドラー connection ! ResumeReading } }
setHandler(bytesIn, new InHandler { override def onPush(): Unit = { //push されたときに呼ばれるイベントハンドラー val elem = grab(bytesIn) //bytesIn に push された要素を取得 connection ! Write(elem.asInstanceOf[ByteString], WriteAck) } })}
Tcp().outgoingConnection ステージの実装
https://github.com/akka/akka/blob/v2.4.2/akka-stream/src/main/scala/akka/stream/impl/io/TcpStages.scala#L171-L287
長いけど akka.io.Tcp の復習でみたコードと比較すると理解しやすい
class TcpStreamLogic(val shape: FlowShape[ByteString, ByteString], val role: TcpRole) extends GraphStageLogic(shape) { implicit def self: ActorRef = stageActor.ref
private def bytesIn = shape.in // 読み込み用のポート private def bytesOut = shape.out // 書き込み用のポート private var connection: ActorRef = _ //TCP コネクションの worker アクター override def preStart(): Unit = { role match { case ob @ Outbound(manager, cmd: akka.io.Tcp.Connect, _, _) ⇒ getStageActor(connecting(ob)) // このステージのアクターの receive を connecting で初期化 manager ! cmd // manager は IO(Tcp) と同じ。 Tcp.Connect コマンドを送る。 } }}
class PullModeClient(remote: InetSocketAddress) extends Actor with ActorLogging { import context.system
override def preStart: Unit = { val manager: ActorRef = IO(Tcp) //IO エクステンションから TCP マネージャーを取得 manager ! Tcp.Connect(remote, pullMode = true) //TCP マネージャーに接続要求を送る } def receive: Receive = connecting}
akka.io.Tcp の対応する部分
Tcp().outgoingConnection ステージの実装
private def connecting(ob: Outbound)(evt: (ActorRef, Any)): Unit = { val (sender, msg) = evt msg match { case c: akka.io.Tcp.Connected => role.asInstanceOf[Outbound].localAddressPromise.success(c.localAddress) //Materialized Value の Promise に localAddress を書き込む connection = sender // sender が TCP コネクションの worker アクター setHandler(bytesOut, readHandler) //bytesOut のイベントハンドラを readHandler に設定 stageActor.become(connected) // このステージのアクターの receive を connected にする( context.become(connected) と同じ) connection ! akka.io.Tcp.Register(self) if (isAvailable(bytesOut)) connection ! ResumeReading pull(bytesIn) //bytesIn から pull して要素を要求 } }}
def connecting: Receive = { case Tcp.Connected(remote, local) => { //TCP 接続の完了 val connection = sender() //sender がコネクション Worker アクター context.become(connected(connection)) connection ! Tcp.Register(self) // 自分自身をコネクション Worker アクターに登録 connection ! Tcp.ResumeReading // サーバーからの受信を再開する } }
akka.io.Tcp の対応する部分
Tcp().outgoingConnection ステージの実装
private def connected(evt: (ActorRef, Any)): Unit = { val (sender, msg) = evt msg match { case akka.io.Tcp.Received(data) => push(bytesOut, data) //R② データを受信したら bytesOut に push する case WriteAck => pull(bytesIn) //W② 書き込み成功の Ack が返ってきたら bytesIn に pull して要素を要求 }}
val readHandler = new OutHandler { override def onPull(): Unit = { //bytesOut で pull されたときに呼ばれるイベントハンドラー connection ! ResumeReading //R① bytesOut に pull 要求がきたら読み込みを再開する }}
setHandler(bytesIn, new InHandler { override def onPush(): Unit = { //bytesIn で push されたときに呼ばれるイベントハンドラー val elem = grab(bytesIn) //bytesIn に push された要素を取得 connection ! Write(elem.asInstanceOf[ByteString], WriteAck) //W① push されたデータを書き込む }})
def connected(connection: ActorRef): Receive = { case Tcp.Received(data) => log.info("received {}", data) // サーバーからデータを受信 case data: ByteString => connection ! Tcp.Write(data, Ack) // ユーザーからデータの送信要求がきたら書き込む。成功したら Ack を返してもらう。 Ack が返るまで受け付けてはいけません!! case Ack => connection ! Tcp.ResumeReading // データの送信が完了したら受信を再開する }
akka.io.Tcp の対応する部分
Tcp().outgoingConnection ステージの実装
private def connected(evt: (ActorRef, Any)): Unit = { val (sender, msg) = evt msg match { case akka.io.Tcp.Received(data) => push(bytesOut, data) //R② データを受信したら bytesOut に push する case WriteAck => pull(bytesIn) //W② 書き込み成功の Ack が返ってきたら bytesIn に pull して要素を要求 }}
val readHandler = new OutHandler { override def onPull(): Unit = { //bytesOut で pull されたときに呼ばれるイベントハンドラー connection ! ResumeReading //R① bytesOut に pull 要求がきたら読み込みを再開する }}
setHandler(bytesIn, new InHandler { override def onPush(): Unit = { //bytesIn で push されたときに呼ばれるイベントハンドラー val elem = grab(bytesIn) //bytesIn に push された要素を取得 connection ! Write(elem.asInstanceOf[ByteString], WriteAck) //W① push されたデータを書き込む }})
def connected(connection: ActorRef): Receive = { case Tcp.Received(data) => log.info("received {}", data) // サーバーからデータを受信 case data: ByteString => connection ! Tcp.Write(data, Ack) // ユーザーからデータの送信要求がきたら書き込む。成功したら Ack を返してもらう。 Ack が返るまで受け付けてはいけません!! case Ack => connection ! Tcp.ResumeReading // データの送信が完了したら受信を再開する }
akka.io.Tcp の対応する部分
欲しいと言うまでデータは来ないようになった
Tcp().outgoingConnection ステージの実装
TCP レイヤーの内部構造サーバー編
class TcpStreamLogic(val shape: FlowShape[ByteString, ByteString], val role: TcpRole) extends GraphStageLogic(shape) { implicit def self: ActorRef = stageActor.ref
private def bytesIn = shape.in // 読み込み用のポート private def bytesOut = shape.out // 書き込み用のポート private var connection: ActorRef = _ //TCP コネクションの worker アクター setHandler(bytesOut, new OutHandler { override def onPull(): Unit = () })
override def preStart(): Unit = { role match { case Inbound(conn, _) => setHandler(bytesOut, readHandler) //bytesOut のイベントハンドラを readHandler に設定 connection = conn getStageActor(connected) // このステージのアクターの receive を connected にする( context.become(connected) と同じ) connection ! Register(self) pull(bytesIn) //bytesIn から pull して要素を要求 } }
private def connected(evt: (ActorRef, Any)): Unit = { val (sender, msg) = evt msg match { case akka.io.Tcp.Received(data) => push(bytesOut, data) //R② データを受信したら bytesOut に push する case WriteAck => pull(bytesIn) //W② 書き込み成功の Ack が返ってきたら bytesIn に pull して要素を要求 } }
val readHandler = new OutHandler { override def onPull(): Unit = { //bytesOut で pull されたときに呼ばれるイベントハンドラー connection ! ResumeReading //R① bytesOut に pull 要求がきたら読み込みを再開する } }
setHandler(bytesIn, new InHandler { override def onPush(): Unit = { //bytesIn で push されたときに呼ばれるイベントハンドラー val elem = grab(bytesIn) //bytesIn に push された要素を取得 connection ! Write(elem.asInstanceOf[ByteString], WriteAck) //W① push されたデータを書き込む } })}
Tcp.IncomingConnection#flow ステージの実装開始の仕方が違うだけで実装は同じ
https://github.com/akka/akka/blob/v2.4.2/akka-stream/src/main/scala/akka/stream/impl/io/TcpStages.scala#L171-L287
Tcp().bind ステージ
実はデータの受信のみでなく、コネクションの受付けまで背圧制御されています
https://github.com/akka/akka/blob/v2.4.2/akka-stream/src/main/scala/akka/stream/impl/io/TcpStages.scala#L30-L141
まとめ
Akka HTTP では• 自分が処理できなければ読み込まない• 相手が処理できなければ書き込まないシステム同士はみんなともだち
Thank you!