zoukankan      html  css  js  c++  java
  • ProtoBuf格式详解

    介绍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_type02,分别为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/


    640?wx_fmt=jpeg

    长按进行关注。





  • 相关阅读:
    团队开发-第一阶段冲刺-10
    团队开发-第一阶段冲刺-09
    Spring(三)
    第五周总结
    Spring(一)
    程序员修炼之道:从小工到专家阅读笔记1
    MyBatis(三)
    MyBatis(二)
    MyBatis
    第四周总结
  • 原文地址:https://www.cnblogs.com/protosec/p/11673328.html
Copyright © 2011-2022 走看看