Bu cevap akka-stream
sü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. system
Akka'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
, Sink
ve Flow
. Yapı taşları, değerlendirmesine dayanan Materializer
ve 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 Source
bir veri yaratıcısıdır, akış için bir giriş kaynağı görevi görür. Her Source
birinin tek bir çıkış kanalı vardır ve giriş kanalı yoktur. Tüm veriler çıkış kanalından neye bağlı olursa akar Source
.
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 Source
sonlu 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 foreach
aracılığıyla - fonksiyonu run
biz 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
take
Yö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
Futures
Sonucunu 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 Sink
temel olarak a'nın tersidir Source
. Bir akışın uç noktasıdır ve bu nedenle veri tüketir. A'nın Sink
tek 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 Sink
yerine kullanılması tercih edilir .
Görüntü boldradius.com'dan alınmıştır .
Sink
Eylemdeki 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 Source
a Sink
ile yapılabilir to
yöntemle. Bu RunnableFlow
, daha sonra Flow
sadece run()
yöntemini çağırarak yürütülebilen bir akışın özel bir biçimini göreceğimiz gibi denir .
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.
Görüntü boldradius.com'dan alınmıştır .
Bir Eğer Flow
bir bağlandığında Source
a yeni Source
sonucudur. Benzer şekilde, bir ile Flow
bağlantılı Sink
bir yeni oluşturur Sink
. Ve Flow
hem a hem de a Source
ile bağlantılı bir Sink
sonuç RunnableFlow
. Bu nedenle, giriş ve çıkış kanalı arasında otururlar, ancak a Source
veya a'ya bağlı olmadıkları sürece kendi başlarına lezzetlerden birine karşılık gelmezler Sink
.
Görüntü boldradius.com'dan alınmıştır .
Daha iyi anlayabilmek Flows
iç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 via
yöntemle bir bağlayabilirsiniz Source
bir 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 invert
ve double
herhangi 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
s1
ve s2
tamamen 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:
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. RunnableGraph
Bir akışın gerçekleşebileceği bir zaman elde ederiz , bu da a'nın Sink
a'ya bağlı olduğu anlamına gelir Source
. Şimdiye kadar her zaman Unit
tü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 Source
ve Sink
ikinci 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 Unit
iç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:
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 map
ve filter
iş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ı clickStream
biraz 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 throttle
işlevi sağlamamasıdır . Bunun yerine bunu kendimiz yazmak zorunda kaldık. Bu işlev ( map
veya filter
iş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:
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ışı, binding
gelen bağlantıların kaynağını alıp gelen bağlantıların bir lavabosuna ileten ile temsil edilir . İçi connectionHandler
bizim lavabo olduğunu, biz akışı ile her bağlantı kolu serverLogic
daha sonra açıklanacaktır. binding
döndürürFuture
sunucu 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.IncomingConnection
isteklerin 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ımserverLogic
inşa bloğu. Şöyle görünebilir:
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 serverLogic
bir ByteString
ve üreten bir akış olduğunu biliyoruz ByteString
. A'yı daha küçük parçalara delimiter
bölebiliriz ByteString
- bizim durumumuzda, yeni satır karakteri meydana geldiğinde gerçekleşmesi gerekir. receiver
bö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. responder
son 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ıvia
iş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:
serverLogic
Bileş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 startServer
komut 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, GraphStage
herhangi 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 shape
değeri geçersiz kılarak sistem tarafından kullanılabilir hale getiriyoruz . Ayrıca , bu sırayla öğeleri almaktan ve yaymaktan sorumlu olan InHandler
ve 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. closeConnection
Bileşen ile bir akım ile kombine edilebilir via
akış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 logic
anlatıldığı gibidir serverLogic
. Dikkate değer tek fark, ona eklediğimizdir closeConnection
. Şimdi aslında DSL'nin ilginç kısmı geliyor. GraphDSL.create
Fonksiyon, bir kurucu kılan b
grafik 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. Concat
Elemanları 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ı serverLogic
bileş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.