Làm thế nào để bắt đầu với Akka Streams? [đóng cửa]


222

Thư viện Akka Streams đã đi kèm với khá nhiều tài liệu . Tuy nhiên, vấn đề chính đối với tôi là nó cung cấp quá nhiều tài liệu - tôi cảm thấy khá choáng ngợp bởi số lượng khái niệm mà tôi phải học. Rất nhiều ví dụ cho thấy cảm thấy rất nặng nề và không thể dễ dàng được dịch sang các trường hợp sử dụng trong thế giới thực và do đó khá bí truyền. Tôi nghĩ rằng nó cung cấp cho quá nhiều chi tiết mà không giải thích làm thế nào để xây dựng tất cả các khối xây dựng với nhau và chính xác nó giúp giải quyết các vấn đề cụ thể như thế nào.

Có nguồn, chìm, dòng chảy, giai đoạn đồ thị, đồ thị một phần, vật chất hóa, DSL đồ thị và nhiều hơn nữa và tôi không biết bắt đầu từ đâu. Các hướng dẫn bắt đầu nhanh được hiểu là một nơi bắt đầu nhưng tôi không hiểu nó. Nó chỉ ném trong các khái niệm được đề cập ở trên mà không giải thích chúng. Hơn nữa, các ví dụ mã không thể được thực thi - có những phần bị thiếu khiến tôi ít nhiều không thể theo dõi văn bản.

Bất cứ ai cũng có thể giải thích các nguồn khái niệm, chìm, dòng chảy, giai đoạn đồ thị, đồ thị một phần, vật chất hóa và có thể một số điều khác mà tôi đã bỏ lỡ bằng những từ đơn giản và với các ví dụ dễ dàng không giải thích từng chi tiết (và có lẽ không cần thiết dù sao sự bắt đầu)?


2
Để biết thông tin, điều này đang được thảo luận trên meta
DavidG

10
Là người đầu tiên bỏ phiếu để đóng cái này (theo chủ đề Meta), trước tiên tôi xin nói rằng câu trả lời của bạn ở đây rất hay . Nó thực sự chuyên sâu và chắc chắn là một tài nguyên rất hữu ích. Tuy nhiên, thật không may, câu hỏi bạn đã hỏi quá rộng đối với Stack Overflow. Nếu bằng cách nào đó câu trả lời của bạn có thể được đăng lên một câu hỏi khác, thì thật tuyệt vời, nhưng tôi không nghĩ nó có thể. Tôi thực sự khuyên bạn nên gửi lại bài này dưới dạng bài đăng trên blog hoặc một cái gì đó tương tự mà bản thân và những người khác có thể sử dụng làm tài liệu tham khảo trong các câu trả lời trong tương lai.
James Donnelly

2
Tôi nghĩ rằng viết câu hỏi này như một bài viết trên blog sẽ không hiệu quả. Vâng, đây là một câu hỏi rộng - và nó là một câu hỏi thực sự tốt. Thu hẹp phạm vi của nó sẽ không cải thiện nó. Câu trả lời được cung cấp là tuyệt vời. Tôi chắc chắn Quora sẽ rất vui khi đưa doanh nghiệp ra khỏi SO cho những câu hỏi lớn.
Mike Slinn

11
@MikeSlinn đừng cố thảo luận với mọi người về những câu hỏi phù hợp, họ mù quáng tuân theo các quy tắc. Miễn là câu hỏi không bị xóa, tôi rất vui và không cảm thấy chuyển sang một nền tảng khác.
kiritsuku

2
@sschaef Làm thế nào phạm vi. Vâng, tất nhiên, các quy tắc là không có giá trị, bản thân tuyệt vời của bạn biết rất nhiều và mọi người cố gắng áp dụng các quy tắc chỉ là mù quáng theo sự cường điệu. / rant nghiêm túc hơn, đây sẽ là một bổ sung tuyệt vời cho bản beta tài liệu, nếu bạn đang ở trong đó. Bạn vẫn có thể áp dụng và đưa nó lên đó, nhưng ít nhất bạn nên thấy rằng nó không phù hợp với trang web chính.
Félix Gagnon-Grenier

Câu trả lời:


506

Câu trả lời này dựa trên akka-streamphiê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 importbá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, SinkFlow. Các khối xây dựng tạo thành một biểu đồ có đánh giá dựa trên Materializervà 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 Sourcelà 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 Sourcecó 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.

Nguồn

Hình ảnh lấy từ boldradius.com .

A Sourcecó 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 Sourcedữ 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 runForeachnày sẽ không khác với foreachhàm được biết đến - thông qua việc runbổ 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 takephươ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 Futureschú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.failchú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 Sinkvề 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 Sinkcó 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 Sinkthay thế.

Bồn rửa

Hình ảnh lấy từ boldradius.com .

Một ví dụ ngắn về Sinkhà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 Sourcevới a Sinkcó thể được thực hiện bằng tophươ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ó .

Dòng chảy

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ó.

lưu lượng

Hình ảnh lấy từ boldradius.com .

Nếu a Flowđược kết nối với một Sourcecái mới Sourcelà kết quả. Tương tự như vậy, một Flowkết nối với một Sinktạo mới Sink. Và một Flowkết nối với cả a SourceSinkkế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 Sourcehoặc a Sink.

Luồng đầy đủ

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 viaphương pháp chúng ta có thể kết nối a Sourcevớ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 invertdoublehoà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

s1s2đạ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:

Cho thấy một luồng là một chuỗi các sự kiện đang diễn ra được sắp xếp theo thời gian

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 RunnableGraphbấ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 SourceSinktham số loại thứ hai và cho Flowtham 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 Unitgiá 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ã:

Logic của ví dụ luồng nhấp chuột

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 throttlechức năng chúng tôi tích lũy số lần nhấp trong vòng 250 mili giây, mapvà các filterchứ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ề clickStreamphứ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 throttlechứ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 maphoặ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ủ:

người phục vụ

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 mkServerlấ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. bindingtrả 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àserverLogickhối xây dựng. Nó có thể trông như thế này:

logic máy chủ

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 đó serverLogiclà một dòng chảy cần ByteStringvà phải tạo ra một ByteString. Với delimiterchúng ta có thể chia một ByteStringphầ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. receiverlà 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. responderlà 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 quaviachứ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:

logic máy chủ và máy chủ kết hợp

Thành serverLogicphầ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 startServertậ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ự GraphStagetrừ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 đè shapegiá trị. Hơn nữa, chúng tôi đã định nghĩa một cái gọi là InHandlervà 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 InHandlerphầ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 closeConnectionphần có thể được kết hợp với một luồng thông qua viaphươ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 welcomechú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. logiclà những gì đã được mô tả như serverLogictrong phần trước. Sự khác biệt đáng chú ý duy nhất là chúng tôi đã thêm closeConnectionvào nó. Bây giờ thực sự đến phần thú vị của DSL. Các GraphDSL.createchức năng làm cho một người thợ xây bsẵ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 Concatphầ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 serverLogicthà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.


4
@monksy Tôi không có kế hoạch xuất bản này ở bất cứ nơi nào khác. Vui lòng xuất bản lại trên blog của bạn nếu bạn muốn. API hiện nay ổn định ở hầu hết các phần, điều đó có nghĩa là bạn thậm chí không cần phải quan tâm đến việc bảo trì (hầu hết các bài viết trên blog về Akka Stream đã lỗi thời vì chúng hiển thị API không còn tồn tại nữa).
kiritsuku

3
Nó sẽ không biến mất. Tại sao nên làm thế?
kiritsuku

2
@sschaef Nó cũng có thể biến mất vì câu hỏi không có chủ đề và đã bị đóng như vậy.
DavidG

7
@Magisch Luôn nhớ: "Chúng tôi không xóa nội dung hay." Tôi không chắc lắm, nhưng tôi đoán câu trả lời này có thể thực sự đủ điều kiện, bất chấp mọi thứ.
Ded

9
Bài đăng này có thể tốt cho tính năng Tài liệu mới của Stack Overflow - một khi nó mở cho Scala.
SL Barth - Phục hồi Monica
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.