Post

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)说明
0Varintint32 / int64 / bool / enum使用 Varint 编码
164-bitfixed64 / double64 位定长
2Length-delimitedstring / bytes / 嵌套消息Varint 类型描述长度,向后读 length 个字节
532-bitfixed32 / float32 位定长

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,为什么快

维度JSONProtobuf
数据格式文本二进制
字段名每次传只传 field_number
解析字符串解析位操作
赋值断言内存偏移写入
大小小很多
This post is licensed under CC BY 4.0 by the author.