Ця відповідь заснована на akka-stream
версії 2.4.2
. API може дещо відрізнятися в інших версіях. Залежність може бути спожита sbt :
libraryDependencies += "com.typesafe.akka" %% "akka-stream" % "2.4.2"
Добре, давайте почнемо. API потоків Akka складається з трьох основних типів. На відміну від Реактивних потоків ці типи набагато потужніші і, отже, більш складні. Передбачається, що для всіх прикладів коду вже існують такі визначення:
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
представляє акторську систему Акка і materializer
представляє контекст оцінювання потоку. У нашому випадку ми використовуємо a ActorMaterializer
, що означає, що потоки оцінюються поверх дійових осіб. Обидва значення позначені як implicit
, що дає компілятору Scala можливість автоматично вводити ці дві залежності, коли вони знадобляться. Ми також імпортуємо system.dispatcher
, що є контекстом для виконання Futures
.
Новий API
Потоки Акки мають такі ключові властивості:
- Вони реалізують специфікацію "Реактивні потоки" , три основні цілі зворотного тиску, асинхронізація та неблокуючі межі та сумісність між різними реалізаціями повністю застосовуються і для потоків Akka.
- Вони забезпечують абстракцію двигуна оцінки потоків, який називається
Materializer
.
- Програми сформульовані в вигляді повторно використовуваних будівельних блоків, які представлені в вигляді трьох основних типів
Source
, Sink
і Flow
. Структурні блоки утворюють графік, оцінка якого ґрунтується на, Materializer
і його потрібно чітко запускати.
Далі буде подано більш глибоке вступ до використання трьох основних типів.
Джерело
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
кінцевими даними, а це означає, що вони з часом припиняться. Не слід забувати, що Реактивні потоки за замовчуванням ледачі та асинхронні. Це означає, що явно потрібно запитати оцінку потоку. У потоках Акка це можна зробити 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
в основному протилежний a 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
Підключення a Source
до аномалії Sink
може бути здійснено to
методом. Він повертає так званий RunnableFlow
, який, як ми пізніше побачимо особливу форму a Flow
- потоку, який можна виконати, просто викликавши його 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 та існуючою системою, але з ними реально нічого не можна зробити. Потоки - це останній фрагмент, що відсутній у базовій абстракції потоків Акка. Вони виконують роль з'єднувача між різними потоками і можуть використовуватися для перетворення його елементів.
Зображення взято з boldradius.com .
Якщо a Flow
підключено до Source
нового, Source
це результат. Аналогічно, Flow
підключений до Sink
твори створює нове Sink
. І Flow
пов'язане як з a, так Source
і Sink
призводить до a RunnableFlow
. Тому вони сидять між входом і вихідним каналом, але самі по собі не відповідають одному з ароматів, доки вони не підключені ні до а, ні до Source
а 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
методу ми можемо з'єднати a Source
з a Flow
. Нам потрібно вказати тип введення, оскільки компілятор не може зробити це для нас. Як ми вже бачимо на цьому простому прикладі, потоки invert
і double
повністю незалежні від будь-яких виробників даних та споживачів. Вони лише перетворюють дані і пересилають їх на вихідний канал. Це означає, що ми можемо повторно використовувати потік серед кількох потоків:
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
і s2
представляють абсолютно нові потоки - вони не діляться жодними даними через свої будівельні блоки.
Безмежні потоки даних
Перш ніж рухатися далі, слід спочатку переглянути деякі ключові аспекти Реактивних потоків. Безмежна кількість елементів може надходити в будь-яку точку і може розміщувати потік у різних станах. Крім потоку, який можна запустити, який є звичайним станом, потік може зупинятися або через помилку, або через сигнал, який позначає, що більше ніяких даних не надходитиме. Потік можна моделювати графічно, позначаючи події на часовій шкалі, як це відбувається тут:
Зображення, зроблене із вступу до реактивного програмування, якого ви не пропустили .
На прикладах попереднього розділу ми вже бачили рухомі потоки. Ми отримуємо RunnableGraph
щоразу, коли потік може бути фактично матеріалізований, це означає, що a Sink
підключений до a Source
. Поки ми завжди матеріалізувались до значення 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)
Для отримання Source
і Sink
другий параметр типу і для Flow
третього параметра типу позначає матеріалізоване значення. У цій відповіді повний сенс матеріалізації не повинен пояснюватися. Однак більш детальну інформацію про матеріалізацію можна знайти в офіційній документації . Поки що єдине, що нам потрібно знати, - це те, що матеріалізована цінність - це те, що ми отримуємо під час запуску потоку. Оскільки нас поки що цікавили лише побічні ефекти, ми отримали Unit
як матеріалізовану цінність. Винятком з цього стала матеріалізація раковини, в результаті якої аFuture
. Це повернуло нам aFuture
, оскільки це значення може позначати, коли потік, який підключений до раковини, закінчився. Поки попередні приклади коду були приємні для пояснення концепції, але вони також були нудними, оскільки ми мали справу лише з кінцевими потоками або з дуже простими нескінченними. Щоб зробити це цікавіше, далі буде роз'яснено повний асинхронний і необмежений потік.
Приклад ClickStream
Як приклад, ми хочемо мати потік, який фіксує кліки подій. Щоб зробити це більш складним, скажімо, ми також хочемо групувати події, що трапляються за короткий час один за одним. Таким чином ми могли легко виявити подвійні, потрійні чи десятикратні кліки. Крім того, ми хочемо відфільтрувати всі одиничні кліки. Зробіть глибокий вдих і уявіть, як би ви вирішили цю проблему в обов’язковому порядку. Надіюсь, ніхто не зможе реалізувати рішення, яке працює правильно з першої спроби. Реактивним чином цю проблему вирішувати неважливо. Насправді рішення настільки просте та зрозуміле для реалізації, що ми можемо навіть висловити його на діаграмі, яка безпосередньо описує поведінку коду:
Зображення, зроблене із вступу до реактивного програмування, якого ви не пропустили .
Сірі поля - це функції, що описують, як один потік перетворюється на інший. За допомогою throttle
функції, яку ми накопичуємо кліки протягом 250 мілісекунд, функції map
і filter
повинні бути зрозумілими. Кольорові кулі являють собою подію, а стрілки зображують, як вони протікають через наші функції. Пізніше на етапі обробки ми отримуємо все менше елементів, що протікають через наш потік, оскільки ми групуємо їх разом і фільтруємо. Код цього зображення виглядатиме приблизно так:
val multiClickStream = clickStream
.throttle(250.millis)
.map(clickEvents => clickEvents.length)
.filter(numberOfClicks => numberOfClicks >= 2)
Всю логіку можна представити лише чотирма рядками коду! У Scala ми могли б написати це ще коротше:
val multiClickStream = clickStream.throttle(250.millis).map(_.length).filter(_ >= 2)
Визначення clickStream
є дещо складнішим, але це лише так, тому що прикладна програма працює на JVM, де фіксувати події клацання не легко. Іншим ускладненням є те, що Akka за замовчуванням не забезпечує throttle
функцію. Натомість нам довелося писати це самостійно. Оскільки ця функція є (як це стосується map
абоfilter
функцій) для багаторазового використання у різних випадках використання, я не зараховую ці рядки до кількості рядків, необхідних для реалізації логіки. Однак в імперативних мовах нормально, що логіку не можна легко використовувати таким чином, що різні логічні кроки відбуваються в одному місці, а не застосовуються послідовно, а це означає, що ми, мабуть, неправильно сформували наш код за допомогою логіки дроселювання. Повний код приклад доступний у виглядісуть і більше не буде обговорюватися тут.
Приклад SimpleWebServer
Що слід замість цього обговорити, це ще один приклад. Хоча потік клацань - хороший приклад, щоб Аккі-Потіки могли працювати з прикладом реального світу, йому не вистачає сил для показу паралельного виконання в дії. Наступний приклад представляє невеликий веб-сервер, який може обробляти кілька запитів паралельно. Веб-сервер повинен мати можливість приймати вхідні з'єднання та отримувати від них послідовності байтів, які представляють друковані знаки ASCII. Ці послідовності байт або рядків повинні бути розділені на всі символи нового рядка на менші частини. Після цього сервер повинен відповідати клієнтові кожною з розділених ліній. Крім того, це може зробити щось інше з рядками та дати особливий маркер відповіді, але ми хочемо зробити це простим у цьому прикладі, а тому не вводити жодних фантазійних особливостей. Пам'ятайте, сервер повинен мати можливість обробляти кілька запитів одночасно, що в основному означає, що жоден запит не може блокувати будь-який інший запит від подальшого виконання. Вирішення всіх цих вимог може бути важким в обов'язковому порядку - проте, для потоків Akka Streams нам не потрібно більше кількох рядків, щоб вирішити будь-яку з них. Спочатку давайте огляд самого сервера:
В основному, є лише три основні будівельні блоки. Перший повинен прийняти вхідні з'єднання. Другий повинен обробляти вхідні запити, а третій - надсилати відповідь. Реалізація всіх цих трьох будівельних блоків лише трохи складніше, ніж реалізація потоку кліків:
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
повертає aFuture
, що завершується, коли сервер був запущений або не вдалося запустити, що може бути випадком, коли порт вже прийнятий іншим процесом. Код, однак, не повністю відображає графіку, оскільки ми не можемо побачити будівельний блок, який обробляє відповіді. Причиною цього є те, що з'єднання вже забезпечує цю логіку само собою. Це двонаправлений потік, а не просто однонаправлений, як потоки, які ми бачили в попередніх прикладах. Як і у випадку з матеріалізацією, такі складні потоки тут не пояснюються. В офіційній документації є багато матеріалу для покриття більш складних графіків потоків. Наразі достатньо знати, що 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
. З delimiter
можна розбити ByteString
на більш дрібні частини - в нашому випадку це має статися , коли відбувається символ нового рядка. receiver
це потік, який приймає всі розділені послідовності байтів і перетворює їх у рядок. Це, звичайно, небезпечне перетворення, оскільки тільки друковані символи ASCII повинні бути перетворені в рядок, але для наших потреб це досить добре. responder
є останнім компонентом і відповідає за створення відповіді та перетворення відповіді в послідовність байтів. На відміну від графіки ми не розділили цей останній компонент надвоє, оскільки логіка є тривіальною. Наприкінці ми з'єднуємо всі потоки через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(_))
Тест веб-сервера може виглядати так:
$ # 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 для довільних програм, ми повинні знати, як будувати власні власні структури управління та комбіновані потоки, які дозволяють вирішити складність наших програм. Хороша новина полягає в тому, що Akka Streams був розроблений з урахуванням потреб користувачів, а для того, щоб коротко ознайомитись із більш складними частинами Akka Streams, ми додамо ще кілька функцій на прикладі нашого клієнта / сервера.
Одне, що ми поки не можемо зробити, це закриття зв'язку. У цей момент він починає дещо складніше, оскільки 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 потоку. Недарма нам тут потрібно зробити багато імперативних кроків. В обмін ми маємо більший контроль над поведінкою наших потоків. У наведеному вище прикладі ми вказуємо лише один вхідний і один вихідний порт і робимо їх доступними для системи шляхом переосмислення shape
значення. Крім того, ми визначили так звані InHandler
та а OutHandler
, які в цьому порядку відповідають за отримання та випромінювання елементів. Якщо ви уважно придивились до прикладу потоку повних клацань, то вже слід розпізнати ці компоненти. У InHandler
ми захопимо елемент, і якщо це рядок з одним символом '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
ми створюємо потік , який може видавати тільки один елемент - повідомлення вітання. logic
це те, що було описано як serverLogic
у попередньому розділі. Єдина помітна відмінність - це те, що ми closeConnection
до неї додали . Тепер фактично виходить цікава частина DSL. GraphDSL.create
Функція робить будівельник b
доступний, який використовується , щоб висловити потік у вигляді графіка. За допомогою ~>
функції можна з'єднати вхідні та вихідні порти між собою. Concat
Компонент , який використовується в прикладі , може зчіплювати елементи і тут використовується випереджати повідомлення вітання перед іншими елементами , які виходять зinternalLogic
. В останньому рядку ми робимо доступними лише вхідний порт логіки сервера та вихідний порт об'єднаного потоку, оскільки всі інші порти залишаються деталями реалізації serverLogic
компонента. Для поглибленого ознайомлення з графіком DSL Streams Akka відвідайте відповідний розділ в офіційній документації . Повний приклад коду складного TCP-сервера та клієнта, який може спілкуватися з ним, можна знайти тут . Щоразу, коли ви відкриваєте нове з'єднання від клієнта, ви повинні побачити вітальне повідомлення, а набравши "q"
клієнта, ви повинні побачити повідомлення, яке повідомляє, що з'єднання було скасовано.
Є ще деякі теми, на які ця відповідь не була висвітлена. Особливо матеріалізація може налякати одного чи іншого читача, але я впевнений, що з матеріалів, які тут висвітлюються, кожен повинен мати можливість самостійно пройти наступні кроки. Як уже було сказано, офіційна документація - це гарне місце для продовження навчання про потоки Akka.