gRPC协议简介

devtools/2024/10/20 4:36:07/

gRPC 是谷歌开源的一套 RPC 协议框架。主要做两件事情:一是数据编码,二是请求映射

数据编码

数据编码顾名思义就是在将请求的内存对像转化成可以传输的字节流发给服务端,并将收到的字节流再转化成内存对像。方法有很多,常见的有 XML、JSON、Protobuf。XML 已经日薄西山,JSON 风头正盛,Protobuf 则方兴未艾。gRPC 默认选用 Protobuf,早期貌似只支持 Protobuf,现在号称也支持 JSON 了,但不知道有多少人在用。

为什么选 Protobuf 呢? Protobuf 在某些场景下的效率要比 JSON 高一些。请大家牢记,天下没有免费的午餐,所有的优化都是有代价的。我们在考虑问题的时候一定要思考选择什么和放弃什么

JSON

要理解 Protobuf 的优化,我们就需要回过头来看 JSON 有什么缺点。这是一段典型的 JSON

{ "int":12345, "str": "hello", "bool": true } 
{ "int":67890, "str": "hello", "bool": false }

头一个缺点是非字符串编码低效。比如 int 字段的值是 12345,内存表示只占两个字节,转成 JSON 却要五个字节。 bool 字段则占了四或五个字节。

再一个缺点就是信息冗余。同一个接口同一个对像,只是 int 字段的值不同,每次都还要传输"int"这个字段名。

等等,这是缺点吗?是!可 JSON 为什么会有这些毛病呢?因为 JSON 在可读性和编码效率之间选择了可读性,所以效率方面做了一定的牺牲。

Protobuf

好了,现在人们觉得效率是主要矛盾了,那就必然会牺牲可读性。为此,Protobuf 一方面选用了 VarInts 对数字进行编码,解决了效率问题;另一方面给每个字段指定一个整数编号,传输的时候只传字段编号,解决了冗余问题。更多细节可参考文章

在传输的时候只传了字段编号固然可以提高传输效率,但接收方如何知道各个编号对应哪个字段呢?只能事先约定了。就像当年地下工作者一样,一人拿一个密码本。Protobuf 使用 .proto 文件当密码本,记录字段和编号的对应关系

message Demo {int32 i = 1;string s = 2;bool b = 3;
}

Protobuf 提供了一系列工具,为 proto 描述的 message 生成各种语言的代码。传输效率上去了,工具链也更加复杂了。如果你给 gRPC 通信抓过包,你一定会怀念 JSON 的。

好了,数据编码问题到此告一段落,我们继续讨论请求映射问题。

因为有 .proto 作为 IDL,Protobuf 确实可以做很多 JSON 不方便做的事情。其中最重的就是 RPC 描述!

package demo.hello;service Greeter {rpc SayHello (HelloRequest) returns (HelloReply) {}
}message HelloRequest {string name = 1;
}message HelloReply {string message = 1;
}

上面的 .proto 文件定义了一个 Greeter 服务,其中有一个 SayHello 的方法,接受 HelloRequest 消息并返回 HelloReply 消息。如何实现这个 Greeter 则是语言无关的,所以叫 IDL。gRPC 就是用了 Protobuf 的 service 来描述 RPC 接口的。

请求映射

接口路径

那问题来了,gRPC 如何映射请求呢?要回答这个问题,首先要回答 gRPC 在底层使用什么传输协议。答案是 HTTP 协议,准确的说,gRPC 使用的是 HTTP/2 协议。不过就我们现在讨论的内容而言,我们暂时可以忽略 HTTP/2 和 HTTP/1 区别。

现在你可以简单认为一个 gRPC 请求就是一个 HTTP 请求(不严格)。这个 HTTP 请求用的是 POST 方法,对应的资源路径则是根据 .proto 定义确定的。我们前文提到的 Greeter 服务对应的路径是/demo.hello.Greeter/SayHello

一个 gRPC 定义包含三个部分,包名、服务名和接口名,连接规则如下

/${包名}.${服务名}/${接口名}

SayHello的包名是demo.hello,服务名是Greeter,接口名是SayHello,所以对应的路径就是 /demo.hello.Greeter/SayHello。如此的朴实无华!

gRPC 协议规定Content-Type header 的取值为application/grpc,当然也可以写成application/grpc+proto。如果你想使JSON 编码,也可以设成application/grpc+json,只要服务支持都行。

消息格式

最后就要确定请求 body 的定义了。如果用的 Protobuf 编码,那 body 肯定是编码后的字节流。那 gRPC 的 HTTP 请求是不是这样呢?

POST /demo.hello.Greeter/SayHello HTTP/1
Host: grpc.demo.com
Content-Type: application/grpc
Content-Length: 1234<protobuf bytes>

答案是否定的!简单来说,gRPC 要求在 Protobuf 字节流前面加一个五字节的前缀,第一个字节表示字节流是否被压缩,后四个字节存储数据长度,并取名叫作 Length-Prefixed Message

熟悉 HTTP 协议的同学都清楚,HTTP 协议本身可以通过 Content-Encoding 表示压缩算法,使用 Content-Length 指定数据长度。gRPC 为什么要重新定义一套机制呢?

流式接口

答案在于 gRPC 支持的另一特性 stream rpc!为方便行文,我们称之为流式接口。所谓流式,就是可以源源不断收发消息。这个跟 HTTP 的一收一发有着显著的差别。

service Greeter {rpc SayHello (HelloRequest) returns (HelloReply) {}rpc SayHello (stream HelloRequest) returns (HelloReply) {}rpc SayHello (HelloRequest) returns (stream HelloReply) {}rpc SayHello (stream HelloRequest) returns (stream HelloReply) {}
}

gRPC 持三种流式接口,定义的办法就是在参数前加上 stream 关键字,分别是:请求流、响应流和双向流。

第一种叫请求流,可以在 RPC 发起之后不断发送新的请求消息。此类接口最典型的使用场景是发推送或者短信。

第二种叫响应流,可以在 RPC 发起之后不断接收新的响应消息。此类接口最典型的使用场景是订阅消息通知。

最后一种是双向流。可以在 RPC 发起之后同时收发消息。此类接口最典型的使用场景是实时语音转字幕。

为了实现流式传输,gRPC 不得不引入所谓的 Length-Prefixed Message。同一个 gRPC 请求的不同消息共用 HTTP 头信息,所以只能给每个消息单独加一个五字节的前缀来表示压缩和长度信息了。

就是因为这五个字节,不管你是 Protobuf 还是 JSON,都注定了 gRPC 只能是二进制协议,UNIX 下常用的文本工具都无法很好地处理 gRPC 的通信内容。

返回状态

gRPC 还定义了自己的返回状态和消息,分别用 grpc-status 和 grpc-message 头传输。所以最简单的 gRPC 通信(非流式调用,unary)内容长成这个样子

请求内容

POST /demo.hello.Greeter/SayHello HTTP/1.1
Host: grpc.demo.com
Content-Type: application/grpc
Content-Length: 1234<Length-Prefixed Message>

响应内容

HTTP/1.1 200 OK
Content-Length: 5678
Content-Type: application/grpc<Length-Prefixed Message>

如果你真的理解前文所讲的内容,那么你现在可以写一个非流式 gRPC 的客户端了。 sniper 框架就自带了一个,源码在这里。

gRPC vs HTTP

最后讲一下 gRPC 跟 HTTP 协议的关系。如果不熟悉HTTP协议可以阅读另一篇文章。

如果单看非流式调用,也就是 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 调用的实际通信内容长这个样子。

请求内容

HEADERS (flags = END_HEADERS) # header frame
:method = POST
:scheme = http
:path = /demo.hello.Greeter/SayHello
:authority = grpc.demo.com
content-type = application/grpc+protoDATA (flags = END_STREAM) # data frame
<Length-Prefixed Message>

响应内容

HEADERS (flags = END_HEADERS) # header frame
:status = 200
content-type = application/grpc+protoDATA # data frame
<Length-Prefixed Message>HEADERS (flags = END_STREAM, END_HEADERS) # header frame
grpc-status = 0

HTTP/2 的 header 和 data 使用独立的 frame(中文译作帧,简单来说也是一种 Length-Prefixed 消息,是 HTTP/2 通信的基本单位) 发送,可以多次发送。HTTP/1.1 只能先发 header 再发 data(不完全准确。自己查,提示 http trunk),HTTP/2 可以交替发送。比如上文中的 gRPC 响应,先发一个 header frame,告知 http 状态;再发一个 data frame,传输 gRPC 消息;最后又发了一个 header frame,告知 grpc-status 状态,这是 gRPC 自定义的状态码

慢着!一般不是先发 header 再发 data 的吗?为什么 gRPC 需要在发完 data 之后才发 grpc-status 头呢?

还是流式接口导致的问题。你想呀,在所有的流式消息没有传输完成之前,服务端也不知道要传什么 grpc-status 呀。

总结

好了,到这里请求映射的问题也分析完了。让我们回到最开始的问题。

如果 gRPC 好,它好在哪里?为了这些好,它又牺牲了哪些方面?我们的业务面临什么问题?gRPC 的优点能否为我所用?gRPC 的缺点会不会给我们带来不便?

这些问题在这篇文章中有过全面的分析,有兴趣的同学可以移步阅读


http://www.ppmy.cn/devtools/120848.html

相关文章

C++基础补充(02)C++其他控制语句break continue goto等

文章目录 1. break2. continue 语句3. goto 语句goto的存在 4. 跳出多重循环4.1 goto 直接跳转4.2 C11及其后版本的 return 语句4.3 使用标志变量 在C中&#xff0c;控制语句用于管理程序的执行流程。常见有 break、continue 和 goto。 1. break break语句主要用于在循环或者s…

分层图 的尝试学习 1.0

分层图&#xff1a; 分层图的最短路&#xff1a; 又叫做 扩点最短路。不把实际位置看做是图上的点&#xff0c;而是把实际位置及其状态的组合&#xff0c;&#xff08;一个点有若干的状态&#xff0c;所以一个点会扩充出来若干点&#xff09;看做是图上的点&#xff0c;然后搜索…

第二十三节:学习拦截器或者使用AOP实现用户token参数请求检测(自学Spring boot 3.x的第六天)

这节记录下如何使用aop或者使用interceptor实现用户请求的是否带token&#xff0c;本文只是简单检查用户请求是否带参数token&#xff0c;并不对token的正确性进行验证。通常要从后台缓存中进行token校验。 第一种方式&#xff1a;拦截器方式 第一步&#xff1a;新建一个拦截器…

redis和mysql端口修改

因为之前有过被删库勒索的情况所以&#xff0c;今天记录一下怎么修改端口。 redis 要修改Redis的端口&#xff0c;您需要编辑Redis配置文件&#xff0c;通常名为redis.conf。 找到Redis配置文件&#xff1a; 在Linux系统上&#xff0c;该文件通常位于/etc/redis/redis.conf…

Python_partial函数和map同时用

partial 在Python中&#xff0c;functools 模块提供了一个 partial 函数&#xff0c;它可以用来创建一个部分函数。部分函数是指固定某些参数的函数。这在你需要重复使用同一个函数&#xff0c;但每次都只用到其中一部分参数的情况下非常有用。 下面是一个使用 functools.par…

windows下安装rabbitMQ并开通管理界面和允许远程访问

如题&#xff0c;在windows下安装一个rabbitMQ server&#xff1b;然后用浏览器访问其管理界面&#xff1b;由于rabbitMQ的默认账号guest默认只能本机访问&#xff0c;因此需要设置允许其他机器远程访问。这跟mysql的思路很像&#xff0c;默认只能本地访问&#xff0c;要远程访…

Nodejs多版本切换工具NVM

1 nvm介绍 NVM&#xff08;Node Version Manager&#xff09;是一个用于管理多个Node.js版本的工具&#xff0c;它允许用户在同一台计算机上安装和切换不同版本的Node.js。这对于开发者来说非常有用&#xff0c;因为不同的项目可能需要不同版本的Node.js环境。 NVM功能特性&a…

Python 封装 socket 为 [TCP/UDP/MULTICAST] 服务端

在新线程中创建 TCP/UDP/MULTICAST 协议的服务端套接字&#xff0c;接收客户端的连接请求或数据&#xff0c;并调用 on_recv 回调函数处理数据。 #!/usr/bin/env python # -*- coding: utf-8 -*- import socket import threading import multiprocessingclass ServerSocket:de…