Protocol Buffers (亦称,protobuf) 是Google的轻便高效的结构化数据存储格式,它独立于语言,独立于平台,可扩展。你可以在 protobuf's documentation on the Google Developers site 找到它。

Protocol Buffers可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前提供了 C++、Java、Python 三种语言的 API。

为什么不使用XML?同XML相比,Protocol buffers在序列化结构化数据方面有许多优点(Google官方提出):

  • 更简单;
  • 数据描述文件只需原来的1/10至1/3;
  • 解析速度是原来的20倍至100倍;
  • 减少了二义性;
  • 生成了更容易在编程中使用的数据访问类。

类似的协议还有MessagePack等。

protobuf协议格式

使用 Protocol Buffers,你需要在.proto文件中定义你想要序列化结构的 protocol buffer 消息类型。

每个 protocol buffer 消息都是包含一系列键值对信息的小逻辑记录。下面是一个基础的、定义了一个包含关于人的信息的.proto 文件示例 :

// addressbook.proto
syntax = "proto2";

package tutorial;  
message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phone = 4;
}

message AddressBook {
  repeated Person person = 1;
}

.proto文件以包声明开始,防止名字冲突。

正如你所看到的,这个消息格式是很简单的——每一个消息类型由一个或多个唯一“tag”字段(等号后的数数字" = 1"," = 2"),每一个编号字段有一个名称(name)和值类型(value type)字段。值类型可以是数字(doublefloatint64uint64int32uint32等)、布尔值(bool)、字符串(string)、原始字节(bytes)、枚举型(enum)等,甚至可以是上面定义的protocol buffer消息类型,你可以分层次的结构化你的数据。你可以指定可选字段(optional)、必填字段(required)、和repeated字段(repeated,相当于std的vector,个人理解类似JSON的array)。

对于proto3 ,与proto2最明显的区别就是 proto3不支持required字段,也不支持显式的optional字段(默认使用optional );proto3也不支显示的持默值。上面的文件文件应该定义成如下格式:

// addressbook.proto
syntax = "proto3";

package tutorial;
message Person {
  string name = 1;
  int32 id = 2;
  string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phone = 4;
}

message AddressBook {
  repeated Person person = 1;
}

有关.proto文件的写作风请查看 Protocol Buffer 风格指南

有关.proto 文件更详细的信息,请阅读Protocol Buffer 语言指南Protocol Buffer 语言指南(proto3)

想要了解protobuf协议的二进制存储格式请参阅 Protocol Buffer Encoding

如不做特殊说明,本篇文章下面的章节将以proto3为主。

编译proto文件

现在你有了一个 addressbook.proto文件,下面要做的是如果通过protoc.proto文件生成一个C++类。

  1. 首先你需要安装Protocol Buffers编译器(protoc),下载Protocol Buffers 并按README文件中的方法进行安装;
  2. 运行protoc编译器,指定源目录$SRC_DIR(你的应用程序源代码所在目录,即.proto文件所在目录 - 如果不指定则使用当前目录),指定目的目录$DST_DIR(C++类将要生成的目录,通常和$SRC_DIR相同),指定.proto文件。命令格式如下:

    protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto

由于你想要生成C++类,所以你需要使用--cpp_out选项 – 其他语言也提供类似选项。

protoc -I="F:\packheader\proto" --cpp_out="F:\packheader\src" "F:\packheader\proto\addressbook.proto"

在你指定的目的目录会生成下列文件:

  • addressbook.pb.h,类的头文件。
  • addressbook.pb.cc,类的source文件。

更详细的信息请参看:C++ Generated Code

The Protocol Buffer API

查看你上面生成的类文件(addressbook.pb.haddressbook.pb.cc),在addressbook.pb.h文件里,可以看到addressbook.proto文件中定义的所有消息字段。在Person 类中,可以看到编译器为每个字段生成的存取器。例如:对于 nameidemail和 phone 字段,生成了下面的方法(函数):

// Person
// optional string name = 1;
inline void Person::clear_name();
inline const ::std::string& Person::name();
inline void Person::set_name(const ::std::string& value);
inline void Person::set_name(const char* value);
inline void Person::set_name(const char* value, size_t size);
inline ::std::string* Person::mutable_name();
inline ::std::string* Person::release_name();
inline void Person::set_allocated_name(::std::string* name);

// optional int32 id = 2;
inline void Person::clear_id();
inline ::google::protobuf::int32 Person::id();
inline void Person::set_id(::google::protobuf::int32 value);

// optional string email = 3;
inline void Person::clear_email();
inline const ::std::string& Person::email();
inline void Person::set_email(const ::std::string& value);
inline void Person::set_email(const char* value);
inline void Person::set_email(const char* value, size_t size);
inline ::std::string* Person::mutable_email();
inline ::std::string* Person::release_email();
inline void Person::set_allocated_email(::std::string* email);

// repeated .tutorial.Person.PhoneNumber phone = 4;
inline int Person::phone_size() const;
inline void Person::clear_phone();
inline const ::tutorial::Person_PhoneNumber& Person::phone(int index) const;
inline ::tutorial::Person_PhoneNumber* Person::mutable_phone(int index);
inline ::tutorial::Person_PhoneNumber* Person::add_phone();
inline ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >*
Person::mutable_phone();
inline const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >&
Person::phone() const;

正如你看到的那样,getters 方法恰好使用字段名的小写,setter 方法以set_开头,每一个字段都有一个clear_ 方法用于清除字段(回到empty状态)。

数字类型的id 字段只有简单的存取器方法,name和 email因为是字符串,所以他们有一对扩展方法 — 一个mutable_ getter能够让你直接得到这个字符串的指针,它也是一个setter。注意:即使时 email没有初始化你也可以调用mutable_email() ,它会自动初始化一个空字符串。set_allocated_方法设置新的字符串到当前字段,并释放之前的字符串。release_方法释放字段的所有权并且返回一个字符串的指针。

Repeated字段也有一些特殊的方法,如果你查看phone 字段的方法,你可以:

  • 查看repeated 字段的 _size (换句话说,Person关联了多少字段);
  • 根据索引号获得电话号;
  • 更新指定索引号的电话号;
  • 添加电话号(重复标量类型有一个add_方法能够让你传递一个新值)。

想要了解更详细的信息,请查看Protocol Buffer Basics: C++,不过该篇文章介绍的是proto2

使用Protocol Buffer C++ API

下面是一段刚才生成的Protocol Buffer C++ API的代码:

// proto_test.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>

using namespace std;

int main()
{
    // 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;

    // 创建一个 person 对象并赋值
    tutorial::Person input_person;
    input_person.set_name("john");
    input_person.set_id(123);
    input_person.set_email("123@163.com");
    ::tutorial::Person_PhoneNumber* input_person_phone = input_person.add_phone();
    input_person_phone->set_type(tutorial::Person_PhoneType_WORK);
    input_person_phone->set_number("15112345678");

    // Serialize person to byte array
    int size = input_person.ByteSize();
    char* data = new char[size];
    input_person.SerializeToArray(data, size);

    // Parse person from byte array
    tutorial::Person output_person;
    output_person.ParseFromArray(data, size);

    // output person info
    cout << "name: " << output_person.name() << endl;
    cout << "id: " << output_person.id() << endl;
    cout << "email: " << output_person.email() << endl;
    const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& output_person_phones = output_person.phone();
    for each (::tutorial::Person_PhoneNumber output_person_phone in output_person_phones)
    {
        cout << "phone type: " << output_person_phone.type() << endl;
        cout << "phone number: " << output_person_phone.number() << endl;
    }

    // free data
    delete data;

    // Optional:  Delete all global objects allocated by libprotobuf.
    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}

GOOGLE_PROTOBUF_VERIFY_VERSION 是一个宏定义,他不是严格必须的,在使用C++ Protocol Buffer 库之前使用它能够验证库文件是否与当前编译的头文件兼容,如果检测到不兼容,程序将会终止。注意每一个 .pb.cc 文件会在启动时自动调用这个宏。

注意在程序结束前使用ShutdownProtobufLibrary() ,这个操作会删除所有的被Protocol Buffer库分配的全局对象。

扩展

Any 消息类型让你使用嵌入类型的消息即使你没有.proto的定义。一个Any包含一个作为bytes的任意序列化消息,伴随着充当一个全局唯一标识符,并解析为消息类型的URL。要使用Any类型,你需要 import google/protobuf/any.proto

Any 类型的消息定义如下:

import "google/protobuf/any.proto";
message ErrorStatus {
  string message = 1;
  google.protobuf.Any details = 2;
}

封装PackFrom() 和解压PackTo()一个Any类型的方法如下:

// create Foo
ErrorStatus err;
err.set_message("1");
cout << "has details: " << err.has_details() << endl;    // ASSERT(err.has_details() == false)

// Storing an arbitrary message type in Any.
::google::protobuf::Any* any = new ::google::protobuf::Any;
any->PackFrom(input_person);
err.set_allocated_details(any);
cout << "has details: " << err.has_details() << endl;  // ASSERT(err.has_details() == true)

// Unpack Any Type to an arbitrary message type
const ::google::protobuf::Any& what_type = err.details();
if (what_type.Is<tutorial::Person>()) {
    tutorial::Person p;
    what_type.UnpackTo(&p);
    cout << "name: " << p.name() << endl;
    cout << "id: " << p.id() << endl;
    cout << "email: " << p.email() << endl;
}

err.clear_details();        // must init with pointer
cout << "has details: " << err.has_details() << endl;  // ASSERT(err.has_details() == false)

proto3中可以使用map类型,map类型可以定义成如下格式

map<key_type, value_type> map_field = N;

 key_type 可以是任何整型或者字符串类型(可以是除了浮点指针类型和 bytes类型之外的任何标量 类型 , value_type 可以是任何类型。

map类型的添加方法很特殊,添加数据最简单的方法是使用标准map语法:

::google::protobuf::Map< ::std::string, ::std::string >* map = test.mutable_values();
cout << "size: " << f.values_size() << endl;   // size = 0
// add to map
(*map)["a"] = "a1";
(*map)["c"] = "a1";
(*map)["a"] = "a1";
cout << "size: " << f.values_size() << endl;   // size = 3

也可以使用如下方法:

auto& map = *test.mutable_values();
map["a"] = "a1";

更多的消息类型详见:Language Guide (proto3) ,使用方法请参看:C++ Generated Code

由于众所周知的原因,访问上面提到的网站需要借助工具。

标签: C++, protobuf, Protocol Buffer

仅有一条评论

  1. 梁金华

    写的非常好

添加新评论