Google Protobuf协议分析

protobuf 是google开源的一个序列化框架,类似xml,json,最大的特点是基于二进制,比传统的XML表示同样一段内容要短小得多。还可以定义一些可选字段,用于服务端与客户端通信。前面几篇文章说了protobuf的用法,看到网上也没有分析protobuf协议的文章,就利用一些时间写了 protobuf 的协议分析。

protobuf协议核心思想
基于128bits的数值存储方式(Base 128 Varints)
数据表示方式:每块数据由接连的若干个字节表示(小的数据用1个字节就可以表示),每个字节最高位标识本块数据是否结束(1:未结束,0:结束),低7位表示数据内容。(可以看出数据封包后体积至少增大14.2%)

数字1的表示方法为:0000 0001,这个容易理解
数字300的表示方法为:1010 1100 0000 0010

protobuf字节序是小端字节序,所以这个数字实际是0000 0010 1010 1100

1010 1100 0000 0010
→ 010 1100  000 0010

如下:
000 0010  010 1100
→  000 0010 ++ 010 1100
→  10 0101100
→  256 + 32 + 8 + 4 = 300

基于序号的协议字段映射(类似key-value结构)
所以字段可以乱序,可缺段(记optional)

message person{
    required string name      = 1;
    required string country  = 2;
    optional int32 age           = 3;
}

效果相当于json数据:person = [{1: “john”}, {2:  “USA”}, {3: 30}],其中{3: 30} 还可以不传,person还可以传成 [{2:  “USA”}, {1: “john”}],对端仍旧可以正常解析。

基于无符号数的带符号数表示(ZigZag 编码)

原始的带符号数 ZigZag编码后的表示
0 0
-1 1
1 2
-2 3
2147483647 4294967294
-2147483648 4294967295

使用 zigzag 编码,充分利用基于128bits的数值存储(Base 128 Varints)的技术,只需要加多1个位来表示符号。当绝对值小的数字非常有利,这种方式可以有效减少协议内容长度。
sint32类型编码如下:
(n << 1) ^ (n >> 31)

sint64类型编码如下:
(n << 1) ^ (n >> 63) 

协议数据结构
protobuf怎么在一长串二进制中表示若干个数据?

做法就是每块数据前加一个数据头,表示数据类型及协议字段序号。
msg1_head + msg1 + msg2_head + msg2 + …
数据头也是基于128bits的数值存储方式,一般1个字节就可以表示:

message Test1 {
    required int32 a = 1;
}

如上创建了 Test1 的结构并且把 a 设为 2,序列化好的二进制数据为:
0000 1000 0000 0010
以上数据转成十六进制也就是 08 02,其中 8 是怎么得到的?

000 1000
低3位表示数据类型:0,其他表示协议字段序号:1,加上最高位0, 结果就是8
数据类型的表示如下:

类型 含义 用于哪些数据类型
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

写在最后
protobuf的优缺点
优点前面也提到了,主要有两个:
1、序列化和反序列化效率比 xml 和 json 都高(这个protobuf 自己做了测试,链接要翻墙);
2、字段可以乱序,欠缺,因此可以用来兼容旧的协议,或者是减少协议数据。

但是字段允许乱序欠缺,反过来也是缺点。所以这里总结 protobuf 两个缺点,一个跟这有关:
1、如果字段过多,或者嵌套过深,都会影响反序列化效率,解析每一块数据都要根据序号找到对应的位置然后再插入到已解析好的数据中。
2、数据基于128bits的存储方式,单块数据比较大时效率很受影响。解析数据需要取到所有字节的低7位,然后再拼成一整块数据。

以上两个缺点,特别是对于erlang这类没有指针的语言来说,代价就相当昂贵。
3、协议序号也要占空间,序号越大占空间越大,当序号小于16时无需额外增加字节就可以表示。

protobuf 分析题
最后贴一道分析题,如果看得懂,基本就了解 protobuf 的协议内容了
message Test2 {
    required int32 a = 3;
}
创建了Test2结构,a 赋值 150, 结果是
0001 1000 1001 0110  0000 0001
这个数写成十进制就是 24 150 1,怎么得到这个数据?

参考:https://developers.google.com/protocol-buffers/docs/encoding

《Google Protobuf协议分析》上有1条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注