秀杰空间

秀杰笔记
做些有意义的事情
  1. 首页
  2. Linux/Unix C/C++
  3. 正文

C++使用protobuf快速入门简明教程

2023年2月14日 10096点热度 6人点赞 0条评论

protobuf介绍

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

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

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

支持开发语言C++、Java、Python、Objective-C、C#、Ruby、Go、PHP、Dart、Javascript。本文主要使用C++介绍protobuf使用。

官方网站: https://github.com/protocolbuffers/protobuf

官方文档: https://protobuf.dev/

protobuf使用过程

  1. 创建 .proto 文件(IDL文件),定义需要处理的结构化数据。
  2. protoc 工具,将 .proto 文件转换为 C++、Golang、Java、Python 等多种语言的代码API
  3. 调用API实现序列化、反序列化以及读写

通讯录地址簿示例教程

第一步, 在 addressbook.proto 文件中定义数据结构

syntax = "proto2";
// xiujie.cn

// 包名(命名空间)
package tutorial;

// Person类定义
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;
}

逐行解读 addressbook.proto

  • syntax, protobuf 目前有两个版本 proto2 和 proto3。如果不设置 syntax 即默认为 proto2。 syntax="proto3" 必须位于 .proto 文中除去注释和空行的第一行。
  • package, 即包名声明符是可选的,用来防止不同的消息类型有命名冲突。C++中的命名空间。
  • message, 消息类型关键字,类似于C++/Java中的class关键字。使用工具生成代码,每个message都会生成一个名字与之对应的C++类。Person类名称, name,id,email,phones是该类名称的4个字段,类型分包是string,int32,string,class。
  • 每个字段的最前面是修饰符。 proto2版本有三种修饰符.分别为required、 optional、repeated。
  • required: 字段属性为必填字段。若不设置,则会导致编解码异常,导致消息被丢弃。
  • optional: 字段属性为可选字段。发送方可以选择性根据需要进行设置;对于optional属性的字段,可以通过default关键字为字段设置默认值,即当发送方没有对该字段进行设置的时候,将使用默认值。如果没有对字段设置默认值,就会根据特定的类型给字段赋予特定的默认值。对于bool类型,默认值为false;对于string类型,默认值为空字符串;对于数值类型,默认值为0;对于枚举类型,默认值是枚举类型中的第一个值。
  • repeated: 字段属性为可重复字段,该字段可以包含[0,n]个元素,字段中的元素顺序被保留。
  • 字段名前是protobuf的数据类型包括string,int32,double等。protobuf数据类型与C++语言数据类型对应关系请参考
  • 字段名 = 后面的数字称为标识符,每个字段都需要提供一个唯一的标识符。标识符用来在消息的二进制格式中识别各个字段,一旦使用就不能够再改变,标识符的取值范围为 [1, 2^29 - 1] 。1-15的标识符比其他较高标识符在二进制编码中少一个字节。尽量使用1-15定义字段序号。
  • enum,枚举类型关键字。枚举常量的值必须在32位整数范围内,因为enum值是使用可编码方式存储的,对负数存储不够高效,因此不推荐在enum中使用负数。​枚举类型可以定义在message内,也可以定义在message外。
  • [default = HOME]在 optional属性的字段,可以通过default关键字为字段设置默认值。
  • 单行注释可以用//,多行注释使用/* */。

第二步, 使用 protoc 工具,将 addressbook.proto 文件转换为 C++ 接口代码

下载安装protoc工具

官方下载: https://github.com/protocolbuffers/protobuf/releases/latest

国内加速下载:
windows

  • https://ghproxy.com/https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-win32.zip
  • https://ghproxy.com/https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-win64.zip

mac

  • https://ghproxy.com/https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-osx-universal_binary.zip
  • https://ghproxy.com/https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-osx-x86_64.zip
  • https://ghproxy.com/https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-osx-aarch_64.zip
  • brew install protobuf

linux

  • https://ghproxy.com/https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-linux-x86_64.zip
  • https://ghproxy.com/https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-linux-x86_32.zip
  • https://ghproxy.com/https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-linux-aarch_64.zip

    编译addressbook.proto生成C++接口代码

    mkdir src
    protoc -I=./   --cpp_out=./src/   ./addressbook.proto

    代码目录结构如下:

    ├── addressbook.proto
    └── src
    ├── addressbook.pb.cc
    └── addressbook.pb.h

第三步,调用API实现序列化、反序列化以及读写

下载protobuf源码生成编译库文件

# // 官方下载: https://github.com/protocolbuffers/protobuf/releases/latest
wget https://ghproxy.com/https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protobuf-all-21.12.zip
unzip protobuf-all-21.12.zip
cd protobuf-all-21.12
./configure
make
sudo make install
sudo ldconfig # refresh shared library cache.

编写调用接口代码实现序列化、反序列化 main.cpp

// xiujie.cn
// c++ -std=c++0x addressbook.pb.cc main.cpp `pkg-config --cflags --libs protobuf`
#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;

// 标准输入填充Person
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;
    }
  }
}
// 遍历 通讯录地址簿列表 打印每一项信息
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;
    }
  }
}

// 主函数: 从存储文件中读取通讯录地址簿列表, 通过标准输入新增一条通讯录, 写入存储文件
int main(int argc, char* argv[]) {
  if (argc != 2) {
    cerr << "Usage:  " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
    return -1;
  }
  tutorial::AddressBook 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;
    }
  }
  // 遍历打印列表
  ListPeople(address_book);
  // 通过标准输入新增一条通讯录
  PromptForAddress(address_book.add_people());
  {
    // 写入存储文件
    fstream output(argv[1], ios::out | ios::trunc | ios::binary);
    if (!address_book.SerializeToOstream(&output)) { // 从对象序列化到文件流
      cerr << "Failed to write address book." << endl;
      return -1;
    }
  }
  // 可选的: 删除所有由 libprotobuf 分配的全局对象。
  google::protobuf::ShutdownProtobufLibrary();
  return 0;
}

编译

c++ -std=c++0x addressbook.pb.cc main.cpp `pkg-config --cflags --libs protobuf`

运行

# 第一次运行
./a.out abc.db
abc.db: File not found.  Creating a new file.
Enter person ID number: 1
Enter name: zhangsan
Enter email address (blank for none): zhangsan@163.com           
Enter a phone number (or leave blank to finish): 123456
Is this a mobile, home, or work phone? mobile
Enter a phone number (or leave blank to finish): 654321
Is this a mobile, home, or work phone? work
Enter a phone number (or leave blank to finish): 

# 第二次运行
./a.out abc.db
Person ID: 1
  Name: zhangsan
  E-mail address: zhangsan@163.com
  Mobile phone #: 123456
  Work phone #: 654321
Enter person ID number: 

代码解析 addressbook.pb.h main.cpp

在 addressbook.pb.h 代码中 Person类的 name,id,email,phones字段操作方法:

// optional string name = 1;
bool has_name() const;
void clear_name();
const std::string& name() const;
template <typename ArgT0 = const std::string&, typename... ArgT>
void set_name(ArgT0&& arg0, ArgT... args);
std::string* mutable_name();
PROTOBUF_NODISCARD std::string* release_name();
void set_allocated_name(std::string* name);

// optional int32 id = 2;
bool has_id() const;
void clear_id();
int32_t id() const;
void set_id(int32_t value);

// optional string email = 3;
bool has_email() const;
void clear_email();
const std::string& email() const;
template <typename ArgT0 = const std::string&, typename... ArgT>
void set_email(ArgT0&& arg0, ArgT... args);
std::string* mutable_email();
PROTOBUF_NODISCARD std::string* release_email();
void set_allocated_email(std::string* email);

// repeated .tutorial.Person.PhoneNumber phones = 4;
int phones_size() const;
void clear_phones();
::tutorial::Person_PhoneNumber* mutable_phones(int index);
::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::tutorial::Person_PhoneNumber >*
    mutable_phones();
const ::tutorial::Person_PhoneNumber& phones(int index) const;
::tutorial::Person_PhoneNumber* add_phones();
const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::tutorial::Person_PhoneNumber >&
    phones() const;

Person类的可选字段: 小写字段名()方法获取字段值方法, has_方法判断是否已设置值, set_方法设置值, clear_方法清理已设置值, mutable_方法返回对象字段指针.
Person类的重复字段: 小写字段名(索引序号)方法获取指定索引字段值, _size方法返回重复字段大小, add_方法返回新增字段对象指针, mutable_方法返回可用于更新指定索引字段指针.

Message类是Person类的基类.
Message类一些方法:

std::string DebugString() const;        // 调试打印类所有字段方法
std::string ShortDebugString() const;   // 调试打印类所有字段方法
std::string Utf8DebugString() const;    // 调试打印类所有字段方法
void PrintDebugString() const;          // 调试打印类所有字段方法

bool SerializeToString(string* output) const;// 序列化到二进制字符串
bool ParseFromString(const string& data);    // 反序列化二进制字符串
bool SerializeToOstream(ostream* output) const;// 序列化文件流
bool ParseFromIstream(istream* input);         // 反序列化文件流

注意事项

proto定义类型和C++数据类型对照关系

proto类型 编码说明 C++类型
double - double
float - float
int32 可变长度编码.负数编码效率低.包含负数建议使用sint32 int32
int64 可变长度编码.负数编码效率低.包含负数建议使用sint64 int64
uint32 可变长度编码. uint32
uint64 可变长度编码. uint64
sint32 可变长度编码. 有符号.负数编码效率高. int32
sint64 可变长度编码. 有符号.负数编码效率高. int64
fixed32 固定4字节. 数值大于228,比uint32有效率. uint32
fixed64 固定8字节.数值大于256,比uint64有效率. uint64
sfixed32 固定4字节 int32
sfixed64 固定8字节 int64
bool - bool
string 必须是utf-8编码 string
bytes 任何字节序列 string

proto定义类型缺省值

proto类型 缺省值
string 空字符串
bytes 空
bool false
numeric types 0
enums 一个定义枚举值

为了达到前后消息类型兼容的目的,扩展Message消息类型的时候需要注意一下几点:

  1. 不要更改任何已有的字段的数值标识。
  2. 所添加的字段属性必须是optional 或者repeated类型,如果扩展required类型,会导致旧的消息解析异常
  3. 非required字段可以移除。要保证它们的标示在新的消息类型中不再使用。不建议移除可添加注释表示已废弃。移除可导致老旧代码编译出错。
  4. 一个非required的字段可以转换为一个扩展,反之亦然——只要它的类型和标识号保持不变。
  5. int32, uint32, int64, uint64,和bool是全部兼容的,这意味着可以将这些类型中的一个转换为另外一个,而不会破坏向前、 向后的兼容性。如果解析出来的数字与对应的类型不相符,那么结果就像在C++中对它进行了强制类型转换一样(例如,如果把一个64位数字当作int32来 读取,那么它就会被截断为32位的数字)。
  6. sint32和sint64是互相兼容的,但是它们与其他整数类型不兼容。
  7. string和bytes是兼容的——只要bytes是有效的UTF-8编码。
  8. 嵌套消息与bytes是兼容的——只要bytes包含该消息的一个编码过的版本。
  9. fixed32与sfixed32是兼容的,fixed64与sfixed64是兼容的。
  10. 可以定义枚举类型,尽量不要使用枚举类型定义字段. 新增的枚举值,在老旧编译程序中不识别. 建议使用int32定义枚举对象,注释添加枚举类型.
标签: pb protobuf
最后更新:2023年2月16日

秀杰

做些有意义的事情

点赞
< 上一篇

文章评论

您需要 登录 之后才可以评论

COPYRIGHT © 2023 个人笔记. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang

京ICP备11019155号-2