“介绍protobuf编码格式。”
protobuf是一种数据交换格式,又称PB编码,由Google开源,类似于Json、XML,但其内部是纯二进制格式,比Json,XML等格式要更精炼,主要用于数据的序列化和反序列化,目前官方提供了JAVA、Python、C++等多种语言的实现。
PB格式的解析依赖于消息文件,在其实现中,.proto定义了各个消息项的id值。
直观地,PB编码就是将一个结构体的内容编码成二进制流。例如一段json数据:
{
"id":176,
"age":24,
"name":"xieyifenxi",
}
.proto文件的定义如下:
message Person {
required int32 id = 1;
optional int32 age = 2;
required string name = 3;
}
则json数据编码成PB格式则是:
08 b0 01 10 18 1a 0a 78 69 65 79 69 66 65 6E 78 69
通常,在协议解析的过程中碰到的PB编码,是没有.proto文件的,解析的时候,只需要根据数据内容,解析出每一项内容即可,而每一项内容的含义,一般通过分析得到。
在许多APP的数据流中,都会存在protobuf编码。本文将通过对PB编码进行介绍,使大家了解如何在协议分析过程中对其进行解析。
01
—
数据结构
通过前面的例子,可以看到PB的数据结构就是每项数据独立编码,包含一个表示数据类型wire_type和字段序号field_number的数据头,和对应的数据段内容。即HEAD1+MSG1+HEAD2+MSG2+……
在数据头中,字段的序号field_number在整个数据结构中是唯一的,并且可以乱序、缺失和嵌套,序号是在.proto文件中定义的,协议分析中关心的意义不是很大。
数据类型wire_type则表示数据段内容是什么类型,在protobuf官网上描述了类型的含义:
https://developers.google.com/protocol-buffers/docs/encoding
常见的数据类型wire_type为0和2,分别为Varint类型和Length-delimited变长度数据类型,掌握了这两个类型,基本上在协议解析中,处理PB编码就基本没有障碍了。Varint类型一般就是int数据,而Length-delimited变长度数据类型通常就是字符串数组等数据。
数据头中数据类型和字段序号的组合方式是:(field_number << 3) | wire_type
当然,field_number是Varint类型,需遵循Varint的编码规则。
例如,文首的例子中,id、age、name对应的编码值为:
1 <<< 3 | 0 =0x08
2 <<< 3 | 0 = 0x10
3 <<< 3 | 2 = 0x1a
数据头之后,是数据段,它包含了被编码的数据,不同类型的数据编码格式不同,后面的章节将介绍对应具体类型的编码方法。
02
—
Varint
Varint是一种对数字进行编码的方法,将数字编码成不定长的二进制数据,数值越小,编码后的字节越少。
编码规则如下:
每个字节的最高位表示下一字节是否仍然是编码的内容,若最高位为1,则下一字节仍然是编码的数字的一部分,若该位为0,则编码到本字节结束。每个字节的后7位,则由小端表示的数字的二进制值,在高位补0凑齐7的倍数位组成。
例如,数值345,其二进制值为 1 0101 1001,在高位补0后分成两个7位 000 0010和 101 1001,则Varint编码结果为:
1 101 1001 0 000 0010
即0xD9 0x02
对文首的例子,由于id 176的二进制值为1011 0000,每七位编码成一个字节,因此,需要用两个字节来表示:
1011 0000 0000 0001
即0xB0 0x01
而age 24的二进制值为 1 1000,则只需要一个字节来表示:
0001 1000
即0x18
前面只是弄明白了int32的Varint编码,对协议解析来说,一般已经够用了,除非这个被编码的数,在取出后需要用其特定的含义来进行计算,因为在PB编码中,还考虑了对负数进行Varint编码。
当我们按照同样的逻辑对负数进行Varint编码时,会发现,负数编码后占用的字节会很多,这不太合算,因此ZigZag编码在PB中被使用,使得Varint编码可以用较少的位数来对负数进行编码。
PB编码中提供了sint32和sint64类型,使用ZigZag编码,让所有的负数都使用正数表示,计算方式如下:
sint32:
(n << 1) ^ (n >> 31)
sint64:
(n << 1) ^ (n >> 63)
即:
原始值0,通过计算,得到ZigZag表示值为0;
原始值-1,通过计算,得到ZigZag表示值为1;
原始值1,通过计算,得到ZigZag表示值为2;
原始值-2,通过计算,得到ZigZag表示值为3;
原始值2147483647,通过计算,得到ZigZag表示值为4294967294;
原始值-2147483648,通过计算,得到ZigZag表示值为4294967295。
依此类推
在协议还原中,对一个Varint编码的值,想要知道它表达的是int32,还是sint32,就只有想办法找到其对应的.proto文件才可以。
03
—
Length-delimited
Length-delimited就是对可变长度的数据,在编码时,将长度和数据编码在一起,类似于TLV结构的LV部分,前面为数据长度,后面为由数据长度决定的数据内容,数据长度采用的是Varint编码。
例如文首的例子里,name的值为"xieyifenxi"的长度为10,则编码为:
0a 78 69 65 79 69 66 65 6E 78 69
其中,0x0a为长度值的Varint编码,之后紧接着的是值的内容。
这相当的简单。
对protobuf编码的详解就介绍到这里了,有疑问,可以联系我,或者上其官网了解。它的官网是https://developers.google.com/protocol-buffers/
长按进行关注。