在前面几篇讨论里我们介绍了scala-gRPC的基本功能和使用方法,我们基本确定了选择gRPC作为一种有效的内部系统集成工具,主要因为下面gRPC支持的几种服务模式:

、Unary-Call:独立的一对client-request/server-response,是我们常用的http交互模式

、Server-Streaming:client发出一个request后从server端接收一串多个response

、Client-Streaming:client向server发送一串多个request后从server接收一个response

、Bidirectional-Streaming:由client首先发送request启动连接,然后在这个连接上两端可以不断交互信息。

很明显,gRPC支持双向的streaming。那么如果能把gRPC中ListenableFuture和StreamObserver这两种类型转成akka-stream的基本类型应该就能够实现所谓的reactive-gRPC了。如果我们能用akka-stream编程方式实现gRPC服务调用的话,可能会遭遇下面的场景:在服务端我们只需要实现一种akka-stream的Flow把进来的request转化成出去的response,如下:

// Unary case
Flow[Request].map(computeResponse) // Server streaming
Flow[Request].flatMapConcat(computeResponses) // Client streaming
Flow[Request].fold(defaultResponse)(computeResponse) // Bidirectional streaming
Flow[Request].flatMapConcat(computeResponses)

当然,这是个akka-stream Flow,我们可以在这个Flow里调用任何akka-stream提供的功能,如:

Flow[Request]
.throttle(, .millis, , ThrottleMode.Shaping)
.map(computeResponse)

在客户端我们可以直接经客户端stub调用Flow,如下:

Source
.single(request)
.via(stub.doSomething)
.runForeach(println)

刚好,beyond-the-lines gRPCAkkaStream开源项目提供这么一种gRPC StreamObserver到aka-stream Flow转换桥梁。下面是gRPCAkkaStream的使用示范。先从Unary-Call开始:下面是.proto文件的IDL服务描述:

syntax = "proto3";
package learn.grpc.akka.stream.services;
message NumPair {
int32 num1 = ;
int32 num2 = ;
}
message Num {
int32 num = ;
}
message SumResult {
int32 result = ;
}
service SumNumbers {
rpc SumPair(NumPair) returns (SumResult) {}
}

我们看看编译后自动产生的SumGrpcAkkaStream.scala文件中一些相关类型和函数:

服务界面描述:

trait SumNumbers extends AbstractService {
override def serviceCompanion = SumNumbers
def sumPair: Flow[learn.grpc.akka.stream.services.sum.NumPair, learn.grpc.akka.stream.services.sum.SumResult, NotUsed]
}

我们看到服务函数sumPair是一个akka-stream Fow[NumPair,SumResult,NotUsed]。下面是具体实现SumNumbers.sumPair代码:

class gRPCAkkaStreamService extends SumGrpcAkkaStream.SumNumbers {
val logger: Logger = Logger.getLogger(classOf[gRPCAkkaStreamService].getName)
override def sumPair: Flow[NumPair, SumResult, NotUsed] = {
logger.info(s"*** calling sumPair ... ***")
Flow[NumPair].map {
case NumPair(a,b) => {
logger.info(s"serving ${a} + ${b} = ???")
SumResult(a + b)
}
}
}

产生的客户端stub源代码如下:

 class SumNumbersStub(
channel: Channel,
options: CallOptions = CallOptions.DEFAULT
) extends AbstractStub[SumNumbersStub](channel, options) with SumNumbers {
override def sumPair: Flow[learn.grpc.akka.stream.services.sum.NumPair, learn.grpc.akka.stream.services.sum.SumResult, NotUsed] =
Flow[learn.grpc.akka.stream.services.sum.NumPair].flatMapConcat(request =>
Source.fromFuture(
Grpc.guavaFuture2ScalaFuture(
ClientCalls.futureUnaryCall(channel.newCall(METHOD_SUM_PAIR, options), request)
)
)
) def stub(channel: Channel): SumNumbersStub = new SumNumbersStub(channel)

我们可以通过stub来调用sumPair方法,如下:

  val channel = ManagedChannelBuilder
.forAddress(host,port)
.usePlaintext(true)
.build() val stub = SumGrpcAkkaStream.stub(channel) def addPair(num1: Int, num2: Int): Source[String,NotUsed] = {
logger.info(s"Requesting to add $num1, $num2")
Source
.single(NumPair(num1,num2))
.via(stub.sumPair)
.map(r => s"the result: ${r.result}")
}

下面是Unary-Call的具体调用方式:

object UnaryCallClient extends App {
implicit val system = ActorSystem("UnaryClient")
implicit val mat = ActorMaterializer.create(system) val client = new gRPCAkkaStreamClient("localhost", ) client.addPair(,).runForeach(println) scala.io.StdIn.readLine()
mat.shutdown()
system.terminate() }

在Server-Streaming中一个request返回的是stream of responses。IDL的描述如下:

service SumNumbers {
rpc SumPair(NumPair) returns (SumResult) {}
rpc GenIncsFrom(Num) returns (stream Num) {}
}

编译后自动产生的service trait如下:

 trait SumNumbers extends AbstractService {
override def serviceCompanion = SumNumbers
def sumPair: Flow[learn.grpc.akka.stream.services.sum.NumPair, learn.grpc.akka.stream.services.sum.SumResult, NotUsed]
def genIncsFrom: Flow[learn.grpc.akka.stream.services.sum.Num, learn.grpc.akka.stream.services.sum.Num, NotUsed]
}

这个服务函数genIncsFrom是Flow[Num,Num,NotUsed],它的具体实现如下:

class gRPCAkkaStreamService extends SumGrpcAkkaStream.SumNumbers {
val logger: Logger = Logger.getLogger(classOf[gRPCAkkaStreamService].getName)
override def genIncsFrom: Flow[Num, Num, NotUsed] = {
logger.info("*** calling genIncsFrom")
Flow[Num].mapConcat {
n => ( to n.num).map {m =>
logger.info(s"genIncFrom producing num: ${m}")
Num(m)
}
}
}
}

因为输出response是一个stream,可以用mapConcat展平Seq来产生一个。在客户方调用服务函数genIncsFrom的方式如下:

  def genIncNumbers(len: Int): Source[Int,NotUsed] = {
logger.info(s"Requesting to produce ${len} inc numbers")
Source
.single(Num(len))
.via(stub.genIncsFrom)
.map(n => n.num)
}

我们还是用runForeach来运算这个Source:

object ServerStreamingClient extends App {
implicit val system = ActorSystem("ServerStreamingClient")
implicit val mat = ActorMaterializer.create(system) val client = new gRPCAkkaStreamClient("localhost", ) client.genIncNumbers().runForeach(println) scala.io.StdIn.readLine()
mat.shutdown()
system.terminate() }

再来看看Client-Streaming是如何通过reactive-stream实现的。IDL服务描述如下:

service SumNumbers {
rpc SumPair(NumPair) returns (SumResult) {}
rpc GenIncsFrom(Num) returns (stream Num) {}
rpc SumStreamNums(stream Num) returns (SumResult) {}
}

自动产生的service接口如下:

  trait SumNumbers extends AbstractService {
override def serviceCompanion = SumNumbers
def sumPair: Flow[learn.grpc.akka.stream.services.sum.NumPair, learn.grpc.akka.stream.services.sum.SumResult, NotUsed]
def genIncsFrom: Flow[learn.grpc.akka.stream.services.sum.Num, learn.grpc.akka.stream.services.sum.Num, NotUsed]
def sumStreamNums: Flow[learn.grpc.akka.stream.services.sum.Num, learn.grpc.akka.stream.services.sum.SumResult, NotUsed]
}

sumStreamNums Flow实现如下:

  override def sumStreamNums: Flow[Num, SumResult, NotUsed] = {
logger.info("*** calling sumStreamNums")
Flow[Num].fold(SumResult()) {
case (a, b) =>
logger.info(s"receiving operand ${b.num}")
SumResult(b.num + a.result)
}
}

request是一个stream,可以用aggregation来汇总成一个response。在客户端调用stub.sumStreamNums:

  def sumManyNumbers(nums: Seq[Int]): Source[String,NotUsed] = {
logger.info(s"Requesting to sum up ${nums}")
Source(nums.map(Num(_)).to[collection.immutable.Iterable])
.via(stub.sumStreamNums)
.map(r => s"the result: ${r.result}")
} object ClientStreamingClient extends App {
implicit val system = ActorSystem("ClientStreamingClient")
implicit val mat = ActorMaterializer.create(system) val client = new gRPCAkkaStreamClient("localhost", ) client.sumManyNumbers(Seq(,,,)).runForeach(println) scala.io.StdIn.readLine()
mat.shutdown()
system.terminate() }

最后我们示范一下BiDirectional-Streaming。先用IDL定义一个流输入输出的服务函数keepAdding:

service SumNumbers {
rpc SumPair(NumPair) returns (SumResult) {}
rpc GenIncsFrom(Num) returns (stream Num) {}
rpc SumStreamNums(stream Num) returns (SumResult) {}
rpc KeepAdding(stream Num) returns (stream SumResult) {}
}

这个函数的实现代码:

  override def keepAdding: Flow[Num, SumResult, NotUsed] = {
Flow[Num].scan(SumResult()) {
case (a,b) =>
logger.info(s"receiving operand ${b.num}")
SumResult(b.num + a.result)
}
}

这个服务函数的作用是把一串输入数字逐个相加并输出当前结果。我们可以用scan来实现这样的功能。下面是客户端调用服务的示范代码:

  def ContSum(nums: Seq[Int]): Source[String,NotUsed] = {
logger.info(s"Requesting to sum up ${nums}")
Source(nums.map(Num(_)).to[collection.immutable.Iterable])
.throttle(, .millis, , ThrottleMode.shaping)
.map { n =>
logger.info(s"Sending number: $n")
n
}
.via(stub.keepAdding)
.map(r => s"current sum = ${r.result}")
}

用下面这段代码运算:

object BiDiStreamingClient extends App {
implicit val system = ActorSystem("BiDiStreamingClient")
implicit val mat = ActorMaterializer.create(system) val client = new gRPCAkkaStreamClient("localhost", ) client.ContSum(Seq(,,,)).runForeach(println) scala.io.StdIn.readLine()
mat.shutdown()
system.terminate() }

好,下面是本次讨论涉及的所有源代码:

project/scalapb.sbt

addSbtPlugin("com.thesamet" % "sbt-protoc" % "0.99.18")

resolvers += Resolver.bintrayRepo("beyondthelines", "maven")

libraryDependencies ++= Seq(
"com.thesamet.scalapb" %% "compilerplugin" % "0.7.1",
"beyondthelines" %% "grpcakkastreamgenerator" % "0.0.5"
)

build.sbt

import scalapb.compiler.Version.scalapbVersion
import scalapb.compiler.Version.grpcJavaVersion name := "gRPCAkkaStreamDemo" version := "0.1" scalaVersion := "2.12.6" resolvers += Resolver.bintrayRepo("beyondthelines", "maven") libraryDependencies ++= Seq(
"com.thesamet.scalapb" %% "scalapb-runtime" % scalapbVersion % "protobuf",
"io.grpc" % "grpc-netty" % grpcJavaVersion,
"com.thesamet.scalapb" %% "scalapb-runtime-grpc" % scalapbVersion,
"io.monix" %% "monix" % "2.3.0",
// for GRPC Akkastream
"beyondthelines" %% "grpcakkastreamruntime" % "0.0.5"
) PB.targets in Compile := Seq(
scalapb.gen() -> (sourceManaged in Compile).value,
// generate the akka stream files
grpc.akkastreams.generators.GrpcAkkaStreamGenerator() -> (sourceManaged in Compile).value
)

src/main/protobuf/sum.proto

syntax = "proto3";

package learn.grpc.akka.stream.services;

message NumPair {
int32 num1 = ;
int32 num2 = ;
} message Num {
int32 num = ;
} message SumResult {
int32 result = ;
} service SumNumbers {
rpc SumPair(NumPair) returns (SumResult) {}
rpc GenIncsFrom(Num) returns (stream Num) {}
rpc SumStreamNums(stream Num) returns (SumResult) {}
rpc KeepAdding(stream Num) returns (stream SumResult) {}
}

src/main/scala/gRPCAkkaStreamService.scala

package learn.grpc.akka.stream.services.impl

import akka.NotUsed
import akka.stream.scaladsl.Flow
import learn.grpc.akka.stream.services.sum._
import java.util.logging.Logger class gRPCAkkaStreamService extends SumGrpcAkkaStream.SumNumbers {
val logger: Logger = Logger.getLogger(classOf[gRPCAkkaStreamService].getName) override def sumPair: Flow[NumPair, SumResult, NotUsed] = {
logger.info(s"*** calling sumPair ... ***")
Flow[NumPair].map {
case NumPair(a, b) => {
logger.info(s"serving ${a} + ${b} = ???")
SumResult(a + b)
}
}
} override def genIncsFrom: Flow[Num, Num, NotUsed] = {
logger.info("*** calling genIncsFrom ... ***")
Flow[Num].mapConcat {
n =>
( to n.num).map { m =>
logger.info(s"genIncFrom producing num: ${m}")
Num(m)
}
}
} override def sumStreamNums: Flow[Num, SumResult, NotUsed] = {
logger.info("*** calling sumStreamNums ... ***")
Flow[Num].fold(SumResult()) {
case (a, b) =>
logger.info(s"receiving operand ${b.num}")
SumResult(b.num + a.result)
}
} override def keepAdding: Flow[Num, SumResult, NotUsed] = {
Flow[Num].scan(SumResult()) {
case (a,b) =>
logger.info(s"receiving operand ${b.num}")
SumResult(b.num + a.result)
}
}
}

src/main/scala/gRPCAkkaStreamServer.scala

package learn.grpc.akka.stream.server

import java.util.logging.Logger

import akka.actor.ActorSystem
import akka.stream.ActorMaterializer
import io.grpc.Server
import learn.grpc.akka.stream.services.impl.gRPCAkkaStreamService
import io.grpc.ServerBuilder
import learn.grpc.akka.stream.services.sum._
class gRPCServer(server: Server) { val logger: Logger = Logger.getLogger(classOf[gRPCServer].getName) def start(): Unit = {
server.start()
logger.info(s"Server started, listening on ${server.getPort}")
sys.addShutdownHook {
// Use stderr here since the logger may has been reset by its JVM shutdown hook.
System.err.println("*** shutting down gRPC server since JVM is shutting down")
stop()
System.err.println("*** server shut down")
}
()
} def stop(): Unit = {
server.shutdown()
} /**
* Await termination on the main thread since the grpc library uses daemon threads.
*/
def blockUntilShutdown(): Unit = {
server.awaitTermination()
}
} object DemoServer extends App {
implicit val system = ActorSystem("UnaryServer")
implicit val mat = ActorMaterializer.create(system)
val server = new gRPCServer(
ServerBuilder
.forPort()
.addService(
SumGrpcAkkaStream.bindService(
new gRPCAkkaStreamService
)
).build()
)
server.start()
// UnaryServer.blockUntilShutdown()
scala.io.StdIn.readLine()
mat.shutdown()
system.terminate() }

src/main/scala/gRPCAkkaStreamClient.scala

package learn.grpc.akka.stream.client
import learn.grpc.akka.stream.services.sum._
import java.util.logging.Logger import akka.stream.scaladsl._
import akka.NotUsed
import akka.actor.ActorSystem
import akka.stream.{ActorMaterializer, ThrottleMode}
import scala.concurrent.duration._
import io.grpc._
class gRPCAkkaStreamClient(host: String, port: Int) {
val logger: Logger = Logger.getLogger(classOf[gRPCAkkaStreamClient].getName) val channel = ManagedChannelBuilder
.forAddress(host,port)
.usePlaintext(true)
.build() val stub = SumGrpcAkkaStream.stub(channel) def addPair(num1: Int, num2: Int): Source[String,NotUsed] = {
logger.info(s"Requesting to add $num1, $num2")
Source
.single(NumPair(num1,num2))
.via(stub.sumPair)
.map(r => s"the result: ${r.result}")
}
def genIncNumbers(len: Int): Source[Int,NotUsed] = {
logger.info(s"Requesting to produce ${len} inc numbers")
Source
.single(Num(len))
.via(stub.genIncsFrom)
.map(n => n.num)
}
def sumManyNumbers(nums: Seq[Int]): Source[String,NotUsed] = {
logger.info(s"Requesting to sum up ${nums}")
Source(nums.map(Num(_)).to[collection.immutable.Iterable])
.throttle(, .millis, , ThrottleMode.shaping)
.map { n =>
logger.info(s"Sending number: $n")
n
}
.via(stub.sumStreamNums)
.map(r => s"the result: ${r.result}")
}
def ContSum(nums: Seq[Int]): Source[String,NotUsed] = {
logger.info(s"Requesting to sum up ${nums}")
Source(nums.map(Num(_)).to[collection.immutable.Iterable])
.throttle(, .millis, , ThrottleMode.shaping)
.map { n =>
logger.info(s"Sending number: $n")
n
}
.via(stub.keepAdding)
.map(r => s"current sum = ${r.result}")
}
} object UnaryCallClient extends App {
implicit val system = ActorSystem("UnaryClient")
implicit val mat = ActorMaterializer.create(system) val client = new gRPCAkkaStreamClient("localhost", ) client.addPair(,).runForeach(println) scala.io.StdIn.readLine()
mat.shutdown()
system.terminate() } object ServerStreamingClient extends App {
implicit val system = ActorSystem("ServerStreamingClient")
implicit val mat = ActorMaterializer.create(system) val client = new gRPCAkkaStreamClient("localhost", ) client.genIncNumbers().runForeach(println) scala.io.StdIn.readLine()
mat.shutdown()
system.terminate() } object ClientStreamingClient extends App {
implicit val system = ActorSystem("ClientStreamingClient")
implicit val mat = ActorMaterializer.create(system) val client = new gRPCAkkaStreamClient("localhost", ) client.sumManyNumbers(Seq(,,,)).runForeach(println) scala.io.StdIn.readLine()
mat.shutdown()
system.terminate() } object BiDiStreamingClient extends App {
implicit val system = ActorSystem("BiDiStreamingClient")
implicit val mat = ActorMaterializer.create(system) val client = new gRPCAkkaStreamClient("localhost", ) client.ContSum(Seq(,,,)).runForeach(println) scala.io.StdIn.readLine()
mat.shutdown()
system.terminate() }

ScalaPB(5):用akka-stream实现reactive-gRPC的更多相关文章

  1. Akka Stream文档翻译:Motivation

    动机 Motivation The way we consume services from the internet today includes many instances of streami ...

  2. 报错:Flink Could not resolve substitution to a value: ${akka.stream.materializer}

    报错现象: Exception in thread "main" com.typesafe.config.ConfigException$UnresolvedSubstitutio ...

  3. Akka Stream之Graph

    最近在项目中需要实现图的一些操作,因此,初步考虑使用Akka Stream的Graph实现.从而学习了下: 一.介绍 我们知道在Akka Stream中有三种简单的线性数据流操作:Source/Flo ...

  4. Lagom学习 六 Akka Stream

    lagom中的stream 流数据处理是基于akka stream的,异步的处理流数据的.如下看代码: 流式service好处是: A: 并行:  hellos.mapAsync(8, name -& ...

  5. Akka Stream文档翻译:Quick Start Guide: Reactive Tweets

    Quick Start Guide: Reactive Tweets 快速入门指南: Reactive Tweets (reactive tweets 大概可以理解为“响应式推文”,在此可以测试下GF ...

  6. Actors编程模型

    Actors模型(Actor model)首先是由Carl Hewitt在1973定义, 由Erlang OTP (Open Telecom Platform) 推广,其 消息传递更加符合面向对象的原 ...

  7. PLAY2.6-SCALA(四) 请求体解析器

    一个http请求是一个请求头后面跟着一个请求体,头部信息比较短,可以安全的缓存在内存中,在Play中头部信息使用RequestHeader类进行建模.请求体的内容可能较大,使用流stream的形式进行 ...

  8. Akka(27): Stream:Use case-Connecting Slick-dbStream & Scalaz-stream-fs2

    在以前的博文中我们介绍了Slick,它是一种FRM(Functional Relation Mapper).有别于ORM,FRM的特点是函数式的语法可以支持灵活的对象组合(Query Composit ...

  9. Akka(17): Stream:数据流基础组件-Source,Flow,Sink简介

    在大数据程序流行的今天,许多程序都面临着共同的难题:程序输入数据趋于无限大,抵达时间又不确定.一般的解决方法是采用回调函数(callback-function)来实现的,但这样的解决方案很容易造成“回 ...

  10. Akka(18): Stream:组合数据流,组件-Graph components

    akka-stream的数据流可以由一些组件组合而成.这些组件统称数据流图Graph,它描述了数据流向和处理环节.Source,Flow,Sink是最基础的Graph.用基础Graph又可以组合更复杂 ...

随机推荐

  1. 网站开发进阶(三)Windows NAT端口映射

    Windows NAT端口映射 由于有需求进行端口映射,又不想装乱七八糟的软件,Windows本身自带的路由远程访问配置太麻烦,还要两块网卡,坑爹啊. 其实Windows本身命令行支持配置端口映射,条 ...

  2. [驱动注册]platform_driver_register()与platform_device_register()

    [驱动注册]platform_driver_register()与platform_device_register()      设备与驱动的两种绑定方式:在设备注册时进行绑定及在驱动注册时进行绑定. ...

  3. ssh keygen命令实现免密码通信(git库获取操作权限:开发人员添加到git库中,获取操作权限)

    先看两个机器实现免密码登陆通讯: 假设 A 为客户机器,B为目标机: 要达到的目的: A机器ssh登录B机器无需输入密码: 加密方式选 rsa|dsa均可以,默认dsa 做法: 1.登录A机器 2.s ...

  4. Android 客户端与服务器交互

    在android中有时候我们不需要用到本机的SQLite数据库提供数据,更多的时候是从网络上获取数据,那么Android怎么从服务器端获取数据呢?有很多种,归纳起来有 一:基于Http协议获取数据方法 ...

  5. Unity的资源管理

    本篇文章翻译自Unity的官方文档,原文地址:https://docs.unity3d.com/Manual/BehindtheScenes.html Unity自动导入资源的同时为你管理这些资源产生 ...

  6. 将文件内容转化为byte数组返回

    如何将文件内容转化为byte数组并返回呢?对于这个问题,我献上我第一次成功的代码~ package com.succez.task1; import java.io.ByteArrayOutputSt ...

  7. 使用mpvue开发微信小程序

    更多内容请查看 我的新博客 地址 : 前言 16年小程序刚出来的时候,就准备花点时间去学学.无奈现实中手上项目太多,一个接着一个,而且也没有开发小程序的需求,所以就一拖再拖. 直到上周,终于有一个小程 ...

  8. 微软云消息队列 Azure service bus queue

    前言 第一次使用消息队列,遇到了一些问题:同一个消息有多次出列.是一个消息只入列一次,还是多次?还是因为出列问题,出列了多次? Microsoft Azure service bus queue Az ...

  9. Kudu vs HBase

    本文由  网易云发布. 背景 Cloudera在2016年发布了新型的分布式存储系统--kudu,kudu目前也是apache下面的开源项目.Hadoop生态圈中的技术繁多,HDFS作为底层数据存储的 ...

  10. java线程之创建线程类

    1.extends Thread方法 class Person extends Thread { int sum1 = 50; // 含参构造器 public Person(String name) ...