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语法
消息类型实例:
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使用实例:
message Person {
required int32 age = 1;
required string name = 2;
}
message Family {
repeated Person person = 1;
}
int main(int argc, char* argv[])
{
GOOGLE_PROTOBUF_VERIFY_VERSION;
Family family;
Person* person;
// 添加一个家庭成员,John
person = family.add_person();
person->set_age(25);
person->set_name("John");
// 添加一个家庭成员,Lucy
person = family.add_person();
person->set_age(23);
person->set_name("Lucy");
// 添加一个家庭成员,Tony
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 |
枚举
当需要定义一个消息类型的时候,我们可能想为某一个字段指定预定义列表中的值。这个时候就需要用到枚举
如:
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文件进行扩展,而不需要直接区编辑原文件。
例如有原文件:
message Foo{
//...
extensions 100 to 199;
}
上述extensions 100 to 199表示此范围内的标识号被保留为扩展用。我们在扩展文件中就可以使用这些标识号了。
extend Foo{
optional int32 bar = 126;
}
上述为扩展。当用户的Foo消息被编码的时候,数据的传输格式与用户在Foo里定义新字段的效果是完全一样的。然而,要在程序代码中访问扩展字段的方法与访问普通的字段稍有不同——生成的数据访问代码为扩展准备了特殊的访问函数来访问它。例如,下面是如何在C++中设置bar的值:
Foo foo;
foo.SetExtentions(bar, 15);
注释
与c++注释风格相同。双斜杠
向上且向下兼容更新消息
当在需求不断增加的过程中,数据结构也会不断变化,这个时候就需要我们去更新消息。怎么才能做到更新消息不会影响之前的数据和代码。这个时候我们更新消息需要遵循以下几个原则:
- 不要更改任何已有的字段的数值标识
- 所添加的字段必须是optional或者repeated。
包名称解析
为了防止消息明明冲突,我们往往会在文件的开始出生命包,包的作用相当于命名空间。在编译成C++代码时也是namespace。例如:
package foo.bar;
message open{
///...
}
在C++对open进行访问的时候的访问方式为:
foo::bar::open test;
C++程序使用protobuffer
按照上面的规则我们可以设计出合理的protobuffer类型。然后下一步就是将proto文件生成C++头文件和实现文件,将.proto文件编译成C接口的方法如下:
protoc -I=SOURCE_DIR --cpp_out=DIST_DIR test.proto
使用proto生成的头文件进行编译时需要链接protobuffer库。具体为:
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字段,而前者不允许
void* parray = (char*)malloc(256);
//API
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字段,而前者不允许
//C++string序列化和序列化API
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
//文件描述符的序列化和序列化API
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
//C++ stream 序列化/反序列化API
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://colobu.com/2015/01/07/Protobuf-language-guide/
https://worktile.com/tech/share/prototol-buffershttp://tech.meituan.com/serialization_vs_deserialization.html