protobuf反序列化多条消息问题
Protocol buffer是google开源的又一利器,主要用于结构化数据存储与数据交换,类似于XML,但相比XML,它更小、更快、也更简单,只需使用protobuf对数据结构进行一次描述,即可利用各种不同的语言(包括C++、java、python等,同时还包括很多种语言的绑定插件)从各种不同的数据流(文件、字符串流等)对结构化数据轻松读写。但由于其使用二进制存储,相比XML,其可读性差。
Protocol buffer在google code上的页面,源码及文档都可以从中获取。
http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/overview.html
运行了源码中附带的AddressBook的例子,发现protobuf的确很强大,使用起来也很方便,只需在.proto文件中描述数据结构,然后用protoc生成对应的cpp文件(包含描述结构对应的类),便可通过SerializeToOstream将结构数据写到输出流中,通过ParseFromIstream从输入流中提取(还有相应的字符串流读写方法)。
Protobuf通过repeated提供类似于数组的结构。
在一个key/value存储的系统中,我为每个key/value的位置建了一项索引,索引的内容包括魔数、标志、key、key对应的偏移及大小,在.proto文件中定义如下:
message Index {
required int32 magic = 1;
required int32 flag = 2;
required uint64 key = 3;
required uint64 offset = 4;
required uint64 size = 5;
}
由于索引项有多个,故应在定义一个Message,里面包含多个Index,如:
message IndexArray {
repeated Index idx = 1;
}
但这样就存在一个问题,当对个index被添加到IndexArray之后,每次读取其中一个index都需要先从文件流中分析出一个IndexArray,再获取某一项index的信息,但某个index都应该能被快速随机访问,而不需要先读出所有的index。
于是我想protobuf应该是根据流的当前位置来向输出流中序列化结构的,同时在parse的时候也是根据输入流的当前位置起开始分析;于是只要依次向输出流Serialize多个结构,在提取时,先将输入流定位到指定偏移就能Parse出指定的index。于是做了个小测试:
/* 依次向文件流中序列化10个index */
fstream output(argv[1], ios::out | ios::trunc | ios::binary);
int i;
for(i = 0; i < 10; i++) {
Index idx;
idx.set_flag(0);
idx.set_magic(1);
idx.set_key(0);
idx.set_offset(i);
idx.set_size(1);
idx.SerializeToOstream(&output);
}
output.close();
/* 依次从文件流中提取10个index */
fstream input(argv[1], ios::in | ios::binary);
int I;
for(i = 0; i < 10; i++) {
Index idx;
idx.ParseFromIstream(&input);
cout << idx.offset() << endl;
}
测试后发现,再向流种序列化10个index后,文件大小100B(因sint采用vint格式存储的,每个index10B,所以总的大小跟预想是一样的),但在提取时,每次只能提取到最后一个结构(offset为9),之后提示9次不能parse对象,即parse依次后,输入流就到了末尾,而且得到的是最后一个结构的信息。将测试例子改编了多个版本,得出的结论都是一样的,说明ParseFromIstream并不像预想的一样从流的当前偏移提取一个对象。
对于protocol在数据转换与存储上的表现我很满意,而且将其应用于网络数据的交换可以极大的减小编程工作量。但其序列化过程与提取过程为何不能对应起来,难道每次就只能向流种序列化一个结构?对于我刚说到的应用应该很普遍(即使同种结构可以使用repeated,想流中序列化多种不同的结构还是会遇到上述问题),为何protocol buffer不能处理呢,我忽略了些什么?
2011年12月16日补充:
http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/techniques.html
Streaming Multiple Messages
If you want to write multiple messages to a single file or stream, it is up to you to keep track of where one message ends and the next begins. The Protocol Buffer wire format is not self-delimiting, so protocol buffer parsers cannot determine where a message ends on their own. The easiest way to solve this problem is to write the size of each message before you write the message itself. When you read the messages back in, you read the size, then read the bytes into a separate buffer, then parse from that buffer. (If you want to avoid copying bytes to a separate buffer, check out the CodedInputStream class (in both C++ and Java) which can be told to limit reads to a certain number of bytes.)
从上述描述可以看出,PB的格式是非自描述性的,ParseFromIstream会读完整个流,parse里面的key-value对,并依次根据key的值(key由字段号标识)设置value。