浅谈 Prorobuf 的自动反射功能

Protobuf 的介绍

  Protobuf 是一种序列化工具,用来将程序的数据结构序列化成字节流,方便网络传输。将数据结构序列化成字节流之前,首先你得向 Protobuf 提供一个 .proto文件,用来描述你的数据结构。举个例子,假设你想存储公司员工的信息,那么首先你需要描述一下员工的信息是怎样的:

1
2
3
4
5
6
package company;
message Employee {
required string name = 1;
required string email = 2;
};

  将文件保存为company.proto,接着用 Protobuf 编译器自动生成一个Employee类:

1
protoc --cpp_out . company.proto

  根据生成的Employee类,我们就可以将员工信息序列成字节流,或从字节流读取员工信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include "company.pb.h"
#include <string>
#include <iostream>
#include <fstream>
using namespace std;
void writer(const string &name, const string &email) {
company::Employee employee;
employee.set_name(name);
employee.set_email(email);
fstream output("company.bin", ios::out | ios::binary | ios::trunc);
employee.SerializeToOstream(&output);
}
void reader() {
company::Employee employee;
fstream input("company.bin", ios::in | ios::binary);
employee.ParseFromIstream(&input);
cout << "name: " << employee.name() << endl;
cout << "email: " << employee.email() << endl;
}
int main(int argc, char *argv[])
{
// Verify that the version of the library that we linked against is
// compatible with the version of the headers we compiled against.
GOOGLE_PROTOBUF_VERIFY_VERSION;
writer("senlin", "senlin@gmail.com");
reader();
return 0;
}

  编译并运行这个例子:

1
2
3
4
clang++ -o main main.cpp company.pb.cc -lprotobuf
./main
name: senlin
email: senlin@gmail.com

Protobuf 的自动反射

  在网络编程里面,程序往往会发送多种类型的消息给另一方。然而,在上面的例子里面,writer()并没有往company.bin里面记录消息的类型以及消息的个数,那么即使我们得到company.bin的内容,我们又怎能知道里面是否包含Employee类型的消息呢?如果包含Employee类型的消息,那么到底有多少个Employee呢?
  解决这个问题很简单,只需要让writer()在写入消息之前,先写入一个消息的 header,这个 header 包含消息的类型以及消息的长度,那么reader()就可以通过读取这个 header 获取消息的类型和消息的长度了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// pseudocode
void reader()
{
fstream input("company.bin", ios::in | ios::binary);
string msgType = readMessageType(&input);
int msgLen = readMessageLen(&input);
for (int i = 0; i < msgLen; ++i) {
if (msgType == "company.Employee") {
company::Employee employee;
// read data from employee
}
else if (msgType == "company.Boss") {
// ...
}
}
}

  上面的代码存在一个问题,假设程序需要增加另一种消息类型,那么就需要修改reader()的实现,然而,从良好的工程实践上来看,应该避免改动reader()的实现。


  所幸的是,我们可以实现一个createMessage()函数,根据消息类型名,自动创建具体的消息对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <google/protobuf/message.h>
#include <google/protobuf/descriptor.h>
using namespace google;
protobuf::Message *createMessage(const std::string &type_name)
{
protobuf::Message* message = NULL;
const protobuf::Descriptor* descriptor =
protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName(type_name);
if (descriptor) {
const protobuf::Message* prototype =
protobuf::MessageFactory::generated_factory()->GetPrototype(descriptor);
if (prototype) {
message = prototype->New();
}
}
return message;
}

  这样每次增加消息类型时,就不用修改reader()的实现了。我们传递一个dispatcherreader()函数,dispatcher是一个消息分发器,用户可以注册不同的回调函数,它会根据消息的类型,自动调用相应的回调函数去处理消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// pseudocode
void reader(MessageDispatcher &dispatcher)
{
fstream input("company.bin", ios::in | ios::binary);
string msgType = readMessageType(&input);
int msgLen = readMessageLen(&input);
for (int i = 0; i < msgLen; ++i) {
protobuf::Message *msg = createMessage(msgType);
dispatcher.OnMessage(msg);
delete msg;
}
}

参考资料