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