Protocol Buffers的应用与分析


1  Protocol Buffers的介绍

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

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

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

  PB XML
转换后的大小 21011KB 43202KB
解析时间(100次循环) 18610ms 169251ms
完成解析所写代码行数 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做了很多事情:

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

 

更多详情见请继续阅读下一页的精彩内容:

  • 1
  • 2
  • 下一页

相关内容