最近看到一个关于序列化的面试题,就去看了一些protobuf的序列化实现方式,这里总结一下,主要内容就来自于官网。protobuf的基本特性这里就不详细介绍了,我关心的其实主要是下面几点问题;
- 基本类型的序列化
- 嵌入类型的序列化
- repeat的序列化
- map的序列化
基本类型的序列化
protobuf的的序列化是基于idl的,发送方和接受方都保存有idl。因此protobuf的序列化方式更加安全(没有idl很难解析),效率也更高(不需要记录key的名字)。在二进制字节编码中,只需要保留field的序号,长度(可选)和值即可,如图所示:
key+(len)+value | key+(len)+value | key+(len)+value | key+(len)+value ...
key的编码
key是由序号左移3位,与类型标志位按位或之后的结果,
| 类型 | 含义 | 适用范围 |
|---|---|---|
| 0 | varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
| 1 | 64-bit | fixed64, sfixed64, double |
| 2 | 长度有限类型 | string、bytes、嵌入类型、重复类型 |
| 5 | 32-bit | fixed32, sfixed32, float |
比如官网的例子,08 96 01中第一位标识key的编码
08是key的部分
→ 1 <<3 | 0
也就是0000 1000
key的index的编码方式也是varint。可以看下后面的代码实现
value的编码
然后是value的编码,这里其实要分3类, 一类是整形都是采用了varint+zigzag编码对应0;第二类是浮点数,就是采用常规的编码对应1、5;第三类是有限长度的编码对应2
varint
首先说明下varint的编码,思路很简单,很多数字的编码采用的是固定长度的字符串编码完成,比如典型的int64就是8位,int32就是4位。 但是大多数场景下传递的数字都很小。varint的思路就是用最高位bit标识是否有后续字符,这样在数字较小的情况下可以用更小的位数完成数字的表达。 比如1只用一个字节标识0000 0001,300需要2个字符1010 1100 0000 0010标识。
96 01是value的部分,实际上是150
96 01 = 1001 0110 0000 0001
→ 000 0001 ++ 001 0110
→ 10010110
→ 128 + 16 + 4 + 2 = 150
可以看出varint的比较擅长标识小数字,这样导致一个问题是如果是补码形势的负数很难表示,因此protobuf采用另外一种ZigZag的编码方式将所有的负数都映射成大于0的数来表示,经过这样的转换绝对值小的数需要用的字符编码长度也更小。
n = (n << 1) ^ (n >> 31) // 32位编码
n = (n << 1) ^ (n >> 63) // 64位编码
浮点数
其次说明下浮点数 浮点数比较简单就是常规的编码模式,因此protobuf实际上没有对浮点数进行压缩。
string的编码
最后说优先有限的编码 比如说官网给的例子,如果是对字符串的编码
message Test2 {
optional string b = 2;
}
对于testing编码后结果为 12 07 74 65 73 74 69 6e 67 12是2 << 3 |2 之后的结果 07标识字符串的长度(长度的编码也是varint编码) 74 65 73 74 69 6e 67是testing的字符编码
对于byte的编码和字符串是相同的,
除此之外如果是一个嵌套类型的话其实也是一样,比如说
message Test3 {
optional Test1 c = 3;
}
嵌套类型的编码
如果内置的test1的结构体是150的话
1a // key (3<<3 | 2)
03 // 后面value的长度是3
08 96 01 // 是test1的编码,上文介绍过
repeat类型的编码
其次对于repeat类型也是一样的,
message Test4 {
repeated int32 d = 4 [packed=true];
}
如果d是3、270和86942的编码,实际上会被编辑成如下形势:
22 // key (4<<3 | 2)
06 // 后面6个字节都是value
03 // 3的varint编码
8E 02 // 270的varint编码
9E A7 05 // 86942的varint编码
map的编码
实际上map的编码也是一样的map在protobuf的里面相当于是嵌入和结构体加上map
message Test5 {
map[string]string e = 5
}
等同于
message Test5 {
repeat Test6 e = 5
}
message Test6 {
string key = 1
striing value = 2
}