Dawn's Blogs

分享技术 记录成长

0%

Triple协议 (3) gRPC协议

gRPC 是谷歌开源的一套 RPC 协议框架,基本上也是最为广泛使用的 RPC 协议。理解 gRPC 就要从两方面去理解:数据编码(序列化)和数据传输。

数据编码(序列化)

数据编码顾名思义就是在将请求的内存对像转化成可以传输的字节流发给服务端,并将收到的字节流再转化成内存对像。gRPC 默认使用 Protobuf 进行序列化,也支持 Json 格式的序列化。

Json

Json 序列化的优点:

  • 可读性高。

缺点:

  • 编码低效,是非二进制编码。
  • 信息冗余,对于同一个 Json 对象,需要传输 key。

Protobuf

Protobuf 序列化的优点:

  • 编码高效,是二进制编码,且会进行压缩。
  • 信息不存在冗余,给每一个字段一个编号,传输时只需要传输这个编号即可。

缺点:

  • 人类不可读性。

Protobuf 需要 IDL 描述接口,使用 IDL 进行强约束有好处也有坏处

好处就是对字段有了约束,只需要传输编号,避免信息冗余。坏处就是需要相关的工具链(protoc),为 IDL 生成代码。

数据传输

gRPC 使用的是 HTTP/2 协议,可以简单的认为一个 gRPC 请求就是一个 HTTP 请求,这个 HTTP 请求用的是 POST 方法。

请求 Request

一个 gRPC 的请求报文如下,包括:

  • HTTP Header
  • HTTP Body:又分为 Length-Prefixed Message 和 Protobuf 消息。

image2022-1-27_15-20-18

HTTP Header

一个 gRPC 定义包含三个部分,包名、服务名和接口名。在发起 HTTP 请求时,Path 路径如下:

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

gRPC 协议规定Content-Type 的取值为 application/grpc(默认,使用 protobuf 编码)、application/grpc+proto(使用 protobuf 编码)、application/json(使用 json 编码)。

HTTP Body

HTTP Body 并不会直接存放 Protobuf 消息,而是先添加 5 个字节的 Length-Prefixed Message 头部,其中用 4 个字节明确 Protobuf 消息的长度,1 字节表示消息是否被压缩过。

为什么要多此一举呢?这是因为,gRPC 支持流式消息,即在 HTTP/2 的 1 条 Stream 中,通过 DATA 帧发送多个 gRPC 消息,而 Length-Prefixed Message 就可以将不同的消息分离开。

因为 Length-Prefixed Message 的五个字节,导致 gRPC 只能是二进制协议,即使是 json 进行编码,curl 和浏览器(web)都不能原生的支持 gRPC,只能用专用的工具!

返回 Response

一个 gRPC 响应报文如下,包括:

  • HTTP 头部:包括 Header 和 Tailer。
  • HTTP Body:又分为 Length-Prefixed Message 和 Protobuf 消息。

image2022-1-27_15-24-18

其中 HTTP 头部被分为 Header 和 Tailer。Tailer 中的 grpc-status 和 grpc-message 是在 DATA 帧的最后,这样就允许服务器在发送完消息后再给出错误码。

gRPC 中以 grpc-status 和 grpc-message 表示自己的返回状态和消息。

HTTP 2 中的 HEADER 帧和 DATA 帧是独立的,对于一个 gRPC 响应可以先发一个 HEADER 帧告知 HTTP 状态,再发送 DATA 帧传输 gRPC 消息,最后发送一个 HEADER 帧传输 grpc-status。

一般都是先发 HEADER 再发 DATA,为什么 gRPC 需要在发完 DATA 之后才发 grpc-status 头呢

因为式传输,在所有的流式消息没有传输完成之前,服务端也不知道要传什么 grpc-status。

gRPC 中的 Tailer 设计

Tailer 介绍

H1.1 中的 Tailer

HTTP 协议在返回数据的时候通常是先发送 Header 信息,再发送 Body 数据。但 Trailers 是一类特殊的 Header,它们是在 Body 传输结束后才发送给客户端的。因为发送顺序不同,所以,在 HTTP/1.1 中 Trailers 只能跟 chunked 传输编码配合使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked
Trailer: MD5

7\r\n
Mozilla\r\n
9\r\n
Developer\r\n
7\r\n
Network\r\n
0\r\n
MD5: 68b329da9893e34099c7d8ad5cb9c940\r\n
\r\n

Trailer 头里面表示数据传输结束后还有额外的 Header,Header 的名字为 MD5。可以指定多个,以逗号分隔。所以在分段数据之后又额外发送了所有数据的 MD5 值用作校验。因为 Body 的内容是动态生成的,不可能事先得到它的 MD5 值。只能是一边传输一边计算,等传完了也就计算好了,然后使用 Trailer 头「补发」给客户端。

H2 中的 Tailer

到了 HTTP/2 时代,因为有了帧的概念,所以 Header 和 Body 可以并发传输,不再有先发 Header 再传 Body 的限制。因此,在 HTTP/2 中,Trailers 不需要依赖 chunked 传输编码,所有的响应都能发送 Trailers 信息

gRPC 中的 Tailer

gRPC 中为什么用到了 Tailer?原因就是流传输,在流式接口中因为一个帧可能携带多条数据,所以无法事先确定数据的长度(所以不可能使用 Content-Length 头)

长度不确定,为什么不使用 chunked 传输?

假如客户端跟服务器之前有一个代理,代理收到响应之后开始将数据转发给客户端。首先就是把 Header 部分发送给客户端,于是调用方确定本次的状态码为 200,成功了。然后逐段转发数据部分,如果代理在转发完第一段数据后服务端异常退出了,那代理需要给客户端发送什么信号呢?

因为状态码已经发出去,所以没办法把 200 改成 5xx 了。也不能直接发送 0\r\n 来结束 chunked 传输,这样客户端没有办法得知服务器已经异常退出的信息。唯一能做的就是直接关闭对应的底层连接,但这样会因为客户端创建新的连接而消耗额外的资源。

所以需要找一种尽量复用底层连接的条件下通知客户端服务器出错的办法,最终 gRPC 团队决定使用 Trailers 来传输,Tailer 中携带 grpc-status 来反应响应的状态。

问题

Tailer 导致了不支持浏览器(Chrome 认为 Tailer 会导致安全问题),而 Length-Prefixed Message 同样导致无法直接用 curl + json 的方式来调试 gRPC 接口,必须使用专门的工具。

然有了消息前缀,那完全可以把 Trailers 的职能转移到消息前缀里,比如可以设置一个特殊的前缀来传输 grpc-status 等字段。如果当初这样做的话,那么现在也就可以直接在浏览器调用 gRPC 接口了,可以惜木已成舟。