ProtoBuf(Google Protocol Buffers)—— C++开发具体教程

2023-11-06

C++开发)教程

操作流程

  • 1、如何在一个 .proto 文件中定义 message
  • 2、如何使用 protocol buffer 编译器,生成C++类
  • 3、如何使用 C++ protocol buffer 的 API 读写 message

我们将要使用的示例是一个非常简单的 “地址簿” 应用程序,可以在文件中读写联系人的详细信息。地址簿中的每个人都有姓名ID电子邮件地址联系电话

1、定义你的 protocol 格式

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

1.1、protocol 字段格式

消息由至少一个字段组合而成,类似于C语言中的结构体,每个字段都有一定的格式:

数据类型 字段名称 = 唯一的编号标签值;
  • 字段名称:protobuf建议以下划线命名而非驼峰式
  • 唯一的编号标签:代表每个字段的一个唯一的编号标签,这些数字以 message 二进制格式 标识你的字段,并且一旦你的 message 被使用,这些编号就无法再更改。需要说明的是标签在1到15范围的采用一个字节进行编码,
    • 通常将标签1到15用于频繁发生的消息字段。标签号 1-15 比起更大数字需要少一个字节进行编码,因此以此进行优化,你可以决定将这些标签用于常用或重复的元素,
    • 16 到 2047 范围内的字段编号占用两个字节。因此,你应该为非常频繁出现的 message 元素保留字段编号 1 到 15。
    • 此外不能使用protobuf系统预留的编号标签(19000~19999)。

1.2、指定字段规则

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

1.3、.proto文件

//文件名:addressbook.proto

syntax = "proto2";//proto版本
//.proto文件新增一个可选的package声明符,用来防止不同的消息类型有命名冲突。
//包的声明符会根据使用语言的不同影响生成的代码。对于C++,产生的类会被包装在C++的命名空间中。
package tutorial;package声明符

message Person {
  required string name = 1;//姓名,= 1 二进制编码中使用的唯一 “标记”
  required int32 id = 2;//ID
  optional string email = 3;//email

  enum PhoneType {//枚举消息类型
    MOBILE = 0;//proto3版本中,首成员必须为0,成员不应有相同的值
    HOME = 1;
    WORK = 2;
  }
//
  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phones = 4;//phones为数组
}

message AddressBook {
  repeated Person people = 1;
}

Person 包含 PhoneNumber message , AddressBook 包含 Person message。你甚至可以定义嵌套在其他 message 中的 message 类型 -​​ 如你所见,PhoneNumber 类型在 Person 中定义。
如果你希望其中一个字段具有预定义的值列表中的值,你还可以定义枚举类型 - 此处你指定(枚举)电话号码,它的值可以是 MOBILE,HOME 或 WORK 之一。

2、编译你的 Protocol Buffers

既然你已经有了一个 .proto 文件,那么你需要做的下一件事就是生成你需要读写AddressBook(以及 Person 和 PhoneNumber ) message 所需的类。
为此,你需要在 .proto 上运行 protocol buffer 编译器 protoc

protoc addressbook.proto --cpp_out=./

在这里插入图片描述

  • protoc:protobuf自带的编译工具,将.proto文件生成指定的类
  • –cpp_out:指定输出特定的语言和路径

通过protoc工具编译.proto文件时,编译器将生成所选择语言的代码,这些代码可以操作在.proto文件中定义的消息类型,包括获取、设置字段值,将消息序列化到一个输出流中,以及从一个输入流中解析消息。

对C++来说,编译器会为每个.proto文件生成一个.h文件和一个.cc文件,.proto文件中的每一个消息有一个对应的类。

3、The Protocol Buffer API(message设置)

编译器为你创建了哪些类和函数。如果你查看 addressbook.pb.h,你会发现你在 addressbook.proto 中指定的每条 message 都有一个对应的类。仔细观察 Person 类,你可以看到编译器已为每个字段生成了访问器。例如,对于 name ,id,emailphone 字段,你可以使用以下方法:

//setter 方法以 set_ 开头
 // required name
  inline bool has_name() const;//has_ 方法,如果设置了该字段,则返回 true
  inline void clear_name();//clear_ 方法,可以将字段重新设置回 empty 状态
  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();

  // required id
  inline bool has_id() const;//has_ 方法,如果设置了该字段,则返回 true
  inline void clear_id();//clear_ 方法,可以将字段重新设置回 empty 状态
  inline int32_t id() const;
  inline void set_id(int32_t value);

  // optional email
  inline bool has_email() const;//has_ 方法,如果设置了该字段,则返回 true
  inline void clear_email();//clear_ 方法,可以将字段重新设置回 empty 状态
  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();

  // repeated phones
  inline int phones_size() const;//检查 repeated 字段长度(换句话说,与此人关联的电话号码数)
  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();//在 message 中添加另一个电话号码同时之后也可进行再修改(repeated 的标量类型有一个 add_,而且只允许你传入新值)

4、标准 Message 方法

每个 message 类还包含许多其他方法,可用于检查或操作整个 message,包括:

  • bool IsInitialized() const;: 检查是否已设置所有必填 required 字段
  • string DebugString() const;: 返回 message 的人类可读表达,对调试特别有用
  • void CopyFrom(const Person& from);用给定的 message 的值覆盖 message
  • void Clear();: 将所有元素清除回 empty 状态

5、解析和序列化

每个 protocol buffer 类都有使用 protocol buffer 二进制格式 读写所选类型 message 的方法。包括:

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

6、写入一个 Message

地址簿应用程序能够做的第一件事可能是将个人详细信息写入的地址簿文件。为此,你需要创建并填充 Protocol Buffer 类的实例,然后将它们写入输出流。

这是一个从文件中读取 AddressBook 的程序,根据用户输入向其添加一个新 Person,并将新的 AddressBook 重新写回文件。其中直接调用或引用 protocol 编译器生成的代码部分将高亮显示。

“直接调用或引用 protocol 编译器生成的代码部分” 采用注释 @@@ 的方式标出

#include <iostream>
#include <fstream>
#include <string>
#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);//设置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);//设置email
  }

  while (true) {
    cout << "Enter a phone number (or leave blank to finish): ";
    string number;
    getline(cin, number);
    if (number.empty()) {
      break;
    }
    // @@@ Person::PhoneNumber
    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") {
      // @@@ Person
      phone_number->set_type(tutorial::Person::MOBILE);
    } else if (type == "home") {
      // @@@ Person
      phone_number->set_type(tutorial::Person::HOME);
    } else if (type == "work") {
      // @@@ Person
      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[]) {
  //验证你没有意外链接到与你编译的头文件不兼容的库版本。
  //如果检测到版本不匹配,程序将中止。请注意,每个 .pb.cc 文件在启动时都会自动调用此宏。
  GOOGLE_PROTOBUF_VERIFY_VERSION;

  if (argc != 2) {
    cerr << "Usage:  " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
    return -1;
  }
  // @@@ AddressBook
  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;
    // @@@ ParseFromIstream
    } else if (!address_book.ParseFromIstream(&input)) {//解析给定字符串到 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);
    // @@@ SerializeToOstream
    if (!address_book.SerializeToOstream(&output)) {//将 message 写入给定的 C++ 的 ostream
      cerr << "Failed to write address book." << endl;
      return -1;
    }
  }

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

  return 0;
}

另请注意在程序结束时调用 ShutdownProtobufLibrary()。所有这一切都是删除 Protocol Buffer 库分配的所有全局对象。对于大多数程序来说这是不必要的,因为该过程无论如何都要退出,并且操作系统将负责回收其所有内存。但是,如果你使用了内存泄漏检查程序,该程序需要释放每个最后对象,或者你正在编写可以由单个进程多次加载和卸载的库,那么你可能希望强制使用 Protocol Buffers 来清理所有内容。

7、读取一个 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++) {//proto设置为4
    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;
}

8、扩展一个 Protocol Buffer

在发布使用 protocol buffer 的代码之后,无疑早晚有一天你将会想要 “改进” protocol buffer 的定义。如果你希望你的新 buffer 向后兼容,并且你的旧 buffer 是向前兼容的(实际上你一定想要这种兼容性) - 那么你需要遵循一些规则。在新版本的 protocol buffer 中:

  • 你不得更改任何现有字段的字段编号
  • 你不得添加或删除任何 required 字段
  • 你可以删除 optional 或 repeated 的字段
  • 你可以添加新的 optional 或 repeated
    字段,但必须使用新的标记号(即从未在此协议缓冲区中使用的编号,甚至包括那些已删除的字段的编号)

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

参考

1、https://www.jianshu.com/p/d2bed3614259
2、https://blog.csdn.net/m0_46083365/article/details/103718080
3、https://blog.csdn.net/wobutianl/article/details/70241614
4、https://blog.csdn.net/tennysonsky/article/details/73810180

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

ProtoBuf(Google Protocol Buffers)—— C++开发具体教程 的相关文章

随机推荐

  • 【Python进阶学习】根据数据绘制省份热力地图(源码)

    输入 pro sales csv 省份与值 数据 province 省份列 deal 值列 输出 中国地图 html 优化 显示省份名称 使用notepad 打开中国地图 html 搜索 series 在1900多行这个 3 增加以下内容
  • mybatis utf8mb4 java_java.sql.SQLException: Unsupported character encoding 'utf8mb4'.

    四月 12 2017 3 47 52 下午 org apache catalina core StandardWrapperValve invoke 严重 Servlet service for servlet SpringMVC in c
  • 深度学习拾遗

    深度学习 hinton bp算法 李飞飞 吴恩达 黄广斌 路奇 深度学习优化的超参数 1 学习率 学习率 learning rate或作lr 是指在优化算法中更新网络权重的幅度大小 学习率可以是恒定的 逐渐降低的 基于动量的或者是自适应的
  • 面向高维优化问题的混沌粒子群混合蝴蝶优化算法(Matlab代码实现)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 2 运行结果 3 Matlab代码及详细文章讲解 4 参考文献 1 概述 文献来源 摘要 为了解决蝶形优化
  • 值类别 左值引用 右值引用

    文章目录 值类别 关系图 示例 左值引用声明符 语法 示例 反汇编 伪代码 右值引用声明符 语法 示例 反汇编 伪代码 相关参考 值类别 每个 C 表达式 运算符带上其操作数 字面量 变量名等 可按照两种独立的性质加以辨别 类型和值类别 v
  • 快手java开发面试经验大全

    1 自我介绍 2 java集合 hashmap详细介绍 关键参数 线程安全的集合 队列和栈 用两个栈实现队列算法 3 jvm结构 4 多线程锁 synchronized reentrantlock countdownlatch 锁升级 5
  • 回归(regression)和logistic regression

    回归 回归 就是 回归本质 的意思 用一个函数去拟合一组数据 xi yi x i y i 随着数据越来越多 用来拟合的这个曲线就越来越接近真实的情况 这里 xi x i可以是一个向量 假设 xi Rn x i in R n 若用线性回归的方
  • 如何创建 且在idea中操作vue3项目

    一 使用vue的控制台创建vue3项目 1 首先使用vue的控制台创建基础的vue框架 win r 唤出控制台 输入cmd 在控制台中输入 vue ui 弹出一个网页窗口 在弹出的窗口中点击下面这个地方 选择你要存放的地址 点击再次创建项目
  • Long 类型比较 判断相等问题引发的问题

    项目场景 最近上线以后遇到的一个问题 在这里记录一下 问题描述 测试的时候没问题 放到生产上就有问题了 原因分析 当时在测试上是没问题的 在生产上用同样的环境 同样的数据测试也是没问题的 把生产上数据同步到本地测试了一下 果然有问题 解决方
  • 物联网技术

    作者 阏男秀 链接 https www zhihu com question 50125636 answer 124938067 来源 知乎 著作权归作者所有 商业转载请联系作者获得授权 非商业转载请注明出处 物联网技术之四 无线自组网 无
  • springboot(一):构建最简单的springboot项目

    springboot应该是在spring体系基础上发展起来的 使用springboot 可以快速构建开发项目 并快速集成相关组件 很多开源的组件都有springboot的实现了 有人说它的设计理念为约定大于配置 就是好比说在springbo
  • 为什么Centos装上以后,执行apt-get命令提示没有该命令

    CentOS的软件安装工具不是apt get 是yum yum y install 你要安装的文件
  • java最大堆空间会自动扩增吗_最大化Java堆空间

    我试图在 Java中使用非常大的方阵 n 1e6或更大的数量级 矩阵不是稀疏的 所以我没有看到很多方法将它们表示为2D数组 这需要n 2 sizeof int 位的内存 显然 我正在获得堆溢出错误 即使添加编译器标志来使用我的机器将允许的大
  • 二十九.刷题.19

    输入两点坐标 X1 Y1 X2 Y2 0 lt x1 x2 y1 y2 lt 1000 计算并输出两点间的距离 include
  • centos7.3 32位 安装ssh实现远程登陆

    centos7 3 32位 安装ssh实现远程登陆 安装ssh sudo yum install openssh 关闭防火墙 永久禁用 sudo systemctl disable firewalld 重启ssh sudo systemct
  • 流动的数据——使用 RxJS 构造复杂单页应用的数据逻辑

    感谢作者徐飞的授权发布 作者 徐飞 网名民工精髓V 曾任Teambition前端架构师 苏宁云计算中心前端架构师 有十年以上大型企业应用前端架构及开发经验 熟悉AngularJS等框架 对Web组件化有一些思考 博客地址 https git
  • 微软server2012服务器端客户端,windows server 2012 datacenter配置iSCSI目标服务器和客户端配置...

    您好 1 在添加角色时 添加 文件和存储服务 文件和iSCSI服务 iSCSI目标存储提供程序 VDS和VSS硬件提供程序 这项主要的作用是什么 在配置iSCSI目标服务器时 有没有必要安装 这个应该 是 让 iscsi 卷 支持 VSS
  • gitee配置SSH公钥

    第一步 找个地方打开 git bash 然后输入生成ssh公钥的命令 ssh keygen t rsa C your email 然后敲四次回车生成公钥 第二步 输入查看公钥的命令 cat ssh id rsa pub 结果如下 第三步 将
  • 如何在微信小程序中优雅地发送异步请求

    一 微信小程序运行环境 微信小程序的 javascript 运行环境和浏览器不同 页面的脚本逻辑是在JsCore中运行 JsCore是一个没有窗口对象的环境 所以不能在脚本中使用window 也无法在脚本中操作组件 JsCore中也没有 X
  • ProtoBuf(Google Protocol Buffers)—— C++开发具体教程

    ProtoBuf C 开发 教程 C 开发 教程 1 定义你的 protocol 格式 1 1 protocol 字段格式 1 2 指定字段规则 1 3 proto文件 2 编译你的 Protocol Buffers 3 The Proto