您的位置:首页 > Web前端

protocol buffer 编解码

2015-10-22 19:44 274 查看
平时的开发中使用pb格式协议较多,大致了解了一下pb的编解码,即序列化和反序列化。

本文参考官方文档: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

和打印出来的结果一致
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  pb 编解码 protobuf