คำตอบนี้จะขึ้นอยู่กับรุ่นakka-stream
2.4.2
API อาจแตกต่างกันเล็กน้อยในเวอร์ชันอื่น การพึ่งพาสามารถใช้ได้โดยsbt :
libraryDependencies += "com.typesafe.akka" %% "akka-stream" % "2.4.2"
เอาล่ะเริ่มกันเลย API ของ Akka Streams ประกอบด้วยสามประเภทหลัก ตรงกันข้ามกับReactive Streamsประเภทนี้มีประสิทธิภาพมากกว่าและซับซ้อนกว่ามาก มันจะสันนิษฐานว่าสำหรับตัวอย่างโค้ดทั้งหมดมีคำจำกัดความต่อไปนี้อยู่แล้ว:
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
แสดงถึงระบบการแสดงของ Akka และmaterializer
แสดงถึงบริบทการประเมินของสตรีม ในกรณีของเราเราใช้ a ActorMaterializer
ซึ่งหมายความว่ามีการประเมินสตรีมด้านบนของนักแสดง ค่าทั้งสองถูกทำเครื่องหมายเป็นimplicit
ซึ่งทำให้คอมไพเลอร์ Scala มีความเป็นไปได้ที่จะฉีดการพึ่งพาทั้งสองนี้โดยอัตโนมัติเมื่อใดก็ตามที่มีความจำเป็น เรายังนำเข้าซึ่งเป็นบริบทการดำเนินการสำหรับsystem.dispatcher
Futures
API ใหม่
Akka Streams มีคุณสมบัติที่สำคัญเหล่านี้:
- พวกเขาใช้ข้อมูลจำเพาะของReactive Streamsซึ่งมีสามเป้าหมายหลักคือ backpressure, async และ non-blocking boundary และการทำงานร่วมกันระหว่างการใช้งานที่แตกต่างกัน
Materializer
พวกเขาให้เป็นนามธรรมสำหรับเครื่องมือการประเมินผลสำหรับลำธารซึ่งเรียกว่า
- โปรแกรมมีสูตรเป็นกลุ่มอาคารที่นำมาใช้ใหม่ซึ่งจะแสดงเป็นสามประเภทหลัก
Source
, และSink
Flow
Building Block สร้างกราฟซึ่งการประเมินจะยึดตามMaterializer
และจำเป็นต้องทริกเกอร์อย่างชัดเจน
ในการแนะนำต่อไปนี้ลึกลงไปในวิธีการใช้ทั้งสามประเภทหลักจะได้รับ
แหล่ง
A Source
เป็นผู้สร้างข้อมูลซึ่งทำหน้าที่เป็นแหล่งอินพุตไปยังสตรีม แต่ละSource
ช่องมีช่องสัญญาณออกเดียวและไม่มีช่องสัญญาณเข้า Source
ข้อมูลทั้งหมดที่ไหลผ่านช่องทางส่งออกไปยังสิ่งที่จะเชื่อมต่อกับ
ภาพที่นำมาจากboldradius.com
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
ข้อมูลด้วยข้อมูล จำกัด ซึ่งหมายความว่าพวกเขาจะยุติในที่สุด หนึ่งไม่ควรลืมว่าปฏิกิริยารีสตรีมจะขี้เกียจและไม่ตรงกันโดยค่าเริ่มต้น นี่หมายความว่าจะต้องขอการประเมินสตรีมอย่างชัดเจน ใน Akka Streams สิ่งนี้สามารถทำได้ผ่าน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
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
การเชื่อมต่อ a Source
กับ a 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
ไหล
แหล่งข้อมูลและ sinks นั้นยอดเยี่ยมถ้าคุณต้องการการเชื่อมต่อระหว่าง Akka stream และระบบที่มีอยู่ แต่ก็ไม่สามารถทำอะไรกับมันได้ โฟลว์เป็นชิ้นสุดท้ายที่ขาดหายไปในสิ่งที่เป็นนามธรรมของ Akka Streams พวกเขาทำหน้าที่เป็นตัวเชื่อมต่อระหว่างกระแสต่าง ๆ และสามารถใช้เพื่อแปลงองค์ประกอบ
ภาพที่นำมาจากboldradius.com
หากมีการFlow
เชื่อมต่อกับSource
ใหม่Source
เป็นผล ในทำนองเดียวกันFlow
การเชื่อมต่อกับสร้างใหม่Sink
Sink
และFlow
เชื่อมต่อกับทั้งสองSource
และผลลัพธ์ในSink
RunnableFlow
ดังนั้นพวกเขานั่งอยู่ระหว่างอินพุทและเอาท์พุทช่อง แต่ด้วยตัวเองไม่ตรงกับหนึ่งในรสชาติตราบเท่าที่พวกเขาไม่ได้เชื่อมต่อกับทั้งหรือSource
Sink
ภาพที่นำมาจาก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
พารามิเตอร์ประเภทที่สองและสำหรับFlow
พารามิเตอร์ประเภทที่สามแสดงถึงค่าวัสดุ ตลอดคำตอบนี้จะไม่อธิบายความหมายที่แท้จริงของการเป็นรูปเป็นร่าง อย่างไรก็ตามรายละเอียดเพิ่มเติมเกี่ยวกับการเป็นตัวเป็นตนสามารถพบได้ในเอกสารที่เป็นทางการ สำหรับตอนนี้สิ่งเดียวที่เราต้องรู้คือค่าที่เป็นรูปธรรมคือสิ่งที่เราได้รับเมื่อเราทำการสตรีม เนื่องจากเราสนใจเฉพาะผลข้างเคียงเท่านั้นเราจึงได้รับUnit
คุณค่าที่เป็นรูปธรรม Future
ข้อยกเว้นนี้เป็นเป็นตัวเป็นตนของอ่างล้างจานซึ่งส่งผลให้ มันทำให้เรากลับมาFuture
เนื่องจากค่านี้สามารถแสดงได้เมื่อกระแสที่เชื่อมต่อกับอ่างล้างมือได้สิ้นสุดลงแล้ว จนถึงตอนนี้ตัวอย่างโค้ดก่อนหน้านี้เป็นสิ่งที่ดีที่จะอธิบายแนวคิด แต่พวกเขาก็น่าเบื่อเพราะเราจัดการเฉพาะกับสตรีมที่ จำกัด หรือกับอินฟินิทที่ง่ายมาก เพื่อให้น่าสนใจยิ่งขึ้นในการอธิบายการสตรีมแบบอะซิงโครนัสและแบบไม่ จำกัด ที่เต็ม
คลิกสตรีมตัวอย่าง
ตัวอย่างเช่นเราต้องการให้สตรีมที่จับภาพเหตุการณ์การคลิก เพื่อให้ท้าทายยิ่งขึ้นสมมติว่าเราต้องการจัดกลุ่มการคลิกเหตุการณ์ที่เกิดขึ้นในระยะเวลาอันสั้นหลังจากกัน วิธีนี้เราสามารถค้นหาการคลิกสองครั้งสามครั้งหรือสิบเท่าได้อย่างง่ายดาย นอกจากนี้เราต้องการกรองการคลิกทั้งหมดทั้งหมด หายใจเข้าลึก ๆ และจินตนาการว่าคุณจะแก้ปัญหาอย่างไรในลักษณะที่จำเป็น ฉันเดิมพันว่าจะไม่มีใครสามารถใช้โซลูชันที่ทำงานอย่างถูกต้องในการลองครั้งแรก ในรูปแบบที่ตอบโต้ปัญหานี้เป็นเรื่องที่แก้ไม่ได้ ในความเป็นจริงการแก้ปัญหานั้นง่ายและตรงไปตรงมาเพื่อให้เราสามารถแสดงได้ในแผนภาพที่อธิบายพฤติกรรมของรหัสโดยตรง:
ภาพที่นำมาจากการแนะนำในการเกิดปฏิกิริยาการเขียนโปรแกรมที่คุณได้รับหายไป
กล่องสีเทาเป็นฟังก์ชั่นที่อธิบายถึงวิธีการที่กระแสหนึ่งถูกเปลี่ยนเป็นอื่น ด้วยthrottle
ฟังก์ชั่นที่เราได้รับคลิกภายใน 250 มิลลิวินาทีmap
และfilter
ฟังก์ชั่นควรอธิบายด้วยตนเอง ลูกกลมสีแสดงถึงเหตุการณ์และลูกศรแสดงให้เห็นถึงวิธีที่พวกเขาไหลผ่านฟังก์ชั่นของเรา ต่อมาในขั้นตอนการประมวลผลเราจะได้รับองค์ประกอบที่น้อยลงที่ไหลผ่านสตรีมของเราเนื่องจากเราจัดกลุ่มพวกเขาเข้าด้วยกันและกรองพวกเขาออก รหัสสำหรับภาพนี้จะมีลักษณะดังนี้:
val multiClickStream = clickStream
.throttle(250.millis)
.map(clickEvents => clickEvents.length)
.filter(numberOfClicks => numberOfClicks >= 2)
ตรรกะทั้งหมดสามารถแสดงได้ในรหัสสี่บรรทัดเท่านั้น! ใน Scala เราสามารถเขียนให้สั้นลงได้:
val multiClickStream = clickStream.throttle(250.millis).map(_.length).filter(_ >= 2)
คำจำกัดความของclickStream
ความซับซ้อนมากขึ้นเล็กน้อย แต่เป็นเพียงกรณีนี้เนื่องจากโปรแกรมตัวอย่างทำงานบน JVM ซึ่งไม่สามารถจับภาพเหตุการณ์การคลิกได้อย่างง่ายดาย ภาวะแทรกซ้อนอื่นคือ Akka โดยค่าเริ่มต้นไม่มีthrottle
ฟังก์ชั่น แต่เราต้องเขียนด้วยตัวเอง เนื่องจากฟังก์ชั่นนี้เป็น (เพราะเป็นกรณีของmap
หรือfilter
ฟังก์ชั่น) นำมาใช้ซ้ำในกรณีการใช้งานที่แตกต่างกันฉันไม่นับบรรทัดเหล่านี้เป็นจำนวนบรรทัดที่เราจำเป็นต้องใช้ตรรกะ อย่างไรก็ตามในภาษาที่จำเป็นนั้นเป็นเรื่องปกติที่ตรรกะไม่สามารถนำกลับมาใช้ใหม่ได้อย่างง่ายดายและขั้นตอนทางตรรกะที่แตกต่างกันเกิดขึ้นในที่เดียวแทนที่จะใช้ตามลำดับซึ่งหมายความว่าเราอาจจะพลาดโค้ดของเราด้วยตรรกะการควบคุมปริมาณ ตัวอย่างโค้ดแบบเต็มมีให้ในแบบส่วนสำคัญและจะไม่ถูกกล่าวถึงที่นี่อีกต่อไป
ตัวอย่าง SimpleWebServer
สิ่งที่ควรพิจารณาแทนเป็นอีกตัวอย่างหนึ่ง ในขณะที่กระแสการคลิกเป็นตัวอย่างที่ดีที่ให้ Akka Streams จัดการตัวอย่างในโลกแห่งความเป็นจริง แต่ขาดพลังในการแสดงการดำเนินการแบบขนาน ตัวอย่างถัดไปจะแสดงถึงเว็บเซิร์ฟเวอร์ขนาดเล็กที่สามารถจัดการคำขอได้หลายคำขอพร้อมกัน เว็บเซิร์ฟเวอร์จะสามารถยอมรับการเชื่อมต่อขาเข้าและรับลำดับไบต์จากสิ่งเหล่านั้นที่เป็นสัญญาณ ASCII ที่พิมพ์ได้ ลำดับไบต์หรือสตริงเหล่านี้ควรแยกอักขระบรรทัดใหม่ทั้งหมดออกเป็นส่วนย่อย ๆ หลังจากนั้นเซิร์ฟเวอร์จะตอบกลับไปยังไคลเอนต์ด้วยแต่ละบรรทัดการแยก หรืออาจทำอย่างอื่นกับเส้นและให้โทเค็นคำตอบพิเศษ แต่เราต้องการทำให้มันง่ายในตัวอย่างนี้และดังนั้นจึงไม่แนะนำคุณสมบัติแฟนซีใด ๆ โปรดจำไว้ว่า เซิร์ฟเวอร์จะต้องสามารถจัดการคำขอหลายรายการได้ในเวลาเดียวกันซึ่งโดยทั่วไปหมายความว่าไม่มีการอนุญาตให้บล็อกคำขออื่นใดจากการดำเนินการต่อไป การแก้ไขข้อกำหนดเหล่านี้ทั้งหมดอาจทำได้ยากด้วยวิธีที่จำเป็น - ด้วย Akka Streams อย่างไรก็ตามเราไม่ควรต้องมีมากกว่าสองสามบรรทัดเพื่อแก้ปัญหาเหล่านี้ อันดับแรกเรามาดูภาพรวมของเซิร์ฟเวอร์กัน:
โดยพื้นฐานมีหน่วยการสร้างหลักเพียงสามรายการเท่านั้น คนแรกต้องยอมรับการเชื่อมต่อเข้ามา คนที่สองต้องการจัดการคำขอที่เข้ามาและคนที่สามต้องส่งคำตอบ การใช้ Building Block ทั้งสามนี้มีความซับซ้อนเพียงเล็กน้อยกว่าการติดตั้งสตรีมคลิก:
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
ใช้เวลา (นอกเหนือจากที่อยู่และพอร์ตของเซิร์ฟเวอร์) ยังเป็นระบบนักแสดงและ materializer เป็นพารามิเตอร์โดยนัย โฟลว์การควบคุมของเซิร์ฟเวอร์ถูกแสดงโดย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?
ตัวอย่างรหัสเต็มรูปแบบของเซิร์ฟเวอร์ TCP นี้ง่ายสามารถพบได้ที่นี่ เราไม่เพียง แต่สามารถเขียนเซิร์ฟเวอร์กับ Akka Streams เท่านั้น แต่ยังเป็นไคลเอนต์ อาจมีลักษณะเช่นนี้:
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()
ไคลเอ็นต์รหัส TCP เต็มสามารถพบได้ที่นี่ รหัสมีลักษณะค่อนข้างคล้ายกัน แต่ตรงกันข้ามกับเซิร์ฟเวอร์เราไม่จำเป็นต้องจัดการการเชื่อมต่อขาเข้าอีกต่อไป
กราฟที่ซับซ้อน
ในส่วนก่อนหน้านี้เราได้เห็นวิธีที่เราสามารถสร้างโปรแกรมง่าย ๆ อย่างไรก็ตามในความเป็นจริงมันมักจะไม่เพียงพอที่จะพึ่งฟังก์ชั่นในตัวเพื่อสร้างสตรีมที่ซับซ้อนมากขึ้น ถ้าเราต้องการที่จะใช้ Akka Streams สำหรับโปรแกรมที่ต้องการเราจำเป็นต้องรู้วิธีการสร้างโครงสร้างการควบคุมที่กำหนดเองของเราเองและกระแสรวมที่ช่วยให้เราสามารถจัดการกับความซับซ้อนของแอปพลิเคชันของเรา ข่าวดีก็คือ Akka Streams ได้รับการออกแบบให้ปรับขนาดตามความต้องการของผู้ใช้และเพื่อให้คุณได้แนะนำสั้น ๆ เกี่ยวกับส่วนที่ซับซ้อนมากขึ้นของ Akka Streams เราได้เพิ่มคุณสมบัติเพิ่มเติมให้กับลูกค้า / เซิร์ฟเวอร์ของเรา
สิ่งหนึ่งที่เราทำไม่ได้คือการปิดการเชื่อมต่อ ณ จุดนี้มันเริ่มมีความซับซ้อนเพิ่มขึ้นเล็กน้อยเพราะสตรีม API ที่เราเคยเห็นมาไม่ได้อนุญาตให้เราหยุดสตรีมที่จุดใดก็ได้ อย่างไรก็ตามมีสิ่งที่เป็น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 นี้ดูยุ่งยากกว่า Flow API มาก ไม่น่าแปลกใจที่เราต้องทำตามขั้นตอนที่จำเป็นมากมายที่นี่ ในการแลกเปลี่ยนเราสามารถควบคุมพฤติกรรมของสตรีมของเราได้มากขึ้น ในตัวอย่างข้างต้นเราระบุเพียงหนึ่งอินพุตและหนึ่งพอร์ตออกและทำให้พร้อมใช้งานสำหรับระบบโดยการแทนที่shape
ค่า นอกจากนี้เรายังได้นิยามสิ่งที่เรียกว่าInHandler
และ a 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
ขณะนี้ฟังก์ชันใช้การเชื่อมต่อที่เข้ามาเป็นพารามิเตอร์ ภายในร่างกายเราใช้ DSL ที่ช่วยให้เราสามารถอธิบายพฤติกรรมการสตรีมที่ซับซ้อน เมื่อwelcome
เราสร้างกระแสที่สามารถปล่อยองค์ประกอบเดียว - ข้อความต้อนรับ logic
เป็นสิ่งที่อธิบายไว้serverLogic
ในส่วนก่อนหน้า ข้อแตกต่างที่โดดเด่นเพียงอย่างเดียวคือเราเพิ่มcloseConnection
เข้าไป ตอนนี้เป็นส่วนที่น่าสนใจของ DSL จริงๆแล้ว GraphDSL.create
ฟังก์ชั่นที่ทำให้ผู้สร้างb
ที่มีอยู่ซึ่งจะใช้ในการแสดงสตรีมเป็นกราฟ ด้วย~>
ฟังก์ชั่นที่เป็นไปได้ในการเชื่อมต่อพอร์ตอินพุตและเอาต์พุตด้วยกัน Concat
ส่วนประกอบที่ใช้ในตัวอย่างสามารถเชื่อมองค์ประกอบและมีการใช้ที่นี่เพื่อย่อหน้าข้อความต้อนรับในด้านหน้าขององค์ประกอบอื่น ๆ ที่ออกมาจากinternalLogic
. ในบรรทัดสุดท้ายเราจะสร้างพอร์ตอินพุตของเซิร์ฟเวอร์ตรรกะและพอร์ตเอาต์พุตของสตรีมที่ต่อกันพร้อมใช้งานเพราะพอร์ตอื่นทั้งหมดจะยังคงมีรายละเอียดการใช้งานของserverLogic
ส่วนประกอบ สำหรับการแนะนำในเชิงลึกกับ DSL กราฟของ Akka ลำธารเยี่ยมชมส่วนที่สอดคล้องกันในเอกสารอย่างเป็นทางการ ตัวอย่างรหัสเต็มรูปแบบของเซิร์ฟเวอร์ TCP ซับซ้อนและของไคลเอนต์ที่สามารถสื่อสารกับมันสามารถพบได้ที่นี่ เมื่อใดก็ตามที่คุณเปิดการเชื่อมต่อใหม่จากไคลเอนต์คุณจะเห็นข้อความต้อนรับและโดยการพิมพ์"q"
บนไคลเอนต์คุณควรเห็นข้อความที่บอกคุณว่าการเชื่อมต่อถูกยกเลิก
ยังมีบางหัวข้อที่ไม่ได้ครอบคลุมโดยคำตอบนี้ โดยเฉพาะอย่างยิ่งการทำให้เป็นรูปเป็นร่างอาจทำให้ผู้อ่านคนหนึ่งหรืออีกคนกลัว แต่ฉันแน่ใจว่าเนื้อหาที่กล่าวมานี้ทุกคนควรจะสามารถไปขั้นตอนต่อไปได้ด้วยตนเอง ดังที่ได้กล่าวไปแล้วเอกสารอย่างเป็นทางการเป็นสถานที่ที่ดีในการเรียนรู้เกี่ยวกับ Akka Streams ต่อไป