Akkaストリームを始めるには?[閉まっている]


222

Akka Streamsライブラリには、すでに非常に豊富なドキュメントが付属しています。しかし、私にとっての主な問題は、提供する資料が多すぎることです。私は、学ばなければならない概念の数に圧倒されています。ここに示されている多くの例は非常に重く感じられ、実際の使用例に簡単に変換できないため、非常に難解です。すべてのビルディングブロックを一緒に構築する方法や、特定の問題の解決にどのように役立つかを説明せずに、詳細が多すぎると思います。

ソース、シンク、フロー、グラフステージ、部分グラフ、具体化、グラフDSLなどがあり、どこから始めればよいかわかりません。クイックスタートガイドが出発点であることを意味するが、私はそれを理解していません。それはそれらを説明せずに上記の概念を投入するだけです。さらに、コード例を実行することはできません-欠けている部分があり、テキストをたどることが多かれ少なかれ不可能になっています。

ソース、シンク、フロー、グラフステージ、部分グラフ、具体化などの概念を説明できますか?単純な言葉で見逃したいくつかの詳細や、すべての詳細を説明しない簡単な例で説明できますか?始まり)?


2
参考までに、これはメタ
DavidG

10
これを閉じるために最初に投票した人(メタスレッドに続いて)として、最初にここであなたの答えは素晴らしいと言います。これは本当に詳細であり、確かに非常に役立つリソースです。ただし、残念ながら、あなたが尋ねた質問はスタックオーバーフローにはあまりに広範です。どういうわけかあなたの答えが別の言葉で書かれた質問に投稿できるなら、それは素晴らしいですが、私はそうはならないと思います。これをブログ投稿として再送信するか、自分や他の人が将来の回答の参照リソースとして使用できる類似のものとして再送信することを強くお勧めします。
James Donnelly

2
この質問をブログの投稿として書いても効果がないと思います。はい、これは幅広い質問です-そしてそれは本当に良い質問です。スコープを狭めても改善されません。提供される答えは素晴らしいです。Quoraが大きな質問に対してSOからビジネスを取り除いて喜んでくれると確信しています。
マイクスリン2016

11
@MikeSlinnは適切な質問についてSOの人々と話し合うことを試みません。彼らは盲目的にルールに従います。質問が削除されない限り、私は満足しています。別のプラットフォームに移動する気はありません。
キリツク2016

2
@sschaefどのように教訓的。ええ、もちろん、ルールは何の価値もありません。あなたの偉大な自己は、はるかによく知っており、ルールを適用しようとするすべての人がただ盲目的に誇大宣伝に従っています。/ rant。より真剣に言えば、これはドキュメントのベータ版に追加するのに最適です。あなたはまだそれを適用してそこに置くことができますが、少なくともそれがメインサイトにあまり適合していないことを見るはずです。
フェリックス・ガニオン-グルニエ

回答:


506

この回答はakka-streamバージョンに基づいてい2.4.2ます。他のバージョンでは、APIが若干異なる場合があります。依存関係はsbtで消費できます:

libraryDependencies += "com.typesafe.akka" %% "akka-stream" % "2.4.2"

では、始めましょう。Akka StreamsのAPIは、3つの主要なタイプで構成されています。Reactive Streamsとは対照的に、これらのタイプははるかに強力であり、したがってより複雑です。すべてのコード例について、次の定義がすでに存在していると想定されています。

import scala.concurrent._
import akka._
import akka.actor._
import akka.stream._
import akka.stream.scaladsl._
import akka.util._

implicit val system = ActorSystem("TestSystem")
implicit val materializer = ActorMaterializer()
import system.dispatcher

import文は、型宣言のために必要とされます。systemAkkaのアクターシステムをmaterializer表し、ストリームの評価コンテキストを表します。この例では、を使用していますActorMaterializer。これは、ストリームがアクターの上で評価されることを意味します。両方の値はとしてマークされますimplicit。これにより、Scalaコンパイラーは、これらの2つの依存関係が必要なときにいつでも自動的に挿入できるようになります。またsystem.dispatcher、の実行コンテキストであるもインポートしFuturesます。

新しいAPI

Akkaストリームには、次の主要なプロパティがあります。

  • これらは、Reactive Streams仕様を実装しています。その3つの主要な目標は、バックプレッシャー、非同期および非ブロッキング境界、および異なる実装間の相互運用性は、Akkaストリームにも完全に適用されます。
  • これらは、ストリームと呼ばれる評価エンジンの抽象化を提供しますMaterializer
  • プログラムは、3つの主要なタイプとして表現され、再利用可能なビルディング・ブロックとして処方されSourceSinkそしてFlow。ビルディングブロックは、評価がに基づいておりMaterializer、明示的にトリガーする必要があるグラフを形成します。

以下では、3つの主要なタイプの使用方法のより深い紹介が与えられます。

ソース

A Sourceはデータ作成者であり、ストリームへの入力ソースとして機能します。それぞれSourceに単一の出力チャネルがあり、入力チャネルはありません。すべてのデータは、出力チャネルを介してに接続されているものに流れSourceます。

ソース

boldradius.comから取得した画像。

A Sourceは複数の方法で作成できます。

scala> val s = Source.empty
s: akka.stream.scaladsl.Source[Nothing,akka.NotUsed] = ...

scala> val s = Source.single("single element")
s: akka.stream.scaladsl.Source[String,akka.NotUsed] = ...

scala> val s = Source(1 to 3)
s: akka.stream.scaladsl.Source[Int,akka.NotUsed] = ...

scala> val s = Source(Future("single value from a Future"))
s: akka.stream.scaladsl.Source[String,akka.NotUsed] = ...

scala> s runForeach println
res0: scala.concurrent.Future[akka.Done] = ...
single value from a Future

上記の場合Source、有限のデータをに与えました。つまり、それらは最終的に終了します。Reactive Streamsはデフォルトではレイジーで非同期であることを忘れないでください。つまり、ストリームの評価を明示的に要求する必要があります。Akkaストリームでは、これはrun*メソッドを介して行うことができます。これrunForeachは、よく知られているforeach関数と同じです- run追加により、ストリームの評価を要求することを明示的にします。有限データは退屈なので、無限データを続けます。

scala> val s = Source.repeat(5)
s: akka.stream.scaladsl.Source[Int,akka.NotUsed] = ...

scala> s take 3 runForeach println
res1: scala.concurrent.Future[akka.Done] = ...
5
5
5

このtake方法を使用すると、無期限に評価することを防ぐ人工的な停止点を作成できます。アクターのサポートが組み込まれているため、アクターに送信されるメッセージをストリームに簡単にフィードすることもできます。

def run(actor: ActorRef) = {
  Future { Thread.sleep(300); actor ! 1 }
  Future { Thread.sleep(200); actor ! 2 }
  Future { Thread.sleep(100); actor ! 3 }
}
val s = Source
  .actorRef[Int](bufferSize = 0, OverflowStrategy.fail)
  .mapMaterializedValue(run)

scala> s runForeach println
res1: scala.concurrent.Future[akka.Done] = ...
3
2
1

Futures結果は異なるスレッドで非同期に実行されていることがわかります。上記の例では、着信要素のバッファーは必要ないためOverflowStrategy.fail、バッファーオーバーフローでストリームが失敗するように構成できます。特にこのアクターインターフェイスを介して、任意のデータソースを通じてストリームをフィードできます。データが同じスレッドで作成されたか、別のスレッドで作成されたか、別のプロセスで作成されたか、またはインターネット経由でリモートシステムから取得されたかは関係ありません。

シンク

A Sinkは基本的にの反対ですSource。これはストリームのエンドポイントであるため、データを消費します。A Sinkには単一の入力チャネルがあり、出力チャネルはありません。Sinksストリームを評価せずに再利用可能な方法でデータコレクターの動作を指定する場合に特に必要です。既知のrun*方法ではこれらのプロパティを使用できないため、Sink代わりに使用することをお勧めします。

シンク

boldradius.comから取得した画像。

動作中の短い例Sink

scala> val source = Source(1 to 3)
source: akka.stream.scaladsl.Source[Int,akka.NotUsed] = ...

scala> val sink = Sink.foreach[Int](elem => println(s"sink received: $elem"))
sink: akka.stream.scaladsl.Sink[Int,scala.concurrent.Future[akka.Done]] = ...

scala> val flow = source to sink
flow: akka.stream.scaladsl.RunnableGraph[akka.NotUsed] = ...

scala> flow.run()
res3: akka.NotUsed = NotUsed
sink received: 1
sink received: 2
sink received: 3

をに接続するSourceにはSinktoメソッドを使用します。これは、いわゆるを返します。これはRunnableFlow、後ほど特別な形式のFlowa- run()メソッドを呼び出すだけで実行できるストリームです。

実行可能なフロー

boldradius.comから取得した画像。

もちろん、シンクに到着するすべての値をアクターに転送することもできます。

val actor = system.actorOf(Props(new Actor {
  override def receive = {
    case msg => println(s"actor received: $msg")
  }
}))

scala> val sink = Sink.actorRef[Int](actor, onCompleteMessage = "stream completed")
sink: akka.stream.scaladsl.Sink[Int,akka.NotUsed] = ...

scala> val runnable = Source(1 to 3) to sink
runnable: akka.stream.scaladsl.RunnableGraph[akka.NotUsed] = ...

scala> runnable.run()
res3: akka.NotUsed = NotUsed
actor received: 1
actor received: 2
actor received: 3
actor received: stream completed

フロー

データソースとシンクは、Akkaストリームと既存のシステム間の接続が必要な場合に最適ですが、実際には何もできません。フローは、Akka Streamsベースの抽象化で最後に欠けている部分です。これらは異なるストリーム間のコネクタとして機能し、その要素を変換するために使用できます。

フロー

boldradius.comから取得した画像。

a Flowがa に接続されている場合、結果はSource新しいSourceです。同様に、にFlow接続Sinkすると新しいが作成されますSink。そして、Flowa Sourceとaの両方に接続すると、Sink結果はになりRunnableFlowます。したがって、これらは入力チャネルと出力チャネルの間に位置しますが、a Sourceまたはaに接続されていない限り、それ自体はフレーバーの1つに対応しませんSink

フルストリーム

boldradius.comから取得した画像。

の理解を深めるためにFlows、いくつかの例を見てみましょう。

scala> val source = Source(1 to 3)
source: akka.stream.scaladsl.Source[Int,akka.NotUsed] = ...

scala> val sink = Sink.foreach[Int](println)
sink: akka.stream.scaladsl.Sink[Int,scala.concurrent.Future[akka.Done]] = ...

scala> val invert = Flow[Int].map(elem => elem * -1)
invert: akka.stream.scaladsl.Flow[Int,Int,akka.NotUsed] = ...

scala> val doubler = Flow[Int].map(elem => elem * 2)
doubler: akka.stream.scaladsl.Flow[Int,Int,akka.NotUsed] = ...

scala> val runnable = source via invert via doubler to sink
runnable: akka.stream.scaladsl.RunnableGraph[akka.NotUsed] = ...

scala> runnable.run()
res10: akka.NotUsed = NotUsed
-2
-4
-6

経由viaする方法我々は、接続することができますSourceFlow。コンパイラーが入力タイプを推測できないため、入力タイプを指定する必要があります。我々はすでに、この単純な例で見ることができるように、流れinvertdouble任意のデータ生産者と消費者からは完全に独立しています。データを変換し、出力チャネルに転送するだけです。つまり、複数のストリーム間でフローを再利用できます。

scala> val s1 = Source(1 to 3) via invert to sink
s1: akka.stream.scaladsl.RunnableGraph[akka.NotUsed] = ...

scala> val s2 = Source(-3 to -1) via invert to sink
s2: akka.stream.scaladsl.RunnableGraph[akka.NotUsed] = ...

scala> s1.run()
res10: akka.NotUsed = NotUsed
-1
-2
-3

scala> s2.run()
res11: akka.NotUsed = NotUsed
3
2
1

s1s2完全に新しいストリームを表します-それらはビルディングブロックを通じてデータを共有しません。

無制限のデータストリーム

次に進む前に、まず、Reactive Streamsの主要な側面のいくつかを再検討する必要があります。無制限の数の要素が任意の時点に到達し、ストリームをさまざまな状態にすることができます。通常の状態である実行可能なストリームの他に、エラーまたはそれ以上データが到着しないことを示す信号によってストリームが停止する場合があります。次のように、タイムラインでイベントをマークすることにより、ストリームをグラフィカルにモデル化できます。

ストリームが時間順に並べられた進行中のイベントのシーケンスであることを示します

あなたが見逃していたリアクティブプログラミングの紹介から撮影した画像。

前のセクションの例ではすでに実行可能なフローを見てきました。RunnableGraphストリームが実際に実体化できるときはいつでもa を取得SinkSourceます。つまり、a はに接続されます。これまでのところ、私たちは常に値Unitにマテリアライズしました。これはタイプで見ることができます:

val source: Source[Int, NotUsed] = Source(1 to 3)
val sink: Sink[Int, Future[Done]] = Sink.foreach[Int](println)
val flow: Flow[Int, Int, NotUsed] = Flow[Int].map(x => x)

SourceSink第二型パラメータとのためのFlowマテリアライズド値表す第三の型パラメータ。この回答を通して、具体化の完全な意味は説明されません。ただし、具体化の詳細については、公式ドキュメントをご覧ください。とりあえず知る必要があるのは、ストリームを実行したときに実体化された値が得られるものであることです。今まで副作用にしか興味がなかったのでUnit、具体化した値として取得しました。これの例外は、シンクの実体化で、これによりが発生しましたFuture。それは私たちに戻ってくれましたFuture、この値は、シンクに接続されているストリームがいつ終了したかを示すことができるためです。これまでのところ、前のコード例は概念を説明するのに適していましたが、有限のストリームまたは非常に単純な無限のストリームしか処理しなかったため、それらも退屈でした。より興味深いものにするために、以下では、完全な非同期の無制限のストリームについて説明します。

ClickStreamの例

例として、クリックイベントをキャプチャするストリームが必要です。これをより困難にするために、お互いに短時間で発生するクリックイベントもグループ化するとします。この方法で、ダブルクリック、トリプルクリック、または10倍クリックを簡単に見つけることができます。さらに、すべてのシングルクリックを除外します。深呼吸して、命令型の方法でその問題を解決する方法を想像してください。最初の試行で正しく機能するソリューションを実装できる人は誰もいないと思います。この問題は事後対応的に解決するのは簡単です。実際、ソリューションは非常にシンプルで簡単に実装できるため、コードの動作を直接説明する図で表すこともできます。

クリックストリームの例のロジック

あなたが見逃していたリアクティブプログラミングの紹介から撮影した画像。

灰色のボックスは、あるストリームが別のストリームに変換される方法を説明する関数です。このthrottle関数を使用するmapと、250ミリ秒以内にクリック数が累積されますfilter。色のオーブはイベントを表し、矢印はそれらが私たちの機能をどのように流れるかを表します。処理ステップの後半では、ストリームを通過する要素が少なくなり、それらをグループ化してフィルターで除外しているためです。この画像のコードは次のようになります。

val multiClickStream = clickStream
    .throttle(250.millis)
    .map(clickEvents => clickEvents.length)
    .filter(numberOfClicks => numberOfClicks >= 2)

ロジック全体をたった4行のコードで表すことができます。Scalaでは、さらに短く書くことができます。

val multiClickStream = clickStream.throttle(250.millis).map(_.length).filter(_ >= 2)

の定義clickStreamは少し複雑ですが、サンプルプログラムがJVMで実行され、クリックイベントのキャプチャが簡単にできないため、これは当てはまります。別の問題は、Akkaがデフォルトでthrottle機能を提供しないことです。代わりに、私たちは自分でそれを書かなければなりませんでした。この関数は(mapor filter関数の場合と同様に)さまざまなユースケースで再利用できるため、ロジックの実装に必要な行数にこれらの行を数えません。ただし、命令型言語では、ロジックを簡単に再利用できず、さまざまな論理ステップが順番に適用されるのではなく、すべて1か所で発生するのが普通です。つまり、スロットルロジックでコードを誤った形にした可能性があります。完全なコード例は、要旨とここではこれ以上議論してはなりません。

SimpleWebServerの例

代わりに議論すべきは別の例です。クリックストリームは、Akka Streamsに実際の例を処理させる良い例ですが、実際の並列実行を表示する力がありません。次の例は、複数の要求を並行して処理できる小さなWebサーバーを表しています。Webサーバーは、着信接続を受け入れ、それらから印刷可能なASCII記号を表すバイトシーケンスを受信できる必要があります。これらのバイトシーケンスまたは文字列は、すべての改行文字で小さな部分に分割する必要があります。その後、サーバーは分割された各行でクライアントに応答します。代わりに、それは行で何か他のことを行い、特別な応答トークンを与えることができますが、この例ではそれを単純に保ちたいので、凝った機能を導入しません。覚えて、サーバーは、同時に複数の要求を処理できる必要があります。つまり、基本的に、他の要求がそれ以上実行されるのをブロックする要求は許可されません。これらすべての要件を解決することは、命令的な方法では難しい場合があります。ただし、Akka Streamsを使用すると、これらのいずれかを解決するために数行を超える必要はありません。まず、サーバー自体の概要を見てみましょう。

サーバ

基本的に、主な構成要素は3つだけです。最初のものは着信接続を受け入れる必要があります。2番目は着信要求を処理する必要があり、3番目は応答を送信する必要があります。これら3つのビルディングブロックをすべて実装することは、クリックストリームを実装するよりも少しだけ複雑です。

def mkServer(address: String, port: Int)(implicit system: ActorSystem, materializer: Materializer): Unit = {
  import system.dispatcher

  val connectionHandler: Sink[Tcp.IncomingConnection, Future[Unit]] =
    Sink.foreach[Tcp.IncomingConnection] { conn =>
      println(s"Incoming connection from: ${conn.remoteAddress}")
      conn.handleWith(serverLogic)
    }

  val incomingCnnections: Source[Tcp.IncomingConnection, Future[Tcp.ServerBinding]] =
    Tcp().bind(address, port)

  val binding: Future[Tcp.ServerBinding] =
    incomingCnnections.to(connectionHandler).run()

  binding onComplete {
    case Success(b) =>
      println(s"Server started, listening on: ${b.localAddress}")
    case Failure(e) =>
      println(s"Server could not be bound to $address:$port: ${e.getMessage}")
  }
}

この関数mkServerは、(サーバーのアドレスとポートの他に)アクターシステムとマテリアライザも暗黙的なパラメータとして受け取ります。サーバーの制御フローはで表されbinding、着信接続のソースを受け取り、着信接続のシンクに転送します。connectionHandlerシンクであるの内部では、フローによってすべての接続を処理しserverLogicます。これについては後で説明します。bindingを返すFutureサーバーが起動したとき、または起動に失敗したときに完了します。これは、ポートが別のプロセスによってすでに使用されている場合に発生することがあります。ただし、応答を処理する構成要素が見えないため、コードはグラフィックを完全には反映していません。これは、接続自体がすでにこのロジックを提供しているためです。これは双方向フローであり、前の例で見たフローのような単方向フローではありません。マテリアライゼーションの場合と同様に、このような複雑なフローについてはここでは説明しません。公式ドキュメントは、より複雑なフローグラフをカバーするための材料をたくさん持っています。今のところ、これはTcp.IncomingConnection、要求を受信する方法と応答を送信する方法を知っている接続を表すことを知っていれば十分 です。まだ足りない部分はserverLogic建築用ブロック。次のようになります。

サーバーロジック

この場合も、ロジックをいくつかの単純なビルディングブロックに分割して、プログラムのフローを形成することができます。最初に、バイトシーケンスを行に分割します。改行文字を見つけるたびに分割する必要があります。その後、生のバイトを扱うのは面倒なので、各行のバイトを文字列に変換する必要があります。全体として、複雑なプロトコルのバイナリストリームを受信する可能性があり、受信した生データの操作が非常に困難になります。読みやすい文字列が得られたら、答えを作成できます。単純化の理由から、私たちの場合、答えは何でもかまいません。最後に、答えをネットワーク経由で送信できる一連のバイトに変換し直す必要があります。ロジック全体のコードは次のようになります。

val serverLogic: Flow[ByteString, ByteString, Unit] = {
  val delimiter = Framing.delimiter(
    ByteString("\n"),
    maximumFrameLength = 256,
    allowTruncation = true)

  val receiver = Flow[ByteString].map { bytes =>
    val message = bytes.utf8String
    println(s"Server received: $message")
    message
  }

  val responder = Flow[String].map { message =>
    val answer = s"Server hereby responds to message: $message\n"
    ByteString(answer)
  }

  Flow[ByteString]
    .via(delimiter)
    .via(receiver)
    .via(responder)
}

私たちserverLogicは、それがを取り、ByteStringを生成しなければならないフローであることをすでに知っていByteStringます。ではdelimiterByteStringを小さな部分に分割できます。この場合、改行文字が発生するたびに発生する必要があります。receiver分割されたバイトシーケンスをすべて受け取り、文字列に変換するフローです。もちろん、これは危険な変換です。印刷可能なASCII文字だけを文字列に変換する必要があるからですが、私たちのニーズには十分です。responder最後のコンポーネントであり、回答を作成し、回答をバイトのシーケンスに変換します。グラフィックとは対照的に、ロジックは簡単なので、この最後のコンポーネントを2つに分割しませんでした。最後に、すべてのフローを接続しますvia関数。この時点で、冒頭で述べたマルチユーザープロパティを処理したかどうかを尋ねる場合があります。そして、すぐには明らかにならないかもしれませんが、確かにそうしました。このグラフィックを見ると、より明確になるはずです。

サーバーとサーバーロジックの組み合わせ

serverLogicコンポーネントは何もなく、小さな流れが含まれている流れではありません。このコンポーネントは、要求である入力を受け取り、応答である出力を生成します。フローは複数回構築することができ、それらはすべて互いに独立して機能するため、このマルチユーザープロパティをネストすることで実現します。すべての要求は独自の要求内で処理されるため、短期実行要求は、以前に開始された長期実行要求をオーバーランする可能性があります。ご参考までに、serverLogic以前に示したその定義はもちろん、その内部定義のほとんどをインライン化することにより、はるかに短く書くことができます。

val serverLogic = Flow[ByteString]
  .via(Framing.delimiter(
      ByteString("\n"),
      maximumFrameLength = 256,
      allowTruncation = true))
  .map(_.utf8String)
  .map(msg => s"Server hereby responds to message: $msg\n")
  .map(ByteString(_))

Webサーバーのテストは次のようになります。

$ # Client
$ echo "Hello World\nHow are you?" | netcat 127.0.0.1 6666
Server hereby responds to message: Hello World
Server hereby responds to message: How are you?

上記のコード例を正しく機能させるには、最初にサーバーを起動する必要があります。これは、startServerスクリプトで示されています。

$ # Server
$ ./startServer 127.0.0.1 6666
[DEBUG] Server started, listening on: /127.0.0.1:6666
[DEBUG] Incoming connection from: /127.0.0.1:37972
[DEBUG] Server received: Hello World
[DEBUG] Server received: How are you?

この単純なTCPサーバーの完全なコード例は、こちらにあります。Akka Streamsでサーバーを作成できるだけでなく、クライアントも作成できます。次のようになります。

val connection = Tcp().outgoingConnection(address, port)
val flow = Flow[ByteString]
  .via(Framing.delimiter(
      ByteString("\n"),
      maximumFrameLength = 256,
      allowTruncation = true))
  .map(_.utf8String)
  .map(println)
  .map(_ ⇒ StdIn.readLine("> "))
  .map(_+"\n")
  .map(ByteString(_))

connection.join(flow).run()

完全なコードのTCPクライアントはここにあります。コードは非常に似ていますが、サーバーとは対照的に、着信接続を管理する必要はもうありません。

複雑なグラフ

前のセクションでは、フローから単純なプログラムを構築する方法を見てきました。ただし、実際には、すでに組み込まれている関数に依存してより複雑なストリームを構築するだけでは十分でないことがよくあります。Akka Streamsを任意のプログラムに使用できるようにしたい場合は、アプリケーションの複雑さに取り組むための独自のカスタム制御構造と結合可能なフローを構築する方法を知る必要があります。良いニュースは、Akka Streamsがユーザーのニーズに合わせて拡張できるように設計されていることです。AkkaStreamsのより複雑な部分を簡単に紹介するために、クライアント/サーバーの例にいくつかの機能を追加します。

まだできないことの1つは、接続を閉じることです。この時点で、これまで見てきたストリームAPIでは任意のポイントでストリームを停止できないため、少し複雑になります。ただし、GraphStage抽象化があり、これを使用して、任意の数の入力ポートまたは出力ポートを持つ任意のグラフ処理ステージを作成できます。最初にサーバー側を見てみましょう。ここでは、次の新しいコンポーネントを紹介しますcloseConnection

val closeConnection = new GraphStage[FlowShape[String, String]] {
  val in = Inlet[String]("closeConnection.in")
  val out = Outlet[String]("closeConnection.out")

  override val shape = FlowShape(in, out)

  override def createLogic(inheritedAttributes: Attributes) = new GraphStageLogic(shape) {
    setHandler(in, new InHandler {
      override def onPush() = grab(in) match {
        case "q"
          push(out, "BYE")
          completeStage()
        case msg ⇒
          push(out, s"Server hereby responds to message: $msg\n")
      }
    })
    setHandler(out, new OutHandler {
      override def onPull() = pull(in)
    })
  }
}

このAPIは、フローAPIよりもはるかに扱いにくいように見えます。当然のことながら、ここでは多くの必須の手順を実行する必要があります。その代わりに、ストリームの動作をより細かく制御できます。上記の例では、1つの入力ポートと1つの出力ポートのみを指定し、shape値をオーバーライドすることでシステムで利用できるようにしています。さらに、いわゆるとを定義しましInHandlerOutHandler。これらは、この順序で要素の送受信を行います。完全なクリックストリームの例をよく見ると、これらのコンポーネントをすでに認識しているはずです。では、InHandler要素を取得し、それが1文字の文字列である場合は'q'、ストリームを閉じます。ストリームが間もなく終了することをクライアントに確認する機会を与えるために、文字列を出力します"BYE"その後、すぐにステージを閉じます。closeConnection成分を介してストリームと組み合わせることができるviaフローに関するセクションで導入された方法。

接続を閉じることができるだけでなく、新しく作成された接続へのウェルカムメッセージを表示できると便利です。これを行うには、もう一度もう少し進める必要があります。

def serverLogic
    (conn: Tcp.IncomingConnection)
    (implicit system: ActorSystem)
    : Flow[ByteString, ByteString, NotUsed]
    = Flow.fromGraph(GraphDSL.create() { implicit b ⇒
  import GraphDSL.Implicits._
  val welcome = Source.single(ByteString(s"Welcome port ${conn.remoteAddress}!\n"))
  val logic = b.add(internalLogic)
  val concat = b.add(Concat[ByteString]())
  welcome ~> concat.in(0)
  logic.outlet ~> concat.in(1)

  FlowShape(logic.in, concat.out)
})

関数serverLogic は着信接続をパラメーターとして受け取ります。本体の内部では、複雑なストリームの動作を説明できるDSLを使用しています。ではwelcome、ウェルカムメッセージという1つの要素のみを出力できるストリームを作成します。前のセクションでlogic説明しserverLogicたとおりです。唯一の顕著な違いは、追加closeConnectionしたことです。DSLの興味深い部分が実際に登場します。このGraphDSL.create関数はビルダーをb使用可能にし、ストリームをグラフとして表現するために使用されます。この~>機能により、入力ポートと出力ポートを相互に接続することができます。Concat例で使用される成分は、要素を連結することができ、ここから出てくる他の要素の前にウェルカムメッセージを付加するために使用されinternalLogic。最後の行では、サーバーロジックの入力ポートと連結ストリームの出力ポートのみを使用可能にします。これは、他のすべてのポートがserverLogicコンポーネントの実装の詳細を維持するためです。Akka StreamsのグラフDSLの詳細については、公式ドキュメントの対応するセクションをご覧ください。複雑なTCPサーバーと、それと通信できるクライアントの完全なコード例は、こちらにあります。クライアントから新しい接続を開くたびに、ウェルカムメッセージが表示さ"q"れ、クライアントに入力すると、接続がキャンセルされたことを示すメッセージが表示されます。

この回答でカバーできなかったトピックがまだいくつかあります。特に実体化は読者を怖がらせるかもしれませんが、ここでカバーされている資料で、誰もが自分で次のステップに進むことができると確信しています。すでに述べたように、公式ドキュメントはAkka Streamsについて学び続けるための良い場所です。


4
@monksyこれを他の場所で公開する予定はありませんでした。必要に応じて、ブログで自由に再公開してください。最近のAPIはほとんどの部分で安定しています。つまり、おそらくメンテナンスについて気にする必要もありません(Akka Streamsに関するほとんどのブログ記事は、もう存在しないAPIを示しているため古くなっています)。
キリツク

3
消えません。なぜそうすべきなのでしょうか?
キリツク

2
@sschaef質問はトピックから外れており、そのためクローズされたため、表示されなくなる可能性があります。
DavidG 2016

7
@Magisch常に覚えておいてください:「良いコンテンツは削除しません。」よくわかりませんが、すべてにかかわらず、この答えは実際に当てはまるかもしれません。
Deduplicator

9
この投稿は、Stack Overflowの新しいドキュメンテーション機能に適している可能性があります。
SLバース-モニカを
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.