[Protobuf]: C++ Reference

[Protobuf]: C++ 句法

通过创建一个简单的示例应用程序,它向您展示了如何在 .proto文件中定义消息格式,使用protobuf编译器。
使用 C++ 协议缓冲区 API 来写入和读取消息。

这不是在 C++ 中使用协议缓冲区的综合指南。 有关更详细的参考信息,请参阅协议缓冲区语言指南 (proto2)、协议缓冲区语言指南 (proto3)、C++ API 参考、C++ 生成代码指南和编码参考。

1. 为什么要使用协议缓冲区?

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

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

  • 原始内存数据结构可以以二进制形式发送/保存。随着时间的推移,这是一种脆弱的方法,因为必须使用完全相同的内存布局、字节序等来编译接收/读取代码。此外,由于文件以原始格式积累数据,并且为该格式连接的软件副本是四处传播,很难扩展格式。
  • 您可以发明一种特殊方式将数据项编码为单个字符串——例如将 4 个整数编码为“12:3:-23:67”。这是一种简单而灵活的方法,尽管它确实需要编写一次性编码和解析代码,并且解析会产生很小的运行时成本。这最适合编码非常简单的数据。
  • 将数据序列化为 XML。这种方法非常有吸引力,因为 XML 是(某种程度上)人类可读的,并且有许多语言的绑定库。如果您想与其他应用程序/项目共享数据,这可能是一个不错的选择。然而,众所周知,XML 是空间密集型的,编码/解码它会给应用程序带来巨大的性能损失。此外,导航 XML DOM 树比通常导航类中的简单字段要复杂得多。

​ Protocol Buffers 是灵活、高效、自动化的解决方案,可以准确地解决这个问题。使用protobuf,可以编写要存储的数据结构的 .proto 描述。由此,protocol buffer编译器创建了一个类,该类以高效的二进制格式实现protobuf数据的自动编码和解析。生成的类为构成protocol buffer的字段提供了 getter 和 setter,并将读写协议缓冲区的细节作为一个单元处理。重要的是,协议缓冲区格式支持随着时间的推移扩展格式的想法,这样代码仍然可以读取使用旧格式编码的数据。

2. Defining Your Protocol Format

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

syntax = "proto2";

package tutorial; // just like namespace

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 文件以**package包**声明开头,这有助于防止不同项目之间的命名冲突。在 C++ 中,您生成的类将放置在与包名称匹配的命名空间中(类似于namespace)。

  • 接下来,**消息message**定义。消息只是包含一组类型字段的聚合。许多标准的简单数据类型都可用作字段类型,包括boolint32floatdoublestring。您还可以通过使用其他消息类型作为字段类型来为您的消息添加进一步的结构——在上面的例子中,Person 消息包含 PhoneNumber 消息,而 AddressBook 消息包含 Person 消息。您甚至可以定义嵌套在其他消息中的消息类型——如您所见,PhoneNumber 类型是在 Person 中定义的。如果您希望您的字段之一具有预定义的值列表之一,您还可以定义枚举类型enum - 在这里您希望指定电话号码可以是以下电话类型之一:MOBILE、HOME 或 WORK。

每个元素上的“= 1”、“= 2”标记标识字段在二进制编码中使用的唯一“标签”。标记数字 1-15 需要比更高数字少一个字节来编码,因此作为一种优化,您可以决定将这些标记用于常用或重复的元素,而将标记 16 和更高的标记用于不太常用的可选元素。重复字段中的每个元素都需要重新编码标签编号,因此重复字段特别适合这种优化。

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

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

关于required字段的使用,在第一篇文章中介绍了使用细节,在proto3中这已经不是必须的了。

3. 编译Protocol buffer

现在有了 .proto,接下来需要做的是生成读取和写入 AddressBook(以及 Person 和 PhoneNumber)消息所需的类。 为此,需要在 .proto 上运行protocol buffer编译器: protoc:

现在运行编译器,指定源目录(您的应用程序的源代码所在的位置——如果您不提供值,则使用当前目录)、目标目录(您希望生成的代码所在的位置;通常与 $ 相同) 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.

4. The Protocol Buffer API

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

  // 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_方法,如果该字段已设置,则返回true。最后,每个字段都有一个clear_方法,可以将字段取消设置回其空状态。

虽然数字 id 字段只有上面描述的基本访问器集,但 name 和 email 字段有几个额外的方法,因为它们是字符串——一个 mutable_getter 可以让你直接获得指向字符串的指针,以及一个额外的 setter。请注意,即使尚未设置电子邮件,您也可以调用 mutable_email();它将自动初始化为空字符串。如果在这个例子中你有一个单一的消息字段,它也会有一个 mutable_法而不是一个set_方法。

重复字段也有一些特殊的方法——如果你查看重复电话字段的方法,你会发现你可以

检查重复字段的_size(换句话说,有多少电话号码与此人相关联)。
使用其索引获取指定的电话号码。
更新指定索引处的现有电话号码。
向消息中添加另一个电话号码,然后您可以对其进行编辑(重复的标量类型有一个 add_,它只允许您传入新值)。
有关协议编译器为任何特定字段定义生成哪些成员的详细信息,请参阅 C++ 生成的代码参考。

Enums and Nested Classes

生成的代码包括对应于您的 .proto 枚举的 PhoneType 枚举。 您可以将此类型称为 Person::PhoneType,将其值称为 Person::MOBILE、Person::HOME 和 Person::WORK(实现细节稍微复杂一些,但您无需了解它们即可 使用枚举)。

编译器还为您生成了一个名为 Person::PhoneNumber 的嵌套类。 如果您查看代码,您会发现“真正的”类实际上称为 Person_PhoneNumber,但是在 Person 内部定义的 typedef 允许您将其视为嵌套类。 唯一不同的情况是,如果您想在另一个文件中前向声明类——您不能在 C++ 中前向声明嵌套类型,但您可以前向声明 Person_PhoneNumber。

Standard Message Methods

每个消息类还包含许多其他方法,可让您检查或操作整个消息,包括:

  • bool IsInitialized() const;: checks if all the required fields have been set.
  • string DebugString() const;: returns a human-readable representation of the message, particularly useful for debugging.
  • void CopyFrom(const Person& from);: overwrites the message with the given message’s values.
  • void Clear();: clears all the elements back to the empty state.

Parsing and Serialization

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

  • bool SerializeToString(string* output) const;: 序列化消息并将字节存储在给定的字符串中。 请注意,字节是二进制的,而不是文本; 我们只使用 string 类作为一个方便的容器。
  • bool ParseFromString(const string& data);: parses a message from the given string.
  • bool SerializeToOstream(ostream* output) const;: writes the message to the given C++ ostream.
  • bool ParseFromIstream(istream* input);: parses a message from the given C++ istream.

这些只是为解析和序列化提供的几个选项。 同样,请参阅消息 API 参考以获取完整列表。

5. Writing A Message

现在让我们尝试使用protocol buffer类。 您希望地址簿应用程序能够做的第一件事是将个人详细信息写入地址簿文件。 为此,您需要创建并填充协议缓冲区类的实例,然后将它们写入输出流。

这是一个程序,它从文件中读取地址簿,根据用户输入向其中添加一个新人员,然后再次将新地址簿写回文件。 直接调用或引用协议编译器生成的代码的部分突出显示。

#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h" // protoc 编译后的头文件
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() 的调用。所有这些都是删除协议缓冲区库分配的任何全局对象。这对于大多数程序来说是不必要的,因为进程无论如何都会退出,操作系统将负责回收其所有内存。但是,如果您使用内存泄漏检查器要求释放每个最后一个对象,或者如果您正在编写一个可能由单个进程加载和卸载多次的库,那么您可能需要强制协议缓冲区清理所有内容.

6. Reading A Message

当然,如果你不能从中获取任何信息,地址簿也没有多大用处! 本示例读取上述示例创建的文件并打印其中的所有信息。

#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;

// Iterates though all people in the AddressBook and prints info about them.
void ListPeople(const tutorial::AddressBook& address_book) {
  for (int i = 0; i < address_book.people_size(); i++) {
    const tutorial::Person& person = address_book.people(i);

    cout << "Person ID: " << person.id() << endl;
    cout << "  Name: " << person.name() << endl;
    if (person.has_email()) {
      cout << "  E-mail address: " << person.email() << endl;
    }

    for (int j = 0; j < person.phones_size(); j++) {
      const tutorial::Person::PhoneNumber& phone_number = person.phones(j);

      switch (phone_number.type()) {
        case tutorial::Person::MOBILE:
          cout << "  Mobile phone #: ";
          break;
        case tutorial::Person::HOME:
          cout << "  Home phone #: ";
          break;
        case tutorial::Person::WORK:
          cout << "  Work phone #: ";
          break;
      }
      cout << phone_number.number() << endl;
    }
  }
}

// Main function:  Reads the entire address book from a file and prints all
//   the information inside.
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 (!address_book.ParseFromIstream(&input)) {
      cerr << "Failed to parse address book." << endl;
      return -1;
    }
  }

  ListPeople(address_book);

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

  return 0;
}

7. Extending a Protocol Buffer

在您发布使用协议缓冲区的代码后,迟早您会想要“改进”协议缓冲区的定义。如果你想让你的新缓冲区向后兼容,而你的旧缓冲区向前兼容——而且你几乎肯定想要这样——那么你需要遵循一些规则。在新版本的协议缓冲区中:

  • 不得更改任何现有字段的标签编号。
  • 不得添加或删除任何必填字段。
  • 可以删除可选或重复的字段。
  • 可以添加新的可选字段或重复字段,但您必须使用新的标签号(即从未在此协议缓冲区中使用过的标签号,甚至没有被删除的字段使用)。
    (这些规则有一些例外,但很少使用。)

如果您遵循这些规则,旧代码将很高兴地读取新消息并忽略任何新字段。对于旧代码,被删除的可选字段将仅具有其默认值,而被删除的重复字段将为空。新代码还将透明地读取旧消息。但是,请记住,旧消息中不会出现新的可选字段,因此您需要明确检查它们是否使用 has_ 进行设置,或者在您的 .proto 文件中使用 [default = value] 提供合理的默认值在标签号之后。如果未为可选元素指定默认值,则使用特定于类型的默认值:对于字符串,默认值为空字符串。对于布尔值,默认值为 false。对于数字类型,默认值为零。另请注意,如果您添加了一个新的重复字段,您的新代码将无法判断它是留空(通过新代码)还是从不设置(通过旧代码),因为它没有 has_ 标志。

8. Optimization Tips

C++ Protocol Buffers 库经过了极大的优化。 但是,正确使用可以进一步提高性能。 以下是一些技巧,可帮助您最大限度地提高库中的速度:

尽可能重用消息对象。 消息试图保留它们分配用于重用的任何内存,即使它们被清除。 因此,如果您连续处理许多具有相同类型和相似结构的消息,那么每次重用相同的消息对象以减轻内存分配器的负载是一个好主意。 但是,随着时间的推移,对象可能会变得臃肿,特别是如果您的消息“形状”不同,或者您偶尔构建的消息比平时大得多。 您应该通过调用 SpaceUsed 方法监视消息对象的大小,并在它们变得太大时将其删除。
您系统的内存分配器可能没有针对从多个线程分配大量小对象进行优化。 尝试改用 Google 的 tcmalloc。

9. Advanced Usage

协议缓冲区的用途不仅仅是简单的访问器和序列化。 请务必浏览 C++ API 参考以了解您还可以使用它们做什么。

协议消息类提供的一项关键特性是反射。 您可以遍历消息的字段并操作它们的值,而无需针对任何特定的消息类型编写代码。 使用反射的一种非常有用的方法是将协议消息与其他编码(例如 XML 或 JSON)相互转换。 反射的更高级用途可能是找到相同类型的两个消息之间的差异,或者开发一种“协议消息的正则表达式”,您可以在其中编写与某些消息内容匹配的表达式。 如果您发挥自己的想象力,就有可能将 Protocol Buffers 应用于比您最初预期的更广泛的问题!

反射由 Message::Reflection 接口提供。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Protobuf(C++) 3.1.0.0是Google开发的一种数据序列化协议,它使用.proto文件定义消息类型,并通过protobuf编译器将proto文件编译成C++代码。每个消息类型都对应一个C++类,这个类继承自google::protobuf::Message。 Protobuf的用途不仅限于简单的访问和序列化,它的应用范围非常广泛,可以根据自己的想象力将Protobuf应用到各种问题中。 如果想要在C++项目中使用Protobuf,可以使用CMake来管理项目。CMake是一个跨平台的构建工具,可以帮助我们快速和方便地构建和管理项目。在使用CMake时,可以通过protobuf来管理C++项目的依赖库。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [ProtoBuf C++入门解析一](https://blog.csdn.net/qq135595696/article/details/125825476)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [cmake-protobuf-example:使用 CMake 通过 protobuf 管理 C++ 项目的最小示例](https://download.csdn.net/download/weixin_42112894/20229488)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值