首页 > 代码库 > 第二篇:人人都爱Protocol Buffers

第二篇:人人都爱Protocol Buffers

protocol buffers是google提供的一种将结构化数据进行序列化和反序列化的方法,其优点是语言中立,平台中立,可扩展性好,目前在google内部大量用于数据存储,通讯协议等方面。PB在功能上类似XML,但是序列化后的数据更小,解析更快,使用上更简单。用户只要按照proto语法在.proto文件中定义好数据的结构,就可以使用PB提供的工具(protoc)自动生成处理数据的代码,使用这些代码就能在程序中方便的通过各种数据流读写数据。PB目前支持Java, C++和Python3种语言。另外,PB还提供了很好的向后兼容,即旧版本的程序可以正常处理新版本的数据,新版本的程序也能正常处理旧版本的数据。

1.如何使用Protocol Buffers?

常规用法:在一个地址的proto文件了定义通讯薄消息格式,一个addressBook,这个结构由几个可重复的person组成,一个peron有两个必须的name和id字段,一个email可选字段,Phonenumber可重复字段,其中Phonenumber由number和byte组成

message Person {  required string name = 1;  required int32 id = 2;  optional string email = 3;   enum PhoneType {    MOBILE = 0;    HOME = 1;    WORK = 2;  }   message PhoneNumber {    required string number = 1;    optional PhoneType type = 2 [default = HOME];  }   repeated PhoneNumber phone = 4;} message AddressBook {  repeated Person person = 1;}

使用PB提供的工具 protoc根据.proto文件自动生成处理消息的代码

2.使用PB提供的工具 protoc根据.proto文件自动生成处理消息的代码
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
addressbook.pb.h,
addressbook.pb.cc

3.程序使用生成的代码来读写(序列化,反序列化)和操作(get,set)消息
fstream output(argv[1], ios::out | ios::trunc | ios::binary);
address_book.SerializeToOstream(&output));

生产者:产生消息,填充内容,并序列化保存
消费者:读取数据,反序列化得到消息,使用消息

 

自定义消息

常规使用要求消息生产者和消费者在编译的时候确定消息格式(.proto文件),生产者和消费者在消息格式上紧耦合,当消息格式发生变化时,消费者必须重新编译才能理解新的格式。有没有让消费者可以动态的适应消息格式的变化?这是完全可行的。生产者把定义的消息格式.proto文件和消息作为一个完整的消息序列保存,完整保存的消息我称之为Wrapper message,原来的消息称之为payload message。消费者把wrapper message反序列化,先得到payload message的消息类型,然后根据类型信息得到payload message,最后通过反射机制来使用该消息。通过这种方式消费者只需要了解这一种wrapper message的格式就能够适应各种payload message的格式。这也是PB官网给出的解决方案:Self-describing Messages

wrapper message的定义如下所示,第一个字段保存payload message的类型信息(由于message可以内嵌message,而.proto文件可以import 其他.proto,所以这里使用FileDescriptorSet),第二个字段是payload message的类型名字符串,第三个字段是payload message序列化后的数据。

message SelfDescribingMessage {  // Set of .proto files which define the type.  required FileDescriptorSet proto_files = 1;  // Name of the message type.  Must be defined by one of the files in proto_files.  required string type_name = 2;  // The message data.  required bytes message_data = http://www.mamicode.com/3;}

实现

下面通过改造tutorial例子程序,演示自描述消息的实现方式。

生产者:add_person.cc

1. 使用 protoc生成代码时加上参数–descriptor_set_out,输出类型信息(即SelfDescribingMessage的第一个字段内容)到一个文件,这里假设文件名为desc.set,
protoc –cpp_out=. –descriptor_set_out=desc.set addressbook.proto
2. payload message使用方式不需要修改
tutorial::AddressBook address_book;
PromptForAddress(address_book.add_person());//这个函数不需要任何修改
3. 在保存时使用文件desc.set内容填充SelfDescribingMessage的第一个字段,使用AddressBook
AddressBook的full name填充SelfDescribingMessage的第二个字段,AddressBook序列化后的数据填充第三个字段。最后序列化SelfDescribingMessage保存到文件中。

utorial::SelfDescribingMessage sdmessage;fstream desc(argv[2], ios::in | ios::binary);sdmessage. mutable_proto_files()->ParseFromIstream(&desc);sdmessage.set_type_name((address_book.GetDescriptor())->full_name());sdmessage.clear_message_data();address_book.SerializeToString(sdmessage.mutable_message_data());fstream output(argv[1], ios::out | ios::trunc | ios::binary);sdmessage.SerializeToOstream(&output));

 

消费者:list_people.cc

List_people.cc编译时需要知道SelfDescribingMessage,不需要知道AddressBook,运行时可以正常操作AddressBook消息。
1. 首先反序列化SelfDescribingMessage

tutorial::SelfDescribingMessage sdmessage;fstream input(argv[1], ios::in | ios::binary);sdmessage.ParseFromIstream(&input));

2. 通过第一个字段得到FileDescriptorSet,通过第二个字段取得消息的类型名,使用DescriptorPool得到payload message的类型信息Descriptor

SimpleDescriptorDatabase db;for(int i=0;i<sdmessage.proto_files().file_size();i++){    db.Add(sdmessage.proto_files().file(i));  }DescriptorPool pool(&db);const Descriptor *descriptor = pool.FindMessageTypeByName(sdmessage.type_name());

3. 使用DynamicMessage new出这个类型的一个空对象,从第三个字段反序列化得到原来的message对象

DynamicMessageFactory factory(&pool);Message *msg = factory.GetPrototype(descriptor)->New();msg->ParseFromString(sdmessage.message_data());

4. 通过Message的reflection接口操作message的各个字段

3.2 实现

1. 动态定义消息,生成类型信息

FileDescriptorProto file_proto;file_proto.set_name("foo.proto"); // create dynamic message proto names "Pair"DescriptorProto *message_proto = file_proto.add_message_type();message_proto->set_name("Pair"); FieldDescriptorProto *field_proto = NULL; field_proto = message_proto->add_field();field_proto->set_name("key");field_proto->set_type(FieldDescriptorProto::TYPE_STRING);field_proto->set_number(1);field_proto->set_label(FieldDescriptorProto::LABEL_REQUIRED); field_proto = message_proto->add_field();field_proto->set_name("value");field_proto->set_type(FieldDescriptorProto::TYPE_UINT32);field_proto->set_number(2);field_proto->set_label(FieldDescriptorProto::LABEL_REQUIRED);DescriptorPool pool;const FileDescriptor *file_descriptor = pool.BuildFile(file_proto);const Descriptor *descriptor = file_descriptor->FindMessageTypeByName("Pair");

2. 根据类型信息使用DynamicMessage new出这个类型的一个空对象

// build a dynamic message by "Pair" protoDynamicMessageFactory factory(&pool);const Message *message = factory.GetPrototype(descriptor); // create a real instance of "Pair"Message *pair = message->New();

3. 通过Message的reflection操作message的各个字段

// write the "Pair" instance by reflectionconst Reflection *reflection = pair->GetReflection(); const FieldDescriptor *field = NULL;field = descriptor->FindFieldByName("key");reflection->SetString(pair, field, "my key");field = descriptor->FindFieldByName("value");reflection->SetUInt32(pair, field, 1234);

此时动态生成的pair对象内容为

key: "my key"value: 1234

3.3 代码

完整代码也不多,直接贴上:

#include <iostream>#include <google/protobuf/descriptor.h>#include <google/protobuf/descriptor.pb.h>#include <google/protobuf/dynamic_message.h> using namespace std;using namespace google::protobuf; int main(int argc, const char *argv[]){    FileDescriptorProto file_proto;    file_proto.set_name("foo.proto");     // create dynamic message proto names "Pair"    DescriptorProto *message_proto = file_proto.add_message_type();    message_proto->set_name("Pair");     FieldDescriptorProto *field_proto = NULL;     field_proto = message_proto->add_field();    field_proto->set_name("key");    field_proto->set_type(FieldDescriptorProto::TYPE_STRING);    field_proto->set_number(1);    field_proto->set_label(FieldDescriptorProto::LABEL_REQUIRED);     field_proto = message_proto->add_field();    field_proto->set_name("value");    field_proto->set_type(FieldDescriptorProto::TYPE_UINT32);    field_proto->set_number(2);    field_proto->set_label(FieldDescriptorProto::LABEL_REQUIRED);     // add the "Pair" message proto to file proto and build it    DescriptorPool pool;    const FileDescriptor *file_descriptor = pool.BuildFile(file_proto);    const Descriptor *descriptor = file_descriptor->FindMessageTypeByName("Pair");    cout << descriptor->DebugString();     // build a dynamic message by "Pair" proto    DynamicMessageFactory factory(&pool);    const Message *message = factory.GetPrototype(descriptor);    // create a real instance of "Pair"    Message *pair = message->New();     // write the "Pair" instance by reflection    const Reflection *reflection = pair->GetReflection();    const FieldDescriptor *field = NULL;    field = descriptor->FindFieldByName("key");    reflection->SetString(pair, field, "my key");    field = descriptor->FindFieldByName("value");    reflection->SetUInt32(pair, field, 1234);     cout << pair->DebugString();    delete pair;    return 0;}

3.4 另一种实现方式:动态编译

上面是动态消息的一种方式,我们还可以使用PB 提供的 google::protobuf::compiler 包在运行时动态编译指定的.proto 文件来使用其中的 Message。这样就可以通过修改.proto文件实现动态消息,有点类似配置文件的用法。完成这个工作主要的类叫做 importer,定义在 importer.h 中。
Foo.proto内容如下:

message Pair {    required string key = 1;    required uint32 value = 2; }

下面的代码实现同样的动态消息:

#include <iostream>#include <google/protobuf/descriptor.h>#include <google/protobuf/descriptor.pb.h>#include <google/protobuf/dynamic_message.h>#include <google/protobuf/compiler/importer.h> using namespace std;using namespace google::protobuf;using namespace google::protobuf::compiler; int main(int argc, const char *argv[]){    DiskSourceTree sourceTree;    //look up .proto file in current directory    sourceTree.MapPath("", "./");    Importer importer(&sourceTree, NULL);    //runtime compile foo.proto    importer.Import("foo.proto");     const Descriptor *descriptor = importer.pool()->FindMessageTypeByName("Pair");    cout << descriptor->DebugString();     // build a dynamic message by "Pair" proto    DynamicMessageFactory factory;    const Message *message = factory.GetPrototype(descriptor);    // create a real instance of "Pair"    Message *pair = message->New();     // write the "Pair" instance by reflection    const Reflection *reflection = pair->GetReflection();     const FieldDescriptor *field = NULL;    field = descriptor->FindFieldByName("key");    reflection->SetString(pair, field, "my key");    field = descriptor->FindFieldByName("value");    reflection->SetUInt32(pair, field, 1111);     cout << pair->DebugString();     delete pair;     return 0;}

4. 动态自描述消息

4.1 分析

好了,到此为止我们已经可以通过自描述消息解放消费者,通过动态消息解放生产者。最后介绍的大杀器是两者的结合:动态自描述消息,彻底解放生产者和消费者。
仍以上面的消息为例说明:

message pair {  required string key = 1;  required uint32 value = 2;  }

这次我们不使用第二章介绍的wrapper message方式,改为通过文件格式约定实现自描述,网络通信协议可参考这种方式。
生产者和消费者商定文件格式如下:

 

uint32:Magic          uint32:Length            FileDescriptorProto        uint32:Length     string:MessageData

4.2 实现

生产者

1. 动态定义消息,生成类型信息;根据类型信息生成一个空的message对象;通过Message的reflection操作message的各个字段。这些和动态消息处理一致,这里就不赘述了。
2. 使用CodedOutputStream写文件,依次保存如下信息:
a) MAGCI_NUM, 消费者可以用来验证文件格式是否一致或者格式是否错误。
b) FileDescriptorProto序列化后数据的size
c) 序列化的FileDescriptorProto数据
d) Payload message序列化后数据的size
e) 序列化的Payload message数据
代码如下:

const unsigned int MAGIC_NUM=2988;int fd = open("dpb.msg", O_WRONLY|O_CREAT,0666);ZeroCopyOutputStream* raw_output = new FileOutputStream(fd);CodedOutputStream* coded_output = new CodedOutputStream(raw_output); coded_output->WriteLittleEndian32(MAGIC_NUM);string data;file_proto.SerializeToString(&data);coded_output->WriteVarint32(data.size());coded_output->WriteString(data); data.clear();pair->SerializeToString(&data);coded_output->WriteVarint32(data.size());coded_output->WriteString(data);; delete coded_output;delete raw_output;close(fd);

消费者

1. 使用CodedInputStream读取文件,先通过MAGIC_NUM判断文件格式是否正确,然后反序列化FileDescriptorProto,得到payload message的类型信息

FileDescriptorProto file_proto;int fd = open("dpb.msg", O_RDONLY);ZeroCopyInputStream* raw_input = new FileInputStream(fd);CodedInputStream* coded_input = new CodedInputStream(raw_input);unsigned int magic_number;coded_input->ReadLittleEndian32(&magic_number);if (magic_number != MAGIC_NUM) {        cerr << "File not in expected format." << endl;        return 1;} uint32 size;coded_input->ReadVarint32(&size); char* text = new char[size + 1];coded_input->ReadRaw(text, size);text[size] = \0;file_proto.ParseFromString(text);DescriptorPool pool;const FileDescriptor *file_descriptor = pool.BuildFile(file_proto);const Descriptor *descriptor = file_descriptor->FindMessageTypeByName("Pair");

2. 使用DynamicMessage new出这个类型的一个空对象,从文件中读取messagedata反序列化得到原来的message
DynamicMessageFactory factory(&pool);

const Message *message = factory.GetPrototype(descriptor);  // create a real instance of "Pair" Message *pair = message->New(); coded_input->ReadVarint32(&size); text = new char[size + 1]; coded_input->ReadRaw(text, size); text[size] = \0; pair->ParseFromString(text);

3. 通过Message的reflection即可操作message的各个字段

5. 天下没有免费的午餐

自描述和动态生成得到的灵活性不是免费的午餐,那么下面我们就以文中的例子来分析一下动态自描述消息相对静态消息在空间和时间上的变化。
1. 空间:由于PB主要用于数据存储和通讯协议,下面分别分析:

以Tutorial中的AddressBook为例分析数据存储的使用场景,添加如下两条记录:

Person ID: 1  Name: Peter  E-mail address: peter@gmail.com  Home phone #: 13777777777  Work phone #: 13788888888  Mobile phone #: 13799999999Person ID: 2  Name: Tom  E-mail address: tom@gmail.com   Mobile phone #: 13888888888

 

使用方式内容字节数
静态消息AddressBook120
第二章自描述消息FileDescriptorSet(3+302)
type_name(2+20)
message_data(2+120)
449

这里需要注意的是表面上看数据量增加了274%,实际上增加的是固定的329字节,即当文件越来越大的时候这部分开销是不会增加的。

以第四章动态自描述消息为例分析在通讯协议中使用PB的应用场景

pair消息内容为:key: "jianhao"value: 8888
使用方式内容字节数
静态消息Pair12
动态自描述消息MAGIC_NUM
FileDescriptorProto length
FileDescriptorProto
Message length
Pair
64

注意:在网络通讯中由于一次通讯需要传输一次完整的类型信息,所以消息越大越划算。
2. 时间:通过测试对比静态消息和动态自描述消息在日常的使用场景下的效率。
测试中的消息类型如下:

message Pair {    required string key = 1;    required uint32 value = 2; }

生产者:
静态消息使用方式:

pair.set_key("my key");pair.set_value(i);pair.SerializeToArray(buffer,100);

动态消息使用方式:

const Reflection *reflection = pair->GetReflection();const FieldDescriptor *field = NULL;field = descriptor->FindFieldByName("key");reflection->SetString(pair, field, "my key");field = descriptor->FindFieldByName("value");reflection->SetUInt32(pair, field, i);pair->SerializeToArray(buffer,100);
消息使用方式循环1M时间消耗循环10M消耗时间
静态消息0.37s3.64s
动态消息1.65s16.51s

由于绝对时间和机器环境有关,所以相对值更有意义。从上面的测试可知动态消息的赋值和序列化时间是静态消息的赋值和序列化的4倍。

消费者:
静态消息使用方式:

pair.ParseFromArray(buffer,100);key=pair.key();value=pair.value()+i;

动态自描述消息有两种使用方式:
1.仅反序列化&操作payload message,常用于数据存储

pair->ParseFromArray(buffer,100);const Reflection *reflection = pair->GetReflection(); const FieldDescriptor *field = NULL;field = descriptor->FindFieldByName("key");key=reflection->GetString(*pair, field);field = descriptor->FindFieldByName("value");value=reflection->GetUInt32(*pair, field)+i;

2.先反序列化payload message的类型信息,然后动态生成一个空的该类型对象,然后反序列化并操作该对象,常用于通讯协议

FileDescriptorProto file_proto;file_proto.ParseFromArray(descbuffer,300);DescriptorPool pool;const FileDescriptor *file_descriptor = pool.BuildFile(file_proto);const Descriptor *descriptor = file_descriptor->FindMessageTypeByName("Pair"); // build a dynamic message by "Pair" protoDynamicMessageFactory factory;const Message *message = factory.GetPrototype(descriptor);Message *pair = message->New();pair->ParseFromArray(buffer,100);const Reflection *reflection = pair->GetReflection(); const FieldDescriptor *field = NULL;field = descriptor->FindFieldByName("key");key=reflection->GetString(*pair, field);field = descriptor->FindFieldByName("value");value=reflection->GetUInt32(*pair, field)+i;
消息使用方式循环1M时间消耗循环10M消耗时间
静态消息0.48s4.85s
动态自描述消息(存储方式)2.01s17.28s
动态自描述消息(通讯方式)28.24s283.98s

从上面的测试可知动态自描述消息的反序列化和操作时间是静态消息的反序列化和操作的4倍左右。但是如果加上对类型信息的反序列化得化则性能急剧下降到静态消息的接近60倍。

 

 

本文转载自桂南的文章。原文链接http://www.searchtb.com/2012/09/protocol-buffers.html

第二篇:人人都爱Protocol Buffers