RPC 核心概念 03:序列化与传输协议
RPC 核心概念 03:序列化与传输协议
一次 RPC 调用的"数据流"主要由两部分构成:
- 序列化协议:决定数据如何变成字节序列;
- 传输协议:决定字节序列如何在网络上传输。
理解二者的本质,才能在不同业务场景做出正确选型。
一、序列化协议
1.1 为什么需要序列化?
进程内的对象是有指针、有结构的;网络只能传字节。我们必须把对象**编码(marshal)成字节流,对端再解码(unmarshal)**回对象。
1.2 主流序列化协议对比
| 协议 | 编码方式 | 性能 | 体积 | 可读性 | 跨语言 | 典型框架 |
|---|---|---|---|---|---|---|
| JSON | 文本 | 慢 | 大 | 好 | 全 | HTTP API |
| XML | 文本 | 慢 | 大 | 好 | 全 | SOAP |
| Protobuf | 二进制 TLV | 快 | 小 | 差 | 强 | gRPC、tRPC |
| Thrift Binary | 二进制 | 快 | 小 | 差 | 强 | Apache Thrift |
| MessagePack | 二进制 | 快 | 小 | 差 | 强 | Redis、Fluentd |
| Avro | 二进制 + Schema | 快 | 小 | 差 | 强 | Kafka、Hadoop |
| FlatBuffers | 零拷贝 | 极快 | 中 | 差 | 强 | 游戏、移动端 |
1.3 选型建议
| 场景 | 推荐 |
|---|---|
| 浏览器/移动端调试 | JSON |
| 内部高性能 RPC | Protobuf |
| 大规模数据存储 | Avro |
| 实时游戏低延迟 | FlatBuffers |
| 海量日志 | MessagePack / Protobuf |
二、Protobuf 序列化原理(再深入)
2.1 TLV 编码
field_number wire_type payloadwire_type 共 6 种:
| Wire Type | 名称 | 用于的类型 |
|---|---|---|
| 0 | Varint | int32, int64, bool, enum |
| 1 | 64-bit | fixed64, double |
| 2 | Length-delimited | string, bytes, embedded message, repeated |
| 3 | Start group | (废弃) |
| 4 | End group | (废弃) |
| 5 | 32-bit | fixed32, float |
2.2 Varint:变长整数
小的数字用更少字节:
- 0~127:1 字节
- 128~16383:2 字节
- 大数最多 10 字节
每个字节最高位作为"还有后续字节"的标识。
150 = 10010110 00000001 ↑MSB=1(继续) ↑MSB=0(结束) 解码:去掉 MSB → 0010110 0000001 → 翻转 → 0000001 0010110 → 1502.3 ZigZag 编码
由于 Varint 对负数(高位全 1)效率极差,sintXX类型先做 ZigZag 转换:
0 → 0 -1 → 1 1 → 2 -2 → 3 ...把符号位"折叠"到末位,让小绝对值的负数也只占一个字节。
2.4 实战:手算编码
message Person { int32 age = 1; }Person{age: 100}的编码:
[tag(field=1,type=0)] [varint(100)] 0x08 0x64总共 2 字节。
三、传输协议
3.1 OSI 模型回顾
应用层 HTTP, gRPC, Thrift, tRPC 传输层 TCP, UDP, QUIC 网络层 IPRPC 关注"应用层 + 传输层"。
3.2 直接基于 TCP
很多自研 RPC 框架(如早期 Dubbo、自研协议)直接基于 TCP,自己定义"魔数 + 长度 + 头部 + body"格式:
+--------+--------+--------+--------+ | MAGIC | VERSION| TYPE | RESERVED| +--------+--------+--------+--------+ | MSG_LENGTH | +-----------------------------------+ | HEADER | +-----------------------------------+ | BODY | +-----------------------------------+优点:极致性能;缺点:缺乏标准基础设施支持(如 LB、Ingress 都对 HTTP 友好)。
3.3 基于 HTTP/1.1
最大优势是通用性——所有网络设施都认识 HTTP。但 HTTP/1.1 有头部冗余、队头阻塞等问题,不适合高性能 RPC。
3.4 基于 HTTP/2(gRPC 的选择)
HTTP/2 的关键特性:
- 二进制帧:取代文本协议;
- 多路复用:单 TCP 上并行多个 stream,消除队头阻塞;
- HPACK 头压缩:极大减少头部体积;
- 服务端推送;
- 流式传输。
这正是 gRPC 选择 HTTP/2 的原因。一次 gRPC 调用 = 一个 HTTP/2 stream,请求/响应都用 trailers 携带 status。
3.5 基于 QUIC(HTTP/3)
QUIC 基于 UDP,解决了 TCP 队头阻塞、连接迁移等问题。gRPC 已实验性支持。
3.6 tRPC 的多协议设计
tRPC 不绑死任何协议,协议是可插拔的:
- 默认
trpc协议(基于 TCP+protobuf) - 可配置 HTTP+JSON
- 可与 gRPC 互通
- 也支持自定义协议
server:service:-name:trpc.app.server.Helloprotocol:trpc# 切到 grpc / http 改一行四、连接管理
4.1 短连接 vs 长连接
| 模式 | 适用 |
|---|---|
| 短连接 | 调用极少、跨网络层 |
| 长连接 | RPC、WebSocket、数据库 |
RPC 默认使用长连接 + 多路复用。
4.2 连接池
为目标服务维护一组连接:
ServiceA → [ conn1, conn2, conn3 ] → ServiceB关键参数:
max_idle:空闲上限max_active:活跃上限idle_timeout:空闲超时
4.3 Keep-Alive
定期发送心跳维持长连接、检测半开连接:
client:keepalive_time:30skeepalive_timeout:10s五、半包与粘包
TCP 是面向字节流的,没有消息边界。如果应用层不处理就会出现:
- 粘包:两条消息粘到一起;
- 半包:一条消息被切成两段。
解决方案:
- 定长:每条消息固定长度(适合简单协议);
- 分隔符:用特殊字节作结束符(如 HTTP 用
\r\n\r\n); - TLV/Header+Body(最常用):先读固定长度的 header,header 中带 body 长度,再读 body。
Protobuf RPC 框架普遍使用第三种。
六、流量控制与背压
HTTP/2 内置流量控制:每个 stream 都有 window,接收方处理不过来会让发送方阻塞。tRPC、gRPC 都依赖该机制做反压,避免下游被打爆。
七、压缩
减少带宽消耗,但增加 CPU 开销。常见算法:
| 算法 | 速度 | 压缩比 |
|---|---|---|
| gzip | 中 | 中 |
| snappy | 快 | 低 |
| zstd | 较快 | 高 |
| lz4 | 极快 | 低 |
gRPC/tRPC 都支持在调用级别启用压缩:
client.Hello(ctx,req,client.WithCompressType(codec.CompressTypeGzip))八、实战:抓包看一次 gRPC 调用
# 前置:开启 tcpdumpsudotcpdump-ilo0-wgrpc.pcap port9000# 用 wireshark 打开# 设置 HTTP/2 解析(Edit > Preferences > Protocols > HTTP2)你会看到:
- HTTP/2 帧:HEADERS、DATA、TRAILER;
- HEADERS 中的
:path = /package.Service/Method; - DATA 中的 protobuf 字节;
- TRAILER 中的
grpc-status: 0。
九、序列化性能优化技巧
- 消息池化:使用
sync.Pool复用 message 对象; - 零拷贝:尽量用
bytes类型而非string频繁转换; - 批量调用:合并多次小调用为一次;
- 预分配 slice/map 容量;
- 慎用反射:protojson 比 json.Marshal 慢,优先二进制。
十、小结
- 序列化决定数据格式,传输协议决定数据怎么走;
- Protobuf 通过 TLV+Varint+ZigZag 实现紧凑高效;
- HTTP/2 是 RPC 的现代标配,多路复用解决了队头阻塞;
- 解决粘包:长度前缀法是工业标准;
- 选型时综合考虑性能、可读性、生态。
下一篇我们将进入分布式 RPC 的灵魂:服务发现与负载均衡。
