Scala 如何开始使用Akka Streams?

Scala 如何开始使用Akka Streams?,scala,akka-stream,Scala,Akka Stream,Akka Streams图书馆已经提供了相当多的功能。然而,对我来说,主要的问题是它提供了太多的材料——我觉得我必须学习的概念太多了。这里显示的很多示例都很重,不能很容易地转化为现实世界的用例,因此非常深奥。我认为它给出了太多的细节,没有解释如何将所有构建块构建在一起,以及它如何帮助解决具体问题 有源、汇、流、图阶段、部分图、物化、图DSL等等,我不知道从哪里开始。这本应该是一个起点,但我不明白。它只是把上面提到的概念放进去,没有解释它们。此外,代码示例无法执行-缺少的部分使我或多或少无法理解文

Akka Streams图书馆已经提供了相当多的功能。然而,对我来说,主要的问题是它提供了太多的材料——我觉得我必须学习的概念太多了。这里显示的很多示例都很重,不能很容易地转化为现实世界的用例,因此非常深奥。我认为它给出了太多的细节,没有解释如何将所有构建块构建在一起,以及它如何帮助解决具体问题

有源、汇、流、图阶段、部分图、物化、图DSL等等,我不知道从哪里开始。这本应该是一个起点,但我不明白。它只是把上面提到的概念放进去,没有解释它们。此外,代码示例无法执行-缺少的部分使我或多或少无法理解文本


有人能解释一下源、汇、流、图阶段、局部图、物化以及其他一些我用简单的语言和简单的例子遗漏的概念吗?这些例子并不能解释每一个细节(在开始时可能并不需要)?

此答案基于
akka stream
版本
2.4.2
。其他版本中的API可能略有不同。依赖项可由以下人员使用:


好的,让我们开始吧。Akka Streams的API包括三种主要类型。与之相反,这些类型功能更强大,因此更复杂。假设对于所有代码示例,已经存在以下定义:

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
语句
system
表示Akka的参与者系统,
materializer
表示流的评估上下文。在我们的例子中,我们使用了
Actormatarializer
,这意味着流是在actor之上进行评估的。这两个值都标记为
implicit
,这使Scala编译器能够在需要时自动注入这两个依赖项。我们还导入了
system.dispatcher
,这是的执行上下文

一种新的API Akka流具有以下关键特性:

  • 他们实现了,其三个主要目标:背压、异步和非阻塞边界以及不同实现之间的互操作性也完全适用于Akka流
  • 它们为流的评估引擎提供了一个抽象,称为
    Materializer
  • 程序被描述为可重用的构建块,表示为三种主要类型
    接收器
    。构建块形成一个图形,其计算基于
    物化器
    ,需要显式触发
以下将对如何使用这三种主要类型进行更深入的介绍

来源
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
在上述情况下,我们向
源提供了有限的数据,这意味着它们最终将终止。人们不应该忘记,在默认情况下,反应流是惰性和异步的。这意味着必须显式地请求对流进行求值。在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
方法,我们可以创建一个人工停止点,防止我们无限期地进行评估。由于actor支持是内置的,我们还可以轻松地向流提供发送给actor的消息:

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
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
我们可以看到,
期货
在不同的线程上异步执行,这解释了结果。在上面的示例中,传入元素不需要缓冲区,因此使用
OverflowStrategy.fail
我们可以配置流在缓冲区溢出时失败。特别是通过这个actor接口,我们可以通过任何数据源为流提供信息。数据是由同一线程创建的、由不同线程创建的、由另一个进程创建的,还是来自Internet上的远程系统,这些都无关紧要

下沉
接收器
基本上与
相反。它是流的端点,因此使用数据。
接收器
只有一个输入通道,没有输出通道<当我们希望以可重用的方式指定数据采集器的行为而不评估流时,特别需要代码>接收器。已知的
run*
方法不允许我们使用这些属性,因此最好使用
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
可以使用
to
方法将
连接到
接收器
。它返回一个所谓的
RunnableFlow
,我们稍后将看到一种特殊形式的
——一种只需调用其
run()
方法即可执行的流

这张照片是从中国拍摄的

当然,可以将到达接收器的所有值转发给参与者:

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
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 streams和现有系统之间的连接,那么数据源和接收器是非常好的,但是使用t
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
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)
val multiClickStream = clickStream
    .throttle(250.millis)
    .map(clickEvents => clickEvents.length)
    .filter(numberOfClicks => numberOfClicks >= 2)
val multiClickStream = clickStream.throttle(250.millis).map(_.length).filter(_ >= 2)
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}")
  }
}
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)
}
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(_))
$ # 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?
$ # 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?
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()
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)
    })
  }
}
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)
})