gRPC是如何高效调用的

直接操作网络协议编程,容易让业务开发过程陷入复杂的网络处理细节。RPC 框架以编程语言中的本地函数调用形式,向应用开发者提供网络访问能力,这既封装了消息的编解码,也通过线程模型封装了多路复用,对业务开发很友好。

其中,Google 推出的 gRPC 是性能最好的 RPC框架之一,它支持Java、Javascript.Python、GoLang、C++、Object-C、Android、Ruby 等多种编程语言,还支持安全验证等特性,得到了广泛的应用,比如微服务中的 Envoy、分布式机器学习中的 Tensorflow,甚至华为去年推出重构互联网的NewIP 术,都使用了gRPC框架。

然而,网络上教你使用gRPC框架的教程很多,却很少去谈 gRPC 是如何编码消息的。这样一旦在大型分布式系统中出现疑难杂症,需要通过网络报文去定位问题发生在哪个系统、主机、进程中时,你就会毫无头绪。即使我们掌握了 HTTP/2和 Protobuf 协议,但若不清楚gRPC 的编码规则,还是无法分析抓取到的 gRPC 报文。而且,gRPC 支持单向、双向的流式RPC调用,编程相对复杂一些,定位流式RPC调用引发的 bug 时,更需要我们掌握aRPC的编码原理。

这一讲,我就将以 gRPC 官方提供的 example: @data transmisstion 为例,介绍 gRPC的编码流程。在这一过程中,会顺带回顾 HTTP/2 和 Protobuf 协议,加深你对它们的理解。虽然这个示例使用的是 Python 语言,但基于gRPC框架,你可以轻松地将它们转换为其他编程语言。

如何使用gRPC框架实现远程调用?

我们先来简单地看下 gRPC 框架到底是什么。RPC 的全称是 Remote Procedure Call,即远程过程调用,它通过本地函数调用,封装了跨网络、跨平台、跨语言的服务访问,大大简化了应用层编程。其中,函数的入参是请求,而函数的返回值则是响应。

gRPC就是一种RPC框架,在你定义好消息格式后,针对你选择的编程语言,gRPC为客户端生成发起RPC请求的Stub 类,以及为服务器生成处理RPC请求的Service类(服务器只需要继承、实现类中处理请求的函数即可)。如下图所示,很明显,gRPC 主要服务于面向对象的编程语言。

gRPC支持 QUIC、HTTP/1 等多种协议,但鉴于 HTTP/2 协议性能好,应用场景又广泛,因此HTTP/2是gRPC的默认传输协议。gRPC也支持JSON 编码格式,但在忽略编码细节的RPC调用中,高效的 Protobuf 才是最佳选择!因此,这一讲仅基于 HTTP/2和 Protobuf介绍 gRPC的用法。

gRPC 可以简单地分为三层,包括底层的数据传输层,中间的框架层(框架层又包括 C 语言实现的核心功能,以及上层的编程语言框架),以及最上层由框架层自动生成的 Stub 和Service 类,如下图所示:

接下来我们以官网上的edata transmisstion 为例,先看看如何使用 gRPC 构建 Python 语言的 gRPC 环境很简单,你可以参考官网上的 QuickStart。

使用aRPC前,先要根据 Protobuf语法,编写定义消息格式的 proto 文件。在这个例子中只有1种请求和1种响应,且它们很相似,各含有1个整型数字和1个字符串,如下所示

ini 复制代码
package demo;

message Request {
fint64 client_id = 1;
string request_data = 2;
}

message Response { 
int64 server_id = 1;
string response_data = 2; 
}

请注意,这里的包名 demo 以及字段序号 1、2,都与后续的 gRPC 报文分析相关。 接着定义 service,所有的 RPC方法都要放置在 service 中,这里将它取名为 GRPCDemo。GRPCDemo 中有4个方法,后面 3 个流式访问的例子我们呆会再谈,先来看简单的一元访问模式 SimpleMethod 方法,它定义了1个请求对应1个响应的访问形式。其中,SimpleMethod 的参数 Request 是请求,返回值 Response 是响应。注意,分析报文时会用到这里的类名 GRPCDemo 以及方法名 SimpleMethod。

scss 复制代码
service GRPCDemo {
rpc SimpleMethod (Request) returns (Response);
}

用 grpc_tools 中的 protoc 命令,就可以针对刚刚定义的 service,生成含有GRPCDemoStub 类和 GRPCDemoServicer 类的 demo_pb2 grpc.py 文件(实际上还包括完成 Protobuf 编解码的 demo pb2.py),应用层将使用这两个类完成 RPC问。我简化了官网上的 Python 客户端代码,如下所示:

python 复制代码
with grpc.insecure_channel("ocalhost:23333") as channel:
    asstub = demo_pb2_grpc.GRPCDemoStub(channel)
    request = demo_pb2.Request(client_id=1,request_data="called by Python client")
    response = stub.SimpleMethod(request)

示例中客户端与服务器都在同一台机器上,通过 23333 端口访问。客户端通过 stub 对象的SimpleMethod 方法完成了 RPC 访问。而服务器端的实现也很简单,只需要实现GRPCDemoServicer 父类的 SimpleMethod 方法,返response 响应即可:

python 复制代码
class DemoServer(demo_pb2_grpc.GRPCDemoServicer):
    def SimpleMethod(self, request, context):
        response = demo_pb2.Response(
            server id=1,
            response_data="Python server SimpleMethod Ok!!!!")
    return response

gRPC是如何编码的

定位复杂的网络问题,都需要抓取、分析网络报文。如果你在 Windows 上抓取网络报文,可以使用 Wireshark 工具,如果在 Linux上抓包可以使用 tcpdump 工具。需要注意,23333 不是HTTP 常用的80或者443端口,所以Wireshark 默认不会把它解析为 HTTP/2 协议。你需要鼠标右键点击报文,选择"解码为" (Decode as),将 23333 端口的报文设置为 HTTP/2 解码器。

先来分析蓝色方框中的 HTTP/2 头部。请求中有2个关键的 HTTP 头部,path和content-type,它们决定了RPC方法和具体的消息编码格式。path 的值为"/demo.GRPCDemo/SimpleMethod",通过"/包名服务名/方法名"的形式确定了RPC方法。content-type 的值为"application/grpc",确定消息编码使用 Protobuf 格式。

HTTP/2包体并不会直接存放 Protobuf 消息,而是先要添加 5个字节的 Length-PrefixedMessage 头部,其中用4个字节明确 Protobuf 消息的长度(1个字节表示消息是否做过压缩),即上图中的桔色方框。为什么要多此一举呢?这是因为,gRPC 支持流式消息,即在HTTP/2的1条Stream 中,通过 DATA 发送多个gRPC消息,而Length-PrefixedMessage 就可以将不同的消息分离开。

最后分析 Protobuf消息,这里仅以 client id 字段为例,对上一讲的内容做个回顾。在 proto文件中 client id 字段的序号为1,因此首字节 00001000 中前 5位表示序号为1的client io字段,后3位表示字段的值类型是 varint 格式的数字,因此随后的字节 00000001 表示字段值为1。序号为2的request data 字段请你结合上一讲的内容,试着做一下解析,看看字符串"called by Python client"是怎样编码的。

再来看服务器发回的响应,点开 Wireshark 中的响应报文后如下图所示

其中 DATA 帧同样包括 Length-Prefixed Message 和 Protobuf,与 RPC 请求如出一辙,这里就不再赘述了,我们重点看下 HTTP/2 头部。你可能留意到,响应头部被拆成了2个部分其中grpc-status 和grpc-message 是在 DATA 后发送的,这样就允许服务器在发送完消息后再给出错误码。

这种将部分 HTTP 头部放在包体后发送的技术叫做 Trailer,@ RFC7230 文档对此有详细的介绍。其中,RPC请求中的 TE: trailers 头部,就说明客户端支持 Trailer 头部。在RPC响应中,grpc-status 头部都会放在最后发送,因此它的flags的 EndStream 标志位为 1。

可以看到,gRPC中的 HTTP 头部与普通的 HTTP 请求完全一致,因此,它兼容当下互联网中各种七层负载均衡,这使得 gRPC 可以轻松地跨越公网使用。

gRPC流模式协议编码

说完一元模式,我们再来看流模式 RPC 调用的编码方式。

所谓流模式,是指 RPC通讯的一方可以在1次 RPC 调用中,持续不断地发送消息,这对订阅、推送等场景很有用。流模式共有 3 种类型,包括客户端流模式、服务器端流模式,以及两端双向流模式。在@data transmisstion 官方示例中,对这3种流模式都定义了 RPC方法如下所示:

vbscript 复制代码
service GRPCDemo {
    rpc ClientStreamingMethod (stream Request) returns Response);
    rpc ServerStreamingMethod (Request) returns (stream Response);
    rpc BidirectionalStreamingMethod (stream Request) returns (stream Response);
}

不同的编程语言处理流模式的代码很不一样,这里就不一一列举了,但通讯层的流模式消息编码是一样的,而且很简单。这是因为,HTTP/2 协议中每个Stream就是天然的1次RPC请求,每个RPC消息又已经通过 Length-Prefixed Message 头部确立了边界,这样,在Stream 中连续地发送多个 DATA ,就可以实现流模式 RPC。我画了一张示意图,你可以对照它理解抓取到的流模式报文。

相关推荐
潘多编程1 小时前
Spring Boot微服务架构设计与实战
spring boot·后端·微服务
2402_857589361 小时前
新闻推荐系统:Spring Boot框架详解
java·spring boot·后端
2401_857622661 小时前
新闻推荐系统:Spring Boot的可扩展性
java·spring boot·后端
Amagi.3 小时前
Spring中Bean的作用域
java·后端·spring
侠客行03173 小时前
xxl-job调度平台之任务触发
java·后端·源码
2402_857589363 小时前
Spring Boot新闻推荐系统设计与实现
java·spring boot·后端
J老熊3 小时前
Spring Cloud Netflix Eureka 注册中心讲解和案例示范
java·后端·spring·spring cloud·面试·eureka·系统架构
Benaso4 小时前
Rust 快速入门(一)
开发语言·后端·rust
sco52824 小时前
SpringBoot 集成 Ehcache 实现本地缓存
java·spring boot·后端
原机小子4 小时前
在线教育的未来:SpringBoot技术实现
java·spring boot·后端