gRPC Java 和Golang下server 端消息发送源码分析比较

gRPC Java 和Golang下server 端消息发送源码比较

概述

RPC是平时开发中经常用到的通信框架,gRPC是Google版本的rpc,开发中涉及到跨语言或者单纯通信需求时,gRPC是个不错的选择。
gRPC的接口定义使用谷歌自己的protobuf,java+golang 两种开发语言下protoc的编译详见https://yq.aliyun.com/articles/679610,这里不解释protobuf概念了,自行github。这里重点从源码层对比下如下通信设计中,从服务端实时推送消息到服务端,java和golang两种语言环境下服务端实现的差异性,以及编码过程中的注意点。

接口定义

先看下接口文件的定义:

service EasyAgentService {
    rpc registerSidecar(RegisterRequest) returns (RegisterResponse);
    rpc readyForControl(ControlRequest)  returns (stream ControlResponse);
    rpc reportEvent(Event)               returns (EmptyResponse);
}

想要实现的功能

服务端开启服务监听,客户端主动与服务端连接并注册信息,服务端hold住连接流stream并实时推送消息到客户端,客户端通过reportEvent上报执行结果。

接口说明

接口 作用 调用次数
registerSidecar 客户端主动连接服务端 连接成功仅调用一次,失败重试
readyForControl 客户端发起通话 连接成功仅调用一次,失败重试
reportEvent 客户端上报执行结果 多次

java/golang 服务端生成代码比较

java 服务端源码分析

文件列表
gRPC Java 和Golang下server 端消息发送源码分析比较

java 服务端需要继承实现的三个主要方法

gRPC Java 和Golang下server 端消息发送源码分析比较

三个方法都有两个参数,第一个参数是客户端报上来的请求参数,第二个参数服务端业务代码里可以把反馈给到客户端的顶层对象,为什么说是顶层对象,因为到io.grpc.stub.StreamObserver这里已经封装了底层通信的细节,业务代码只能通过以下三个interface来响应客户端:

gRPC Java 和Golang下server 端消息发送源码分析比较

有一个细节可以注意一下,三个接口的返回值都是 void,这会直接影响到服务端的业务逻辑编写,下面对比了golang版本生成的代码我们仔具体比较下。这里简单说下这三个接口的作用:

接口名 作用 返回值
onNext 服务端推送消息 void
onCompleted 服务端推送EOF void
onError 服务端推送错误信息 void

golang 服务端关键源码

文件列表

gRPC Java 和Golang下server 端消息发送源码分析比较

golang 服务端需要继承实现的三个主要方法

gRPC Java 和Golang下server 端消息发送源码分析比较

跟java版本比较,golang 服务端生成的三个接口着重看下ReadyForControl的EasyAgentService_ReadyForControlServer参数,该接口提供了一个Send方法,且有返回值 error,这个跟java的代码实现不一样,这回给业务代码带来哪些影响呢,再回顾下开头设计这三个接口的初衷:

服务端hold住连接流stream并实时推送消息到客户端

下面我们来看下由于服务端生成代码差异带来的两种不同的业务代码写法。

java golang 服务端关键业务代码比较

golang 版本 readyForControl实现

gRPC Java 和Golang下server 端消息发送源码分析比较

服务端接收到readyForControl以后,保存一个EasyAgentService_ReadyForControlServer对象到Map里,然后启动一个for select 循环,for循环里通过281行的channel与其他业务模块进行交互,正常情况channel里没数据时,for 循环会阻塞在281行,当有数据时则能实时拿到数据并通过stream下发。着重关注下276/277/287行,在stream结束或者有异常情况下服务端可以实时感知到,服务端可以根据stream的状态判断是否需要退出循环。小结下,golang这边使用channel的特性+对stream状态的实时感知,可以完美达到“服务端hold住连接流stream并实时推送消息到客户端”的功效,没有其他后顾之忧。下面我们再来看下java端实现同样逻辑的代码。

java 版本 readyForControl实现

gRPC Java 和Golang下server 端消息发送源码分析比较

跟golang版本比较,java版本readyForControl的实体也是一个大循环。java没有channel这么好用的数据结构,不过可以通过redis等强大的支撑实现跟channel一样的机制,184行用brpop的方式阻塞监听redis里的消息队列,功效跟golang的channel一样;但java这边循环体里的处理就没有golang那边那么干净利落了,原因如下:

  • java这边的StreamObserver 只提供三个无返回值的操作接口;
  • java这变在流中断的情况下,onNext/onCompleted仍然可以正常调用;
  • 流异常或者结束并无通知。

所以java的循环里不知道何时需要退出循环,如果异常情况不退出循环,在客户端多次调用readyForControl时服务端就灾难了,服务端无法判断 哪个流是正确的,已经关闭的流有很大概率能消费redis的数据并成功onNext出去,这样活着的流就拿会不到数据。为了解决这个问题,再次使用了redis,客户端每次注册上来时保存一个注册时间到redis里【agentid->time】,注册时间传入while循环,循环体里每次取到数据判断一下redis里的注册时间有无发生变化,如果变化了说明客户端再次
注册上来,需要把任务放回redis,然后退出当前循环;新注册上来的循环不会退出。这样解决了矛盾,但增加了程序复杂度,后续维护起来成本较高。相比之下,实现同样的业务逻辑,golang的实现方式显得优雅很多。

总结

通过上面两个代码片段,比较了java和golang 生成grpc 服务端代码的差异,以及完成同样业务逻辑的差别。带来差别的主要原因是java版本grpc服务端通信的实现是无发送确认的,同时通信流也是无状态的,这个差异最终导致了两种不同的代码逻辑。

思考

java的实现还可以优化么,如果redis这边用订阅模式是不是效果更好,小熊熊在这抛砖引玉了,希望大神阅后出招。

上一篇:Linux查看端口占用情况,并强制释放占用的端口


下一篇:MacOS Java+golang build protoc gRPC 代码生成