protobuffer学习总结
protobuffer是google开发的一种数据描述语言,它能够将结构化的数据序列化,并切可以将序列化的数据进行反序列化恢复原有的数据结构。一般用于数据存储以及通信协议方面。
如果是第一次使用protobuffer,我们可以将其与json或者xml进行类比,其实它与json或xml类似都可以作为数据的存储方式,不同的是json和xml是文本格式,而protobuffer是二进制格式。二进制格式不利于使用者直观的阅读,但是与json以及xml相比它有更多的优点。
protoBuffer相比于xml的优点
- 更加简介
- 体积小:消息大小只需要xml的1/10~1/3
- 解析速度快:解析速度比xml快20~100倍
- 使用proto Buffer的编译器,可以生成方便在编程中使用的数据访问代码.
- 具有更好的兼容性,很好的支持向上或向下兼容的特性
- 提供多种序列化的出口和入口,如文件流,string流,array流等等
protobuffer语法
消息类型实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| Package example; message Person{ required string name = 1; required int32 id = 2; optional string email = 3; enum PhoneType{ mobile = 1; home = 2; work = 3; } message PhoneNumber{ required string number = 1; optional PhoneType type = 2; } repeated PhoneNumber phone = 4; }
|
指定字段规则
protobuffer中字段规则包括一下三种:
- required:实例中必须包含的字段
- optional:实例中可以选择性包含的字段,若实例没有指定,则为默认值,若没有设置该字段的默认值,其值是该类型的默认值。如string默认值为””,bool默认值为false,整数默认值为0。
- repeated: 可以有多个值的字段,这类变量类似于vector,可以存储此类型的多个值。
由于一些历史原因,基本数值类型的repeated的字段并没有被尽可能地高效编码。在新的代码中,用户应该使用特殊选项[packed=true]来保证更高效的编码。
一般情况下慎重使用required字段,当此字段一定是必要的时候才使用。
repeated使用实例:
1 2 3 4 5 6 7 8
| message Person { required int32 age = 1; required string name = 2; } message Family { repeated Person person = 1; }
|
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 36 37
| int main(int argc, char* argv[]) { GOOGLE_PROTOBUF_VERIFY_VERSION; Family family; Person* person; person = family.add_person(); person->set_age(25); person->set_name("John"); person = family.add_person(); person->set_age(23); person->set_name("Lucy"); person = family.add_person(); person->set_age(2); person->set_name("Tony"); int size = family.person_size(); cout << "这个家庭有 " << size << " 个成员,如下:" << endl; for(int i=0; i<size; i++) { Person psn = family.person(i); cout << i+1 << ". " << psn.name() << ", 年龄 " << psn.age() << endl; } getchar(); return 0; }
|
数据类型
protobuffer中的数据类型与C++数据类型之间的关联如下图:
protobuffer类型 |
C++类型 |
double |
double |
float |
float |
int32 |
int32 |
int64 |
int64 |
uint32 |
uint32 |
uint64 |
uint64 |
sint32 |
int32 |
sint64 |
int64 |
fixed32 |
uint32 |
fixed64 |
uint64 |
sfixed32 |
uint32 |
sfixed64 |
uint64 |
bool |
bool |
string |
string |
bytes |
string |
枚举
当需要定义一个消息类型的时候,我们可能想为某一个字段指定预定义列表中的值。这个时候就需要用到枚举
如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| message SearchRequest { required string query = 1; optional int32 page_number = 2; optional int32 result_per_page = 3 [default = 10]; enum Corpus { UNIVERSAL = 0; WEB = 1; IMAGES = 2; LOCAL = 3; NEWS = 4; PRODUCTS = 5; VIDEO = 6; } optional Corpus corpus = 4 [default = UNIVERSAL]; }
|
变量标识号
在proto数据结构中,每一个变量都有唯一的数字标识。这些标识符的作用是在二进制格式中识别各个字段的,一旦开始使用就不可再改变。
此处需要注意的是1-15之内的标号在存储的时候只占一个字节,而大于15到162047之间的需要占两个字符,所以我们尽量为频繁使用的字段分配1-15内的标识号
。另外19000-19999之内的标识号已经被预留,不可用。最大标识号为2^29-1。
嵌套
protobuffer中的消息可以嵌套消息,也就是在一个message中定义另一个message。如上面实例可以看出。
扩展
我们可以通过扩展对proto文件进行扩展,而不需要直接区编辑原文件。
例如有原文件:
1 2 3 4
| message Foo{ //... extensions 100 to 199; }
|
上述extensions 100 to 199表示此范围内的标识号被保留为扩展用。我们在扩展文件中就可以使用这些标识号了。
1 2 3
| extend Foo{ optional int32 bar = 126; }
|
上述为扩展。当用户的Foo消息被编码的时候,数据的传输格式与用户在Foo里定义新字段的效果是完全一样的。然而,要在程序代码中访问扩展字段的方法与访问普通的字段稍有不同——生成的数据访问代码为扩展准备了特殊的访问函数来访问它。例如,下面是如何在C++中设置bar的值:
1 2
| Foo foo; foo.SetExtentions(bar, 15);
|
注释
与c++注释风格相同。双斜杠
向上且向下兼容更新消息
当在需求不断增加的过程中,数据结构也会不断变化,这个时候就需要我们去更新消息。怎么才能做到更新消息不会影响之前的数据和代码。这个时候我们更新消息需要遵循以下几个原则:
- 不要更改任何已有的字段的数值标识
- 所添加的字段必须是optional或者repeated。
包名称解析
为了防止消息明明冲突,我们往往会在文件的开始出生命包,包的作用相当于命名空间。在编译成C++代码时也是namespace。例如:
1 2 3 4
| package foo.bar; message open{ ///... }
|
在C++对open进行访问的时候的访问方式为:
C++程序使用protobuffer
按照上面的规则我们可以设计出合理的protobuffer类型。然后下一步就是将proto文件生成C++头文件和实现文件,将.proto文件编译成C接口的方法如下:
1
| protoc -I=SOURCE_DIR --cpp_out=DIST_DIR test.proto
|
使用proto生成的头文件进行编译时需要链接protobuffer库。具体为:
1
| g++ main.cpp test.pb.cc -lprotobuf
|
protobuffer编译为C++代码的常用接口
对于C++来说,编译器会为每个.proto文件生成一个.h文件和.cc文件。.proto文件中的每一个消息对应一个类。
protobuffer中常用的函数:
- has_name() :判断是否有当前成员
- clear_name() :清空该成员变量值
- name() :获取成员的变量值
- set_name(string) :设置变量值
- set_name(const char*):设置变量值
- set_name(int) :设置变量值
- clear() :清空所有元素为空状态
- void CopyFrom(person):从给定的对象复制。
- mutable_name() :获取变量name的指针
- add_name() :为repeated变量增加值
- ByteSize() :获取变量所占的字节数
若有元素data属性为repeated,其行为类似于vector,则此时则可用下列函数:
- add_data() : 添加data元素,返回值为Date*类型。
- data_size() : 获取repeated元素size,即元素的个数。
- data(i) : 获取data中地i个元素。
- ByteSize() : 获取序列化之后的protobuff对象的长度。
- CopyFrom(const ProtoType&): 从一个protobuf对象拷贝到另一个
常用的序列化方法
C数组的序列化与反序列化的API
如果想将其序列为char并通过socket进行传输,这是使用SerializeToArray来达到目的。
*除了下述的SerializeToArray方法之外,还有方法SerializePartialToArray,两者用法相同,其中唯一的区别在于SerializePartialToArray允许忽略required字段,而前者不允许
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| void* parray = (char*)malloc(256); bool ParseFromArray(const void* data, int size); bool SerializeToArray(void* data. int size); void set_people() { wp.set_name("sealyao"); wp.set_id(123456); wp.set_email("sealyaog@gmail.com"); wp.SerializeToArray(parray,256); } void get_people() { rap.ParseFromArray(parray,256); cout << "Get People from Array:" << endl; cout << "\t Name : " <<rap.name() << endl; cout << "\t Id : " << rap.id() << endl; cout << "\t email : " << rap.email() << endl; }
|
C++ String的序列化与反序列化API
除了下述的SerializeToString方法之外,还有方法SerializePartialToString,两者用法相同,其中唯一的区别在于SerializePartialToString允许忽略required字段,而前者不允许
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| bool SerializeToString(string* output) const; bool ParseFromString(const string& data); void set_people() { wp.set_name("sealyao"); wp.set_id(123456); wp.set_email("sealyaog@gmail.com"); wp.SerializeToString(&pstring); } void get_people() { rsp.ParseFromString(pstring); cout << "Get People from String:" << endl; cout << "\t Name : " <<rsp.name() << endl; cout << "\t Id : " << rsp.id() << endl; cout << "\t email : " << rsp.email() << endl; }
|
文件描述符序列化与反序列化API
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
| bool SerializeToFileDescriptor(int file_descriptor) const; bool ParseFromFileDescriptor(int file_descriptor); void set_people() { fd = open(path,O_CREAT|O_TRUNC|O_RDWR,0644); if(fd <= 0){ perror("open"); exit(0); } wp.set_name("sealyaog"); wp.set_id(123456); wp.set_email("sealyaog@gmail.com"); wp.SerializeToFileDescriptor(fd); close(fd); } void get_people() { fd = open(path,O_RDONLY); if(fd <= 0){ perror("open"); exit(0); } rp.ParseFromFileDescriptor(fd); std::cout << "Get People from FD:" << endl; std::cout << "\t Name : " <<rp.name() << endl; std::cout << "\t Id : " << rp.id() << endl; std::cout << "\t email : " << rp.email() << endl; close(fd); }
|
C++ stream 序列化和反序列化API
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
| bool SerializeToOstream(ostream* output) const; bool ParseFromIstream(istream* input); void set_people() { fstream fs(path,ios::out|ios::trunc|ios::binary); wp.set_name("sealyaog"); wp.set_id(123456); wp.set_email("sealyaog@gmail.com"); wp.SerializeToOstream(&fs); fs.close(); fs.clear(); } void get_people() { fstream fs(path,ios::in|ios::binary); rp.ParseFromIstream(&fs); std::cout << "\t Name : " <<rp.name() << endl; std::cout << "\t Id : " << rp.id() << endl; std::cout << "\t email : " << rp.email() << endl; fs.close(); fs.clear(); }
|
参考链接:
http://blog.csdn.net/mycwq/article/details/19622571
http://colobu.com/2015/01/07/Protobuf-language-guide/
https://worktile.com/tech/share/prototol-buffers
http://tech.meituan.com/serialization_vs_deserialization.html
http://blog.csdn.net/weiwangchao_/article/details/16797763
Comment and share