Protocol Buffers的应用与分析

    技术2022-05-19  22

     

    1  Protocol Buffers的介绍

    Protocol Buffers是一种用于序列化结构化数据的机制,它具有灵活、高效、自动化的特点。类似于XML,但是比XML更小巧、快捷、简单。在Google 几乎所有它内部的RPC协议和文件格式都是采用PB。PB具有以下特点:

    平台无关、语言无关高性能 比XML块20-100倍体积小 比XML小3-10倍使用简单兼容性好

    在这里,我做了个小实验,将一个29230KB的自定义格式的文本数据转换成PB和XML:

     PBXML转换后的大小21011KB43202KB解析时间(100次循环)18610ms169251ms完成解析所写代码行数1行50行

    与官方说法的差距,主要可能是因为应用场景不同,我的测试数据中字段比较长

    表1:PB与XML的实验比较

    可见,PB作为一种轻量级的数据协议,在时间、空间上都有一定的优势。

    2  Protocol Buffers的简单应用

    2.1  创建流程

    2.1.1  定义一个.proto文件

    新建一个文件,命名为addressbook.proto,内容如下:

    package tutorial;//命名空间 option java_package = "com.example.tutorial";//生成文件的包名 option java_outer_classname = "AddressBookProtos";//类名 message Person { //要描述的结构化数据 required string name = 1;//required表示这个字段不能为空 required int32 id = 2;//等号后面的内容为数字别名 optional string email = 3;//optional表示可以为空 PhoneNumber {//内部message required string number = 1; optional int32 type = 2; } repeated PhoneNumber phone = 4 } message AddressBook { repeated Person person = 1;//是个集合 } 

    对以上内容的一点解释:

    PB所支持的元类型数据请参考:PB元类型数据修饰符required:这个修饰符应该谨慎使用,滥用会导致后续的修改容易出现兼容性问题;修饰符optional:对于常出现的属性,为节省空间应该取1-16的别名;PB是以key-value的形式来将结构化数据序列化的。它采用了将等号后的数字别名以及属性的类型用varints编码成一个数字,来作为key。

    2.1.2  使用PB编译器

    输入:protoc      -I=$SRC_DIR –java_out=$DST_DIR $SRC_DIR/addressbook.proto其中    -I指定.proto文件所在目录–java_out指定生成java文件所在的目录

    2.1.3  使用PB的API来写入和读取messages

    经过以上步骤,会在指定的$DST_DIR目录下生成一个AddressBookProtos.java的类。在maven中引入protobuf-java这个依赖后,利用这个类,便能序列化/反序列化数据了。生成的代码结构如下:

    class AddressBookProtos{ class Person{ class PhoneNumber{class Builder{} } class Builder{} } class AddressBook{class Builder{} } } 

    可以看到Person、PhoneNumber、AddressBook这些内部类则对应了所定义的那些message。

    2.2  序列化数据及分析

    通过阅读代码可以看到,以上三个类的成员变量都是private类型的,并且,只提供了getter方法,而没有提供setter方法去为数据变量赋值。PB利用了内部类可以访问到外部类中私有成员变量的特性。对外部类的任何赋值操作都需要通过Builder内部类来进行。Builder中有一个指向外部类的引用(名为result),当赋值完成,调用Builder的build()方法时,会把这个对象返回,同时使result指向null。PB通过这样一种方式保证了数据安全性,一旦数据构建完毕,将无法再对其进行修改。拿PhoneNumber这个类来说,对成员变量number、type赋值,需要以如下方式来进行:

    PhoneNumber.Builder builder = PhoneNumber.newBuilder(); //调用setter赋值,setter返回了this,所以可以链式表述 builder.setNumber("111").setType(1); //赋值完成后,调用Builder的build方法,将返回PhoneNumber对象 PhoneNumber phoneNumber = builder.build(); 

    构建完成后,可以调用writeTo方法,将数据写入数据流中。

    2.3  反序列化及分析

    一行代码便能完成反序列化:

    AddressBook list = AddressBook .parseFrom(inputStream或buffer); 

    背后PB做了很多事情:

    根据inputStream或者buffer去构造一个CodedInputStream;然后使用生成代码中的mergeFrom方法,去解析二进制数据:首先调用CodedInputStream的readTag,也就是从中取得key值(int类型),然后通过swtich块来往对象中赋值(PB采用了Base 128 Varints的方式来编码这个数字,后面会介绍这种方式的)。将数据解析完成后,会调用build()方法,将构建好的对象返回。

    3  message的编码特点

    PB之所以解析速度快、所占体积小,很大程度上是由它序列化的编码特点来决定的。

    3.1 Base 128 Varints

    PB采用了Base 128 Varints来变长编码整数:

    变长编码的整数,它可能包含多个byte,对于每个byte的8位,其中后7位表示数值,最高的一位表示是否还有还有另一个byte,0表示没有,1表示有;越前面的byte表示数值的低位,越后面的byte表示数值的高位;

    例子:300   varints  编码为:1010 1100 0000 0010解释如下:300的2进制编码为:0001 0010 1100按照刚才的规则,高低位颠倒,截取最后的7为放在第一个byte,则第一byte为1010 1100(其中最高位1表示,后续还有byte);接着剩下的内容放到第二个byte,为0000 0010(其中最高位0表示,后续无byte,这个数到这里截止了)。于是,合在一起为 1010 1100 0000 0010;

    3.2 Key-Value

    如前所述,PB的message是一系列的key-value对,在二进制数据中,使用varints数字(包含了别名以及属性类型信息)来作为key,进而通过由PB编译器生成的代码来构造以及解析数据。PB将 key编码成下面的结构: X YYYY ZZZ其中:最高位X表示是否还有后续的byte来编码数字别名;YYYY用于编码别名,定义了多余16个属性,则需要用到额外的byte,所以出现频率高的字段应当取1-16的别名);ZZZ表示这个字段的类型,PB支持的属性的对应规则如下表:

    TypeMeaningUsed For0Varintint32, int64, uint32, uint64, sint32,sint64, bool, enum164-bitfixed64, sfixed64, double2Length-delimitedstring, bytes, embedded messages,packed repeated fields3Start groupgroups (deprecated)4End groupgroups (deprecated)532-bitfixed32, sfixed32, floa

    表2:PB 属性对应规则例子:required int32 a=1;  在应用中给a赋值150   ,序列化后08 96 01

    08代表的是key 0 0001 000, 最高位为0,表示这个key为一个byte,中间四位表示a的数字别名,最后三位表示a的属性类型;96 01代表的是value,二进制为:1001 0110 0000 0001→ 001 0110    000 0001(去掉最高位)→ 22              +  1*2^7 = 150

    3.3 Zig-Zag

    采用varints的方式编码有符号的整数,效率比较差,因为负数的最高位是1,这样就导致了情况类似于编码一个很大的数。

    为了解决这个问题,Protocol Buffers定义了sint32/sint64属性,他们采用了“之字形”(ZigZag)编码的方式,将负数编码成正数,交替进行。看了下表就很好理解了:

    Signed OriginalEncoded As00-1112-232147483647429496729421474836484294967295

    表3:Zig-Zag编码规则利用这个方式,可以有效地节省存储空间,也能提高解析效率。

    了解了以上内容,对于其他数据类型的编码,也是很好理解的,大家可以参考官方文档,这里不做详述。

    4 其他

    官方文档中,有提到PB提供了RPC的接口,但是没有提供具体实现。当在的.proto文件中,加入如下定义:

    service XXX { rpc MMM(request) returns(response); } 

    PB便会为你生成一个代表这个服务的XXX虚类,通过实现这个类中的abstract MMM方法,以及提供RpcChannel的实现,你便可以利用Protocol Buffers实现你的RPC了。

    第三方的RPC实现大家可以参考ThirdPartyRPC

    在这里,我利用了第三方实现protobuf-socket-rpc,写了一个小例子,有兴趣的可以看看。如下:Protocol buffer的rpc例子

    5 小结

    PB具有跨平台、解析速度快、序列化数据体积小、扩展性高、使用简单的特点。但是我们也可以看到,相比于XML,PB的数据,并不是自然可读的;同时它生成的代码不是纯pojo,对于代码有一定的侵入性。在你的项目中,如果对于以上缺点要求并不高,可以尝试着使用PB。

     


    最新回复(0)