美文网首页
PalletOne技术讲堂之 Protobuf原理及使用

PalletOne技术讲堂之 Protobuf原理及使用

作者: PalletOne | 来源:发表于2018-09-14 11:17 被阅读0次

    原创: PalletOne Pallet  8月17日

    讲师简介:

    郭立华:PalletOne高级工程师、虚拟机及合约管理模块负责人。从事互联网、广电行业软件研发、架构设计以及多年技术管理工作,对fabric、比特币等区块链有深入的研究与实际开发经验。

    一、protobuf介绍

    Google Protocol Buffer的简称,最初是Google公司内部的混合语言数据标准,适合做数据存储或RPC 数据交换格式。它是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式,相比xml,解析速度快快约20-100倍,序列化数据也更非常简洁、紧凑,序列化之后的数据量约为1/3到1/10。

    二、语法规则

    协议是由一系列的消息组成的。因此最重要的就是定义通信时使用到的消息格式。消息由至少一个字段组合而成,类似于C语言中的结构。每个字段都有一定的格式。

    protoc有protoc2和protoc3两种版本,其语法存在一定的差别,下面以protoc2作为基本语法规则进行讲解。

    字段格式:

    限定修饰符① | 数据类型② | 字段名称③ | = | 字段编码值④ | [字段默认值⑤]

    ①.限定修饰符包含required\optional\repeated

    Required: 表示是一个必须字段,必须相对于发送方,在发送消息之前必须设置该字段的值,对于接收方,必须能够识别该字段的意思。

    Optional:表示是一个可选字段,可选对于发送方,在发送消息时,可以有选择性的设置或者不设置该字段的值。对于接收方,如果能够识别可选字段就进行相应的处理,如果无法识别,则忽略该字段,消息中的其它字段正常处理。 

    Repeated:表示该字段可以包含多个元素,可以看作是在传递一个数组的值。

    ②.数据类型:N 表示打包的字节并不是固定。而是根据数据的大小或者长度。

    protobuf 数据类型描述打包C++语言映射

    bool布尔类型1字节bool

    double64位浮点数Ndouble

    float32为浮点数Nfloat

    int3232位整数、Nint

    uin32无符号32位整数Nunsigned int

    int6464位整数N__int64

    uint6464为无符号整Nunsigned __int64

    sint3232位整数,处理负数效率更高Nint32

    sing6464位整数 处理负数效率更高N__int64

    fixed3232位无符号整数4unsigned int32

    fixed6464位无符号整数8unsigned __int64

    sfixed3232位整数、能以更高的效率处理负数4unsigned int32

    sfixed6464为整数8unsigned __int64

    string只能处理ASCII字符Nstd::string

    bytes用于处理多字节的语言字符、如中文Nstd::string

    enum可以包含一个用户自定义的枚举类型uint32N(uint32)enum

    message可以包含一个用户自定义的消息类型Nobject of class

    ③.字段名称:字段名称的命名规则与C、C++、Java等语言的变量命名方式基本相同。

    ④.字段编码值:用于通信双方互相识别对方的字段,其中相同的编码值,其限定修饰符和数据类型也必须相同。

    ⑤.默认值:当在传递数据时,对于required数据类型,如果用户没有设置值,则使用默认值传递到对端。

    在编码过程中,需要注意以下几点: 

    import:类似c语言中的include,通过import导入需要的多个文件。 

    package:通过给每个文件指定一个package名称,避免名称冲突。 

    message:支持嵌套消息,可以包含另一个消息作为其字段,也可以内部定义新的消息。 

    enum:枚举的定义和C++相同,其值必须大于等于0的整数。

    三、怎么使用

    1、安装:

    源码下载地址:https://github.com/google/protobuf 

    安装依赖的库: autoconf automake libtool curl make g++ unzip  ,安装步骤:

    1 $ ./autogen.sh

    2 $ ./configure

    3 $ make & make check & make install 

    另一种方法是直接下载对应文件:

    https://github.com/google/protobuf/releases/

    2、编写

    下面以实例stu.protoc文件的编写规则,并对protoc2和protoc3的区别进行总结说明:

    /*

    1、语法标记,可以支持proto2语法和proto3的语法,需要添加syntax,错误提示默认添加为proto2

    2、只保留repeated标记数组类型, optional和required都被去掉了

    3、map支持

    4、字段default标记不能使用了

    5、枚举默认值一定是0

    6、多种语言支持及json序列号

    */

    syntax = "proto3"; //3需要添加syntax

    package protoc;

    //import "" //可以导入其他文件

    message Person {

        //optional,proto3中将optional和required都被去掉了

        string name = 1; //required

        int32 id = 2; //required

        string email = 3 ;//optional

        enum PhoneType {

            MOBILE = 0; //proto3 枚举默认值一定是0

            HOME = 1;

            ORK = 2;

        }

        message PhoneNumber {

            string number = 1; //required

            PhoneType type = 2; //optional [default = HOME];proto3 取消default,对于同一段序列化后的数据, 如果序列化端的default和反序列化端的default描述不一样会导致最终结果完全不一致

            //即: 同一个数据两个结果, 这是不可预测的结果, 因此去掉这个特性

        }

        repeated PhoneNumber phone = 4;

        map projects = 5;// 2不支持map

    }

    message AddressBook {

        repeated Person person_info = 1;

    }

    3、编译

    编译命令,可以指定具体语言输出类型,例如go语言:

    protoc --go_out = out_Directory    XXX.proto 

    例如:

    protoc --go_out=./     stu.proto 

    编译完成后会在本地生成对应的go文件

    4、代码使用

    生成对应语言格式化文件后就可以直接引用了,例如go语言下,编解码过程中可以采用如下方式进行相应的代码处理。编码:

    ps := &pb.Person{

        Name: "palletone",

        Id:    1,

        Email: "ptn@palliums.org",

    }

    encPs, err := proto.Marshal(ps)

    解码:

    decPs := &pb.Person{}

    err := proto.Unmarshal(msg.Payload, decPs)

    四、编码原理 

    引用https://blog.csdn.net/zxhoo/article/details/53228303

    Protobuf定义了消息描述语法(proto语法)和消息编码格式,并且提供了主流语言的代码生成器(protoc)。

    Protobuf消息由字段(field)构成,每个字段有其规则(rule)、数据类型(type)、字段名(name)、tag,以及选项(option)。比如下面这段代码描述了由10个字段构成的Test消息:

    序列化时,消息字段会按照tag顺序,以key+val的格式,编码成二进制数据。以下面这段Java代码为例:

    byte[] data = Test.newBuilder()

      .setA(3).setB(2).setC(1)

      .build().toByteArray();

    序列化之后,可以把data里的数据想象成下面这样:

    proto2语法定义了3种字段规则:required、optional、repeated。proto3语法去掉了required规则,只剩下optional(默认)和repeated两种。由上图可知,如果没有给optional和repeated字段赋值,那么字段是不会出现在序列化后的数据中的。详细的编码规则,请继续阅读。

    数据划分

    Protobuf消息序列化之后,会产生二进制数据。这些数据(精确到bit)按照含义不同,可以划分为6个部分:MSB flag、tag、编码后数据类型(wire type)、长度(length)、字段值(value)、以及填充(padding)。后文会图解这些部分的具体含义,这里先约定好图中消息各部分使用的颜色:

    Key+Value

    前面说过,消息的每一个字段,都会以key+val的形式,序列化为二进制数据。val比较好猜测,那么key具体是什么呢?答案是这样:key = tag << 3 | wire_type。也就是说,key的前3个比特是wire type,剩下的比特是tag值。Protobuf支持丰富的数据类型,但是编码之后,只剩下Varint(0)、64-bit(1)、Length-delimited(2)<定界符>和32-bit(5)这4种(还有两种已经废弃了,本文不讨论)类型,用3个比特来表示,足够了。以前面定义的Test消息为例:

    byte[] data = Test.newBuilder()

      .setA(3).setB(2).setC(1)

      .build().toByteArray();

    序列化之后的数据有6个字节,是下面这个样子:

    Varint(可变长度整数)

    用3个bit来表示wire type是够了,但是tag是用剩下的5个bit来表示吗?tag难道不能超过32(2^5)吗?由上图已经知道,答案是否!为了用尽可能少的字节编码消息,Protobuf在多处都使用了Varint这种格式。比如数据类型里的int32、int64,以及tag值和后面将要解释的length值,都使用Varint类型存储。那么Varint到底有什么神奇之处呢?也没有,其实就是用每个字节的前7个bit来表示数据,而最高位的bit(MSB,Most Significant Bit)则用作记号(flag)。文字不太好描述,看一个例子:

    byte[] data2 = Test.newBuilder()

      .setJ(1) // tag=16

      .build().toByteArray();

    由于tag是按Varint编码的,所以要扣掉一个bit(MSB)。再减去wire type占用的3个比特,那么第一个字节里,留给tag值的,实际只剩下4个比特,只能表示0到15。由于Test消息j字段的tag值是16,所以需要两个字节才能表示j字段的key。data2如下图所示(重要的bit进行了旋转,以示提醒):

    64-bit和32-bit

    前面说了,为了节省字节数,tag、length,以及int32、int64等数据类型都是用Varint编码的。那么这种编码方式有什么坏处吗?主要有2处。第一,不利于表示大数。对于比较小的数来说,以0到127为例,用Varint很划算。以浪费1bit和少量额外的计算为代价,只要1个字节就可以表示。但是对于比较大的数,就不划算了。以int32为例,大于2^(4*7) - 1的数,需要用5个字节来表示。看一个例子:

    byte[] data3 = Test.newBuilder()

      .setA(268435456) // 2^28

      .build()

      .toByteArray();

    序列化之后的数据如下图所示:

     也就是说,如果某个消息的某个int字段大部分时候都会取比较大的数,那么这个字段使用Varint这种变长类型来编码就没什么好处。对于这种情况,Protobuf定义了64-bit和32-bit两种定长编码类型。使用64-bit编码的数据类型包括fixed64、sfixed64和double;使用32-bit编码的数据类型包括fixed32、sfixed32和float。以Test消息e字段(fixed32)为例:

    byte[] data4 = Test.newBuilder()

      .setE(268435456) // 2^28

      .build()

      .toByteArray();

    序列化之后的数据如下图所示:

     ZigZag

    Varint编码格式的第二缺点是不适合表示负数,以int32和-1为例:

    byte[] data5 = Test.newBuilder()

      .setA(-1)

      .build()

      .toByteArray();

    Protobuf想让int32和int64在编码格式上兼容,所以-1需要占用10个字节,如下图所示:

    为了克服这个缺陷,Protobuf提供了sint32和sint64两种数据类型。如果某个消息的某个字段出现负数值的可能性比较大,那么应该使用sint32或sint64。这两种数据类型在编码时,会先使用ZigZag编码将负数映射成正数,然后再使用Varint编码。ZigZag编码规则如下图所示:

     以Test消息的d字段(sint32)为例:

    byte[] data6 = Test.newBuilder()

      .setD(-2) // sint32

      .build()

      .toByteArray();

    序列化之后的数据如下图所示:

    Length-delimited

    如前所述,64-bit和32-bit是定长编码格式,长度固定。Varint是变长编码格式,长度由字节的MSB决定。Length-delimited编码格式则会将数据的length也编码进最终数据,使用Length-delimited编码格式的数据类型包括string、bytes和自定义消息。以string为例:

    byte[] data7 = Test.newBuilder()

      .setF("hello") // string

      .build()

      .toByteArray();

    序列化之后的数据如下图所示:

    下面是自定义消息的例子:

    byte[] data8 = Test.newBuilder()

      .setI(Test.newBuilder().setA(1))

      .build()

      .toByteArray();

    序列化之后的数据如下图所示:

    repeated

    前面讨论的字段都是optional类型,最多只有一个val,但是repeated字段却可以有多个val。那么repeated字段是如何序列化的呢?以Test消息的g字段为例:

    byte[] data9 = Test.newBuilder()

      .addG(1).addG(2).addG(3)

      .build()

      .toByteArray();

    序列化之后的数据如下图所示:

    可见,repeated字段就是简单的把每个字段值依次序列化而已。

    packed

    如果repeated字段包含的val比较多,那么每个val都带上key是不是比较浪费呢?是的,所以Protobuf提供了packed选项,以Test消息的h字段为例:

    byte[] data10 = Test.newBuilder()

      .addH(1).addH(2).addH(3) // packed

      .build()

      .toByteArray();

    序列化之后的数据如下图所示:

    可见,如果repeated字段设置了packed选项,则会使用Length-delimited格式来编码字段值。

    区块链世界的IP协议高性能分布式账本

    更多有价值的悄悄话,欢迎加入PalletOne社群

    添加PalletOne小红微信

    加入社区,咨询更多消息

    官网:https://pallet.one/

    官方邮箱:contact@pallet.one

     Telegram:http://t.me/PalletOneOfficialEN

    Github:https://github.com/PalletOne

    Twitter:https://twitter.com/PalletOne_org

    Medium:ttps://medium.com/palletone

    更多官方咨询,关注公众号获得

    相关文章

      网友评论

          本文标题:PalletOne技术讲堂之 Protobuf原理及使用

          本文链接:https://www.haomeiwen.com/subject/vqzkgftx.html