Akka Akışı'na nasıl başlanır? [kapalı]


222

Akka Streams kütüphanesi zaten oldukça zengin bir dokümantasyon ile geliyor . Ancak, benim için asıl sorun, çok fazla malzeme sağlaması - öğrenmem gereken kavramların sayısından çok bunalmış hissediyorum. Orada gösterilen birçok örnek çok ağır hissediyor ve gerçek dünyadaki kullanım durumlarına kolayca çevrilemiyor ve bu nedenle oldukça ezoterik. Tüm yapı taşlarını birlikte nasıl inşa edeceğinizi ve belirli sorunları çözmeye tam olarak nasıl yardımcı olduğunu açıklamadan çok fazla ayrıntı verdiğini düşünüyorum.

Kaynaklar, lavabolar, akışlar, grafik aşamaları, kısmi grafikler, materyalizasyon, bir grafik DSL ve çok daha fazlası var ve nereden başlayacağımı bilmiyorum. Hızlı başlangıç kılavuzu bir başlangıç yeri olması gerekiyordu ama bunu anlamıyorum. Sadece yukarıda belirtilen kavramları açıklamadan atar. Ayrıca kod örnekleri yürütülemez - metnin takip edilmesini neredeyse imkansız kılan eksik parçalar var.

Herkes kavramları, lavaboları, akışları, grafik aşamalarını, kısmi grafikleri, materyalizasyonu ve belki de basit sözcüklerle ve her ayrıntıyı açıklamayan (ve muhtemelen yine de ihtiyaç duyulmayan) kolay örneklerle kaçırdığım bazı şeyleri açıklayabilir mi? başlangıç)?


2
Bilgi için, bu meta
DavidG

10
Bunu kapatmak için oy veren ilk kişi olarak (Meta iş parçacığının ardından), öncelikle burada cevabınızın harika olduğunu söyleyeyim . Gerçekten derinlemesine ve kesinlikle çok yararlı bir kaynak. Ancak maalesef sorduğunuz soru Yığın Taşması için çok geniş. Bir şekilde cevabınız farklı bir şekilde ifade edilen bir soruya gönderilebilirse, o zaman harika, ama bunun olabileceğini düşünmüyorum. Bunu bir blog yazısı veya kendiniz ve başkalarının gelecekteki cevaplarda referans kaynağı olarak kullanabileceği benzer bir şey olarak tekrar göndermenizi şiddetle tavsiye ederim.
James Donnelly

2
Bu soruyu bir blog yazısı olarak yazmak etkili olmayacağını düşünüyorum. Evet, bu geniş bir soru - ve bu gerçekten iyi bir soru. Kapsamını daraltmak onu geliştirmeyecektir. Verilen cevap müthiş. Eminim Quora, büyük sorular için SO'dan iş almaktan mutluluk duyacaktır.
Mike Slinn

11
@MikeSlinn, SO ile uygun sorular hakkında tartışmaya çalışmaz, kurallara körü körüne uyarlar. Soru kaldırılmadığı sürece mutluyum ve farklı bir platforma geçmeyi düşünmüyorum.
kiritsuku

2
@sschaef Ne kadar bilgiçlik. Evet, elbette, kurallar hiçbir şeye değmez, büyük benliğiniz çok daha iyi bilir ve kuralları uygulamaya çalışan herkes sadece körü körüne takip ediyor. / farfaralık. daha da ciddisi, bu belgelerin beta sürümüne büyük bir katkı olur. Yine de uygulayabilir ve oraya koyabilirsiniz, ancak en azından ana site için mükemmel bir uygun olmadığını görmelisiniz.
Félix Gagnon-Grenier

Yanıtlar:


506

Bu cevap akka-streamsürüme dayanmaktadır 2.4.2. API, diğer sürümlerde biraz farklı olabilir. Bağımlılık sbt tarafından tüketilebilir :

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

Tamam, başlayalım. Akka Akışlarının API'si üç ana türden oluşur. Reaktif Akımların aksine , bu türler çok daha güçlü ve dolayısıyla daha karmaşıktır. Tüm kod örnekleri için aşağıdaki tanımların zaten var olduğu varsayılmaktadır:

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İfadeleri tip bildirimleri için gereklidir. systemAkka'nın aktör sistemini materializer, akışın değerlendirme bağlamını temsil eder. Bizim durumumuzda a kullanırız ActorMaterializer, yani akarsu aktörlerin üstünde değerlendirilir. Her iki değer olarak işaretlenir implicit, bu da Scala derleyicisine gerektiğinde bu iki bağımlılığı otomatik olarak ekleme imkanı verir. Ayrıca system.dispatcher, bir yürütme bağlamı olan ithalat Futures.

Yeni Bir API

Akka Akışları şu temel özelliklere sahiptir:

  • Onlar uygulamak Reaktif Akışları spesifikasyonu olan üç ana hedefleri karşı basıncı, zaman uyumsuz ve engellenmeyen sınırları ve birlikte çalışabilirlik farklı uygulamalar arasında tam da Akka Akışları başvurabilirim.
  • Akımlar için bir değerlendirme motoru için bir soyutlama sağlarlar Materializer.
  • Programlar, üç ana tür olarak temsil edilir yeniden yapı taşları olarak formüle edilir Source, Sinkve Flow. Yapı taşları, değerlendirmesine dayanan Materializerve açıkça tetiklenmesi gereken bir grafik oluşturur .

Aşağıda üç ana tipin nasıl kullanılacağı hakkında daha derin bir giriş yapılacaktır.

Kaynak

A Sourcebir veri yaratıcısıdır, akış için bir giriş kaynağı görevi görür. Her Sourcebirinin tek bir çıkış kanalı vardır ve giriş kanalı yoktur. Tüm veriler çıkış kanalından neye bağlı olursa akar Source.

Kaynak

Görüntü boldradius.com'dan alınmıştır .

A Source, birden çok şekilde oluşturulabilir:

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

Yukarıdaki durumlarda Sourcesonlu verilerle besledik , yani sonunda sona erecekler. Unutulmamalıdır ki, Reaktif Akımlar varsayılan olarak tembel ve eşzamansızdır. Bu, derhal akışın değerlendirilmesini talep etmek zorunda olduğu anlamına gelir. Akka Akarsularında bu run*yöntemlerle yapılabilir . runForeachİyi bilinen hiçbir farklı olurdu foreacharacılığıyla - fonksiyonu runbiz akışının bir değerlendirme için sormak açık hale getirir yanı. Sonlu veriler sıkıcı olduğu için sonsuz olanla devam ediyoruz:

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

takeYöntem ile süresiz olarak değerlendirmemizi engelleyen yapay bir durma noktası oluşturabiliriz. Aktör desteği yerleşik olduğundan, akışı bir aktöre gönderilen mesajlarla kolayca besleyebiliriz:

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

FuturesSonucunu açıklayan farklı evrelerde eşzamansız olarak yürütüldüğünü görebiliriz . Yukarıdaki örnekte, gelen elemanlar için bir tampon gerekli değildir ve bu nedenle OverflowStrategy.fail, akımın bir arabellek taşması üzerinde başarısız olması gerektiğini yapılandırabiliriz. Özellikle bu aktör arayüzü ile akışı herhangi bir veri kaynağı aracılığıyla besleyebiliriz. Verilerin aynı iş parçacığı tarafından, farklı bir iş parçacığı tarafından, başka bir işlem tarafından mı yoksa İnternet üzerinden uzak bir sistemden mi geldiği önemli değildir.

Lavabo

A Sinktemel olarak a'nın tersidir Source. Bir akışın uç noktasıdır ve bu nedenle veri tüketir. A'nın Sinktek bir giriş kanalı vardır ve çıkış kanalı yoktur. Sinksözellikle veri toplayıcının davranışını yeniden kullanılabilir bir şekilde ve akışı değerlendirmeden belirtmek istediğimizde gereklidir. Zaten bilinen run*yöntemler bize bu özelliklere izin vermez, bu nedenle bunun Sinkyerine kullanılması tercih edilir .

Lavabo

Görüntü boldradius.com'dan alınmıştır .

SinkEylemdeki kısa bir örnek :

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

Bir bağlama Sourcea Sinkile yapılabilir toyöntemle. Bu RunnableFlow, daha sonra Flowsadece run()yöntemini çağırarak yürütülebilen bir akışın özel bir biçimini göreceğimiz gibi denir .

Çalıştırılabilir Akış

Görüntü boldradius.com'dan alınmıştır .

Bir lavaboya gelen tüm değerleri bir aktöre iletmek elbette mümkündür:

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

Akış

Akka akışları ve mevcut bir sistem arasında bir bağlantıya ihtiyacınız varsa veri kaynakları ve lavabolar mükemmeldir, ancak onlarla gerçekten bir şey yapamazsınız. Akka Akarsu üssü soyutlamasındaki akışlar eksik olan son parçadır. Farklı akışlar arasında bir bağlayıcı görevi görürler ve öğelerini dönüştürmek için kullanılabilirler.

Akış

Görüntü boldradius.com'dan alınmıştır .

Bir Eğer Flowbir bağlandığında Sourcea yeni Sourcesonucudur. Benzer şekilde, bir ile Flowbağlantılı Sinkbir yeni oluşturur Sink. Ve Flowhem a hem de a Sourceile bağlantılı bir Sinksonuç RunnableFlow. Bu nedenle, giriş ve çıkış kanalı arasında otururlar, ancak a Sourceveya a'ya bağlı olmadıkları sürece kendi başlarına lezzetlerden birine karşılık gelmezler Sink.

Tam Akış

Görüntü boldradius.com'dan alınmıştır .

Daha iyi anlayabilmek Flowsiçin bazı örneklere bakacağız:

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 viayöntemle bir bağlayabilirsiniz Sourcebir ile Flow. Giriş türünü belirtmeliyiz çünkü derleyici bizim için çıkarım yapamaz. Bu basit örnekte zaten görebildiğimiz gibi, akışlar invertve doubleherhangi bir veri üreticisi ve tüketicisinden tamamen bağımsızdır. Yalnızca verileri dönüştürür ve çıkış kanalına iletirler. Bu, birden fazla akış arasında bir akışı yeniden kullanabileceğimiz anlamına gelir:

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

s1ve s2tamamen yeni akışları temsil ediyorlar - yapı taşları üzerinden herhangi bir veri paylaşmıyorlar.

Sınırsız Veri Akışları

Devam etmeden önce Reaktif Akımların bazı önemli yönlerini tekrar gözden geçirmeliyiz. Sınırsız sayıda öğe herhangi bir noktaya ulaşabilir ve farklı durumlara bir akış koyabilir. Her zamanki durum olan çalıştırılabilir bir akışın yanı sıra, bir akış ya bir hata ya da başka verinin gelmeyeceğini gösteren bir sinyal aracılığıyla durdurulabilir. Akış, zaman çizelgesindeki olayları burada olduğu gibi işaretleyerek grafiksel olarak modellenebilir:

Bir akışın zamanında sıralanan devam eden olaylar dizisi olduğunu gösterir

Eksik olduğunuz Reaktif Programlamaya giriş bölümünden alınan resim .

Önceki bölümdeki örneklerde çalıştırılabilir akışlar gördük. RunnableGraphBir akışın gerçekleşebileceği bir zaman elde ederiz , bu da a'nın Sinka'ya bağlı olduğu anlamına gelir Source. Şimdiye kadar her zaman Unittürlerde görülebilen değere ulaştık :

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)

For Sourceve Sinkikinci tip parametresi ve Flowüçüncü tip parametresi için gerçekleşen değer belirtilir. Bu cevap boyunca maddileşmenin tam anlamı açıklanmayacaktır. Bununla birlikte, maddileştirme ile ilgili daha fazla ayrıntı resmi belgelerde bulunabilir . Şimdilik bilmemiz gereken tek şey, gerçekleşen değerin bir akış yürüttüğümüzde elde ettiğimiz değer olmasıdır. Şimdiye kadar sadece yan etkilerle ilgilendiğimiz Unitiçin somutlaştırılmış değer elde ettik . Bunun istisnası, bir lavabonun gerçekleşmesi idi Future. Bize geri verdiFutureçünkü bu değer, lavaboya bağlı akımın ne zaman sona erdiğini gösterebilir. Şimdiye kadar, önceki kod örnekleri kavramı açıklamak güzeldi, ama onlar da sıkıcıydı çünkü sadece sonlu akışlarla veya çok basit sonsuzlarla uğraştık. Daha ilginç hale getirmek için, aşağıda tam bir eşzamansız ve sınırsız akış açıklanacaktır.

ClickStream Örneği

Örnek olarak, tıklama etkinliklerini yakalayan bir akışımız olmasını istiyoruz. Bunu daha da zorlaştırmak için, birbirinden kısa bir süre sonra gerçekleşen tıklama etkinliklerini de gruplandırmak istediğimizi varsayalım. Bu şekilde çift, üçlü veya on kat tıklamaları kolayca keşfedebiliriz. Ayrıca, tüm tek tıklamaları filtrelemek istiyoruz. Derin bir nefes alın ve bu problemi zorunlu bir şekilde nasıl çözeceğinizi hayal edin. Eminim hiç kimse ilk denemede doğru çalışan bir çözüm uygulayamaz. Tepkisel bir şekilde bu sorunun çözülmesi önemsizdir. Aslında, çözümü uygulamak o kadar basit ve basittir ki, bunu doğrudan kodun davranışını açıklayan bir diyagramda bile ifade edebiliriz:

Tıklama akışı örneğinin mantığı

Eksik olduğunuz Reaktif Programlamaya giriş bölümünden alınan resim .

Gri kutular, bir akışın diğerine nasıl dönüştürüldüğünü tanımlayan işlevlerdir. throttleİşlev ile 250 milisaniye içinde tıklama biriktiririz ve mapve filterişlevleri kendi kendini açıklayıcı olmalıdır. Renkli küreler bir olayı temsil eder ve oklar fonksiyonlarımızdan nasıl aktıklarını gösterir. Daha sonra işleme adımlarında, akışımıza akan gittikçe daha az element alıyoruz, çünkü bunları bir araya getirip filtreliyoruz. Bu resmin kodu şöyle görünecektir:

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

Bütün mantık sadece dört kod satırında gösterilebilir! Scala'da daha da kısa yazabiliriz:

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

Tanımı clickStreambiraz daha karmaşıktır, ancak bu sadece örnek programın tıklama olaylarının yakalanmasının kolayca mümkün olmadığı JVM'de çalıştığı için geçerlidir. Başka bir komplikasyon, Akka'nın varsayılan olarak throttleişlevi sağlamamasıdır . Bunun yerine bunu kendimiz yazmak zorunda kaldık. Bu işlev ( mapveya filterişlevleri için olduğu gibi ) farklı kullanım durumlarında yeniden kullanılabilir olduğundan, bu satırları mantığı uygulamak için gereken satır sayısına saymıyorum. Ancak zorunlu dillerde, mantığın bu kadar kolay bir şekilde tekrar kullanılamayacağı ve farklı mantıksal adımların sırayla uygulanması yerine tek bir yerde gerçekleşmesi normaldir; Tam kod örneği,özü ve daha fazla Burada tartışılan edilmeyecektir.

SimpleWebServer Örneği

Bunun yerine tartışılması gereken başka bir örnek. Tıklama akışı, Akka Akışlarının gerçek bir dünya örneğini ele almasına izin vermek için güzel bir örnek olsa da, paralel yürütmeyi eylem halinde gösterme gücünden yoksundur. Sonraki örnek, birden çok isteği paralel olarak işleyebilen küçük bir web sunucusunu temsil edecektir. Ağ kesimi gelen bağlantıları kabul edebilecek ve bunlardan yazdırılabilir ASCII işaretlerini temsil eden bayt dizileri alabilecektir. Bu bayt dizileri veya dizeleri tüm yeni satır karakterlerinde daha küçük parçalara bölünmelidir. Bundan sonra, sunucu istemciye bölünmüş satırların her biri ile cevap verecektir. Alternatif olarak, çizgilerle başka bir şey yapabilir ve özel bir cevap jetonu verebilir, ancak bu örnekte basit tutmak istiyoruz ve bu nedenle herhangi bir fantezi özellik sunmuyoruz. Hatırlamak, sunucunun aynı anda birden fazla isteği işleyebilmesi gerekir, bu da temelde hiçbir isteğin başka bir isteğin daha fazla yürütülmesini engellemesine izin verilmediği anlamına gelir. Tüm bu gereklilikleri çözmek zor olabilir - Akka Akışı ile, bunların herhangi birini çözmek için birkaç hatta daha fazla ihtiyacımız olmamalıdır. İlk olarak, sunucunun kendisine bir genel bakış yapalım:

sunucu

Temel olarak, sadece üç ana yapı taşı vardır. İlki gelen bağlantıları kabul etmelidir. İkincisinin gelen istekleri yerine getirmesi ve üçüncüsünün yanıt göndermesi gerekir. Bu üç yapı taşının tümünü uygulamak, tıklama akışını uygulamaktan sadece biraz daha karmaşıktır:

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

İşlev mkServer(adres ve sunucunun bağlantı noktasının yanı sıra) örtük parametreler olarak bir aktör sistemi ve bir materyalizer de alır. Sunucunun kontrol akışı, bindinggelen bağlantıların kaynağını alıp gelen bağlantıların bir lavabosuna ileten ile temsil edilir . İçi connectionHandlerbizim lavabo olduğunu, biz akışı ile her bağlantı kolu serverLogicdaha sonra açıklanacaktır. bindingdöndürürFuturesunucu başlatıldığında veya başlatma başarısız olduğunda tamamlanır; bu, bağlantı noktasının zaten başka bir işlem tarafından alınması durumunda olabilir. Ancak kod, yanıtları işleyen bir yapı taşı göremediğimiz için grafiği tamamen yansıtmıyor. Bunun nedeni, bağlantının zaten bu mantığı tek başına sağlamasıdır. Bu, iki yönlü bir akıştır ve önceki örneklerde gördüğümüz akışlar gibi yalnızca tek yönlü bir akış değildir. Gerçekleştirme durumunda olduğu gibi, bu tür karmaşık akışlar burada açıklanmayacaktır. Resmi belgeler daha karmaşık akış grafikleri kapsayacak şekilde malzemenin bol vardır. Şimdilik, Tcp.IncomingConnectionisteklerin nasıl alınacağını ve nasıl yanıt gönderileceğini bilen bir bağlantıyı temsil ettiğini bilmek yeterlidir . Hala eksik olan kısımserverLogicinşa bloğu. Şöyle görünebilir:

sunucu mantığı

Bir kez daha, mantığı hep birlikte programımızın akışını oluşturan birkaç basit yapı taşına bölebiliyoruz. Öncelikle, yeni satır karakteri bulduğumuzda yapmamız gereken bayt dizimizi satırlara ayırmak istiyoruz. Bundan sonra, her bir satırın baytlarının bir dizeye dönüştürülmesi gerekir, çünkü ham baytlarla çalışmak zahmetlidir. Genel olarak, gelen ham verilerle çalışmayı son derece zorlaştıracak karmaşık bir protokolün ikili akışını alabiliriz. Okunabilir bir dizgimiz olduğunda, bir cevap oluşturabiliriz. Basitlik nedeniyle, cevap bizim durumumuzda herhangi bir şey olabilir. Sonunda cevabımızı tel üzerinden gönderilebilecek bir bayt dizisine geri dönüştürmeliyiz. Tüm mantığın kodu şöyle görünebilir:

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

Bunun zaten serverLogicbir ByteStringve üreten bir akış olduğunu biliyoruz ByteString. A'yı daha küçük parçalara delimiterbölebiliriz ByteString- bizim durumumuzda, yeni satır karakteri meydana geldiğinde gerçekleşmesi gerekir. receiverbölünmüş bayt dizilerinin tamamını alan ve bunları bir dizeye dönüştüren akıştır. Bu elbette tehlikeli bir dönüşümdür, çünkü yalnızca yazdırılabilir ASCII karakterleri bir dizeye dönüştürülmelidir, ancak ihtiyaçlarımız için yeterince iyidir. responderson bileşendir ve bir yanıt oluşturmaktan ve yanıtı bir bayt dizisine dönüştürmekten sorumludur. Grafiğin aksine, bu son bileşeni ikiye bölmedik, çünkü mantık önemsiz. Sonunda, tüm akışlarıviaişlevi. Bu noktada, başlangıçta bahsedilen çok kullanıcılı mülkle ilgilenip ilgilenmediğimizi sorabiliriz. Gerçekten de hemen belli olmasa da yaptık. Bu grafiğe bakarak daha netleşmelidir:

sunucu ve sunucu mantığı birleştirilmiş

serverLogicBileşen şey ama daha küçük akışları içeren bir akışıdır. Bu bileşen, bir istek olan bir girdi alır ve yanıt olan bir çıktı üretir. Akışlar birden çok kez inşa edilebildiğinden ve hepsi birbirinden bağımsız olarak çalıştığından, bu çok kullanıcılı mülkümüzü yuvalayarak başarıyoruz. Her istek kendi isteği dahilinde ele alınır ve bu nedenle kısa süreli bir talep, daha önce başlatılmış olan uzun süreli bir talebi aşabilir. Merak ediyorsanız, tanımıserverLogic bunun daha önce gösterilen elbette iç tanımlarının çoğunu satır içine alarak çok daha kısa yazılabilir:

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

Web sunucusunun bir testi şöyle görünebilir:

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

Yukarıdaki kod örneğinin düzgün çalışması için, önce startServerkomut dosyası tarafından gösterilen sunucuyu başlatmamız gerekir :

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

Bu basit TCP sunucusunun tam kod örneğini burada bulabilirsiniz . Sadece Akka Akışları ile değil, aynı zamanda müşteri ile de sunucu yazabiliyoruz. Şöyle görünebilir:

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

Tam kod TCP istemcisi bulunabilir burada . Kod oldukça benzer görünüyor ancak sunucunun aksine artık gelen bağlantıları yönetmek zorunda değiliz.

Karmaşık Grafikler

Önceki bölümlerde akışlardan basit programlar nasıl oluşturabileceğimizi gördük. Bununla birlikte, gerçekte, daha karmaşık akışlar oluşturmak için zaten yerleşik işlevlere güvenmek genellikle yeterli değildir. Akka Akışlarını keyfi programlar için kullanabilmek istiyorsak, uygulamalarımızın karmaşıklığını ele almamızı sağlayan kendi özel kontrol yapılarımızı ve birleştirilebilir akışları nasıl oluşturacağımızı bilmemiz gerekir. İyi haber şu ki, Akka Akışları kullanıcıların ihtiyaçlarına göre tasarlandı ve Akka Akışlarının daha karmaşık kısımlarına kısa bir giriş yapmak için müşteri / sunucu örneğimize biraz daha özellik ekliyoruz.

Henüz yapamadığımız bir şey bir bağlantıyı kapatmak. Bu noktada biraz daha karmaşıklaşmaya başlıyor, çünkü şimdiye kadar gördüğümüz akış API'sı bir akışı rastgele bir noktada durdurmamıza izin vermiyor. Bununla birlikte, GraphStageherhangi bir sayıda giriş veya çıkış portu ile rastgele grafik işleme aşamaları oluşturmak için kullanılabilecek bir soyutlama vardır . İlk önce sunucu tarafına bir göz atalım, burada yeni bir bileşen sunuyoruz 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)
    })
  }
}

Bu API, akış API'sından çok daha hantal görünüyor. Hiç şüphe yok ki, burada bir çok zorunlu adım atmalıyız. Buna karşılık, akışlarımızın davranışı üzerinde daha fazla kontrole sahibiz. Yukarıdaki örnekte, yalnızca bir giriş ve bir çıkış bağlantı noktası belirtiyor ve shapedeğeri geçersiz kılarak sistem tarafından kullanılabilir hale getiriyoruz . Ayrıca , bu sırayla öğeleri almaktan ve yaymaktan sorumlu olan InHandlerve a olarak tanımladık OutHandler. Tam tıklama akışı örneğine yakından bakarsanız, bu bileşenleri zaten tanımanız gerekir. GelenInHandler biz bir element kapmak ve tek karakterli bir dize ise 'q', biz akışını kapatmak istiyorum. Müşteriye, akışın yakında kapanacağını bulma şansı vermek için dizeyi yayarız"BYE"ve sonra sahneyi hemen kapatırız. closeConnectionBileşen ile bir akım ile kombine edilebilir viaakışları hakkında bölümünde tanıtılan yöntem.

Bağlantıları kapatmanın yanı sıra, yeni oluşturulan bir bağlantıya hoş geldiniz mesajı gösterebilmemiz de iyi olurdu. Bunu yapmak için bir kez daha biraz daha ileri gitmeliyiz:

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

Fonksiyon serverLogic şimdi gelen bağlantıyı parametre olarak alır. Vücudunun içinde karmaşık akış davranışını tanımlamamızı sağlayan bir DSL kullanıyoruz. Bununla birlikte welcome, yalnızca bir öğe yayabilecek bir akış oluştururuz - karşılama mesajı. önceki bölümde logicanlatıldığı gibidir serverLogic. Dikkate değer tek fark, ona eklediğimizdir closeConnection. Şimdi aslında DSL'nin ilginç kısmı geliyor. GraphDSL.createFonksiyon, bir kurucu kılan bgrafik olarak akımı ifade etmek için kullanılır kullanılabilir. ~>Fonksiyonu ile giriş ve çıkış portlarını birbirine bağlamak mümkündür. ConcatElemanları arada kullanabilirsiniz örnekte kullanılan ve burada kullanılan bileşen çıkıp diğer öğelerin önünde karşılama mesajı başa eklemek içininternalLogic. Son satırda, yalnızca sunucu mantığının giriş bağlantı noktasını ve birleştirilmiş akışın çıkış bağlantı noktasını kullanılabilir hale getiririz, çünkü diğer tüm bağlantı noktaları serverLogicbileşenin uygulama ayrıntısı olarak kalacaktır . Akka Streams DSL grafiğine ayrıntılı bir giriş için resmi belgelerdeki ilgili bölümü ziyaret edin . Karmaşık TCP sunucusunun ve onunla iletişim kurabilen bir istemcinin tam kod örneği burada bulunabilir . İstemciden yeni bir bağlantı açtığınızda, hoş bir mesaj görmelisiniz ve "q"istemciyi yazarak bağlantının iptal edildiğini bildiren bir mesaj görmelisiniz.

Hala bu cevap kapsamında olmayan bazı konular var. Özellikle materyalizasyon bir okuyucuyu ya da diğerini korkutabilir ama eminim ki burada kapsanan materyalle herkes bir sonraki adımlara kendi başlarına gidebilmelidir. Daha önce de belirtildiği gibi, resmi belgeler Akka Akışları hakkında bilgi edinmeye devam etmek için iyi bir yerdir.


4
@monksy Bunu başka bir yerde yayınlamayı düşünmedim. İsterseniz bunu blogunuzda yeniden yayınlamaktan çekinmeyin. Günümüzde API çoğu kısımda kararlıdır, bu da muhtemelen bakımla ilgilenmemeniz gerektiği anlamına gelir (Akka Akışları ile ilgili çoğu blog makalesi artık mevcut olmayan bir API gösterdikleri için güncel değildir).
kiritsuku

3
Kaybolmayacak. Neden olsun ki?
kiritsuku

2
@sschaef Soru konu dışı olduğundan ve bu şekilde kapatıldığı için ortadan kaybolabilir.
DavidG

7
@Magisch Her zaman unutmayın: "İyi içeriği silmiyoruz." Emin değilim, ama sanırım bu cevap her şeye rağmen hak kazanabilir.
Tekilleştirici

9
Bu yayın, Scala için açıldığında Stack Overflow'un yeni Dokümantasyon özelliği için iyi olabilir.
SL Barth - Monica'yı
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.