c++ 的Protobuf学习使用

简介

protocol buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议数据存储等。通信时所传递的信息是通过Protobuf定义的message数据结构进行打包,然后编译成二进制的码流再进行传输或者存储。

Protocol buffers 是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。

Protobuf 使用的时候必须写一个 IDL(Interface description language)文件,在里面定义好数据结构,只有预先定义了的数据结构,才能被序列化和反序列化。其中,序列化是将对象转换二进制数据,反序列化是将二进制数据转换成对象。

教程


本教程提供了一个基本的C++程序员介绍使用协议缓冲区。通过创建一个简单的示例应用程序,它向您展示了如何

  • 在.proto文件中定义消息格式。
  • 使用协议缓冲区编译器。
  • 使用C++协议缓冲区API写入和读取消息。

这不是一个在C++中使用协议缓冲区的全面指南。有关更多详细的参考信息,请参见Protocol Buffer Language Guide(proto2)、Protocol Buffer Language Guide(proto3)、C++ API Reference、C++ Generated Code Guide和Encoding Reference。


我们将要使用的示例是一个非常简单的“地址簿”应用程序,它 可以在文件中读取和写入人们的联系方式。每个人在 地址簿具有姓名、ID、电子邮件地址和联系电话 号码。

如何序列化和检索像这样的结构化数据?有几个 解决这个问题的方法:

  • 原始存储器中数据结构可以以二进制形式发送/保存。这是一种脆弱的方法,因为接收/阅读代码必须 使用完全相同的内存布局、字节序等编译。此外,作为 文件以原始格式积累数据,而软件的副本 这种格式的有线传播,很难扩展 格式。
  • 您可以发明一种特别的方法来将数据项编码为单个 字符串-例如将4个int编码为“12:3:-23:67”。这是一个简单的和 灵活的方法,虽然它确实需要编写一次性编码和 解析代码,并且解析施加小的运行时成本。这样最好 用于编码非常简单的数据。
  • 将数据序列化为XML。这种方法非常有吸引力,因为XML是 (sort的)人类可读,并且有许多 语言。如果你想与其他人共享数据,这是一个很好的选择 应用程序/项目。然而,XML是众所周知的空间密集型,并且 编码/解码它可能对应用施加巨大的性能损失。 此外,导航XMLDOM树比 在类中导航简单字段通常是这样的。

您可以使用Protobuf来代替这些选项。协议缓冲区是 灵活、高效、自动化的解决方案,以准确地解决这一问题。与 Protobuf,可以编写数据结构的.proto描述 希望储存。Protobuf编译器由此创建一个类,该类 实现Protobuf数据的自动编码和解析 高效二进制格式生成的类为 组成协议缓冲区的字段,并负责 作为一个单元阅读协议缓冲器。重要的是,协议 缓冲区格式支持以这种方式随时间扩展格式的想法 代码仍然可以读取用旧格式编码的数据。

示例代码包含在源代码包中的 “examples”目录。

Defining Your Protocol Format

要创建地址簿应用程序,您需要从.proto开始 文件。.proto文件中的定义很简单:您为以下对象添加消息 要序列化的每个数据结构,然后为其指定名称和类型 消息中的每个字段。下面是定义消息的.proto文件, addressbook.proto.

syntax = "proto2";

package tutorial;

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

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

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

  repeated PhoneNumber phones = 4;
}

message AddressBook {
  repeated Person people = 1;
}


正如你所看到的,它的语法类似于C++或Java。让我们把每一部分都看一遍 看看它做了什么。


.proto文件以包声明开始,这有助于防止不同项目之间的命名冲突。在C++中,生成的类将放置在与包名称匹配的命名空间中。

接下来,您有了消息定义。消息包含一组类型化字段。有许多标准的简单数据类型可用作为字段类型,包括boolint32floatdoublestring。你呢 还可以通过使用其他消息类型为消息添加进一步的结构 字段类型-在上面的示例中,Person消息包含PhoneNumber AddressBook消息包含Person消息。你可以的 甚至定义嵌套在其他消息中的消息类型-正如您所看到的, PhoneNumber类型定义在Person内。您还可以定义enum类型 如果您希望您的一个字段具有预定义值列表中的一个, 在这里,您希望指定电话号码可以是下列电话之一 类型:MOBILEHOMEWORK

每个元素上的“= 1”、“= 2”标记标识 字段在二进制编码中使用。字段号1-15需要少一个字节 编码比更高的数字,所以作为一个优化,你可以决定使用这些 通常使用的或重复的元件的编号,留下字段编号16和18。 对于不太常用的可选元素,则更高。中的每个元素 字段需要重新编码字段编号,因此重复的字段尤其 这是优化的好方法。

每个字段必须使用以下修饰符之一进行注释:

  • optional字段可以设置,也可以不设置。如果是可选字段值 则使用默认值。对于简单类型,可以指定 自己的默认值,就像我们在示例中对电话号码type所做的那样。 否则,将使用系统默认值:数值类型为零,为空 字符串为字符串,布尔为假。对于嵌入的消息,默认的 值始终是消息的“默认实例”或“原型”,它 没有设置任何字段。调用访问器以获取 未始终显式设置的可选(或必需)字段 返回该字段的默认值。
  • repeated字段可以重复任何次数(包括零次)。 重复值的顺序将保留在协议缓冲区中。 将重复字段视为动态大小的数组
  • required必须提供字段的值,否则消息 将被视为“未初始化”。如果libprotobuf在debug中编译 模式下,序列化未初始化的消息将导致断言失败。 在优化的生成中,将跳过检查并写入消息 不管怎样。但是,解析未初始化的消息总是会失败(通过 从parse方法返回false)。除此之外,必填字段 行为与可选字段完全相同。

Important

Required Is Forever 您应该非常小心地将字段标记为 required. 如果在某个时候 如果您希望停止编写或发送必填字段,则 将字段更改为可选字段-老读者将考虑消息 而该字段是不完整的,并且可能无意中拒绝或丢弃它们。 您应该考虑为 你的缓冲器。在Google内部 required fields are strongly disfavored; most messages defined in proto2 syntax use 领域受到强烈反对; proto 2语法中定义的大多数消息使用 optional and 和 repeated only. (Proto3 does not support 只有 (Proto3不支持 required fields at all.) 所有的领域)。

您将找到编写.proto文件的完整指南-包括所有 可能的字段类型-在 协议缓冲区语言指南。 不要去寻找类似于类继承的工具- protocol 缓冲器不会这样做。

Compiling Your Protocol Buffers编译协议缓冲区

现在您已经有了.proto,接下来您需要做的就是生成 你需要读和写的类AddressBook(因此Person和 PhoneNumber)消息。为此,您需要运行协议缓冲区 编译器protoc上的.proto

  1. 如果你还没有安装编译器, 下载软件包并按照 在README中的说明。

  2. 现在运行编译器,指定源目录(您的 应用程序的源代码仍然存在-如果您 不提供值),目标目录(您希望 生成代码去;通常与$SRC_DIR相同),和路径你的 .proto.在这种情况下,您……:

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

因为需要C++类,所以使用--cpp_out选项-类似 为其他支持的语言提供了选项。

这将在指定的目标目录中生成以下文件:

    • addressbook.pb.h, the header which declares your generated classes.
    • addressbook.pb.cc, which contains the implementation of your classes.

The Protocol Buffer API协议缓冲区API

让我们看一下生成的代码,看看编译器为您创建了哪些类和函数。如果查看addressbook.pb.h,可以看到在addressbook.proto中指定的每个消息都有一个类。仔细观察Person类,可以看到编译器为每个字段生成了访问器。例如,对于name、id、email和phones字段,可以使用以下方法:

// name
  inline bool has_name() const;
  inline void clear_name();
  inline const ::std::string& name() const;
  inline void set_name(const ::std::string& value);
  inline void set_name(const char* value);
  inline ::std::string* mutable_name();

  // id
  inline bool has_id() const;
  inline void clear_id();
  inline int32_t id() const;
  inline void set_id(int32_t value);

  // email
  inline bool has_email() const;
  inline void clear_email();
  inline const ::std::string& email() const;
  inline void set_email(const ::std::string& value);
  inline void set_email(const char* value);
  inline ::std::string* mutable_email();

  // phones
  inline int phones_size() const;
  inline void clear_phones();
  inline const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& phones() const;
  inline ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >* mutable_phones();
  inline const ::tutorial::Person_PhoneNumber& phones(int index) const;
  inline ::tutorial::Person_PhoneNumber* mutable_phones(int index);
  inline ::tutorial::Person_PhoneNumber* add_phones();

正如您所看到的,getter的名称与小写的字段完全相同,而 setter方法以set_开始。也有has_方法 单数(必需或可选)字段,如果该字段已 准备好了。最后,每个字段都有一个clear_方法,用于将字段恢复为 空的状态。

虽然数字id字段仅具有上述基本访问器集, nameemail字段有两个额外的方法,因为它们 strings -一个mutable_ getter,让你直接得到一个指向字符串的指针, 还有一只二传手请注意,即使mutable_email()为 尚未设置;它将自动初始化为空字符串。如果你 如果在本例中有一个重复的消息字段,它也会有一个email 方法,而不是mutable_方法。

重复字段也有一些特殊的方法-如果您查看 重复的phones字段,您将看到

  • 检查重复字段的_size(换句话说,有多少个电话号码 Person2#2)。
  • 使用索引获取指定的电话号码。
  • 更新指定索引处的现有电话号码。
  • 添加另一个电话号码到消息,然后您可以编辑(重复 标量类型有一个add_,它只是让你传入新值)。

有关协议编译器为哪些成员生成的详细信息 任何特定字段定义,请参见 C++生成代码参考.

枚举和嵌套类

生成的代码包括一个与您的PhoneType对应的.proto枚举 枚举。您可以将此类型称为Person::PhoneType,其值为 Person::MOBILEPerson::HOMEPerson::WORK(实现细节 稍微复杂一点,但您不需要了解它们就可以使用 enum)。

编译器还为您生成了一个名为 Person::PhoneNumber.如果你看一下代码,你可以看到“真实的的” 类实际上被称为Person_PhoneNumber,但内部定义了一个typedef Person允许您将其视为嵌套类。唯一的案子 这会产生不同的地方是,如果你想在 另一个文件-你不能在C++中正向声明嵌套类型,但你可以 forward-declare Person_PhoneNumber

标准消息方法

每个消息类还包含许多其他方法,这些方法允许您检查或 操纵整个消息,包括:

  • bool IsInitialized() const;:检查是否已输入所有必填字段 准备好了。
  • string DebugString() const;:返回一个人类可读的 消息,对调试特别有用。
  • void CopyFrom(const Person& from);:用给定的 消息的价值。
  • void Clear();:将所有元素清零为空状态。

这些方法和下一节中描述的I/O方法实现了 Message所有C++协议缓冲区类共享的接口。有关更多信息, 见 Message的完整API文档。

解析和序列化

最后,每个协议缓冲区类都有写入和阅读消息的方法 所选类型的 二进制格式这些措施 包括:

  • bool SerializeToString(string* output) const;:序列化消息并 存储给定字符串中的字节。注意,字节是二进制的,而不是 text;我们只使用string类作为方便的容器。
  • bool ParseFromString(const string& data);:从给定的 字符串
  • bool SerializeToOstream(ostream* output) const;:将消息写入 C++ #2
  • bool ParseFromIstream(istream* input);:从给定的 C++ istream

这些只是为解析和序列化提供的几个选项。 再一次,请参阅 Message API参考 一份完整的名单。

写消息

现在让我们尝试使用协议缓冲区类。你首先要做的 地址簿应用程序能做的就是把个人详细信息写到你的 地址簿文件。为此,您需要创建并填充 协议缓冲器类,然后将它们写入输出流。

下面是一个程序,它从文件中读取AddressBook,添加一个新的 Person基于用户输入将新的AddressBook写回到它,并将新的undefined写回 文件再次。直接调用或引用由 协议编译器突出显示。

#include 
#include 
#include 
#include "addressbook.pb.h"
using namespace std;

// This function fills in a Person message based on user input.
void PromptForAddress(tutorial::Person* person) {
  cout << "Enter person ID number: ";
  int id;
  cin >> id;
  person->set_id(id);
  cin.ignore(256, '\n');

  cout << "Enter name: ";
  getline(cin, *person->mutable_name());

  cout << "Enter email address (blank for none): ";
  string email;
  getline(cin, email);
  if (!email.empty()) {
    person->set_email(email);
  }

  while (true) {
    cout << "Enter a phone number (or leave blank to finish): ";
    string number;
    getline(cin, number);
    if (number.empty()) {
      break;
    }

    tutorial::Person::PhoneNumber* phone_number = person->add_phones();
    phone_number->set_number(number);

    cout << "Is this a mobile, home, or work phone? ";
    string type;
    getline(cin, type);
    if (type == "mobile") {
      phone_number->set_type(tutorial::Person::MOBILE);
    } else if (type == "home") {
      phone_number->set_type(tutorial::Person::HOME);
    } else if (type == "work") {
      phone_number->set_type(tutorial::Person::WORK);
    } else {
      cout << "Unknown phone type.  Using default." << endl;
    }
  }
}

// Main function:  Reads the entire address book from a file,
//   adds one person based on user input, then writes it back out to the same
//   file.
int main(int argc, char* argv[]) {
  // 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;

  if (argc != 2) {
    cerr << "Usage:  " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
    return -1;
  }

  tutorial::AddressBook address_book;

  {
    // Read the existing address book.
    fstream input(argv[1], ios::in | ios::binary);
    if (!input) {
      cout << argv[1] << ": File not found.  Creating a new file." << endl;
    } else if (!address_book.ParseFromIstream(&input)) {
      cerr << "Failed to parse address book." << endl;
      return -1;
    }
  }

  // Add an address.
  PromptForAddress(address_book.add_people());

  {
    // Write the new address book back to disk.
    fstream output(argv[1], ios::out | ios::trunc | ios::binary);
    if (!address_book.SerializeToOstream(&output)) {
      cerr << "Failed to write address book." << endl;
      return -1;
    }
  }

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

  return 0;
}

注意GOOGLE_PROTOBUF_VERIFY_VERSION宏。这是一个很好的实践-虽然 不是绝对必要的-在使用C++协议之前执行此宏 缓冲库。它验证您没有意外地链接到 版本的库,这是不兼容的版本的标题你 编译。如果检测到版本不匹配,程序将中止。注意事项 每个.pb.cc文件在启动时自动调用此宏。

还请注意在程序结束时对ShutdownProtobufLibrary()的调用。 所有这一切都是删除任何全局对象分配的协议 缓冲库。这对于大多数程序来说是不必要的,因为这个过程是 无论如何都要退出,操作系统将负责回收其所有内存。 但是,如果使用的内存泄漏检查器要求 或者如果你正在写一个可以被加载和卸载的库 多次,则您可能需要强制Protocol Buffers 清理一切

定义消息类型

首先让我们看一个非常简单的例子。假设您要定义一个 请求消息格式,其中每个搜索请求都有一个查询字符串, 您感兴趣的结果的特定页面,以及每个页面的结果数量。下面是您用来定义消息类型的.proto文件。

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
}
  • 文件的第一行指定您使用的是proto3语法:如果 如果您不这样做,协议缓冲区编译器将假定您使用的是 原型2.这一定是 文件的第一个非空、非注释行。
  • SearchRequest息定义指定了三个字段(名称/值 对),一个用于要包含在此类型 留言每个字段都有一个名称和类型。

指定字段类型

在前面的示例中,所有字段都是标量类型:两个整数 (page_numberresults_per_page)和字符串(query)。也可以指定枚举和复合类型,如其他消息类型的领域。

你可能感兴趣的:(C,学习)