透过实例 demo 带你认识 gRPC
本文分享自华为云社区《gRPC介绍以及spring demo构架展示》,作者:gentle_zhou。
gRPC,即 google Remote Procedure Call Protocol;在 gRPC 里,客户端可以直接调用不同机器上的服务应用的方法,就像本地对象一样,所以创建分布式应用和服务就变简单了。
gRPC 是基于定义一个服务,指定一个可以远程调用的带有参数和返回类型的的方法。在服务端,服务实现这个接口并且运行 gRPC 服务处理客户端调用。在客户端,有一个 stub 提供和服务端相同的方法。

数据编码
数据编码即将请求的内存对象转化成可以传输的字节流发送给服务端,并将收到的字节流在转化成内存对象。常见的数据编码方法有 JSON,而 gRPC 则默认选用 protobuf。为什么选用 protobuf 呢?一个是因为它是谷歌自己的产品,二是它作为一种序列化资料结构的协定,在某些场景下传输的效率比 JSON 高。一个.proto 文件里的消息格式如下:

而一个典型的 JSON 格式如下所示:

我们可以看到在 JSON 里,内存方面,int 字段的 12345 会占据 5 个字节,bool 字段的 true 会占据 4 个字节,占据内存就会比较大,编码低效;还有一个缺点就是在 JSON 里,同一个接口同一个对象,只是 int 字段的值不同,每次却都还要传输 int 这个字段名。这样做的好处就是 JSON 的可读性很高,但同样在编码效率方面就会有所牺牲。
而 Protobuf 则是选用了 VarInts 对数字进行编码(VarInts 则是动态的,征用了每个字节的最高位 MSB,如果是 1 表示还有后序字节,如果是 0 表示后面就没字节了,以此来确定表示长度所需要的字节数量,解决了效率问题),同时给每个字段指定一个整数编号,传输的时候只传字段编号(解决了效率和冗余问题)。但是只传字段编号的话,接收方如何知道各个编号对应哪个字段呢?那就需要靠提前约定了。Protobuf 使用.proto 文件当做密码本,记录字段和编号的对应关系。

Protobuf 提供了一系列工具,为 proto 描述的 message 生成各种语言的代码。传输效率上去了,工具链也更加复杂了。
请求映射
IDL,Interactive Data Language 的缩写,交互式数据语言。因为我们有.proto 文件作为 IDL,Protobuf 就可以做到 RPC 描述。比如在.proto 文件里定义一个 Greeter 服务,其中有一个 SayHello 的方法,接受 HelloRequest 消息并返回 HelloReply 消息。如何实现这个 Greeter 则是语言无关的,所以叫 IDL。gRPC 就是用了 Protobuf 的 service 来描述 RPC 接口的。

gRPC 在底层使用的是 HTTP/2 协议。这个 HTTP 请求用的是 POST 方法,对应的资源路径则是根据 .proto 定义确定的。我们前面提到的 Greeter 服务对应的路径是/demo.hello.Greeter/SayHello 。一个 gRPC 定义包含三个部分,包名、服务名和接口名,连接规则如下
/${包名}. ${服务名}/ ${接口名}
SayHello 的包名是 demo.hello,服务名是 Greeter,接口名是 SayHello,所以对应的路径就是 /demo.hello.Greeter/SayHello。
gRPC 支持三种流式接口,定义的办法就是在参数前加上 stream 关键字,分别是:请求流、响应流和双向流。第一种叫请求流,可以在 RPC 发起之后不断发送新的请求消息。此类接口最典型的使用场景是发推送或者短信。第二种叫响应流,可以在 RPC 发起之后不断接收新的响应消息。此类接口最典型的使用场景是订阅消息通知。最后一种是双向流。可以在 RPC 发起之后同时收发消息。此类接口最典型的使用场景是实时语音转字幕。如下就是普通接口和三种流式接口的结构样式:

最简单的 gRPC(非流式调用,unary)请求内容和相应内容如下所示:


如果单看非流式调用,也就是 unary call,gRPC 并不复杂,跟普通的 HTTP 请求也没有太大区别。我们甚至可以使用 HTTP/1.1 来承载 gRPC 流量。但是 gRPC 支持流式接口,这就有点难办了。
我们知道,HTTP/1.1 也是支持复用 TCP 连接的。但这种复用有一个明显的缺陷,所有请求必须排队。也就是说一定要按照请求、等待、响应、请求、等待、响应这样的顺序进行。先到先服务。而在实际的业务场景中肯定会有一些请求响应时间很长,客户端在收到响应之前会一直霸占着 TCP 连接。在这段时间里别的请求要么等待,要么发起新的 TCP 连接。在效率上确实有优化的余地。一言以蔽之,HTTP/1.1 不能充分地复用 TCP 连接。
后来,HTTP/2 横空出世!通过引入 stream 的概念,解决了 TCP 连接复用的问题。你可以把 HTTP/2 的 stream 简单理解为逻辑上的 TCP 连接,可以在一条 TCP 连接上并行收发 HTTP 消息,而无需像 HTTP/1.1 那样等待。
所以 gRPC 为了实现流式接品,选择使用 HTTP/2 进行通信。所以,前文的 Greeter 调用的实际通信内容长这个样子。


HTTP/2 的 header 和 data 使用独立的 frame(帧,简单来说也是一种 Length-Prefixed 消息,是 HTTP/2 通信的基本单位) 发送,可以多次发送。
springboot 里的 grpc demo
整个项目可以分成三个 project:
grpc-springboot-demo-api:proto 文件(syntax=“proto3”; 定义服务,定义请求体,定义回应内容)写好之后用 maven-install 来编译生成所需的类;
grpc-springboot-demo-server:pom 文件(springboot 的启动依赖,grpc 的依赖, api 项目的依赖),springboot 的启动类,GRPC 服务器的启动类,提供服务的业务逻辑实现类
grpc-springboot-demo-consumer:pom 文件(springboot 的启动依赖,grpc 的依赖, api 项目的依赖),springboot 启动类(与服务端启动类无差异),gRPC 客户端(主要作用是监听 gRPC 服务端,开启通道)。
对应 MVC 关系就是:grpc-springboot-demo-api 就是 service(接口,为提供实现);grpc-springboot-demo-server 就相当于 serviceImpl(service 的实现类);grpc-springboot-demo-consumer 就是 controller 的角色。
具体代码可以看:https://blog.csdn.net/Applying/article/details/115024675
拓展
repeated 限定修饰符
repeated 代表可重复,我们可以理解为数组。比如下面的代码:
编译器就会把 Person 认定为数组,而我们在使用 Person,用 add 往里面添加信息,代码如下:AddressBook addBopookReq = AddressBook.newBuilder().addName("Lily").build();
就不需要指定 index 了,直接往数组里添加了一个新的 addressbook,它的名字属性则是 Lily。
参考资料
https://developers.google.com/protocol-buffers/docs/overview
https://taoshu.in/grpc.html
https://grpc.io/
https://grpc.io/docs/languages/java/quickstart/
https://blog.csdn.net/tennysonsky/article/details/73921025
版权声明: 本文为 InfoQ 作者【华为云开发者社区】的原创文章。
原文链接:【http://xie.infoq.cn/article/75589360d34e3a7764aa13883】。文章转载请联系作者。
评论