protocol buffer 编解码
2015-10-22 19:44
274 查看
平时的开发中使用pb格式协议较多,大致了解了一下pb的编解码,即序列化和反序列化。
本文参考官方文档:https://developers.google.com/protocol-buffers/docs/encoding?hl=zh-cn
先看简单一个示例:
将test2序列化后的结果打印出来, 可发现其结果为(十六进制):08 0a 12 03 08 96 01 22 04 74 65 73 74 28 ac 02 28 f4 03
要想了解如何能够得到这样一串二进制或者根据这串二进制还原出对应的pb数据,就需要了解pb的编解码过程。
在编码过程中,pb会将message中的每个字段序列化成一对key-value,即message序列化后的结果是一系列的key-value。key由字段的tag和type决定(tag是字段的序号,例如a = 1中的1、list = 5中的5;type是字段的类型,例如uint32、bytes……),value是字段值经过一定的编码后的内容。
pb对不同的数据类型分类和编号如下:
其中embedded messages是嵌套的message,例如Test2中的test字段。packed repeated fields会在后面提到。
key编码
知道了字段的tag和type,就可以得出编码后的key,计算key的方式为:(tag << 3) | type。例如:
Test2中的字段a的key = (1 << 3) | 0 = 00001000 = 8
Test2中的字段test的key = (2 << 3) | 2 = 00010010 = 10
其他字段同理。
解码key的时候,分别得到tag和type就可以定位到具体的字段
value编码
value编码就是对字段的值进行编码。pb中不同类型的字段编码方式是不一样的。
a.整数编码
pb对整数的编码方式为Varint。Varint是使用一个或多个字节序列化整数的方法。在整数的编码过程中,除了最后一个字节外,其他的字节的最高位都要设置成1,表示后面还有字节没有处理(此位称为most significant bit,简称msb),最后一个字节的最高位为0就表示已经处理完了。需要注意的是,编码过程中字节的存放顺序是逆序的,即最后一个字节放在最开始。举例如下:
整数50的编码过程:
转成二进制:110010
按7位分组:0110010
设置msg:00110010(由于只有一个字节,没有后续的字节了,所以msb设置为0)
整数500的编码过程:
转成二进制:111110100
按7位分组:0000011 1110100
逆序:1110100 0000011
设置msg:11110100 00000011(第一个字节设置msb设为1,表示后续还有字节)
知道了整数的编码过程,就可以根据编码后的整数得到原始的值,过程为:
1.根据msb得到该字段所有的字节
2.去除msb
3.逆序
4.得到原始值
需要注意的是:编码负数时,使用int32、int64和sint32、sint64编码后的结果是不一样的。pb的说明文档在介绍数据类型时提到:在编码负数的时候,sint32、sint64要比int32、int64高效。使用int32或者int64的编码的时候,负数会被看作一个非常大的正数,编码后占据10个字节。使用sint32和sint64编码时,负数和正数都会被一一对应到一个正数,对应关系如下:
计算公式为
sint32: (n << 1) ^ (n >> 31)
sint64: (n << 1) ^ (n >> 63)
b.非整数编码
fixed64, sfixed64, double数据类型编码后为固定的64位。fixed32, sfixed32, float数据类型编码后为固定的32位
c.字符串编码
字符串编码比较简单,编码后的内容为:长度+字符串内容
d.嵌套message编码
嵌套message编码类似于字符串编码,也是长度+内容,内容就是内部嵌套的message编码后的内容
optional & repeated
pb之所以扩展性好,是因为可以将后面新加字段声明为optional和repeated,这样就可以在对方不需要修改代码的情况下兼容老协议。在编码的时候,如果设置了可选字段,则将可选字段编码,反之不作处理。解码的时候,遇到不认识的可选字段跳过就可以了。需要注意的是,repeated是一个列表,可以添加多条数据,这多条数据在编码的时候tag和type都是一样的,所以repeated里面所有的数据编码后key都是一样的。解码的时候,所有key相等的数据即为同一repeated字段中的数据。
packed repeated fields
pb之前的文档中这样提到:数字类型的repeated字段声明的时候在后面加上 [packed=true],编码的时候比较高效。形式如下:
假设一个repeated字段中添加了n条数据,在不加[packed=true]的情况下,编码后的结果对应n个key-value,而且这个n个key是相等的(因为tag和type都相等)。反之如果加了[packed=true],编码后的结果就是一个key-value,只是需要在value前面加上value的长度而已。
模拟编码
了解了pb的编码原理之后,在回过头来看最开始的例子,模拟一下test2的编码过程:
字段a编码(值为10):
key = (1 << 3) | 0 = 08
value:
10的二进制: 1010
按7位分组: 0001010
设置msb: 00001010
value = 00001010 = 0a
a编码 = 08 0a
字段test编码:
test中的t编码(值为150):
key = (1 << 3) | 0 = 08
value:
150的二进制: 10010110
按7位分组: 0000001 0010110
逆序: 0010110 0000001
设置msb: 10010110 00000001
value = 96 01
t编码 = 08 96 01
test编码:
key = (2 << 3) | 2 = 12
test的value长度 = 03
test的value(t的编码) = 08 96 01
test编码 = 12 03 08 96 01
字段b没有设置值,所以不用编码
字段s编码:
key = (4 << 3) | 2 = 22
"test"的UTF-8编码 = 74 65 73 74
"test"的长度 = 04
s编码 = 22 04 74 65 73 74
字段list编码(300, 500):
key = (5 << 3) | 0 = 28
300的value:
300的二进制: 100101100
按7位分组: 0000010 0101100
逆序: 0101100 0000010
设置msb: 10101100 00000010
value = ac 02
300的编码 = 28 ac 02
同理可得500的编码 = 28 f4 03
所以list字段编码 = 28 ac 02 28 f4 03
将上述所有字段编码拼接即可得到test2编码 = 08 0a 12 03 08 96 01 22 04 74 65 73 74 28 ac 02 28 f4 03
和打印出来的结果一致
本文参考官方文档:https://developers.google.com/protocol-buffers/docs/encoding?hl=zh-cn
先看简单一个示例:
message Test1 { required uint32 t = 1; } message Test2 { required uint32 a = 1; required Test1 test = 2; optional uint32 b = 3; optional bytes s = 4; repeated uint32 list = 5; } //定义一个Test2对象,并设置字段值 Test2 test2; test2.set_a(10); Test1 *test1 = test2.mutable_test(); test1->set_t(150); test2.set_s("test"); test2.add_list(300); test2.add_list(500);
将test2序列化后的结果打印出来, 可发现其结果为(十六进制):08 0a 12 03 08 96 01 22 04 74 65 73 74 28 ac 02 28 f4 03
要想了解如何能够得到这样一串二进制或者根据这串二进制还原出对应的pb数据,就需要了解pb的编解码过程。
在编码过程中,pb会将message中的每个字段序列化成一对key-value,即message序列化后的结果是一系列的key-value。key由字段的tag和type决定(tag是字段的序号,例如a = 1中的1、list = 5中的5;type是字段的类型,例如uint32、bytes……),value是字段值经过一定的编码后的内容。
pb对不同的数据类型分类和编号如下:
其中embedded messages是嵌套的message,例如Test2中的test字段。packed repeated fields会在后面提到。
key编码
知道了字段的tag和type,就可以得出编码后的key,计算key的方式为:(tag << 3) | type。例如:
Test2中的字段a的key = (1 << 3) | 0 = 00001000 = 8
Test2中的字段test的key = (2 << 3) | 2 = 00010010 = 10
其他字段同理。
解码key的时候,分别得到tag和type就可以定位到具体的字段
value编码
value编码就是对字段的值进行编码。pb中不同类型的字段编码方式是不一样的。
a.整数编码
pb对整数的编码方式为Varint。Varint是使用一个或多个字节序列化整数的方法。在整数的编码过程中,除了最后一个字节外,其他的字节的最高位都要设置成1,表示后面还有字节没有处理(此位称为most significant bit,简称msb),最后一个字节的最高位为0就表示已经处理完了。需要注意的是,编码过程中字节的存放顺序是逆序的,即最后一个字节放在最开始。举例如下:
整数50的编码过程:
转成二进制:110010
按7位分组:0110010
设置msg:00110010(由于只有一个字节,没有后续的字节了,所以msb设置为0)
整数500的编码过程:
转成二进制:111110100
按7位分组:0000011 1110100
逆序:1110100 0000011
设置msg:11110100 00000011(第一个字节设置msb设为1,表示后续还有字节)
知道了整数的编码过程,就可以根据编码后的整数得到原始的值,过程为:
1.根据msb得到该字段所有的字节
2.去除msb
3.逆序
4.得到原始值
需要注意的是:编码负数时,使用int32、int64和sint32、sint64编码后的结果是不一样的。pb的说明文档在介绍数据类型时提到:在编码负数的时候,sint32、sint64要比int32、int64高效。使用int32或者int64的编码的时候,负数会被看作一个非常大的正数,编码后占据10个字节。使用sint32和sint64编码时,负数和正数都会被一一对应到一个正数,对应关系如下:
计算公式为
sint32: (n << 1) ^ (n >> 31)
sint64: (n << 1) ^ (n >> 63)
b.非整数编码
fixed64, sfixed64, double数据类型编码后为固定的64位。fixed32, sfixed32, float数据类型编码后为固定的32位
c.字符串编码
字符串编码比较简单,编码后的内容为:长度+字符串内容
d.嵌套message编码
嵌套message编码类似于字符串编码,也是长度+内容,内容就是内部嵌套的message编码后的内容
optional & repeated
pb之所以扩展性好,是因为可以将后面新加字段声明为optional和repeated,这样就可以在对方不需要修改代码的情况下兼容老协议。在编码的时候,如果设置了可选字段,则将可选字段编码,反之不作处理。解码的时候,遇到不认识的可选字段跳过就可以了。需要注意的是,repeated是一个列表,可以添加多条数据,这多条数据在编码的时候tag和type都是一样的,所以repeated里面所有的数据编码后key都是一样的。解码的时候,所有key相等的数据即为同一repeated字段中的数据。
packed repeated fields
pb之前的文档中这样提到:数字类型的repeated字段声明的时候在后面加上 [packed=true],编码的时候比较高效。形式如下:
message Test { repeated uint32 a = 1 [packed=true]; }
假设一个repeated字段中添加了n条数据,在不加[packed=true]的情况下,编码后的结果对应n个key-value,而且这个n个key是相等的(因为tag和type都相等)。反之如果加了[packed=true],编码后的结果就是一个key-value,只是需要在value前面加上value的长度而已。
模拟编码
了解了pb的编码原理之后,在回过头来看最开始的例子,模拟一下test2的编码过程:
字段a编码(值为10):
key = (1 << 3) | 0 = 08
value:
10的二进制: 1010
按7位分组: 0001010
设置msb: 00001010
value = 00001010 = 0a
a编码 = 08 0a
字段test编码:
test中的t编码(值为150):
key = (1 << 3) | 0 = 08
value:
150的二进制: 10010110
按7位分组: 0000001 0010110
逆序: 0010110 0000001
设置msb: 10010110 00000001
value = 96 01
t编码 = 08 96 01
test编码:
key = (2 << 3) | 2 = 12
test的value长度 = 03
test的value(t的编码) = 08 96 01
test编码 = 12 03 08 96 01
字段b没有设置值,所以不用编码
字段s编码:
key = (4 << 3) | 2 = 22
"test"的UTF-8编码 = 74 65 73 74
"test"的长度 = 04
s编码 = 22 04 74 65 73 74
字段list编码(300, 500):
key = (5 << 3) | 0 = 28
300的value:
300的二进制: 100101100
按7位分组: 0000010 0101100
逆序: 0101100 0000010
设置msb: 10101100 00000010
value = ac 02
300的编码 = 28 ac 02
同理可得500的编码 = 28 f4 03
所以list字段编码 = 28 ac 02 28 f4 03
将上述所有字段编码拼接即可得到test2编码 = 08 0a 12 03 08 96 01 22 04 74 65 73 74 28 ac 02 28 f4 03
和打印出来的结果一致
相关文章推荐
- javascript 三种编解码方式
- 在PB中如何让用户只能修改新增的数据
- [总结]FFMPEG视音频编解码零基础学习方法
- 我的Protobuf消息设计原则
- 化繁为简--google protobuf
- c++ java中关于protobuf反序列化对象实体和实体处理(函数)关系 (一)
- c++ java中关于protobuf反序列化对象实体和实体处理(函数)关系(二)
- rabbitmq学习
- PB做的史上最强的矢量图监控软件
- PB窗口控件自动缩放的例程
- FFmpeg音视频编解码实践总结
- 小议H.264编解码
- 小议H.264技术发展趋势
- AMR音频编码器概述及文件格式分析
- 音频编码标准发展现状
- 浅议PB中数据窗口缓冲区与数据修改状态(转)散分贴解决方法
- protobuf通过反射来赋值
- gogoprotobuf使用(上)
- gogoprotobuf使用(下)
- Hibernate根据表名获得实体类名及ID名