Як розпочати роботу з Akka Streams? [зачинено]


222

Бібліотека Акківських потоків вже оснащена досить багатою документацією . Однак головна проблема для мене полягає в тому, що вона дає занадто багато матеріалу - я відчуваю себе досить переповненою кількістю понять, які мені доводиться вивчати. Багато наведених там прикладів відчувають велику вагу і не можуть бути легко переведені на випадки використання в реальному світі, і тому є досить езотеричними. Я думаю, що це дає занадто багато деталей, не пояснюючи, як зібрати всі будівельні блоки разом і як саме це допомагає вирішувати конкретні проблеми.

Є джерела, раковини, потоки, етапи графіків, часткові графіки, матеріалізація, графічний DSL та багато іншого, і я просто не знаю, з чого почати. Посібник із швидкого старту має бути початковим місцем, але я цього не розумію. Це просто кидає в зазначені вище поняття, не пояснюючи їх. Крім того, приклади коду неможливо виконати - є відсутні частини, які роблять мене більш-менш неможливим для слідування тексту.

Хтось може пояснити джерела поняття, раковини, потоки, етапи графіків, часткові графіки, матеріалізацію і, можливо, деякі інші речі, які я пропустив простими словами та простими прикладами, які не пояснюють кожну деталь (і які, ймовірно, не потрібні в будь-якому випадку початок)?


2
Для інформації це обговорюється на мета
DavidG

10
Як перша людина, яка проголосувала за те, щоб закрити це (слідкуючи за потоком Meta), дозвольте спочатку сказати, що ваша відповідь тут чудова . Це дійсно глибоко і, безумовно, дуже корисний ресурс. Однак, на жаль, питання, яке ви задали, є занадто широким для переповнення стека. Якщо якимось чином ваша відповідь може бути розміщена на інше сформульоване питання, то приголомшливо, але я не думаю, що це може бути. Я настійно раджу подати цю заявку як повідомлення в блозі чи щось подібне, яке ви та інші можете використовувати як опорний ресурс у майбутніх відповідях.
Джеймс Доннеллі

2
Я думаю, писати це питання як допис у блозі не було б ефективним. Так, це питання широке - і це справді гарне питання. Звуження його обсягу не покращило б її. Надана відповідь приголомшлива. Я впевнений, що Quora буде радий відібрати бізнес від SO на великі питання.
Майк Слінн

11
@MikeSlinn не намагайтеся обговорювати з людьми про відповідні питання, вони сліпо слідують правилам. Поки питання не знімається, я задоволений і не відчуваю переходу на іншу платформу.
kiritsuku

2
@sschaef Як педантично. Так, звичайно, правила нічого не варті, ваше велике «я» знає набагато краще, і кожен, хто намагається застосовувати правила, просто сліпо слідує ажіотажу. / рент. якщо серйозніше, це було б чудовим доповненням до бета-документації, якщо ви в ній. Ви все ще можете подати заявку та поставити її там, але ви хоч би повинні побачити, що це не дуже підходить для основного сайту.
Фелікс Ганьон-Греньє

Відповіді:


506

Ця відповідь заснована на 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.


4
@monksy Я не планував публікувати це більше ніде. Сміливо публікуйте це у своєму блозі, якщо хочете. API на сьогоднішній день є стабільним у більшості частин, а це означає, що вам, мабуть, навіть не потрібно піклуватися про технічне обслуговування (більшість статей у блогах про потоки Akka Streams там застаріли, оскільки вони показують API, який більше не існує).
kiritsuku

3
Він не зникне. Навіщо це робити?
kiritsuku

2
@sschaef Це може цілком зникнути, оскільки питання поза темою і закрите як таке.
DavidG

7
@Magisch Завжди пам’ятайте: «Ми не видаляємо хороший вміст». Я не зовсім впевнений, але, мабуть, ця відповідь може насправді кваліфікуватись, незважаючи на все.
Дедуплікатор

9
Ця публікація може бути корисною для нової функції документації Stack Overflow - коли вона відкриється для Scala.
SL Barth - Відновити Моніку
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.