引言
本文档简要介绍了protobuf中基础的编码规则。
Varint编码
字节序
- 大端序:高字节放低地址,低字节放高地址(符合书写习惯:从左到右)
- 小端序:高字节放高地址,低字节放低地址
定长编码
16位整型——2个字节
32位整型——4个字节
64位整型——8个字节
变长编码
🔔为什么用变长编码: 比如对于一个64位整数来说,需要使用8字节来存储。但是通常情况下我们存储这8个字节的数据通常不会占用8个字节这么多。例如:140(二进制为:1000 1100),只需要占用两个字节,剩余高位全是0,其实是可以不用存储。
-
所以变长编码的本质就是:根据待编码的数据的大小,动态使用不同大小的空间来存储,从而达成减少占用的效果。
-
如何实现:将一个字节(8位)分成
1 + 7:- 最高位位标志位(continuation bit) ,表示当前字节的负载已经结束。(0表示结束,1表示为结束)
- 低7位为负载(payload) ,存储的就是实际的数字的比特位。
-
对于无符号64bit整型,使用变长编码的话,占用的字节数为1~10字节。
有符号数的编码
对于负数来说,protobuf首先将其转换为无符号数,再进行varint的编码。 (称作:zigzag编码)
转换的公式为:
Encoding
字段编码原则
TLV(Tag-Length-Value) ,也就是每一个字段都是按照TLV的原则来进行编码。
Tag
由field number和wire type组成,field number就是我们定义proto文件的时候,每个字段后面的=后面的数字。
由这两个组成Tag的公式为 (field_number << 3) | wire_type
例子解析
message Address {
string addr = 1;
int32 number = 2;
}
// 创建一个Address对象
addr := &Address{
Addr: "XiaRoad",
Number: 523000,
}
对上面的addr的编码过程为(TLV规则):
1. 首先对Addr进行编码:
-
第一个字段Addr
- T: 首先计算field-1的tag:
tag1 = (1 << 3) | 2 = 10 = 0x0A,string 的 wire_type=2。 对tag1进行变长编码,得到的结果依然是0x0A; - L: 随后计算字符串的长度,
len("XiaRoad")=7,对7进行变长编码,得到的结果为0x07; - V: 接着拼接上字符串"XiaRoad"的二进制表示(此处为ASCII):0x58 0x69 0x61 0x52 0x6F 0x61 0x64
- T: 首先计算field-1的tag:
-
最终第一个字段的编码结果为(十六进制) 0A 07 58 69 61 52 6F 61 64
2. 随后对Number进行编码:
-
第二个字段Number
- T: 首先计算field-2的tag:
tag2 = (2 << 3) | 0 = 16 = 0x10,int32的wire_type=0。对tag2进行变长编码,得到的结果依然是0x10; - L: 由于是整型,所以没有不像字符串那样有长度,所以此处就没有Length
- V: 随后是对Number的值,也就是
523000进行变长编码,得到的结果为0xF8 0xF5 0x1F(小端序)
- T: 首先计算field-2的tag:
-
最终将上述的结果拼接,得到第二个字段的编码结果为(十六进制):10 F8 F5 1F
-
最终得到整个的addr对象的编码结果为:
0A 07 58 69 61 52 6F 61 64 10 F8 F5 1F,一共占用13个字节。
字段嵌套的情况
📌protobuf中规定,字段嵌套的wire_type=2,也就是和string的情况类似的处理。T-L-V,L就是被嵌套的字段编码后的长度。
message Address {
string addr = 1;
int32 number = 2;
}
message People {
int32 age = 1;
string name = 2;
Address addr = 3;
}
// 对对象p进行序列化
p := &pb.People{
Age: 20,
Name: "HelloWps",
Addr: &pb.Address{
Addr: "XiaRoad",
Number: 523000,
},
}
例如上面的例子:Address被嵌套进People中。
repeated字段
- 被repeated标识的字段的wire_type=2。
(1 << 3 | 2 = 0x0A) - Length表示的是后续每个element编码叠加后的长度。(例如下面例子中的长度为
0x09)
// repeat的情况
message IntSlice {
repeated int32 elems = 1;
}
//
ele := &pb.IntSlice{
Elems: []int32{100, 200, 300, 400, 500},
}
例如下图所示:
注意
- protobuf中的编码顺序是按照字段的序号来排序的,字段序号小的编码后放在前面。
message Location {
string loc = 3489;
int32 number = 189;
}
// 上面的message中,number首先被编码,然后loc再被编码
例如下图所示:
-
为什么推荐使用1~15的字段序号?
因为1~15的字段序号编码后的tag只占用一个字节。(依据:
(field_number << 3) | wire_type) -
为什么序号不能有0?
同样是依据:
(field_number << 3) | wire_type,因为field_number要左移3位,对数字0进行左移没有意义。