Bagaimana cara memulai dengan Akka Streams? [Tutup]

222

Perpustakaan Akka Streams sudah dilengkapi dengan banyak dokumentasi . Namun, masalah utama bagi saya adalah menyediakan terlalu banyak materi - saya merasa sangat kewalahan dengan jumlah konsep yang harus saya pelajari. Banyak contoh yang ditampilkan di sana terasa sangat berat dan tidak mudah diterjemahkan ke kasus penggunaan dunia nyata dan karenanya cukup esoteris. Saya pikir itu memberikan terlalu banyak detail tanpa menjelaskan bagaimana membangun semua blok bangunan bersama dan bagaimana sebenarnya membantu menyelesaikan masalah tertentu.

Ada sumber, sink, aliran, tahapan grafik, grafik parsial, materialisasi, grafik DSL dan banyak lagi dan saya tidak tahu harus mulai dari mana. The panduan cepat ini dimaksudkan untuk menjadi tempat awal tapi saya tidak mengerti. Itu hanya melempar konsep yang disebutkan di atas tanpa menjelaskannya. Selain itu, contoh kode tidak dapat dieksekusi - ada bagian yang hilang yang membuat saya hampir tidak mungkin mengikuti teks.

Adakah yang bisa menjelaskan sumber konsep, tenggelam, mengalir, tahapan grafik, grafik parsial, materialisasi dan mungkin beberapa hal lain yang saya lewatkan dengan kata-kata sederhana dan dengan contoh mudah yang tidak menjelaskan setiap detail tunggal (dan yang mungkin tidak diperlukan pula di awal mula)?

kiritsuku
sumber
2
Untuk info, ini sedang dibahas di meta
DavidG
10
Sebagai orang pertama yang memberikan suara untuk menutup ini (mengikuti utas Meta), izinkan saya mengatakan bahwa jawaban Anda di sini luar biasa . Ini sangat mendalam dan tentu saja merupakan sumber yang sangat membantu. Namun sayangnya pertanyaan yang Anda ajukan terlalu luas untuk Stack Overflow. Jika entah bagaimana jawaban Anda dapat diposting ke pertanyaan dengan kata yang berbeda, maka hebat, tapi saya rasa itu tidak bisa. Saya sangat menyarankan mengirimkan kembali ini sebagai posting blog atau sesuatu yang serupa yang Anda dan orang lain dapat gunakan sebagai sumber referensi dalam jawaban di masa depan.
James Donnelly
2
Saya pikir menulis pertanyaan ini sebagai posting blog tidak akan efektif. Ya, ini adalah pertanyaan luas - dan ini adalah pertanyaan yang sangat bagus. Mempersempit ruang lingkupnya tidak akan memperbaikinya. Jawaban yang diberikan luar biasa. Saya yakin Quora akan senang untuk mengambil bisnis dari SO untuk pertanyaan besar.
Mike Slinn
11
@ MikeSlinn tidak mencoba untuk berdiskusi dengan orang-orang SO tentang pertanyaan yang tepat, mereka secara membabi buta mengikuti aturan. Selama pertanyaan tidak dihapus, saya senang dan tidak merasa pindah ke platform yang berbeda.
kiritsuku
2
@ sschaef Sangat luar biasa. Ya, tentu saja, peraturan tidak ada nilainya, diri Anda yang hebat tahu jauh lebih baik dan semua orang yang mencoba menerapkan aturan hanya secara membabi buta mengikuti hype. / kata-kata kasar. lebih serius, ini akan menjadi tambahan yang bagus untuk dokumentasi beta, jika Anda di dalamnya. Anda masih dapat mendaftar dan menaruhnya di sana, tetapi Anda setidaknya harus melihat bahwa itu tidak cocok untuk situs utama.
Félix Gagnon-Grenier

Jawaban:

506

Jawaban ini didasarkan pada akka-streamversi 2.4.2. API dapat sedikit berbeda di versi lain. Ketergantungan dapat dikonsumsi oleh sbt :

libraryDependencies += "com.typesafe.akka" %% "akka-stream" % "2.4.2"

Baiklah, mari kita mulai. API Akka Streams terdiri dari tiga jenis utama. Berbeda dengan Aliran Reaktif , tipe ini jauh lebih kuat dan karenanya lebih kompleks. Diasumsikan bahwa untuk semua contoh kode definisi berikut sudah ada:

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

The importpernyataan yang diperlukan untuk deklarasi tipe. systemmewakili sistem aktor Akka dan materializermewakili konteks evaluasi aliran. Dalam kasus kami, kami menggunakan a ActorMaterializer, yang berarti aliran dievaluasi di atas aktor. Kedua nilai ditandai sebagai implicit, yang memberikan kompilator Scala kemungkinan untuk menyuntikkan kedua dependensi ini secara otomatis kapan pun mereka dibutuhkan. Kami juga mengimpor system.dispatcher, yang merupakan konteks eksekusi untuk Futures.

API Baru

Akka Streams memiliki properti-properti utama ini:

  • Mereka menerapkan spesifikasi Reactive Streams , yang memiliki tiga tujuan utama backpressure, batas async dan non-blocking serta interoperabilitas antara implementasi yang berbeda sepenuhnya berlaku untuk Akka Streams juga.
  • Mereka memberikan abstraksi untuk mesin evaluasi untuk aliran, yang disebut Materializer.
  • Program dirumuskan sebagai blok bangunan yang dapat digunakan kembali, yang direpresentasikan sebagai tiga jenis utama Source, Sinkdan Flow. Blok penyusun membentuk grafik yang evaluasinya didasarkan pada Materializerdan perlu dipicu secara eksplisit.

Berikut ini pengantar yang lebih dalam tentang bagaimana menggunakan tiga jenis utama akan diberikan.

Sumber

A Sourceadalah pencipta data, berfungsi sebagai sumber input ke aliran. Masing-masing Sourcememiliki saluran output tunggal dan tidak ada saluran input. Semua data mengalir melalui saluran output ke apa pun yang terhubung ke Internet Source.

Sumber

Gambar diambil dari boldradius.com .

A Sourcedapat dibuat dengan berbagai cara:

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

Dalam kasus di atas kami mengumpankan Sourcedengan data hingga, yang berarti mereka akan berakhir pada akhirnya. Orang tidak boleh lupa, bahwa Streaming Reaktif malas dan asinkron secara default. Ini berarti seseorang harus secara eksplisit meminta evaluasi aliran. Dalam Akka Streams ini dapat dilakukan melalui run*metode. Tidak runForeachada bedanya dengan foreachfungsi terkenal - melalui runpenambahan itu membuat eksplisit bahwa kami meminta evaluasi aliran. Karena data terbatas membosankan, kami melanjutkan dengan data tak terbatas:

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

Dengan takemetode ini kita dapat membuat titik berhenti buatan yang mencegah kita mengevaluasi tanpa batas. Karena dukungan aktor sudah ada di dalamnya, kami juga dapat dengan mudah memberi makan aliran dengan pesan yang dikirim ke aktor:

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

Kita dapat melihat bahwa Futuresdieksekusi secara asinkron pada berbagai utas, yang menjelaskan hasilnya. Dalam contoh di atas tidak diperlukan buffer untuk elemen yang masuk dan oleh karena itu OverflowStrategy.failkita dapat mengonfigurasi bahwa aliran harus gagal pada buffer overflow. Terutama melalui antarmuka aktor ini, kami dapat memberi makan aliran melalui sumber data apa pun. Tidak masalah jika data dibuat oleh utas yang sama, oleh yang berbeda, oleh proses lain atau jika mereka berasal dari sistem jarak jauh melalui Internet.

Wastafel

A Sinkpada dasarnya kebalikan dari a Source. Ini adalah titik akhir dari sebuah aliran dan karenanya mengkonsumsi data. A Sinkmemiliki saluran input tunggal dan tidak ada saluran output. Sinkssangat diperlukan ketika kita ingin menentukan perilaku pengumpul data dengan cara yang dapat digunakan kembali dan tanpa mengevaluasi aliran. Metode yang sudah dikenal run*tidak mengizinkan properti ini bagi kami, oleh karena itu lebih disukai untuk menggunakannya Sink.

Wastafel

Gambar diambil dari boldradius.com .

Contoh singkat Sinkaksi dalam:

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

Menghubungkan a Sourceke a Sinkdapat dilakukan dengan tometode ini. Ia mengembalikan apa yang disebut RunnableFlow, yang nantinya akan kita lihat bentuk khusus dari a Flow- aliran yang dapat dieksekusi hanya dengan memanggil run()metodenya.

Alur Runnable

Gambar diambil dari boldradius.com .

Tentu saja mungkin untuk meneruskan semua nilai yang diterima oleh aktor:

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

Mengalir

Sumber data dan sink sangat bagus jika Anda memerlukan koneksi antara aliran Akka dan sistem yang sudah ada tetapi orang tidak dapat melakukan apa pun dengan mereka. Flows adalah bagian terakhir yang hilang dalam abstraksi dasar Akka Streams. Mereka bertindak sebagai penghubung antara aliran yang berbeda dan dapat digunakan untuk mengubah elemen-elemennya.

Mengalir

Gambar diambil dari boldradius.com .

Jika a Flowterhubung ke Sourceyang baru Sourceadalah hasilnya. Demikian juga, Flowterhubung ke Sinkmenciptakan yang baru Sink. Dan Flowterhubung dengan a Sourcedan Sinkhasil dalam a RunnableFlow. Oleh karena itu, mereka duduk di antara input dan saluran output tetapi dengan sendirinya tidak sesuai dengan salah satu rasa selama mereka tidak terhubung dengan salah satu Sourceatau a Sink.

Aliran Penuh

Gambar diambil dari boldradius.com .

Untuk mendapatkan pemahaman yang lebih baik Flows, kita akan melihat beberapa contoh:

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

Melalui viametode ini kita dapat menghubungkan a Sourcedengan a Flow. Kita perlu menentukan tipe input karena kompiler tidak dapat menyimpulkannya untuk kita. Seperti yang sudah kita lihat dalam contoh sederhana ini, arus invertdan doublesepenuhnya independen dari produsen dan konsumen data. Mereka hanya mengubah data dan meneruskannya ke saluran keluaran. Ini berarti bahwa kami dapat menggunakan kembali aliran di antara banyak aliran:

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

s1dan s2mewakili aliran yang sama sekali baru - mereka tidak membagikan data apa pun melalui blok bangunan mereka.

Streaming Data Tidak Terbatas

Sebelum melanjutkan, pertama-tama kita harus meninjau kembali beberapa aspek kunci dari Aliran Reaktif. Sejumlah elemen tanpa batas dapat tiba di titik mana pun dan dapat mengalirkan aliran di berbagai negara. Selain dari aliran yang dapat dijalankan, yang merupakan keadaan biasa, aliran dapat dihentikan baik melalui kesalahan atau melalui sinyal yang menunjukkan bahwa tidak ada data lebih lanjut akan datang. Aliran dapat dimodelkan dengan cara grafis dengan menandai peristiwa pada timeline seperti halnya di sini:

Menunjukkan bahwa aliran adalah urutan peristiwa yang sedang berlangsung yang dipesan dalam waktu

Gambar diambil dari Pengantar Pemrograman Reaktif yang Anda lewatkan .

Kita telah melihat aliran yang bisa dijalankan dalam contoh-contoh bagian sebelumnya. Kami mendapatkan RunnableGraphsetiap kali aliran benar-benar dapat terwujud, yang berarti bahwa Sinkterhubung ke a Source. Sejauh ini kami selalu terwujud dengan nilai Unit, yang dapat dilihat pada jenis:

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)

Untuk Sourcedan Sinktipe tipe kedua dan untuk Flowtipe tipe ketiga menunjukkan nilai material. Sepanjang jawaban ini, makna penuh dari materialisasi tidak akan dijelaskan. Namun, perincian lebih lanjut tentang materialisasi dapat ditemukan di dokumentasi resmi . Untuk saat ini satu-satunya hal yang perlu kita ketahui adalah bahwa nilai yang terwujud adalah apa yang kita dapatkan ketika kita menjalankan stream. Karena kami hanya tertarik pada efek samping sejauh ini, kami mendapat Unitnilai terwujudnya. Pengecualian untuk ini adalah materialisasi dari wastafel, yang menghasilkan a Future. Itu memberi kami kembaliFuture, karena nilai ini dapat menunjukkan ketika aliran yang terhubung ke wastafel telah berakhir. Sejauh ini, contoh kode sebelumnya bagus untuk menjelaskan konsep tetapi mereka juga membosankan karena kita hanya berurusan dengan aliran terbatas atau dengan yang tak terbatas sangat sederhana. Untuk membuatnya lebih menarik, berikut ini aliran asinkron dan tidak terikat penuh akan dijelaskan.

Contoh ClickStream

Sebagai contoh, kami ingin memiliki aliran yang menangkap peristiwa klik. Untuk membuatnya lebih menantang, katakanlah kita juga ingin mengelompokkan acara klik yang terjadi dalam waktu singkat setelah satu sama lain. Dengan cara ini kami dapat dengan mudah menemukan klik dua kali lipat, tiga kali lipat, atau sepuluh kali lipat. Selain itu, kami ingin memfilter semua klik tunggal. Ambil napas dalam-dalam dan bayangkan bagaimana Anda akan memecahkan masalah itu secara imperatif. Saya yakin tidak ada yang bisa menerapkan solusi yang berfungsi dengan benar pada percobaan pertama. Secara reaktif masalah ini sepele untuk dipecahkan. Sebenarnya, solusinya sangat sederhana dan mudah untuk diterapkan sehingga kita bahkan dapat mengekspresikannya dalam diagram yang secara langsung menggambarkan perilaku kode:

Logika contoh aliran klik

Gambar diambil dari Pengantar Pemrograman Reaktif yang Anda lewatkan .

Kotak abu-abu adalah fungsi yang menggambarkan bagaimana satu aliran diubah menjadi aliran lain. Dengan throttlefungsi yang kami kumpulkan klik dalam 250 milidetik, mapdan filterfungsi harus jelas. Bola warna mewakili suatu peristiwa dan panah menggambarkan bagaimana mereka mengalir melalui fungsi kita. Kemudian dalam langkah-langkah pemrosesan, kami mendapatkan semakin sedikit elemen yang mengalir melalui aliran kami, karena kami mengelompokkannya dan menyaringnya. Kode untuk gambar ini akan terlihat seperti ini:

val multiClickStream = clickStream
    .throttle(250.millis)
    .map(clickEvents => clickEvents.length)
    .filter(numberOfClicks => numberOfClicks >= 2)

Seluruh logika hanya dapat direpresentasikan dalam empat baris kode! Di Scala, kita bisa menulisnya lebih pendek:

val multiClickStream = clickStream.throttle(250.millis).map(_.length).filter(_ >= 2)

Definisi clickStreamsedikit lebih kompleks tetapi ini hanya terjadi karena contoh program berjalan pada JVM, di mana menangkap peristiwa klik tidak mudah dilakukan. Komplikasi lain adalah bahwa Akka secara default tidak menyediakan throttlefungsi. Sebaliknya kami harus menulisnya sendiri. Karena fungsi ini (seperti halnya untuk mapatau filterfungsi) dapat digunakan kembali di kasus penggunaan yang berbeda saya tidak menghitung garis ini untuk jumlah baris yang kami butuhkan untuk melaksanakan logika. Namun dalam bahasa imperatif, adalah normal bahwa logika tidak dapat digunakan kembali dengan mudah dan bahwa langkah-langkah logis yang berbeda terjadi semuanya di satu tempat alih-alih diterapkan secara berurutan, yang berarti bahwa kita mungkin akan salah mengubah kode kita dengan logika pelambatan. Contoh kode lengkap tersedia sebagaiinti dan tidak akan dibahas di sini lebih jauh.

Contoh SimpleWebServer

Yang seharusnya dibahas adalah contoh lain. Meskipun aliran klik adalah contoh yang bagus untuk membiarkan Akka Streams menangani contoh dunia nyata, ia tidak memiliki kekuatan untuk menunjukkan eksekusi paralel dalam aksi. Contoh berikutnya harus mewakili server web kecil yang dapat menangani beberapa permintaan secara paralel. Server web harus dapat menerima koneksi masuk dan menerima urutan byte dari mereka yang mewakili tanda ASCII yang dapat dicetak. Urutan atau string byte ini harus dipisah di semua karakter-baris baru menjadi bagian-bagian yang lebih kecil. Setelah itu, server akan menanggapi klien dengan masing-masing garis yang dipisah. Atau, itu bisa melakukan sesuatu yang lain dengan garis dan memberikan token jawaban khusus, tetapi kami ingin tetap sederhana dalam contoh ini dan karena itu tidak memperkenalkan fitur mewah. Ingat, server harus dapat menangani beberapa permintaan secara bersamaan, yang pada dasarnya berarti bahwa tidak ada permintaan yang diizinkan untuk memblokir permintaan lain dari eksekusi lebih lanjut. Memecahkan semua persyaratan ini bisa sulit dengan cara yang sangat penting - dengan Akka Streams, kita seharusnya tidak perlu lebih dari beberapa baris untuk menyelesaikan semua ini. Pertama, mari kita lihat ikhtisar server itu sendiri:

server

Pada dasarnya, hanya ada tiga blok bangunan utama. Yang pertama harus menerima koneksi masuk. Yang kedua perlu menangani permintaan yang masuk dan yang ketiga perlu mengirim respons. Menerapkan ketiga blok bangunan ini hanya sedikit lebih rumit daripada menerapkan aliran klik:

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}")
  }
}

Fungsi ini mkServermengambil (selain dari alamat dan port server) juga sistem aktor dan materializer sebagai parameter implisit. Aliran kontrol server diwakili oleh binding, yang mengambil sumber koneksi masuk dan meneruskannya ke wastafel koneksi masuk. Di dalam connectionHandler, yang merupakan wastafel kami, kami menangani setiap koneksi dengan aliran serverLogic, yang akan dijelaskan nanti. bindingmengembalikan aFuture, yang selesai ketika server sudah mulai atau gagal, yang bisa jadi kasus ketika port sudah diambil oleh proses lain. Namun kode tersebut tidak sepenuhnya mencerminkan grafik karena kami tidak dapat melihat blok penyusun yang menangani respons. Alasan untuk ini adalah bahwa koneksi sudah menyediakan logika ini dengan sendirinya. Ini adalah aliran dua arah dan bukan hanya aliran satu arah seperti aliran yang telah kita lihat dalam contoh sebelumnya. Seperti halnya untuk materialisasi, aliran kompleks seperti itu tidak akan dijelaskan di sini. The dokumentasi resmi memiliki banyak bahan untuk menutupi grafik aliran yang lebih kompleks. Untuk saat ini sudah cukup untuk mengetahui yang Tcp.IncomingConnectionmewakili koneksi yang tahu bagaimana menerima permintaan dan bagaimana mengirim tanggapan. Bagian yang masih hilang adalahserverLogicblok bangunan. Ini bisa terlihat seperti ini:

logika server

Sekali lagi, kita dapat membagi logika menjadi beberapa blok bangunan sederhana yang semuanya membentuk alur program kita. Pertama kita ingin membagi urutan byte dalam garis, yang harus kita lakukan setiap kali kita menemukan karakter baris baru. Setelah itu, byte dari setiap baris perlu dikonversi ke string karena bekerja dengan byte mentah adalah rumit. Secara keseluruhan kami dapat menerima aliran biner dari protokol yang rumit, yang akan membuat bekerja dengan data mentah yang masuk sangat menantang. Setelah kami memiliki string yang dapat dibaca, kami dapat membuat jawaban. Untuk alasan kesederhanaan, jawabannya bisa apa saja dalam kasus kami. Pada akhirnya, kita harus mengonversi kembali jawaban kita ke urutan byte yang dapat dikirim melalui kabel. Kode untuk seluruh logika mungkin terlihat seperti ini:

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)
}

Kita sudah tahu bahwa itu serverLogicadalah aliran yang membutuhkan ByteStringdan harus menghasilkan a ByteString. Dengan delimiterkita dapat membaginya menjadi ByteStringbagian-bagian yang lebih kecil - dalam kasus kami itu perlu terjadi setiap kali karakter baris baru terjadi. receiveradalah aliran yang mengambil semua urutan byte split dan mengubahnya menjadi string. Ini tentu saja konversi yang berbahaya, karena hanya karakter ASCII yang dapat dicetak yang harus dikonversi ke string tetapi untuk kebutuhan kita cukup bagus. responderadalah komponen terakhir dan bertanggung jawab untuk membuat jawaban dan mengubah jawaban kembali ke urutan byte. Berbeda dengan gambar, kami tidak membagi komponen terakhir ini menjadi dua, karena logikanya sepele. Pada akhirnya, kami menghubungkan semua aliran melaluiviafungsi. Pada titik ini orang mungkin bertanya apakah kami merawat properti multi-pengguna yang disebutkan di awal. Dan memang kami melakukannya meskipun itu mungkin tidak segera jelas. Dengan melihat grafik ini, seharusnya menjadi lebih jelas:

server dan logika server digabungkan

The serverLogickomponen tidak lain adalah sebuah aliran yang berisi arus yang lebih kecil. Komponen ini mengambil input, yang merupakan permintaan, dan menghasilkan output, yang merupakan respons. Karena aliran dapat dibangun beberapa kali dan semuanya bekerja secara independen satu sama lain, kami mencapai melalui properti properti multi-pengguna kami. Setiap permintaan ditangani dalam permintaannya sendiri dan oleh karena itu permintaan jangka pendek dapat mengalahkan permintaan jangka panjang yang telah dimulai sebelumnya. Jika Anda bertanya-tanya, definisi serverLogicyang ditunjukkan sebelumnya tentu saja dapat ditulis jauh lebih pendek dengan menguraikan sebagian besar definisi dalamnya:

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(_))

Tes server web mungkin terlihat seperti ini:

$ # 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?

Agar contoh kode di atas berfungsi dengan benar, pertama-tama kita perlu menjalankan server, yang digambarkan oleh startServerskrip:

$ # 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?

Contoh kode lengkap dari server TCP sederhana ini dapat ditemukan di sini . Kami tidak hanya dapat menulis server dengan Akka Streams tetapi juga klien. Ini mungkin terlihat seperti ini:

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()

Kode lengkap klien TCP dapat ditemukan di sini . Kode ini terlihat sangat mirip tetapi berbeda dengan server kami tidak perlu lagi mengelola koneksi yang masuk.

Grafik Kompleks

Pada bagian sebelumnya kita telah melihat bagaimana kita dapat membangun program sederhana dari aliran. Namun, pada kenyataannya seringkali tidak cukup hanya mengandalkan fungsi yang sudah terpasang untuk membangun aliran yang lebih kompleks. Jika kami ingin dapat menggunakan Akka Streams untuk program yang sewenang-wenang, kami perlu tahu bagaimana membangun struktur kontrol kustom kami sendiri dan aliran yang dapat digabung yang memungkinkan kami menangani kompleksitas aplikasi kami. Berita baiknya adalah Akka Streams dirancang untuk disesuaikan dengan kebutuhan pengguna dan untuk memberi Anda pengantar singkat ke bagian-bagian yang lebih kompleks dari Akka Streams, kami menambahkan beberapa fitur lagi ke contoh klien / server kami.

Satu hal yang belum bisa kami lakukan adalah menutup koneksi. Pada titik ini mulai menjadi sedikit lebih rumit karena API aliran yang telah kita lihat sejauh ini tidak memungkinkan kita untuk menghentikan aliran pada titik arbitrer. Namun, ada GraphStageabstraksi, yang dapat digunakan untuk membuat tahapan pemrosesan grafik sembarang dengan sejumlah port input atau output. Pertama-tama mari kita lihat sisi server, tempat kami memperkenalkan komponen baru, yang disebut 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 ini terlihat jauh lebih rumit daripada aliran API. Tidak heran, kita harus melakukan banyak langkah penting di sini. Sebagai gantinya, kami memiliki kontrol lebih besar atas perilaku aliran kami. Dalam contoh di atas, kami hanya menentukan satu input dan satu port output dan membuatnya tersedia untuk sistem dengan menimpa shapenilainya. Selanjutnya kami mendefinisikan apa yang disebut InHandlerdan a OutHandler, yang dalam urutan ini bertanggung jawab untuk menerima dan memancarkan elemen. Jika Anda melihat dekat dengan contoh aliran klik penuh, Anda harus sudah mengenali komponen-komponen ini. Di InHandlerkami ambil elemen dan jika itu adalah string dengan karakter tunggal 'q', kami ingin menutup aliran. Untuk memberi klien kesempatan untuk mengetahui bahwa aliran akan segera ditutup, kami memancarkan string"BYE"dan kemudian kita segera menutup panggung sesudahnya. The closeConnectionkomponen dapat digabungkan dengan aliran melalui viametode, yang diperkenalkan di bagian tentang arus.

Selain bisa menutup koneksi, alangkah baiknya jika kita bisa menampilkan pesan selamat datang ke koneksi yang baru dibuat. Untuk melakukan ini kita sekali lagi harus melangkah lebih jauh:

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)
})

Fungsi serverLogic sekarang mengambil koneksi yang masuk sebagai parameter. Di dalam tubuhnya kami menggunakan DSL yang memungkinkan kami untuk menggambarkan perilaku aliran yang kompleks. Dengan welcomekami membuat aliran yang hanya bisa memancarkan satu elemen - pesan selamat datang. logicadalah apa yang dijelaskan serverLogicpada bagian sebelumnya. Satu-satunya perbedaan penting adalah bahwa kami menambahkannya closeConnection. Sekarang sebenarnya datang bagian yang menarik dari DSL. The GraphDSL.createFungsi membuat pembangun byang tersedia, yang digunakan untuk mengekspresikan aliran sebagai grafik. Dengan ~>fungsi ini dimungkinkan untuk menghubungkan port input dan output satu sama lain. The Concatkomponen yang digunakan dalam contoh dapat menggabungkan unsur-unsur dan di sini digunakan untuk tambahkan pesan selamat datang di depan unsur-unsur lain yang keluar dariinternalLogic. Pada baris terakhir, kami hanya membuat port input dari logika server dan port output dari aliran gabungan karena semua port lain akan tetap menjadi detail implementasi serverLogickomponen. Untuk pengantar mendalam ke grafik DSL Akka Streams, kunjungi bagian terkait di dokumentasi resmi . Contoh kode lengkap dari server TCP kompleks dan klien yang dapat berkomunikasi dengannya dapat ditemukan di sini . Setiap kali Anda membuka koneksi baru dari klien, Anda akan melihat pesan sambutan dan dengan mengetik "q"pada klien Anda akan melihat pesan yang memberitahu Anda bahwa koneksi telah dibatalkan.

Masih ada beberapa topik yang tidak dicakup oleh jawaban ini. Khususnya materialisasi dapat menakuti satu pembaca atau yang lain, tetapi saya yakin dengan materi yang dibahas di sini setiap orang harus dapat melakukan langkah selanjutnya sendiri. Seperti yang sudah dikatakan, dokumentasi resmi adalah tempat yang bagus untuk terus belajar tentang Akka Streams.

kiritsuku
sumber
4
@monksy Saya tidak berencana untuk menerbitkan ini di tempat lain. Silakan mempublikasikan ini di blog Anda jika Anda mau. API saat ini stabil di sebagian besar bagian, yang berarti Anda mungkin bahkan tidak perlu peduli dengan pemeliharaan (sebagian besar artikel blog tentang Akka Streaming di luar sana sudah usang karena mereka menunjukkan API yang tidak ada lagi).
kiritsuku
3
Itu tidak akan hilang. Kenapa harus begitu?
kiritsuku
2
@ sschaef Mungkin menghilang karena pertanyaannya di luar topik dan telah ditutup.
DavidG
7
@Magisch Selalu ingat: "Kami tidak menghapus konten yang bagus." Saya tidak yakin, tapi saya kira jawaban ini mungkin benar-benar memenuhi syarat, terlepas dari segalanya.
Deduplicator
9
Posting ini mungkin baik untuk fitur Dokumentasi baru Stack Overflow - setelah itu terbuka untuk Scala.
SL Barth - Reinstate Monica