Protobuf 编码和解码原理
Protobuf 编码和解码原理
一、概念
field_number(字段序列):
.proto 文件定义 message,每个字段指定唯一的「字段序列」,如:
1
2
3
4
message User {
string name = 1;
int32 age = 2;
}
wire_type(二进制类型):
Protobuf 有 4 种类型,分布对应不同的编解码规则。 主要区分 message 中的字段类型。
| wire_type | 类型 | 字段类型(message) | 说明 |
|---|---|---|---|
| 0 | Varint | int32 / int64 / bool / enum | 使用 Varint 编码 |
| 1 | 64-bit | fixed64 / double | 64 位定长 |
| 2 | Length-delimited | string / bytes / 嵌套消息 | Varint 类型描述长度,向后读 length 个字节 |
| 5 | 32-bit | fixed32 / float | 32 位定长 |
Varint(可变长度整数)编码
变长整数编码。低 7 bit 存数据,高 1 bit 表示是否还有后续字节,如:
1
2
3
4
5
300 的二进制:100101100
使用 int32 表示需要 4 个字节 。
Varint 编码:10101100 00000010。只需要 2 个字节
核心目的是节约空间
ZigZag(/ˈzɪɡzæɡ/)编码
如果用 Varint 编码表示负数,会占用大大量空间,如: 表示 int32(-1) 需要 10 个字节。 所以引入了 ZigZag 编码,ZigZag 会把负数一一映射成小正数,再用 Varint 编码。
二、二进制结构
Protobuf 本质是 Tag-Length-Value(变种 TLV)结构:
1
2
3
4
[tag] + <length> + [value]
length 是可选的
tag = (field_number << 3) | wire_type
三、Protobuf 编码原理
Tag 如何编码
tag 包含 field_number 和 wire_type
1
2
3
4
tag = (field_number << 3) | wire_type
低 3 位是 wire_type
高位是 field_number
tag 是 Varint
Length-delimited (长度定界)
描述长度,向后读 length 个字节。
1
2
3
4
message User {
string name = 1;
int32 age = 2;
}
如果 name = “abc”,则被编码成:
1
tag + 3 + abc
嵌套 message 编码
子 message 会被编码成 bytes,然父 message 使用 Length-delimited 包裹
Repeated 编码
对于基础类型使用:
1
tag length value value value
非基础类型使用:
1
2
3
tag + value
tag + value
tag + value
四、Protobuf 解码原理
解码是编码的逆运算,流程:顺序扫描二进制流 → 解析 tag → 根据 wire_type 读 value → 写入结构体字段。
二进制流:
1
[ tag ][ value ][ tag ][ value ] ...
解析过程:
1
2
3
4
5
6
while not EOF:
1. 读 tag (varint)
2. 解析 field_number 和 wire_type
3. 根据 wire_type 读取 value
4. 根据 field_number 找到 struct 字段
5. 赋值
如何找到 struct 字段
- 每个 message 有字段编号表
- 通过 field_number 做查找到对应字段
- 然后写入对应内存偏移
五、Q & A
对比 Json,为什么快
| 维度 | JSON | Protobuf |
|---|---|---|
| 数据格式 | 文本 | 二进制 |
| 字段名 | 每次传 | 只传 field_number |
| 解析 | 字符串解析 | 位操作 |
| 赋值 | 断言 | 内存偏移写入 |
| 大小 | 大 | 小很多 |
This post is licensed under CC BY 4.0 by the author.