一、基础
1、序列化与反序列化
序列化:指将结构化的数据按一定的编码规范转成指定格式的过程;
反序列化:指将转成指定格式的数据解析成原始的结构化数据的过程;
举个例子:Person是一个表示人的对象类型,person是一个Person类型的对象,将person存到一个对应的XML文档中的过程就是一种序列化,而解析XML生成对应Person类型对象person的过程,就是一个反序列化的过程。在这里结构化数据指的就是Person类型的数据,一定的编码规范指的就是XML文档的规范。XML是一种简单的序列化方式,用XML序列化的好处是,XML的通用性比较好,另外,XML是一种文本格式,对人阅读比较友好,但是XML方式比较占空间,效率也不是很高。通常,比较高效的序列化都是采用二进制方式的;将要序列化的结构化数据,按一定的编码规范,转成为一串二进制的字节流存储下来,需要用的时候再从这串二进制的字节流中反序列化出对应的结构化的数据。
二、Protobuf编码基础
1、Varints:varints是一种将一个整数序列化为一个或者多个Bytes的方法。越小的整数,使用的Bytes越少。Varints规则如下:
1)每个Bytes的最高位(msb)是标志位。如果该值为1,表示该Bytes后面还有其他Byte;如果该位为0,表示该Byte是最后一个Byte。
2)每个Byte的低7位是用来存数值的位。
3)Varints方法使用小端字节序(反解数值的时候后面的字节方前面)。
举几个例子:
a)以数字1为例:二进制是0000 0001,最高位是0代表后面没有更多字节,剩下的7位就是数值位,"000 0001"显然就是1。
b)以数字300为例:它在Varints规则下的表示形式是1010 1100 0000 0010。
第一个字节是 1010 1100,最高位是1,表示后面还有更多字节;第一个字节内容是后7位即 010 1100;
第二个字节是 0000 0010,最高危是0,表示后面没有更多字节;第二个字节内容是后7位即 000 0010;
因为是“低字节序”,所以实际字节是 000 0010 010 1100=1 0010 1100=300。
2、字段编号(field_num):就是.proto文件中每个字段都有的顺排的那个编号;
3、传输类型(wire_type):每个字段都有一个对应的字段(传输)类型,如下表:
Type |
Meaning |
Used For |
0 |
Varint |
int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 |
64-bit |
fixed64, sfixed64, double |
2 |
Length-delimited |
string, bytes, embedded messages, packed repeated fields |
3 |
Start group |
groups (deprecated) |
4 |
End group |
groups (deprecated) |
5 |
32-bit |
fixed32, sfixed32, float |
4、field:message由一个个字段组成,每个字段包含<<编号,传输类型>,值>,在二进制编码的时候对应下图中的field。
5、具体而言每个field的构成为Tag-[Length]-Value;这里的[Length]是否需要是依据Tag最后三位的wire_type来决定的。
wire_type | 含义 | 二进制结构 |
0 | Varints | Tag-Value |
1、5 | 64-bits/32bits | Tag-Value |
2 | string/嵌套/repeated | Tag-[Length]-Value |
6、Tag:Tag的组成是“field_num << 3 | wire_type”称为Tag(也称为每个filed的key或者键)。
4、pb编解码关键点
1)在消息流中每个Tag(key/键)都是varint,编码方式为:field_num << 3 | wire_type。即,Tag(key/键)由 .proto文件中字段的编号(field_num) 和 传输类型(wire_type)两部分组成。
注:Tag也是Varints编码,其后三位是传输类型(wire_type),之前的数值为是字段编号(field_num)。
注:注意并不是说Tag只能是一个字节,这里说了Tag也是用Varint编码,显然使用Varint编码方式几千/几万的字段序号(field_num)都是可以被表示的。
2)在对一条消息(message)进行编码的时候是把该消息中所有的key-value对序列化成二进制字节流;key和value分别采用不同的编码方式。
3)消息的二进制格式只使用消息字段的字段编号(field_num)作为Tag(key/键)的一部分,字段名和声名类型只能在解析端通过引用参考消息类型的定义(即.proto文件)才能确定。
4)解码的时候解码程序(解码器)读入二进制的字节流,解析出每一个key-value对;如果解码过程中遇到识别不出来的filed_num就直接跳过。这样的机制保证了即使该消息(message)添加了新的字段,也不会影响旧的编/解码程序正常工作。
举例说明:
对于如下message,如果应用程序创建了一个Test1的对象,并把a字段赋值150,那么protobuf会把它编码成这样三个字节:“08 96 01”。接下来解析:
message Test1 {
required int32 a = 1;
}
(1)因为在二进制路里面第一个数字都是key,即key是08,对应二进制0000 1000;根据前面所说知道后三位是000,代表传输类型是0即Varint;前五位是0000 1就是数字1,代表序号field_num是1。
注:因此通过传输类型知道后面传输的是Varint,通过序号知道在message里面的tag是1。
(2)接下来就使用Varint方式对后面的“96 01”解码即可。对应二进制是1001 0110 0000 0001:
第一个字节是1001 0110,最高位是1,代表后面还有更多字节;第一个字节内容是后7位即 001 0110;
第一个字节是0000 0001,最高位是0,代表后面没有更多字节;第二个字节内容是后7位即 000 0001;
因为是采用“低字节序”,所以实际的字节是: 000 0001 001 0110 = 1001 0110 = 150.
编码方式其他的问题:
1、有符号整数编码的问题与zigzag优化
由上面可以知道protocol buffer中所有与传输类型0关联的类型都会被编码为Varints。但是在编码负数的时候,带符号的int类型(sint32和sint64)与“标准”int类型(int32和int64)之间存在着巨大区别。如果将int32或int64用作负数的类型,则结果varint总是十个字节;也就是说像-1、-2这样的负数也会占用比较多的Bytes。实际上他被视为一个非常大的无符号整数。如果使用有符号类型(sint32和sint64)之一,则生成的varint会采用一种改进的ZigZag编码,效率更高。ZigZag编码将有符号数映射到无符号数以便具有较小绝对值的数字(如-1)也具有较小的varint编码值。这样做的方式是通过正整数和负整数来回“曲折”,将-1编码为1,将1编码为2,将-2编码为3…………以此类推。如下表所示:
Signed Original |
Encoded As |
0 |
0 |
-1 |
1 |
1 |
2 |
-2 |
3 |
2 |
4 |
-3 |
5 |
… |
… |
2147483647 |
4294967294 |
-2147483648 |
4294967295 |
2、64-bit(wire_type=1)和32-bit(wire_type=5) 等非varint数字的编码。
这两种的编码方式比较简单,直接在key后面跟上64bits或32bits,采用“小端”字节序。
3、Length-delimited(wire_type=2)字符串的编码。
key-value的格式为 key+length+content ;key的编码方式就是前面所说的,length采用varints编码方式,content就是有length指定的长度的Bytes。
message Test2 {
optional string b = 2;
}
如上结构:实例化一个对象并设置b的值为“testing”,我们可以得到这条消息(message)对应的二进制数据为 “12 07 74 65 73 74 69 6e 67”
1)key是12,二进制为0001 0010;后三位是010,即传输类型为2(Length-delimited);前五位是00010即2,表示序号为2。
2)length是07,代表value的长度是7;
3)再后面代表的就是value值了;例如74(Hex)对应116(D),就是字符‘t’。
4、内嵌消息
如下图是一个拥有内嵌消息的结构Test3,内嵌的消息类型就是上面定义的Test1。
message Test3 {
optional Test1 c = 3;
}
如果对其实例化一个对象并将Test1中的a设置为150,对象编码后的二进制流为“1a 03 08 96 01”。
1)1a二进制为0001 1010,后三位是010,即传输类型为2(Length-delimited);前五位是11100即3表示序号为3。
2)03就表示后面value的长度为3字节;
3)“08 96 01”和第一个例子中编码后的结果一样了。
结论:也就是说内嵌消息会和字符串一样被视为Length-delimited;编码方式亦相同(key+length+content)。
5、可选和可重复元素
这里再剖析一个protobuf编码实例:
对于如下结构 我们实例化一个对象给num1字段赋值10、num2字段赋值1073741824。序列化后的结果十六进制输出和二进制输出分别如下:(过程见 这里)
message Test
{
required int32 num1 = 1;
required fixed32 num2 = 2;
}
十六进制表示为:
080a 1500 0000 40
二进制表示为:
00001000 00001010 00010101 00000000 00000000 00000000 01000000
利用上面说的编码规则对得到的二进制数据进行分析,如下:
第一个字段的Tag解码:
由上面知道Tag也是采用Varint编码的因此最开始依据第一个msb位读取第一个字节(00001000)为第一个字段的Tag,最后3位(000)表示wire_type=0指示了接下来Value的编码采用Varint方式。剩余5位(00001)表示field_number=1表示第一个字段的编号为1;
第一个字段的Value解码:
由wire_type=0可知Value是采用Varint编码。故读取下一个字节(00001010),该字节的第一位msb位为0。故接下来的7位(0001010)表示第一个字段的值。因为只有一个字节因此逆序还是0001010,二进制0001010表示数字为:10
第二个字段的Tag解码:
由同样的办法得第二个字段field_numer=2,wire_type=5。wire_type=5对应fixed32类型的编码方式。
第二个字段的Value解码:
fixed32类型的编码方式因为已经固定取32位,因此不需要msb位。但为了移位方便,还是有按字节逆序编码。因此解码的时候也要逆序回来。
fixed32的编码如下:
00000000 00000000 00000000 01000000
按字节逆序回来:
01000000 00000000 00000000 00000000
二进制表示的值为:1073741824
更多case参见:https://www.cnblogs.com/jialin0x7c9/p/12418487.html