अक्का धाराओं के साथ शुरुआत कैसे करें? [बन्द है]


222

अक्का स्ट्रीम्स लाइब्रेरी पहले से ही काफी दस्तावेज के साथ आती है । हालांकि, मेरे लिए मुख्य समस्या यह है कि यह बहुत अधिक सामग्री प्रदान करता है - मैं उन अवधारणाओं से काफी अभिभूत महसूस करता हूं जिन्हें मुझे सीखना है। बहुत सारे उदाहरणों से पता चलता है कि वे बहुत भारीपन महसूस करते हैं और आसानी से वास्तविक दुनिया में उपयोग किए जाने वाले मामलों का अनुवाद नहीं कर सकते हैं और इसलिए वे काफी गूढ़ हैं। मुझे लगता है कि यह समझाए बिना बहुत सारे विवरण देता है कि सभी बिल्डिंग ब्लॉक्स को एक साथ कैसे बनाया जाए और यह कैसे विशिष्ट समस्याओं को हल करने में मदद करता है।

वहाँ स्रोत, डूब, प्रवाह, ग्राफ चरणों, आंशिक रेखांकन, भौतिकीकरण, एक ग्राफ डीएसएल और एक बहुत अधिक है और मैं अभी नहीं जानता कि कहां से शुरू करना है। त्वरित आरंभ गाइड शुरूआती स्थान करने के लिए है, लेकिन मैं यह समझ में नहीं आता। यह सिर्फ ऊपर वर्णित अवधारणाओं में उन्हें समझाए बिना फेंकता है। इसके अलावा कोड के उदाहरणों को निष्पादित नहीं किया जा सकता है - ऐसे लापता भाग हैं जो पाठ का पालन करना मेरे लिए अधिक या कम असंभव है।

क्या कोई भी अवधारणाओं के सूत्रों, डूब, प्रवाह, ग्राफ चरणों, आंशिक ग्राफ़, भौतिककरण और शायद कुछ अन्य चीजों की व्याख्या कर सकता है जो मैंने सरल शब्दों में याद किया और आसान उदाहरणों के साथ जो हर एक विवरण को स्पष्ट नहीं करते हैं (और जिनकी शायद वैसे भी ज़रूरत नहीं है शुरुवात)?


2
जानकारी के लिए, यह मेटा
डेविड

10
इसे (मेटा थ्रेड के बाद) बंद करने के लिए वोट करने वाले पहले व्यक्ति के रूप में, मैं पहले कहता हूं कि यहां आपका उत्तर बहुत अच्छा है । यह वास्तव में गहराई से है और निश्चित रूप से एक बहुत ही उपयोगी संसाधन है। हालाँकि दुर्भाग्य से आपके द्वारा पूछा गया प्रश्न स्टैक ओवरफ्लो के लिए बहुत व्यापक है। अगर किसी तरह से आपका जवाब एक अलग शब्द प्रश्न के लिए पोस्ट किया जा सकता है, तो भयानक है, लेकिन मुझे नहीं लगता कि यह हो सकता है। मैं इसे एक ब्लॉग पोस्ट या कुछ इसी तरह के रूप में फिर से सबमिट करने की दृढ़ता से सलाह देता हूं जो खुद और अन्य भविष्य के उत्तरों में संदर्भ संसाधन के रूप में उपयोग कर सकते हैं।
जेम्स डोनली

2
मुझे लगता है कि इस प्रश्न को ब्लॉग पोस्ट के रूप में लिखना प्रभावी नहीं होगा। हां, यह एक व्यापक प्रश्न है - और यह वास्तव में एक अच्छा प्रश्न है। इसके दायरे को कम करने से इसमें सुधार नहीं होगा। प्रदान किया गया उत्तर भयानक है। मुझे यकीन है कि Quora बड़े सवालों के लिए SO से दूर व्यापार करने के लिए खुश होगा।
माइक स्लाइन

11
@ मायकेलिन ने एसओ लोगों के साथ उचित प्रश्नों के बारे में चर्चा करने की कोशिश नहीं की, वे आँख बंद करके नियमों का पालन करते हैं। जब तक यह सवाल दूर नहीं हो जाता, मैं खुश हूं और एक अलग मंच पर जाने का मन नहीं करता।
kiritsuku

2
@sschaef कितना पांडित्यपूर्ण। हाँ, निश्चित रूप से, नियम कुछ भी नहीं हैं, आपका महान स्वयं इतना बेहतर जानता है और हर कोई नियमों को लागू करने की कोशिश कर रहा है, बस प्रचार के बाद आँख बंद करके। / शेख़ी। अधिक गंभीरता से, यह प्रलेखन बीटा के लिए एक बढ़िया अतिरिक्त होगा, यदि आप इसमें हैं। आप अभी भी इसे लागू कर सकते हैं और इसे वहां रख सकते हैं, लेकिन आपको कम से कम यह देखना चाहिए कि यह मुख्य साइट के लिए एक महान फिट नहीं है।
फेलिक्स गगनोन-ग्रेनियर

जवाबों:


506

यह उत्तर akka-streamसंस्करण पर आधारित है 2.4.2। एपीआई अन्य संस्करणों में थोड़ा अलग हो सकता है। निर्भरता का सेवन sbt द्वारा किया जा सकता है :

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

ठीक है, चलो शुरू करें। अक्का धाराओं के एपीआई तीन मुख्य प्रकार के होते हैं। प्रतिक्रियाशील धाराओं के विपरीत , ये प्रकार बहुत अधिक शक्तिशाली हैं और इसलिए अधिक जटिल हैं। यह माना जाता है कि सभी कोड उदाहरणों के लिए निम्नलिखित परिभाषाएं पहले से मौजूद हैं:

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प्रतिनिधित्व करता है और धारा के मूल्यांकन के संदर्भ का प्रतिनिधित्व करता है। हमारे मामले में हम एक का उपयोग करते हैं ActorMaterializer, जिसका अर्थ है कि धाराओं का मूल्यांकन अभिनेताओं के ऊपर किया जाता है। दोनों मानों को चिह्नित किया जाता है implicit, जो जरूरत पड़ने पर स्काला संकलक को इन दोनों निर्भरताओं को स्वचालित रूप से इंजेक्ट करने की अनुमति देता है। हम आयात भी करते हैं system.dispatcher, जो कि एक निष्पादन संदर्भ है Futures

एक नया एपीआई

अक्का धाराओं में ये प्रमुख गुण हैं:

  • वे प्रतिक्रियाशील धाराओं के विनिर्देश को कार्यान्वित करते हैं , जिनके तीन मुख्य लक्ष्य बैकएस्प्योर, एसिंक्स और गैर-अवरोधक सीमाएं और विभिन्न कार्यान्वयनों के बीच अंतर पूरी तरह से अक्का धाराओं के लिए भी लागू होते हैं।
  • वे धाराओं के लिए एक मूल्यांकन इंजन के लिए एक अमूर्तता प्रदान करते हैं, जिसे कहा जाता है 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हम कॉन्फ़िगर कर सकते हैं कि एक बफर अतिप्रवाह पर धारा विफल होनी चाहिए। विशेष रूप से इस अभिनेता इंटरफ़ेस के माध्यम से, हम किसी भी डेटा स्रोत के माध्यम से स्ट्रीम को फीड कर सकते हैं। इससे कोई फर्क नहीं पड़ता कि डेटा एक ही थ्रेड द्वारा, किसी भिन्न प्रक्रिया द्वारा, किसी अन्य प्रक्रिया द्वारा या यदि वे इंटरनेट पर दूरस्थ सिस्टम से आते हैं, तो बनता है।

सिंक

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

कैन Sourceको जोड़ने Sinkसे toविधि के साथ किया जा सकता है । यह एक तथाकथित रिटर्न देता हैRunnableFlow , जो कि बाद में हम एक विशेष रूप से देखेंगे 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

बहे

डेटा स्रोत और सिंक महान हैं यदि आपको अक्का धाराओं और एक मौजूदा प्रणाली के बीच एक कनेक्शन की आवश्यकता है, लेकिन वास्तव में उनके साथ कुछ भी नहीं किया जा सकता है। फ्लो अक्का स्ट्रीम बेस एब्सट्रैक्शन में अंतिम लापता टुकड़ा है। वे विभिन्न धाराओं के बीच एक कनेक्टर के रूप में कार्य करते हैं और इसका उपयोग इसके तत्वों को बदलने के लिए किया जा सकता है।

बहे

छवि boldradius.com से ली गई है

यदि कोई नए Flowसे जुड़ा है तो परिणाम है। इसी तरह, एक नए से जुड़ा है । और ए और परिणाम दोनों के साथ जुड़ा हुआ है । इसलिए, वे इनपुट और आउटपुट चैनल के बीच बैठते हैं, लेकिन जब तक कि वे या तो एक या एक से नहीं जुड़े हैं, तब तक स्वयं किसी एक फ्लेवर के अनुरूप नहीं होते हैं ।SourceSourceFlowSinkSinkFlowSourceSinkRunnableFlowSourceSink

पूरी स्ट्रीम

छवि 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विधि हम एक कनेक्ट कर सकते हैं Sourceएक साथ 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जब एक धारा वास्तव में भौतिक हो सकती है, जिसका अर्थ है कि ए एक Sinkसे जुड़ा हुआ है 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दूसरे प्रकार के पैरामीटर और के लिए Flowmaterialized मान दर्शाने तीसरे प्रकार पैरामीटर। इस उत्तर के दौरान, भौतिककरण का पूरा अर्थ नहीं समझाया जाएगा। हालांकि, भौतिकीकरण के बारे में अधिक जानकारी आधिकारिक दस्तावेज में पाई जा सकती है । अभी के लिए हमें केवल यह जानना चाहिए कि भौतिक मूल्य क्या है जो हमें तब मिलता है जब हम एक धारा चलाते हैं। चूंकि हम अब तक केवल साइड इफेक्ट्स में रुचि रखते थे, इसलिए हमें मिलाUnit भौतिक मूल्य के रूप में । इसका अपवाद एक सिंक का भौतिककरण था, जिसके परिणामस्वरूप ए Future। इसने हमें वापस लौटा दियाFuture, क्योंकि यह मान तब निरूपित कर सकता है जब सिंक से जुड़ी धारा समाप्त हो गई हो। अब तक, पिछले कोड उदाहरण अवधारणा को समझाने के लिए अच्छे थे, लेकिन वे उबाऊ भी थे क्योंकि हम केवल परिमित धाराओं या बहुत सरल अनंत लोगों से निपटते थे। इसे और अधिक दिलचस्प बनाने के लिए, निम्नलिखित में एक पूर्ण अतुल्यकालिक और अनबाउंड स्ट्रीम को समझाया जाएगा।

क्लिकस्ट्रीम उदाहरण

एक उदाहरण के रूप में, हम चाहते हैं कि एक ऐसी घटना हो जो घटनाओं को क्लिक करे। इसे और अधिक चुनौतीपूर्ण बनाने के लिए, हम कहते हैं कि हम एक-दूसरे के बाद थोड़े समय में होने वाली घटनाओं पर क्लिक करना चाहते हैं। इस तरह हम आसानी से डबल, ट्रिपल या टेनफोल्ड क्लिक खोज सकते हैं। इसके अलावा, हम सभी सिंगल क्लिक को फ़िल्टर करना चाहते हैं। एक गहरी साँस लें और कल्पना करें कि आप कैसे उस समस्या को अनिवार्य रूप से हल करेंगे। मुझे यकीन है कि कोई भी एक समाधान लागू करने में सक्षम नहीं होगा जो पहली कोशिश में सही ढंग से काम करता है। एक प्रतिक्रियाशील फैशन में यह समस्या हल करने के लिए तुच्छ है। वास्तव में, समाधान लागू करने के लिए इतना सरल और सीधा है कि हम इसे एक आरेख में भी व्यक्त कर सकते हैं जो सीधे कोड के व्यवहार का वर्णन करता है:

क्लिक स्ट्रीम उदाहरण का तर्क

परिचय से लेकर प्रतिक्रियात्मक प्रोग्रामिंग तक की छवि जो आप गायब हैं

ग्रे बॉक्स फ़ंक्शंस हैं जो वर्णन करते हैं कि कैसे एक धारा दूसरे में बदल जाती है। throttleफ़ंक्शन के साथ हम 250 मिलीसेकंड के भीतर क्लिक जमा करते हैं, mapऔर filterफ़ंक्शन स्वयं-व्याख्यात्मक होना चाहिए। रंग गहने एक घटना का प्रतिनिधित्व करते हैं और तीर दर्शाते हैं कि वे हमारे कार्यों के माध्यम से कैसे बहते हैं। बाद में प्रसंस्करण चरणों में, हमें कम और कम तत्व मिलते हैं जो हमारी धारा से बहते हैं, क्योंकि हम उन्हें एक साथ समूहित करते हैं और उन्हें फ़िल्टर करते हैं। इस छवि का कोड कुछ इस तरह दिखाई देगा:

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

पूरे तर्क को कोड की केवल चार लाइनों में दर्शाया जा सकता है! स्काला में, हम इसे और भी छोटा लिख ​​सकते हैं:

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

की परिभाषा clickStreamथोड़ी अधिक जटिल है लेकिन यह केवल मामला है क्योंकि उदाहरण कार्यक्रम जेवीएम पर चलता है, जहां क्लिक घटनाओं पर कब्जा करना आसानी से संभव नहीं है। एक और जटिलता यह है कि डिफ़ॉल्ट रूप से अक्का throttleफ़ंक्शन प्रदान नहीं करता है। इसके बजाय हमें इसे खुद से लिखना था। चूंकि यह फ़ंक्शन है (जैसा कि यह mapया filterफ़ंक्शंस के लिए मामला है ) विभिन्न उपयोग के मामलों में पुन: प्रयोज्य है, मैं इन पंक्तियों को तर्क को लागू करने के लिए आवश्यक लाइनों की संख्या तक नहीं गिनता। हालाँकि, यह अनिवार्य है कि तर्क को आसानी से इस्तेमाल नहीं किया जा सकता है और अलग-अलग तार्किक कदम क्रमिक रूप से लागू होने के बजाय एक ही स्थान पर होते हैं, जिसका अर्थ है कि हम शायद अपने कोड को थ्रॉटलिंग लॉजिक के साथ याद नहीं करेंगे। पूर्ण कोड उदाहरण के रूप में उपलब्ध हैजिस्ट और आगे इस पर चर्चा नहीं की जाएगी।

SimpleWebServer उदाहरण

इसके बजाय जिस पर चर्चा की जानी चाहिए वह एक और उदाहरण है। जबकि क्लिक स्ट्रीम अक्का स्ट्रीम्स को वास्तविक दुनिया के उदाहरण को संभालने देने के लिए एक अच्छा उदाहरण है, इसमें कार्रवाई में समानांतर निष्पादन दिखाने की शक्ति का अभाव है। अगला उदाहरण एक छोटे वेब सर्वर का प्रतिनिधित्व करेगा जो समानांतर में कई अनुरोधों को संभाल सकता है। वेब गंभीर आने वाले कनेक्शन को स्वीकार करने और उनमें से बाइट अनुक्रम प्राप्त करने में सक्षम होगा जो मुद्रण योग्य एएससीआईआई संकेतों का प्रतिनिधित्व करता है। इन बाइट अनुक्रमों या तारों को सभी न्यूलाइन-वर्णों को छोटे भागों में विभाजित किया जाना चाहिए। उसके बाद, सर्वर क्लाइंट को विभाजित लाइनों में से प्रत्येक के साथ प्रतिक्रिया करेगा। वैकल्पिक रूप से, यह लाइनों के साथ कुछ और कर सकता है और एक विशेष उत्तर टोकन दे सकता है, लेकिन हम इसे इस उदाहरण में सरल रखना चाहते हैं और इसलिए किसी भी फैंसी सुविधाओं का परिचय नहीं देते हैं। याद है, सर्वर को एक ही समय में कई अनुरोधों को संभालने में सक्षम होने की आवश्यकता होती है, जिसका मूल रूप से मतलब है कि किसी भी अनुरोध को आगे निष्पादन से किसी अन्य अनुरोध को अवरुद्ध करने की अनुमति नहीं है। इन सभी आवश्यकताओं को हल करना एक कठिन तरीके से कठिन हो सकता है - अक्का धाराओं के साथ, लेकिन हमें इनमें से किसी को हल करने के लिए कुछ लाइनों से अधिक की आवश्यकता नहीं होनी चाहिए। पहले, चलो सर्वर पर एक सिंहावलोकन है:

सर्वर

मूल रूप से, केवल तीन मुख्य बिल्डिंग ब्लॉक हैं। पहले एक को आने वाले कनेक्शनों को स्वीकार करना होगा। दूसरे को आने वाले अनुरोधों को संभालने की जरूरत है और तीसरे को प्रतिक्रिया भेजने की जरूरत है। क्लिक स्ट्रीम को लागू करने की तुलना में इन तीनों बिल्डिंग ब्लॉक्स को लागू करना थोड़ा अधिक जटिल है:

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एक रिटर्नFuture, जो सर्वर के शुरू होने या शुरू होने में विफल हो जाता है, जो तब होता है जब पोर्ट पहले से ही किसी अन्य प्रक्रिया द्वारा लिया गया हो। कोड हालांकि, ग्राफिक को पूरी तरह से प्रतिबिंबित नहीं करता है क्योंकि हम एक बिल्डिंग ब्लॉक नहीं देख सकते हैं जो प्रतिक्रियाओं को संभालता है। इसका कारण यह है कि कनेक्शन पहले से ही इस तर्क को प्रदान करता है। यह एक द्विदिश प्रवाह है और न केवल एक अप्रत्यक्ष प्रवाह है जैसा कि हमने पिछले उदाहरणों में देखा है। जैसा कि यह भौतिककरण के लिए मामला था, ऐसे जटिल प्रवाह को यहां नहीं समझाया जाएगा। आधिकारिक दस्तावेज सामग्री के बहुत अधिक जटिल प्रवाह रेखांकन कवर करने के लिए है। अभी के लिए यह जानना पर्याप्त है कि 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?

इस सरल टीसीपी सर्वर का पूरा कोड उदाहरण यहां पाया जा सकता है । हम न केवल अक्का धाराओं के साथ एक सर्वर लिखने में सक्षम हैं, बल्कि क्लाइंट भी हैं। यह इस तरह लग सकता है:

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

पूर्ण कोड टीसीपी ग्राहक यहां पाया जा सकता है । कोड काफी समान दिखता है, लेकिन सर्वर के विपरीत हमें आने वाले कनेक्शन का प्रबंधन नहीं करना पड़ता है।

जटिल रेखांकन

पिछले खंडों में हमने देखा है कि कैसे हम सरल कार्यक्रमों का निर्माण कर सकते हैं। हालांकि, वास्तव में यह अक्सर अधिक जटिल धाराओं के निर्माण के लिए पहले से ही निर्मित कार्यों पर भरोसा करने के लिए पर्याप्त नहीं है। यदि हम मनमाने कार्यक्रमों के लिए अक्का धाराओं का उपयोग करने में सक्षम होना चाहते हैं, तो हमें यह जानना होगा कि हमें अपने स्वयं के कस्टम नियंत्रण संरचनाओं और दहनशील प्रवाह का निर्माण कैसे करना है जो हमें अपने अनुप्रयोगों की जटिलता से निपटने की अनुमति देता है। अच्छी खबर यह है कि अक्का धाराओं को उपयोगकर्ताओं की आवश्यकताओं के अनुरूप बनाया गया था और अक्का धाराओं के अधिक जटिल हिस्सों में आपको संक्षिप्त परिचय देने के लिए, हम अपने ग्राहक / सर्वर उदाहरण में कुछ और सुविधाएँ जोड़ते हैं।

एक चीज जो हम अभी तक नहीं कर सकते हैं वह है कनेक्शन बंद करना। इस बिंदु पर यह थोड़ा अधिक जटिल होने लगता है क्योंकि हमने अब तक जो स्ट्रीम एपीआई देखा है वह हमें एक मध्यस्थ बिंदु पर एक स्ट्रीम को रोकने की अनुमति नहीं देता है। हालांकि, 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)
    })
  }
}

यह एपीआई प्रवाह एपीआई की तुलना में बहुत अधिक बोझिल दिखता है। कोई आश्चर्य नहीं, हमें यहां बहुत सारे अनिवार्य कदम उठाने होंगे। बदले में, हमारी धाराओं के व्यवहार पर हमारा अधिक नियंत्रण है। उपरोक्त उदाहरण में, हम केवल एक इनपुट और एक आउटपुट पोर्ट को निर्दिष्ट करते हैं और 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 अब आने वाले कनेक्शन को एक पैरामीटर के रूप में लेता है। इसके शरीर के अंदर हम एक डीएसएल का उपयोग करते हैं जो हमें जटिल धारा व्यवहार का वर्णन करने की अनुमति देता है। इसके साथ welcomeहम एक धारा बनाते हैं जो केवल एक तत्व का उत्सर्जन कर सकती है - स्वागत संदेश। पिछले अनुभाग में logicवर्णित किया गया था serverLogic। एकमात्र उल्लेखनीय अंतर यह है कि हमने इसमें जोड़ा closeConnectionहै। अब वास्तव में डीएसएल का दिलचस्प हिस्सा आता है। GraphDSL.createसमारोह एक बिल्डर बनाता bउपलब्ध है, जो एक ग्राफ के रूप में धारा व्यक्त करने के लिए प्रयोग किया जाता है। ~>फ़ंक्शन के साथ इनपुट और आउटपुट पोर्ट को एक दूसरे से जोड़ना संभव है। Concatघटक है कि उदाहरण में प्रयोग किया जाता है तत्व जोड़ सकते हैं और यहाँ प्रयोग किया जाता है कि अन्य तत्व से बाहर आने के सामने स्वागत संदेश पहले जोड़ें करने के लिएinternalLogic। अंतिम पंक्ति में, हम केवल सर्वर लॉजिक के इनपुट पोर्ट और समवर्ती धारा के आउटपुट पोर्ट को उपलब्ध कराते हैं क्योंकि अन्य सभी पोर्ट serverLogicघटक का कार्यान्वयन विवरण बने रहेंगे । अक्का धाराओं के ग्राफ डीएसएल में गहराई से परिचय के लिए, आधिकारिक दस्तावेज में संबंधित अनुभाग पर जाएं । जटिल टीसीपी सर्वर का पूरा कोड उदाहरण और एक क्लाइंट जो इसके साथ संवाद कर सकता है, यहां पाया जा सकता है । जब भी आप क्लाइंट से एक नया कनेक्शन खोलते हैं तो आपको एक स्वागत योग्य संदेश देखना चाहिए और "q"क्लाइंट पर टाइप करके आपको एक संदेश देखना चाहिए जो आपको बताता है कि कनेक्शन रद्द कर दिया गया है।

अभी भी कुछ विषय हैं जो इस उत्तर द्वारा कवर नहीं किए गए थे। विशेष रूप से भौतिककरण एक पाठक या किसी अन्य को डरा सकता है, लेकिन मुझे यकीन है कि यहां जिस सामग्री को कवर किया गया है, वह हर किसी को अपने द्वारा अगले चरणों में जाने में सक्षम होना चाहिए। जैसा कि पहले ही कहा गया है, अक्का धाराओं के बारे में सीखने के लिए आधिकारिक दस्तावेज एक अच्छी जगह है।


4
@monksy मैंने इसे कहीं और प्रकाशित करने की योजना नहीं बनाई। यदि आप चाहते हैं तो अपने ब्लॉग पर इसे पुनः प्रकाशित करने के लिए स्वतंत्र महसूस करें। एपीआई आजकल ज्यादातर हिस्सों में स्थिर है, जिसका मतलब है कि आपको रखरखाव की परवाह करने की ज़रूरत नहीं है (अक्का धाराओं के बारे में अधिकांश ब्लॉग लेख पुराने हैं क्योंकि वे एक एपीआई दिखाते हैं जो अब मौजूद नहीं है)।
kiritsuku

3
यह गायब नहीं होगा। क्यों करना चाहिए?
किरित्सुकु

2
@sschaef यह अच्छी तरह से गायब हो सकता है क्योंकि सवाल ऑफ-टॉपिक है और इसे इस तरह बंद कर दिया गया है।
15

7
@Magisch हमेशा याद रखें: "हम अच्छी सामग्री को नहीं हटाते हैं।" मुझे पूरा यकीन नहीं है, लेकिन मुझे लगता है कि यह उत्तर वास्तव में योग्य हो सकता है, सब कुछ के बावजूद।
डेडुप्लिकेटर

9
यह पोस्ट स्टैक ओवरफ्लो के नए डॉक्यूमेंटेशन फीचर के लिए अच्छा हो सकता है - एक बार जो स्काला के लिए खुलता है।
एसएल बर्थ -
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.