Câu trả lời này dựa trên akka-stream
phiên bản 2.4.2
. API có thể hơi khác nhau trong các phiên bản khác. Sự phụ thuộc có thể được tiêu thụ bởi sbt :
libraryDependencies += "com.typesafe.akka" %% "akka-stream" % "2.4.2"
Được rồi, hãy bắt đầu. API của Akka Streams bao gồm ba loại chính. Trái ngược với Luồng phản ứng , các loại này mạnh hơn rất nhiều và do đó phức tạp hơn. Giả định rằng đối với tất cả các ví dụ mã, các định nghĩa sau đã tồn tại:
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
Các import
báo cáo là cần thiết cho các khai báo loại. system
đại diện cho hệ thống diễn viên của Akka và materializer
đại diện cho bối cảnh đánh giá của luồng. Trong trường hợp của chúng tôi, chúng tôi sử dụng một ActorMaterializer
, có nghĩa là các luồng được đánh giá trên đầu của các tác nhân. Cả hai giá trị được đánh dấu là implicit
, cung cấp cho trình biên dịch Scala khả năng tự động tiêm hai phụ thuộc này bất cứ khi nào chúng cần. Chúng tôi cũng nhập system.dispatcher
, đó là một bối cảnh thực hiện cho Futures
.
API mới
Luồng Akka có các thuộc tính chính sau:
- Họ triển khai đặc tả Luồng phản ứng , có ba mục tiêu chính là áp lực, không đồng bộ và ranh giới không chặn và khả năng tương tác giữa các triển khai khác nhau cũng áp dụng hoàn toàn cho Luồng Akka.
- Chúng cung cấp một sự trừu tượng cho một công cụ đánh giá cho các luồng, được gọi là
Materializer
.
- Chương trình được xây dựng như các khối xây dựng tái sử dụng, được biểu diễn dưới dạng ba loại chính
Source
, Sink
và Flow
. Các khối xây dựng tạo thành một biểu đồ có đánh giá dựa trên Materializer
và cần được kích hoạt rõ ràng.
Sau đây sẽ giới thiệu sâu hơn về cách sử dụng ba loại chính.
Nguồn
A Source
là một người tạo dữ liệu, nó phục vụ như một nguồn đầu vào cho luồng. Mỗi cái Source
có một kênh đầu ra duy nhất và không có kênh đầu vào. Tất cả các dữ liệu chảy qua kênh đầu ra đến bất cứ điều gì được kết nối với Source
.
Hình ảnh lấy từ boldradius.com .
A Source
có thể được tạo theo nhiều cách:
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
Trong các trường hợp trên, chúng tôi đã cung cấp Source
dữ liệu hữu hạn, có nghĩa là cuối cùng chúng sẽ chấm dứt. Không nên quên rằng các luồng Phản ứng là lười biếng và không đồng bộ theo mặc định. Điều này có nghĩa là một cách rõ ràng phải yêu cầu đánh giá luồng. Trong Akka Streams, điều này có thể được thực hiện thông qua các run*
phương thức. Hàm runForeach
này sẽ không khác với foreach
hàm được biết đến - thông qua việc run
bổ sung, nó làm cho rõ ràng rằng chúng tôi yêu cầu đánh giá luồng. Vì dữ liệu hữu hạn là nhàm chán, chúng tôi tiếp tục với dữ liệu vô hạn:
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
Với take
phương pháp chúng ta có thể tạo một điểm dừng nhân tạo ngăn chúng ta đánh giá vô thời hạn. Vì hỗ trợ diễn viên được tích hợp sẵn, chúng tôi cũng có thể dễ dàng cung cấp luồng với các tin nhắn được gửi đến một diễn viên:
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
Chúng ta có thể thấy rằng Futures
chúng được thực thi không đồng bộ trên các luồng khác nhau, điều này giải thích kết quả. Trong ví dụ trên, bộ đệm cho các phần tử đến là không cần thiết và do đó OverflowStrategy.fail
chúng ta có thể cấu hình rằng luồng sẽ bị lỗi khi tràn bộ đệm. Đặc biệt thông qua giao diện diễn viên này, chúng tôi có thể cung cấp luồng thông qua bất kỳ nguồn dữ liệu nào. Sẽ không có vấn đề gì nếu dữ liệu được tạo bởi cùng một luồng, bởi một luồng khác, bởi một quá trình khác hoặc nếu chúng đến từ một hệ thống từ xa qua Internet.
Bồn rửa
A Sink
về cơ bản là ngược lại với a Source
. Nó là điểm cuối của một luồng và do đó tiêu thụ dữ liệu. A Sink
có một kênh đầu vào duy nhất và không có kênh đầu ra. Sinks
đặc biệt cần thiết khi chúng tôi muốn chỉ định hành vi của người thu thập dữ liệu theo cách có thể sử dụng lại mà không cần đánh giá luồng. Các run*
phương thức đã biết không cho phép chúng ta các thuộc tính này, do đó, nó được ưu tiên sử dụng Sink
thay thế.
Hình ảnh lấy từ boldradius.com .
Một ví dụ ngắn về Sink
hành động:
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
Kết nối a Source
với a Sink
có thể được thực hiện bằng to
phương thức. Nó trả về một cái gọi là RunnableFlow
, như sau này chúng ta sẽ thấy một dạng đặc biệt của một Flow
- một luồng có thể được thực thi bằng cách chỉ gọi run()
phương thức của nó .
Hình ảnh lấy từ boldradius.com .
Tất nhiên có thể chuyển tiếp tất cả các giá trị đến mức chìm cho một diễn viên:
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
lưu lượng
Nguồn dữ liệu và bồn rửa là tuyệt vời nếu bạn cần kết nối giữa các luồng Akka và một hệ thống hiện có nhưng người ta không thể thực sự làm gì với chúng. Dòng chảy là phần còn thiếu cuối cùng trong bản tóm tắt cơ sở của Dòng Akka. Chúng hoạt động như một kết nối giữa các luồng khác nhau và có thể được sử dụng để chuyển đổi các phần tử của nó.
Hình ảnh lấy từ boldradius.com .
Nếu a Flow
được kết nối với một Source
cái mới Source
là kết quả. Tương tự như vậy, một Flow
kết nối với một Sink
tạo mới Sink
. Và một Flow
kết nối với cả a Source
và Sink
kết quả trong a RunnableFlow
. Do đó, chúng nằm giữa kênh đầu vào và kênh đầu ra nhưng bản thân chúng không tương ứng với một trong các hương vị miễn là chúng không được kết nối với a Source
hoặc a Sink
.
Hình ảnh lấy từ boldradius.com .
Để hiểu rõ hơn Flows
, chúng ta sẽ xem xét một số ví dụ:
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
Thông qua via
phương pháp chúng ta có thể kết nối a Source
với a Flow
. Chúng ta cần chỉ định loại đầu vào vì trình biên dịch không thể suy ra nó cho chúng ta. Như chúng ta đã thấy trong ví dụ đơn giản này, các luồng invert
và double
hoàn toàn độc lập với bất kỳ nhà sản xuất và người tiêu dùng dữ liệu nào. Họ chỉ chuyển đổi dữ liệu và chuyển tiếp nó đến kênh đầu ra. Điều này có nghĩa là chúng ta có thể sử dụng lại một luồng giữa nhiều luồng:
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
s1
và s2
đại diện cho các luồng hoàn toàn mới - họ không chia sẻ bất kỳ dữ liệu nào thông qua các khối xây dựng của họ.
Luồng dữ liệu không giới hạn
Trước khi tiếp tục, trước tiên chúng ta nên xem lại một số khía cạnh chính của Luồng phản ứng. Một số phần tử không giới hạn có thể đến bất kỳ điểm nào và có thể đặt một luồng ở các trạng thái khác nhau. Ngoài luồng có thể chạy, là trạng thái thông thường, luồng có thể bị dừng do lỗi hoặc qua tín hiệu biểu thị rằng sẽ không có thêm dữ liệu nào đến. Một luồng có thể được mô hình hóa theo cách đồ họa bằng cách đánh dấu các sự kiện trên dòng thời gian như trường hợp ở đây:
Hình ảnh được lấy từ phần Giới thiệu về Lập trình Phản ứng mà bạn đã bỏ lỡ .
Chúng ta đã thấy các luồng có thể chạy được trong các ví dụ của phần trước. Chúng tôi nhận được RunnableGraph
bất cứ khi nào một luồng thực sự có thể được vật chất hóa, có nghĩa là một luồng Sink
được kết nối với a Source
. Cho đến nay chúng ta luôn cụ thể hóa giá trị Unit
, có thể thấy trong các loại:
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)
Cho Source
và Sink
tham số loại thứ hai và cho Flow
tham số loại thứ ba biểu thị giá trị cụ thể hóa. Trong suốt câu trả lời này, ý nghĩa đầy đủ của vật chất hóa sẽ không được giải thích. Tuy nhiên, chi tiết thêm về vật chất hóa có thể được tìm thấy tại các tài liệu chính thức . Bây giờ, điều duy nhất chúng ta cần biết là giá trị cụ thể hóa là những gì chúng ta nhận được khi chạy một luồng. Vì chúng tôi chỉ quan tâm đến tác dụng phụ cho đến nay, chúng tôi nhận được Unit
giá trị cụ thể hóa. Ngoại lệ cho điều này là sự vật chất hóa của một cái bồn, dẫn đến một Future
. Nó đã cho chúng tôi trở lạiFuture
, vì giá trị này có thể biểu thị khi luồng được kết nối với bồn rửa đã kết thúc. Cho đến nay, các ví dụ mã trước đây rất hay để giải thích khái niệm này nhưng chúng cũng nhàm chán vì chúng ta chỉ xử lý các luồng hữu hạn hoặc với các dòng vô hạn rất đơn giản. Để làm cho nó thú vị hơn, trong phần sau đây, một luồng không đồng bộ và không giới hạn đầy đủ sẽ được giải thích.
Ví dụ ClickStream
Ví dụ, chúng tôi muốn có một luồng lưu trữ các sự kiện nhấp chuột. Để làm cho nó khó khăn hơn, giả sử chúng tôi cũng muốn nhóm các sự kiện nhấp chuột xảy ra trong một thời gian ngắn sau khi nhau. Bằng cách này, chúng tôi có thể dễ dàng khám phá gấp đôi, gấp ba hoặc mười lần nhấp. Hơn nữa, chúng tôi muốn lọc ra tất cả các nhấp chuột duy nhất. Hít một hơi thật sâu và tưởng tượng bạn sẽ giải quyết vấn đề đó theo cách bắt buộc như thế nào. Tôi cá rằng không ai có thể thực hiện một giải pháp hoạt động chính xác trong lần thử đầu tiên. Trong một phản ứng thời trang, vấn đề này là tầm thường để giải quyết. Trong thực tế, giải pháp rất đơn giản và dễ thực hiện đến mức chúng ta thậm chí có thể biểu thị nó trong một sơ đồ mô tả trực tiếp hành vi của mã:
Hình ảnh được lấy từ phần Giới thiệu về Lập trình Phản ứng mà bạn đã bỏ lỡ .
Các hộp màu xám là các hàm mô tả cách một luồng được chuyển thành luồng khác. Với throttle
chức năng chúng tôi tích lũy số lần nhấp trong vòng 250 mili giây, map
và các filter
chức năng sẽ tự giải thích. Các quả cầu màu đại diện cho một sự kiện và các mũi tên mô tả cách chúng chảy qua các chức năng của chúng ta. Sau đó trong các bước xử lý, chúng tôi nhận được ngày càng ít các phần tử chảy qua luồng của chúng tôi, vì chúng tôi nhóm chúng lại với nhau và lọc chúng ra. Mã cho hình ảnh này sẽ trông giống như thế này:
val multiClickStream = clickStream
.throttle(250.millis)
.map(clickEvents => clickEvents.length)
.filter(numberOfClicks => numberOfClicks >= 2)
Toàn bộ logic có thể được biểu diễn chỉ trong bốn dòng mã! Trong Scala, chúng ta có thể viết nó thậm chí còn ngắn hơn:
val multiClickStream = clickStream.throttle(250.millis).map(_.length).filter(_ >= 2)
Định nghĩa về clickStream
phức tạp hơn một chút nhưng đây chỉ là trường hợp vì chương trình ví dụ chạy trên JVM, trong đó việc nắm bắt các sự kiện nhấp chuột là không dễ dàng. Một sự phức tạp khác là Akka theo mặc định không cung cấp throttle
chức năng. Thay vào đó chúng tôi phải tự viết nó. Vì hàm này là (vì đó là trường hợp cho các hàm map
hoặc filter
) có thể sử dụng lại trong các trường hợp sử dụng khác nhau, tôi không đếm các dòng này với số dòng chúng ta cần để thực hiện logic. Tuy nhiên, trong các ngôn ngữ bắt buộc, thông thường logic không thể được sử dụng lại một cách dễ dàng và các bước logic khác nhau xảy ra tại một nơi thay vì được áp dụng tuần tự, điều đó có nghĩa là chúng ta có thể đã đánh lừa mã của mình bằng logic điều tiết. Ví dụ mã đầy đủ có sẵn dưới dạngý chính và sẽ không được thảo luận ở đây nữa.
Ví dụ về SimpleWebServer
Những gì nên được thảo luận thay vì là một ví dụ khác. Mặc dù luồng nhấp chuột là một ví dụ hay để cho Akka Stream xử lý một ví dụ trong thế giới thực, nhưng nó thiếu sức mạnh để hiển thị thực thi song song trong hành động. Ví dụ tiếp theo sẽ đại diện cho một máy chủ web nhỏ có thể xử lý song song nhiều yêu cầu. Máy chủ web sẽ có thể chấp nhận các kết nối đến và nhận các chuỗi byte từ chúng đại diện cho các dấu hiệu ASCII có thể in được. Các chuỗi hoặc chuỗi byte này nên được phân chia ở tất cả các ký tự dòng mới thành các phần nhỏ hơn. Sau đó, máy chủ sẽ trả lời máy khách với từng dòng phân chia. Ngoài ra, nó có thể làm một cái gì đó khác với các dòng và đưa ra một mã thông báo câu trả lời đặc biệt, nhưng chúng tôi muốn giữ cho nó đơn giản trong ví dụ này và do đó không giới thiệu bất kỳ tính năng ưa thích nào. Nhớ lại, máy chủ cần có khả năng xử lý nhiều yêu cầu cùng một lúc, điều đó về cơ bản có nghĩa là không có yêu cầu nào được phép chặn bất kỳ yêu cầu nào khác từ việc thực hiện thêm. Việc giải quyết tất cả các yêu cầu này có thể khó khăn theo một cách cấp bách - tuy nhiên với Luồng Akka, chúng ta không cần nhiều hơn một vài dòng để giải quyết bất kỳ yêu cầu nào. Trước tiên, hãy có một cái nhìn tổng quan về chính máy chủ:
Về cơ bản, chỉ có ba khối xây dựng chính. Người đầu tiên cần chấp nhận kết nối đến. Người thứ hai cần xử lý các yêu cầu đến và người thứ ba cần gửi phản hồi. Việc triển khai cả ba khối xây dựng này chỉ phức tạp hơn một chút so với triển khai luồng nhấp chuột:
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}")
}
}
Hàm mkServer
lấy (ngoài địa chỉ và cổng của máy chủ) còn có một hệ thống tác nhân và một bộ tạo vật liệu làm tham số ngầm. Luồng điều khiển của máy chủ được đại diện bởi binding
, nó lấy một nguồn kết nối đến và chuyển tiếp chúng đến một kết nối đến. Bên trong connectionHandler
, đó là bồn rửa của chúng tôi, chúng tôi xử lý mọi kết nối theo dòng chảy serverLogic
, sẽ được mô tả sau. binding
trả lại mộtFuture
, hoàn thành khi máy chủ đã được khởi động hoặc khởi động thất bại, đó có thể là trường hợp khi cổng đã được thực hiện bởi một quy trình khác. Tuy nhiên, mã không phản ánh hoàn toàn đồ họa vì chúng ta không thể thấy một khối xây dựng xử lý các phản hồi. Lý do cho điều này là kết nối đã tự cung cấp logic này. Đó là một luồng hai chiều và không chỉ là một luồng một chiều như các luồng chúng ta đã thấy trong các ví dụ trước. Vì đó là trường hợp vật chất hóa, các dòng phức tạp như vậy sẽ không được giải thích ở đây. Các tài liệu chính thức có rất nhiều tài liệu để trang trải các biểu đồ dòng chảy phức tạp hơn. Cho đến bây giờ cũng đủ để biết rằng Tcp.IncomingConnection
đại diện cho một kết nối biết cách nhận yêu cầu và cách gửi phản hồi. Phần vẫn còn thiếu làserverLogic
khối xây dựng. Nó có thể trông như thế này:
Một lần nữa, chúng ta có thể phân chia logic trong một số khối xây dựng đơn giản, tất cả cùng tạo thành dòng chảy của chương trình của chúng ta. Đầu tiên chúng tôi muốn phân chia chuỗi byte theo dòng, điều chúng tôi phải làm bất cứ khi nào chúng tôi tìm thấy một ký tự dòng mới. Sau đó, các byte của mỗi dòng cần phải được chuyển đổi thành một chuỗi vì làm việc với các byte thô rất cồng kềnh. Nhìn chung, chúng ta có thể nhận được một luồng nhị phân của một giao thức phức tạp, điều này sẽ khiến việc làm việc với dữ liệu thô đến vô cùng khó khăn. Khi chúng ta có một chuỗi có thể đọc được, chúng ta có thể tạo một câu trả lời. Vì lý do đơn giản, câu trả lời có thể là bất cứ điều gì trong trường hợp của chúng tôi. Cuối cùng, chúng ta phải chuyển đổi câu trả lời của mình thành một chuỗi các byte có thể được gửi qua dây. Mã cho toàn bộ logic có thể trông như thế này:
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)
}
Chúng ta đã biết rằng đó serverLogic
là một dòng chảy cần ByteString
và phải tạo ra một ByteString
. Với delimiter
chúng ta có thể chia một ByteString
phần nhỏ hơn - trong trường hợp của chúng ta, nó cần xảy ra bất cứ khi nào một ký tự dòng mới xảy ra. receiver
là luồng lấy tất cả các chuỗi byte phân tách và chuyển đổi chúng thành một chuỗi. Tất nhiên đây là một chuyển đổi nguy hiểm, vì chỉ các ký tự ASCII có thể in nên được chuyển đổi thành một chuỗi nhưng đối với nhu cầu của chúng tôi thì nó đủ tốt. responder
là thành phần cuối cùng và chịu trách nhiệm tạo câu trả lời và chuyển đổi câu trả lời thành chuỗi byte. Trái ngược với đồ họa, chúng tôi đã không chia thành phần cuối cùng này thành hai, vì logic là không đáng kể. Cuối cùng, chúng tôi kết nối tất cả các dòng chảy thông quavia
chức năng. Tại thời điểm này, người ta có thể hỏi liệu chúng tôi có quan tâm đến tài sản nhiều người dùng đã được đề cập ở đầu không. Và thực sự chúng tôi đã làm mặc dù nó có thể không rõ ràng ngay lập tức. Bằng cách nhìn vào đồ họa này, nó sẽ rõ hơn:
Thành serverLogic
phần này không là gì ngoài một luồng chứa các luồng nhỏ hơn. Thành phần này nhận một đầu vào, là một yêu cầu và tạo ra một đầu ra, đó là phản hồi. Vì các luồng có thể được xây dựng nhiều lần và tất cả chúng hoạt động độc lập với nhau, chúng tôi đạt được thông qua việc lồng ghép tài sản đa người dùng này. Mọi yêu cầu được xử lý trong yêu cầu riêng của nó và do đó, một yêu cầu chạy ngắn có thể vượt qua yêu cầu chạy dài đã bắt đầu trước đó. Trong trường hợp bạn tự hỏi, định nghĩa về serverLogic
điều đó đã được hiển thị trước đó tất nhiên có thể được viết ngắn hơn rất nhiều bằng cách nội tuyến hầu hết các định nghĩa bên trong của nó:
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(_))
Một thử nghiệm của máy chủ web có thể trông như thế này:
$ # 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?
Để ví dụ mã trên hoạt động chính xác, trước tiên chúng ta cần khởi động máy chủ, được mô tả bởi startServer
tập lệnh:
$ # 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?
Ví dụ mã đầy đủ của máy chủ TCP đơn giản này có thể được tìm thấy ở đây . Chúng tôi không chỉ có thể viết một máy chủ với Akka Streams mà còn cả máy khách. Nó có thể trông như thế này:
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()
Mã khách hàng TCP đầy đủ có thể được tìm thấy ở đây . Mã trông khá giống nhau nhưng ngược lại với máy chủ, chúng tôi không phải quản lý các kết nối đến nữa.
Đồ thị phức tạp
Trong các phần trước chúng ta đã thấy làm thế nào chúng ta có thể xây dựng các chương trình đơn giản ngoài luồng. Tuy nhiên, trong thực tế thường không đủ nếu chỉ dựa vào các hàm đã được tích hợp sẵn để xây dựng các luồng phức tạp hơn. Nếu chúng ta muốn có thể sử dụng Luồng Akka cho các chương trình tùy ý, chúng ta cần biết cách xây dựng các cấu trúc điều khiển tùy chỉnh của riêng mình và các luồng kết hợp cho phép chúng ta giải quyết sự phức tạp của các ứng dụng. Tin vui là Akka Streams được thiết kế để mở rộng theo nhu cầu của người dùng và để giới thiệu ngắn gọn về các phần phức tạp hơn của Akka Streams, chúng tôi thêm một số tính năng khác vào ví dụ máy khách / máy chủ của chúng tôi.
Một điều chúng ta không thể làm là đóng một kết nối. Tại thời điểm này, nó bắt đầu phức tạp hơn một chút vì API luồng mà chúng ta đã thấy cho đến nay không cho phép chúng ta dừng luồng tại một điểm tùy ý. Tuy nhiên, có sự GraphStage
trừu tượng hóa, có thể được sử dụng để tạo các giai đoạn xử lý đồ thị tùy ý với bất kỳ số lượng cổng đầu vào hoặc đầu ra. Trước tiên chúng ta hãy nhìn vào phía máy chủ, nơi chúng tôi giới thiệu một thành phần mới, được gọi là 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 này trông cồng kềnh hơn rất nhiều so với API luồng. Không có gì ngạc nhiên, chúng tôi phải làm rất nhiều bước bắt buộc ở đây. Đổi lại, chúng tôi có quyền kiểm soát nhiều hơn đối với hành vi của các luồng của chúng tôi. Trong ví dụ trên, chúng tôi chỉ chỉ định một cổng đầu vào và một cổng đầu ra và cung cấp chúng cho hệ thống bằng cách ghi đè shape
giá trị. Hơn nữa, chúng tôi đã định nghĩa một cái gọi là InHandler
và a OutHandler
, theo thứ tự này chịu trách nhiệm nhận và phát ra các phần tử. Nếu bạn nhìn kỹ vào ví dụ dòng nhấp chuột đầy đủ, bạn sẽ nhận ra các thành phần này. Trong InHandler
phần tử chúng ta lấy một phần tử và nếu đó là một chuỗi có một ký tự 'q'
, chúng ta muốn đóng luồng. Để cung cấp cho khách hàng cơ hội phát hiện ra rằng luồng sẽ sớm bị đóng, chúng tôi phát ra chuỗi"BYE"
và sau đó chúng tôi ngay lập tức đóng cửa sân khấu sau đó. Thành closeConnection
phần có thể được kết hợp với một luồng thông qua via
phương thức, được giới thiệu trong phần về các luồng.
Bên cạnh việc có thể đóng các kết nối, sẽ rất tuyệt nếu chúng ta có thể hiển thị thông báo chào mừng đến một kết nối mới được tạo. Để làm điều này, một lần nữa chúng ta phải đi xa hơn một chút:
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)
})
Hàm serverLogic
bây giờ lấy kết nối đến làm tham số. Bên trong cơ thể của nó, chúng tôi sử dụng DSL cho phép chúng tôi mô tả hành vi luồng phức tạp. Với welcome
chúng tôi tạo ra một luồng chỉ có thể phát ra một yếu tố - thông điệp chào mừng. logic
là những gì đã được mô tả như serverLogic
trong phần trước. Sự khác biệt đáng chú ý duy nhất là chúng tôi đã thêm closeConnection
vào nó. Bây giờ thực sự đến phần thú vị của DSL. Các GraphDSL.create
chức năng làm cho một người thợ xây b
sẵn, được sử dụng để diễn tả dòng như một đồ thị. Với ~>
chức năng có thể kết nối các cổng đầu vào và đầu ra với nhau. Thành Concat
phần được sử dụng trong ví dụ có thể ghép các phần tử và ở đây được sử dụng để thêm vào thông điệp chào mừng trước các phần tử khác xuất phát từinternalLogic
. Trong dòng cuối cùng, chúng tôi chỉ cung cấp cổng đầu vào của logic máy chủ và cổng đầu ra của luồng kết nối có sẵn vì tất cả các cổng khác sẽ vẫn là chi tiết triển khai của serverLogic
thành phần. Để có phần giới thiệu chuyên sâu về DSL đồ thị của Luồng Akka, hãy truy cập phần tương ứng trong tài liệu chính thức . Ví dụ mã đầy đủ của máy chủ TCP phức tạp và của một máy khách có thể giao tiếp với nó có thể được tìm thấy ở đây . Bất cứ khi nào bạn mở một kết nối mới từ máy khách, bạn sẽ thấy một thông báo chào mừng và bằng cách gõ "q"
vào máy khách, bạn sẽ thấy một thông báo cho bạn biết rằng kết nối đã bị hủy.
Vẫn còn một số chủ đề không được trả lời bởi câu trả lời này. Đặc biệt là vật chất hóa có thể khiến người đọc sợ hãi hoặc người đọc khác nhưng tôi chắc chắn với tài liệu được đề cập ở đây, mọi người sẽ có thể tự mình thực hiện các bước tiếp theo. Như đã nói, tài liệu chính thức là một nơi tốt để tiếp tục tìm hiểu về Akka Streams.