akka http

34
Akka HTTP 安安安安 @TanUkkii007 2016/3/4

Upload: tanukkii

Post on 21-Apr-2017

1.724 views

Category:

Engineering


3 download

TRANSCRIPT

Page 1: Akka HTTP

Akka HTTP安田裕介

@TanUkkii0072016/3/4

Page 2: Akka HTTP

Akka Stream & HTTPリリースおめでとう!

ノンブロッキングで背圧制御に満ちたエコシステム形成の幕開けになることを期待します

Page 3: Akka HTTP

Akka HTTP とは

• akka IO と Akka Stream を使って、 NIO かつ背圧制御に基づいた HTTP モジュール• 低レベル API の akka-http-core と高レベル API の

akka-http-experimental がある• Akka Stream がシステム内の調和を保つとしたら、 Akka HTTP はシステム間の調和を実現する

Page 4: Akka HTTP

マイクロサービスの難しさ

“every single one of your peer teams suddenly becomes a potential DOS attacker”

周りの全ての同僚チームが、突如 DOS アタッカーになりうるようになった和訳原文

Stevey の “ Google プラットフォームぶっちゃけ話”にでてくるAmazon のマイクロサービス化による AWS 誕生のくだり

システム間のインターフェースとしてもっとも使われる HTTP に背圧制御をもたらす必要がある

Page 5: Akka HTTP

アジェンダ

• Akka Stream の復習• Akka HTTP API

• HTTP レイヤーと TCP レイヤーとの接合• TCP レイヤーの内部構造

Page 6: Akka HTTP

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

Page 7: Akka HTTP

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

Page 8: Akka HTTP

Akka HTTP

Page 9: 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)

Page 10: Akka HTTP

サーバーサイド 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()

Page 11: Akka HTTP

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 などの異なるプロトコルを統一的に扱うことを可能にしている

Page 12: Akka HTTP

• 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 の例

Page 13: Akka HTTP

HTTP レイヤーと TCP レイヤーとの接合

Page 14: Akka HTTP

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 ステージを接続

クライアント編

Page 15: Akka 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 の解釈を接続する

Page 16: Akka HTTP

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 のセマンティクスを解釈する

Page 17: Akka 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 のセマンティクスをのせる動かしてみる

Page 18: Akka 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 に変換}

サーバー編

Page 19: Akka HTTP

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 のセマンティクスをのせる動かしてみる

Page 20: Akka HTTP

TCP レイヤーの内部構造

Page 21: Akka HTTP

TCP レイヤーの内部構造クライアント編

Page 22: Akka HTTP

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 を使う読み込み側の背圧制御のために

Page 23: Akka HTTP

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 だ

Page 24: Akka HTTP

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() はこのように実装されている

Page 25: Akka HTTP

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 の復習でみたコードと比較すると理解しやすい

Page 26: Akka HTTP

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 ステージの実装

Page 27: Akka HTTP

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 ステージの実装

Page 28: Akka HTTP

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 ステージの実装

Page 29: Akka HTTP

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 ステージの実装

Page 30: Akka HTTP

TCP レイヤーの内部構造サーバー編

Page 31: Akka HTTP

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

Page 32: Akka HTTP

Tcp().bind ステージ

実はデータの受信のみでなく、コネクションの受付けまで背圧制御されています

https://github.com/akka/akka/blob/v2.4.2/akka-stream/src/main/scala/akka/stream/impl/io/TcpStages.scala#L30-L141

Page 33: Akka HTTP

まとめ

Akka HTTP では• 自分が処理できなければ読み込まない• 相手が処理できなければ書き込まないシステム同士はみんなともだち

Page 34: Akka HTTP

Thank you!