এই উত্তরটি akka-stream
সংস্করণ ভিত্তিক 2.4.2
। অন্যান্য সংস্করণে এপিআই কিছুটা আলাদা হতে পারে। নির্ভরতা এসবিটি দ্বারা গ্রাস করা যেতে পারে :
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
এবং স্পষ্টভাবে ট্রিগার করা প্রয়োজন।
নিম্নলিখিত তিনটি প্রধান প্রকারটি কীভাবে ব্যবহার করতে হয় তার গভীরতর পরিচয় দেওয়া হবে।
উৎস
এ Source
একটি ডেটা নির্মাতা, এটি স্ট্রিমের ইনপুট উত্স হিসাবে কাজ করে। প্রত্যেকের Source
একক আউটপুট চ্যানেল এবং কোনও ইনপুট চ্যানেল নেই। সমস্ত ডেটা আউটপুট চ্যানেলের মাধ্যমে যা কিছু সংযুক্ত থাকে তার প্রবাহিত হয় Source
।
বোল্ডারডিয়াস ডটকম থেকে নেওয়া ছবি ।
এ Source
একাধিক উপায়ে তৈরি করা যেতে পারে:
scala> val s = Source.empty
s: akka.stream.scaladsl.Source[Nothing,akka.NotUsed] = ...
scala> val s = Source.single("single element")
s: akka.stream.scaladsl.Source[String,akka.NotUsed] = ...
scala> val s = Source(1 to 3)
s: akka.stream.scaladsl.Source[Int,akka.NotUsed] = ...
scala> val s = Source(Future("single value from a Future"))
s: akka.stream.scaladsl.Source[String,akka.NotUsed] = ...
scala> s runForeach println
res0: scala.concurrent.Future[akka.Done] = ...
single value from a Future
উপরের ক্ষেত্রে আমরা Source
সীমাবদ্ধ ডেটা দিয়ে খাওয়ালাম, যার অর্থ তারা শেষ পর্যন্ত শেষ করবে। একটি ভুলে যাওয়া উচিত নয় যে প্রতিক্রিয়াশীল স্ট্রিমগুলি অলস এবং ডিফল্টরূপে অ্যাসিনক্রোনাস। এর অর্থ হ'ল স্ট্রিমের মূল্যায়নের জন্য স্পষ্টভাবে একজনকে অনুরোধ করতে হবে। আক্কা স্ট্রিমগুলিতে run*
পদ্ধতিগুলির মাধ্যমে এটি করা যেতে পারে । runForeach
সুপরিচিত কোন আলাদা হতে হবে foreach
মাধ্যমে - ফাংশন run
উপরন্তু এটা স্পষ্ট যে আমরা স্ট্রীমের মূল্যায়ন জন্য অনুরোধ করে তোলে। সীমাবদ্ধ ডেটা যেহেতু বিরক্তিকর, তাই আমরা অসীম সাথে চালিয়ে যাচ্ছি:
scala> val s = Source.repeat(5)
s: akka.stream.scaladsl.Source[Int,akka.NotUsed] = ...
scala> s take 3 runForeach println
res1: scala.concurrent.Future[akka.Done] = ...
5
5
5
সঙ্গে take
পদ্ধতি আমরা আমাদের করতে বাধা দেয় অনির্দিষ্টকালের মূল্যায়নের একটি কৃত্রিম স্টপ বিন্দু তৈরি করতে পারেন। যেহেতু অভিনেতার সমর্থন অন্তর্নির্মিত, তাই আমরা সহজেই কোনও অভিনেতাকে পাঠানো বার্তাগুলি সহ স্ট্রিমটি ফিড করতে পারি:
def run(actor: ActorRef) = {
Future { Thread.sleep(300); actor ! 1 }
Future { Thread.sleep(200); actor ! 2 }
Future { Thread.sleep(100); actor ! 3 }
}
val s = Source
.actorRef[Int](bufferSize = 0, OverflowStrategy.fail)
.mapMaterializedValue(run)
scala> s runForeach println
res1: scala.concurrent.Future[akka.Done] = ...
3
2
1
আমরা দেখতে পাচ্ছি যে Futures
ভিন্ন ভিন্ন থ্রেডগুলিতে অ্যাসিনক্রোনালি কার্যকর করা হয়, যা ফলাফলটি ব্যাখ্যা করে। উপরের উদাহরণে আগত উপাদানগুলির জন্য একটি বাফার প্রয়োজনীয় নয় এবং তাই এটিওOverflowStrategy.fail
আমরা কনফিগার করতে পারি যে বাফার ওভারফ্লোতে প্রবাহটি ব্যর্থ হওয়া উচিত। বিশেষত এই অভিনেতা ইন্টারফেসের মাধ্যমে আমরা যে কোনও ডেটা উত্সের মাধ্যমে স্ট্রিমটি ফিড করতে পারি। ডেটা একই থ্রেড দ্বারা, অন্য কোনও, অন্য কোনও প্রক্রিয়া দ্বারা তৈরি করা হয়েছে বা তারা ইন্টারনেট থেকে দূরবর্তী সিস্টেম থেকে এসেছে কিনা তা বিবেচ্য নয়।
ডুবা
A Sink
মূলত a এর বিপরীত Source
। এটি একটি স্ট্রিমের শেষ পয়েন্ট এবং তাই ডেটা গ্রহণ করে। এ Sink
এর একটি একক ইনপুট চ্যানেল এবং কোনও আউটপুট চ্যানেল নেই। Sinks
বিশেষত প্রয়োজন হয় যখন আমরা পুনরায় ব্যবহারযোগ্য উপায়ে এবং স্ট্রিমটির মূল্যায়ন না করে ডেটা সংগ্রহকারীর আচরণ নির্দিষ্ট করতে চাই। ইতিমধ্যে জানা run*
পদ্ধতিগুলি আমাদের এই বৈশিষ্ট্যগুলিকে অনুমতি দেয় না, সুতরাং Sink
পরিবর্তে এটি ব্যবহার করা পছন্দ করা হয়।
বোল্ডারডিয়াস ডটকম থেকে নেওয়া ছবি ।
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()
পদ্ধতিটি কল করে সম্পাদন করা যেতে পারে ।
বোল্ডারডিয়াস ডটকম থেকে নেওয়া ছবি ।
অভিনেতার কাছে ডুবে আসা সমস্ত মান অবশ্যই ফরোয়ার্ড করা সম্ভব:
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
ফ্লো
যদি আপনার আক্কা স্ট্রিম এবং একটি বিদ্যমান সিস্টেমের মধ্যে সংযোগের প্রয়োজন হয় তবে ডেটা উত্স এবং সিন্কগুলি দুর্দান্ত are আক্কা স্ট্রীমস বেস বিমূর্তকরণের মধ্যে প্রবাহগুলি সর্বশেষ অনুপস্থিত অংশ। তারা বিভিন্ন স্ট্রিমের মধ্যে সংযোগকারী হিসাবে কাজ করে এবং এর উপাদানগুলিকে রূপান্তর করতে ব্যবহার করা যেতে পারে।
বোল্ডারডিয়াস ডটকম থেকে নেওয়া ছবি ।
যদি কোনও নতুনের Flow
সাথে সংযুক্ত থাকে তবে ফলাফল হয়। তেমনি, একটি সংযুক্ত একটি নতুন তৈরি করে । এবং এ এবং এর ফলাফল উভয়ের সাথে সংযুক্ত । অতএব, তারা ইনপুট এবং আউটপুট চ্যানেলের মধ্যে বসবে তবে তারা স্বাদগুলির কোনওটির সাথে সামঞ্জস্য করবে না যতক্ষণ না তারা একটি বা ক এর সাথে সংযুক্ত না থাকে ।Source
Source
Flow
Sink
Sink
Flow
Source
Sink
RunnableFlow
Source
Sink
বোল্ডারডিয়াস ডটকম থেকে নেওয়া ছবি ।
আরও ভাল বোঝার জন্য 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
দ্বিতীয় ধরনের পরামিতি এবং জন্য Flow
রূপায়িত মান বোঝাতে তৃতীয় টাইপ প্যারামিটার। এই উত্তরের পুরো সময়ে, বস্তুগতকরণের সম্পূর্ণ অর্থ ব্যাখ্যা করা হবে না। তবে, বৈজ্ঞানিককরণ সম্পর্কিত আরও বিবরণ সরকারী ডকুমেন্টেশনে পাওয়া যাবে । আপাতত আমাদের কেবল একটি জিনিস জানতে হবে যে আমরা যখন কোন স্ট্রিম চালাই তখন বস্তুগত মানটি আমরা পাই। যেহেতু আমরা এখন পর্যন্ত কেবলমাত্র পার্শ্ব প্রতিক্রিয়ায় আগ্রহী তাই আমরা 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
কিছুটা আরও জটিল তবে এটি কেবলমাত্র কারণ উদাহরণস্বরূপ প্রোগ্রামটি JVM- এ চলে, যেখানে ক্লিক ইভেন্টগুলি ক্যাপচার করা সহজেই সম্ভব হয় না। আর একটি জটিলতা হ'ল আক্কা ডিফল্টরূপে throttle
ফাংশন সরবরাহ করে না । পরিবর্তে আমাদের এটি নিজের দ্বারা লিখতে হয়েছিল। যেহেতু এই ফাংশনটি (যেমন এটি map
বা filter
ফাংশনগুলির ক্ষেত্রে হয়) বিভিন্ন ব্যবহারের ক্ষেত্রে পুনরায় ব্যবহারযোগ্য আমি যুক্তি বাস্তবায়নের জন্য আমাদের প্রয়োজনীয় লাইনগুলির সংখ্যার সাথে এই লাইনগুলি গণনা করি না। অবশ্যম্ভাবী ভাষায়, এটি স্বাভাবিক যে লজিকটি সহজেই পুনরায় ব্যবহার করা যায় না এবং বিভিন্ন যৌক্তিক পদক্ষেপগুলি যথাযথভাবে প্রয়োগের পরিবর্তে এক জায়গায় সব ঘটে যায়, যার অর্থ আমরা সম্ভবত থ্রোল্টিং যুক্তি দিয়ে আমাদের কোডটি মিস করেছি। সম্পূর্ণ কোড উদাহরণ হিসাবে একটি উপলব্ধসংক্ষেপে এখানে আর কোনও আলোচনা করা হবে না।
সিম্পল ওয়েবার সার্ভার উদাহরণ
পরিবর্তে কী আলোচনা করা উচিত তা অন্য উদাহরণ another ক্লিক স্ট্রিমটি আকা স্ট্রিমগুলিকে বাস্তব বিশ্বের উদাহরণ হ্যান্ডেল করার জন্য একটি দুর্দান্ত উদাহরণ হিসাবে দেখায়, এতে ক্রিয়াকলাপে সমান্তরাল সম্পাদন দেখানোর ক্ষমতা নেই। পরবর্তী উদাহরণটি একটি ছোট ওয়েব সার্ভারের প্রতিনিধিত্ব করবে যা সমান্তরালভাবে একাধিক অনুরোধগুলি পরিচালনা করতে পারে। ওয়েব সেভার আগত সংযোগগুলি গ্রহণ করতে সক্ষম হবে এবং মুদ্রণযোগ্য এএসসিআইআই চিহ্নগুলি উপস্থাপন করে এমনদের কাছ থেকে বাইট ক্রমগুলি গ্রহণ করবে। এই বাইট সিকোয়েন্স বা স্ট্রিংগুলি সমস্ত নতুনলাইন-অক্ষরগুলিতে ছোট অংশগুলিতে বিভক্ত হওয়া উচিত। এর পরে, সার্ভারটি প্রতিটি বিভক্ত লাইনের সাথে ক্লায়েন্টকে প্রতিক্রিয়া জানাবে। বিকল্পভাবে, এটি লাইনগুলির সাথে অন্য কিছু করতে পারে এবং একটি বিশেষ উত্তর টোকেন দিতে পারে, তবে আমরা এই উদাহরণটিতে এটি সহজ রাখতে চাই এবং তাই কোনও অভিনব বৈশিষ্ট্য প্রবর্তন করি না। মনে রাখবেন, সার্ভারকে একই সাথে একাধিক অনুরোধগুলি পরিচালনা করতে সক্ষম হওয়া দরকার যা মূলত এর অর্থ হ'ল কোনও অনুরোধকে পরবর্তী সম্পাদন থেকে অন্য কোনও অনুরোধটিকে ব্লক করার অনুমতি দেওয়া হয় না। এই সমস্ত প্রয়োজনীয়তার সমাধান করা একটি আবশ্যক উপায়ে কঠিন হতে পারে - আক্কা স্ট্রিমের সাথে তবে এগুলির কোনও সমাধান করার জন্য আমাদের কয়েকটি লাইনের চেয়ে বেশি হওয়া উচিত নয়। প্রথমত, সার্ভারের নিজে থেকেই ওভারভিউ দেওয়া উচিত:
মূলত, কেবল তিনটি প্রধান বিল্ডিং ব্লক রয়েছে। প্রথমটির জন্য আগত সংযোগগুলি গ্রহণ করা দরকার। দ্বিতীয়টির জন্য আগত অনুরোধগুলি পরিচালনা করতে হবে এবং তৃতীয়টির একটি প্রতিক্রিয়া প্রেরণ করা দরকার। এই তিনটি বিল্ডিং ব্লকের সমস্ত বাস্তবায়ন ক্লিক স্ট্রিম বাস্তবায়নের চেয়ে কিছুটা জটিল complicated
def mkServer(address: String, port: Int)(implicit system: ActorSystem, materializer: Materializer): Unit = {
import system.dispatcher
val connectionHandler: Sink[Tcp.IncomingConnection, Future[Unit]] =
Sink.foreach[Tcp.IncomingConnection] { conn =>
println(s"Incoming connection from: ${conn.remoteAddress}")
conn.handleWith(serverLogic)
}
val incomingCnnections: Source[Tcp.IncomingConnection, Future[Tcp.ServerBinding]] =
Tcp().bind(address, port)
val binding: Future[Tcp.ServerBinding] =
incomingCnnections.to(connectionHandler).run()
binding onComplete {
case Success(b) =>
println(s"Server started, listening on: ${b.localAddress}")
case Failure(e) =>
println(s"Server could not be bound to $address:$port: ${e.getMessage}")
}
}
ফাংশনটি mkServer
লাগে (ঠিকানার ঠিকানা এবং সার্ভারের বন্দর ছাড়াও) অভিনেতা সিস্টেম এবং কোনও উপাদানকে অন্তর্নিহিত পরামিতি হিসাবে। সার্ভারের নিয়ন্ত্রণ প্রবাহ দ্বারা প্রতিনিধিত্ব করা হয় binding
, যা আগত সংযোগগুলির উত্স গ্রহণ করে এবং আগত সংযোগগুলির একটি ডুবলে ফরোয়ার্ড করে। এর ভিতরে connectionHandler
, যা আমাদের ডোবা, আমরা প্রবাহের মাধ্যমে প্রতিটি সংযোগ পরিচালনা করি serverLogic
, যা পরে বর্ণিত হবে। binding
ফেরত aFuture
, যা সার্ভারটি শুরু হওয়ার পরে বা সম্পূর্ণরূপে ব্যর্থ হওয়ার পরে সম্পূর্ণ হয়, যখন পোর্টটি ইতিমধ্যে অন্য কোনও প্রক্রিয়া দ্বারা গ্রহণ করা হয় তখন এটি হতে পারে। কোডটি অবশ্য গ্রাফিককে পুরোপুরি প্রতিফলিত করে না কারণ আমরা কোনও বিল্ডিং ব্লক দেখতে পাচ্ছি না যা প্রতিক্রিয়াগুলি পরিচালনা করে। এর কারণ হ'ল সংযোগটি ইতিমধ্যে নিজেই এই যুক্তি সরবরাহ করে। এটি পূর্ববর্তী উদাহরণগুলিতে আমরা যে প্রবাহগুলি দেখেছি তা কেবল দ্বি নির্দেশমূলক প্রবাহ এবং কেবল একটি দিকনির্দেশক নয়। যেহেতু এটি বস্তুগতকরণের ক্ষেত্রে ছিল, এই জাতীয় জটিল প্রবাহগুলি এখানে ব্যাখ্যা করা হবে না। সরকারী ডকুমেন্টেশন উপাদান প্রচুর আরো জটিল প্রবাহ গ্রাফ আবরণ রয়েছে। আপাতত এটি জানা যথেষ্ট যে এমন Tcp.IncomingConnection
কোনও সংযোগের প্রতিনিধিত্ব করে যা কীভাবে অনুরোধগুলি গ্রহণ করতে হয় এবং প্রতিক্রিয়াগুলি কীভাবে পাঠাতে হয় তা জানে knows যে অংশটি এখনও অনুপস্থিত তা হ'ল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
প্রবাহটি যা সমস্ত বিভক্ত বাইট ক্রম নেয় এবং সেগুলিকে স্ট্রিংয়ে রূপান্তর করে। এটি অবশ্যই একটি বিপজ্জনক রূপান্তর, যেহেতু কেবল প্রিন্টযোগ্য এএসসিআইআই অক্ষরগুলি একটি স্ট্রিংয়ে রূপান্তর করা উচিত তবে আমাদের প্রয়োজনের জন্য এটি যথেষ্ট ভাল। 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()
সম্পূর্ণ কোড টিসিপি ক্লায়েন্ট এখানে পাওয়া যাবে । কোডটি বেশ অনুরূপ দেখাচ্ছে তবে সার্ভারের বিপরীতে আমাদের আর আগত সংযোগগুলি পরিচালনা করতে হবে না।
জটিল গ্রাফ
পূর্ববর্তী বিভাগগুলিতে আমরা দেখেছি কীভাবে আমরা প্রবাহের বাইরে সহজ প্রোগ্রামগুলি তৈরি করতে পারি। তবে বাস্তবে এটি আরও জটিল স্ট্রিমগুলি নির্মাণের জন্য ইতিমধ্যে অন্তর্নির্মিত ফাংশনগুলির উপর নির্ভর করা প্রায়শই যথেষ্ট নয়। যদি আমরা স্বেচ্ছাসেবী প্রোগ্রামগুলির জন্য আক্কা স্ট্রিম ব্যবহার করতে সক্ষম হতে চাই তবে আমাদের নিজস্ব কাস্টম নিয়ন্ত্রণ কাঠামো এবং সংযুক্তযোগ্য প্রবাহগুলি কীভাবে আমাদের অ্যাপ্লিকেশনগুলির জটিলতা মোকাবেলা করতে সক্ষম তা তৈরি করতে হবে তা আমাদের জানতে হবে। সুসংবাদটি হ'ল আক্কা স্ট্রিমগুলি ব্যবহারকারীদের প্রয়োজনগুলি মাপার জন্য তৈরি করা হয়েছিল এবং আক্কা স্ট্রিমগুলির আরও জটিল অংশগুলির একটি সংক্ষিপ্ত পরিচিতি দেওয়ার জন্য, আমরা আমাদের ক্লায়েন্ট / সার্ভারের উদাহরণে আরও কয়েকটি বৈশিষ্ট্য যুক্ত করি।
একটি জিনিস যা আমরা এখনও করতে পারি না তা হল একটি সংযোগ বন্ধ করা closing এই মুহুর্তে এটি কিছুটা জটিল হয়ে উঠতে শুরু করে কারণ এতক্ষণ আমরা যে স্ট্রিম এপিআই দেখেছি তা আমাদের একটি স্বেচ্ছাচারী বিন্দুতে স্ট্রিম থামাতে দেয় না। তবে, GraphStage
বিমূর্ততা রয়েছে, যা যেকোন সংখ্যক ইনপুট বা আউটপুট পোর্ট সহ স্বেচ্ছাসেবক গ্রাফ প্রসেসিং স্তর তৈরি করতে ব্যবহৃত হতে পারে। প্রথমে সার্ভারের দিকটি একবার দেখুন, যেখানে আমরা একটি নতুন উপাদান প্রবর্তন করি closeConnection
:
val closeConnection = new GraphStage[FlowShape[String, String]] {
val in = Inlet[String]("closeConnection.in")
val out = Outlet[String]("closeConnection.out")
override val shape = FlowShape(in, out)
override def createLogic(inheritedAttributes: Attributes) = new GraphStageLogic(shape) {
setHandler(in, new InHandler {
override def onPush() = grab(in) match {
case "q" ⇒
push(out, "BYE")
completeStage()
case msg ⇒
push(out, s"Server hereby responds to message: $msg\n")
}
})
setHandler(out, new OutHandler {
override def onPull() = pull(in)
})
}
}
এই API টি প্রবাহ API এর চেয়ে অনেক বেশি জটিল দেখাচ্ছে। আশ্চর্যের কিছু নেই, আমাদের এখানে অনেকগুলি প্রয়োজনীয় পদক্ষেপ করতে হবে। বিনিময়ে, আমাদের স্ট্রিমগুলির আচরণের উপর আমাদের আরও নিয়ন্ত্রণ রয়েছে। উপরের উদাহরণে আমরা কেবল একটি ইনপুট এবং একটি আউটপুট পোর্ট নির্দিষ্ট করি এবং shape
মানটিকে ওভাররাইড করে সিস্টেমে এগুলিকে উপলব্ধ করি । তদুপরি আমরা একটি তথাকথিত InHandler
এবং একটি সংজ্ঞায়িত করেছি OutHandler
, যা উপাদানগুলি গ্রহণ এবং নির্গমন করার জন্য এই আদেশে দায়বদ্ধ। আপনি যদি পুরো ক্লিকের স্ট্রিম উদাহরণটি খুব কাছ থেকে দেখে থাকেন তবে আপনার এই উপাদানগুলি ইতিমধ্যে স্বীকৃতি দেওয়া উচিত। ইন InHandler
আমরা একটি উপাদান দখল এবং এটি যদি একটি একক অক্ষর দিয়ে একটি স্ট্রিং 'q'
, আমরা প্রবাহ বন্ধ করতে চান। ক্লায়েন্টটি শীঘ্রই স্ট্রিম বন্ধ হয়ে যাবে তা খুঁজে পাওয়ার সুযোগ দেওয়ার জন্য, আমরা স্ট্রিংটি নির্গত করি"BYE"
এবং তারপরে আমরা তত্ক্ষণাত স্টেজটি বন্ধ করে দিই। closeConnection
কম্পোনেন্ট মাধ্যমে একটি স্ট্রিম সঙ্গে মিলিত হতে পারে via
পদ্ধতি, যা প্রবাহ সম্পর্কে বিভাগে চালু করা হয়।
সংযোগগুলি বন্ধ করতে সক্ষম হওয়া ছাড়াও, যদি আমরা নতুনভাবে তৈরি হওয়া সংযোগটিতে একটি স্বাগত বার্তা দেখাতে পারি তবে এটিও দুর্দান্ত হবে। এটি করার জন্য আমাদের আরও একবার এগিয়ে যেতে হবে:
def serverLogic
(conn: Tcp.IncomingConnection)
(implicit system: ActorSystem)
: Flow[ByteString, ByteString, NotUsed]
= Flow.fromGraph(GraphDSL.create() { implicit b ⇒
import GraphDSL.Implicits._
val welcome = Source.single(ByteString(s"Welcome port ${conn.remoteAddress}!\n"))
val logic = b.add(internalLogic)
val concat = b.add(Concat[ByteString]())
welcome ~> concat.in(0)
logic.outlet ~> concat.in(1)
FlowShape(logic.in, concat.out)
})
ফাংশনটি serverLogic
এখন প্যারামিটার হিসাবে আগত সংযোগ গ্রহণ করে। এর দেহের অভ্যন্তরে আমরা একটি ডিএসএল ব্যবহার করি যা আমাদের জটিল স্ট্রিম আচরণটি বর্ণনা করতে দেয়। সঙ্গে welcome
স্বাগত বার্তা - আমরা একটি স্ট্রিম যে শুধুমাত্র একটি উপাদান নির্গত করতে তৈরি করুন। পূর্ববর্তী বিভাগে যা logic
বর্ণনা করা হয়েছিল তা হল serverLogic
। একমাত্র উল্লেখযোগ্য পার্থক্য হ'ল এটি আমরা এতে যুক্ত closeConnection
করেছি। এখন আসলে ডিএসএল এর আকর্ষণীয় অংশ আসে। GraphDSL.create
ফাংশন একটি রচয়িতা তোলে b
পাওয়া যায়, যা গ্রাফ হিসাবে প্রবাহ প্রকাশ করতে ব্যবহৃত হয়। সঙ্গে ~>
ফাংশন এটা একে অপরের সাথে ইনপুট এবং আউটপুট পোর্ট সংযোগ করা সম্ভব। Concat
কম্পোনেন্ট উদাহরণস্বরূপ ব্যবহার করা হয় উপাদান কনক্যাটেনেট করতে পারেন এবং এখানে ব্যবহার করা হয় অন্যান্য উপাদান যে বাইরে আসতে সামনে স্বাগত বার্তা পূর্বে লিখুন করতেinternalLogic
। শেষ লাইনে, আমরা কেবল সার্ভার লজিকের ইনপুট পোর্ট এবং সংক্ষিপ্ত স্ট্রিমের আউটপুট পোর্টটি উপলভ্য করি কারণ অন্যান্য সমস্ত বন্দরগুলি serverLogic
উপাদানটির বাস্তবায়ন বিশদ হিসাবে থাকবে । আক্কা স্ট্রিমের গ্রাফ ডিএসএল-এর গভীরতার পরিচয়ের জন্য, অফিসিয়াল ডকুমেন্টেশনে সংশ্লিষ্ট বিভাগটি দেখুন । জটিল টিসিপি সার্ভারের এবং তার সাথে যোগাযোগ করতে পারে এমন কোনও ক্লায়েন্টের পুরো কোড উদাহরণটি এখানে পাওয়া যাবে । আপনি যখনই ক্লায়েন্টের কাছ থেকে কোনও নতুন সংযোগ খোলেন তখন আপনাকে স্বাগত বার্তা দেখা উচিত এবং "q"
ক্লায়েন্টের কাছে টাইপ করে আপনার একটি বার্তা দেখতে পাওয়া উচিত যা আপনাকে বলে যে সংযোগটি বাতিল হয়ে গেছে।
এখনও কিছু বিষয় রয়েছে যা এই উত্তরটির আওতায় আসে নি। বিশেষত ধাতবকরণ একজন পাঠক বা অন্যকে ভয় দেখাতে পারে তবে আমি নিশ্চিত যে এখানে যে বিষয়বস্তু রয়েছে তা প্রত্যেকে নিজেরাই পরবর্তী পদক্ষেপে যেতে সক্ষম হবে। যেমন ইতিমধ্যে বলা হয়েছে, সরকারী ডকুমেন্টেশন আক্কা স্ট্রিমগুলি সম্পর্কে শেখা চালিয়ে যাওয়ার জন্য ভাল জায়গা।