摘要
- 头部设计
- 协议体设计
- 编解码
RPC 协议
在实际应用中,大多数 RPC 框架支持多种序列化协议。除此之外,我们还需要考虑以下几个因素:
- 数据压缩:提高传输效率,节省带宽。
- 协议版本升级:确保向后兼容,便于系统迭代。
- 加密:保护数据安全,防止数据泄露。
这些考虑引发了一个重要问题:如何设计一个合适的 RPC 协议?
不同协议:gRPC 协议
gRPC 协议分成头部和 body 两个部分。这是因为 gRPC 是直接基于 HTTP 来实现的,所以 gRPC 的头部就放在 HTTP 协议头,body 就放在 HTTP 协议体里面。
不同协议:Dubbo 协议
Dubbo 协议按照它自己的说法,是分成定长部分和非定长的部分。其中定长部分一般叫协议头,变长部分一般叫做协议体。
Dubbo 在 3.0 里面推出了 Triple 协议,就很接近 gRPC 的设计了。原生的 Dubbo 协议反而和 gRPC 很不一样。
不同协议:TCP 协议
gRPC 和 Dubbo 协议都可以看做是应用层协议,其它协议设计也是类似的,比如 TCP 协议。
协议设计总结
- 协议一般都是分成两个部分:协议头和协议体。
- 协议头包含接收方“如何处理这个消息”的必要信息,具体来说包含描述协议本身的数据,和描述这次请求的数据。
- 协议体则大多数情况下存放请求数据。
从形态上来说,协议体必然是变长的;而协议头可以是定长的,也可以是变长的。在变长协议头的情况下,需要有两个字段来描述长度,一个描述协议头有多长,一个描述协议体有多长。
协议设计:请求
现在设计一下我们自己的协议,请求头部:
- 设计为不定长
- 固定字段:
- 长度字段:用于分割消息
- 版本字段:描述协议版本,用于后续协议升级
- 序列化协议:用于标记采用的序列化协议
- 压缩算法:用于标记协议体是如何被压缩的
- 消息 ID:用于后续支持多路复用
- 服务名
- 方法名
- 不固定字段:这部分主要是链路元数据,例如 trace id、a/b 测试、全链路压测的标记位等
最后的协议体里面就只存放请求参数。
协议设计:请求可变通的点
服务名、方法名和元数据都可以考虑放请求到参数里,之所以放在头部是因为:
如果我们的微服务请求要经过网关、sidecar(service mesh),那么放在头部,这些中间件就可以考虑只解析头部字段,而不必解析整个请求。
例如在 sidecar 上做负载均衡,那么只需要解析到服务名和方法名就可以,根据服务名和方法名找到可用节点,然后做负载均衡。
这些东西经常需要使用到橘色方框里面的内容。
协议设计:响应
现在我们设计一下我们自己的协议,响应头部:
- 设计为不定长
- 固定字段:
- 长度字段:用于分割消息
- 版本字段:描述协议版本,用于后续协议升级
- 序列化协议:用于标记采用的序列化协议
- 压缩算法:用于标记协议体是如何被压缩的
- 消息 ID:用于后续支持多路复用
- 错误:为了解决第二个返回值的问题
最后的协议体里面就只存放请求响应数据。
协议设计:响应错误为什么放头部
主要是实在没地方放。从理论上来说,你有两个选择:
- 放头部,做成一个类似于 serviceName 那种,认为是服务调用本身必需的一种数据。
- 协议体,也就是认为是响应体的一部分。这种做法会更加符合直觉,但是实现起来难度要更高,因为你需要区别响应数据里面,哪部分是用户返回的数据,哪部分是用户返回的错误。
接口设计
首先改造我们的 Request 和 Response,把刚才决定的协议字段加进去。
type Request struct {
HeaderLength uint32 // 协议头长度
BodyLength uint32 // 协议体长度
MessageID uint32 // 消息ID
Version uint8 // 版本
Compressor uint8 // 压缩算法
Serializer uint8 // 序列化协议
ServiceName string // 服务名
MethodName string // 方法名
Meta map[string]string // 扩展字段,用于传递自定义元数据
Data []byte // 协议体
}
type Response struct {
HeaderLength uint32 // 协议头长度
BodyLength uint32 // 协议体长度
MessageID uint32 // 消息ID
Version uint8 // 版本
Compressor uint8 // 压缩算法
Serializer uint8 // 序列化协议
Error []byte // 错误信息
Data []byte // 响应数据
}
总结
- RPC 协议主要包含什么?消息头和消息体。
- 头部包含什么?有什么用?回答几个主要的头部字段,也就是我们刚才讨论的。
- RPC 里面为什么包含长度字段?主要是为了切割消息,也就所谓的粘包问题。
- 头部可以是变长的吗?当然可以。
- 大概描述一下 gRPC 协议?关键点是 gRPC 利用了 HTTP 协议,然后再描述一下 gRPC 的几个常见头部。问到 Dubbo 协议也是类似。
- 为什么尽量把元数据之类的东西放在消息头?主要是考虑到 sidecar 网关之类的东西,这样可以做到解析部分数据,提高性能。
- 如何在 RPC 协议里面支持不同序列化协议/压缩算法……?但凡问到如何在 RPC 协议上支持 XXX 功能,核心都是要在协议本身加上对应的字段,要考虑是放到协议体还是协议头,然后客户端和服务端都要做相应的修改。
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。