当前位置: 首页 > article >正文

Protobuf 学习与实践

Protobuf 学习与实践

  • Protobuf 学习与实践
    • 一、安装 Protobuf
      • 1. 在 Ubuntu 上下载 Protobuf 编译器
        • 1.1 下载地址与版本选择
        • 1.2 安装依赖
        • 1.3 下载与解压
      • 2. 安装 Protobuf
        • 2.1 配置与编译
        • 2.2 增大 Swap 分区(若需要)
        • 2.3 配置环境变量(若自定义安装)
      • 3. 检查安装是否成功
    • 二、什么是 Protobuf?如何入手学习?
      • 1. 序列化概念
      • 2. 什么情况下需要序列化?
      • 3. 有哪些序列化工具?
      • 4. Protobuf 是什么?
        • 4.1 官方定义
        • 4.2 特点
      • 5. Protobuf 使用特点
      • 6. 如何入手学习 Protobuf?
    • 三、Protobuf 快速上手指南
      • 1. 目标
      • 2. 需求
      • 3. 实现步骤
        • 3.1 创建 .proto 文件
        • 3.2 编译 .proto 文件
        • 3.3 序列化与反序列化
        • 3.4 编译与运行
    • 四、Proto3 语法详解
      • 1. 字段规则
      • 2. 消息类型的定义与使用
        • 2.1 定义
        • 2.2 使用
        • 2.3 升级通讯录
      • 3. Enum 类型
        • 3.1 定义规则
        • 3.2 注意事项
        • 3.3 加入通讯录
      • 4. Any 类型
        • 4.1 定义
        • 4.2 加入通讯录
      • 5. Oneof 类型
        • 5.1 定义
        • 5.2 加入通讯录
      • 6. Map 类型
        • 6.1 定义
        • 6.2 加入通讯录
      • 7. 默认值
      • 8. 更新消息
        • 8.1 更新规则
        • 8.2 保留字段 (Reserved)
          • 8.3.1 验证错误删除字段造成的数据损坏
        • 8.3 未知字段
          • 8.3.1 未知字段从哪获取
            • MessageLite 类
            • Message 类
            • Descriptor 类
            • Reflection 类
            • UnknownFieldSet 类
            • UnknownField 类
            • 类之间的关系
            • 关系图示
            • 综合分析与总结
          • 8.3.2 验证未知字段
        • 8.4 前后兼容性
      • 9. 选项 option
        • 9.1 选项分类
        • 9.2 常⽤选项列举
        • 9.3 设置自定义选项
  • 五、通讯录网络版实现
      • 1. 环境搭建
      • 2. 约定双端约定
      • 3. 约定双端交互req/resp
  • 六、性能测试
      • 1. ProtoBuf、XML和JSON对比
        • 1.1 XML(eXtensible Markup Language)
        • 1.2 JSON(JavaScript Object Notation)
        • 1.3 Protobuf(Protocol Buffers)
        • 1.4 选择建议


Protobuf 学习与实践

本文是一篇的 Protocol Buffers (Protobuf) 学习博客,旨在帮助初学者从零开始掌握 Protobuf 的安装、使用、语法及项目实践。内容基于网上学习知识编写。


一、安装 Protobuf

1. 在 Ubuntu 上下载 Protobuf 编译器

1.1 下载地址与版本选择
  • 下载地址: protobuf下载地址
  • 推荐版本:v21.11,稳定且易于在 Ubuntu 上配置。
  • 建议环境:Ubuntu 系统,因其安装过程简单,社区支持丰富。
  • 语言支持:Protobuf 支持多种语言(如 Java、C++、Python 等),可根据需求选择特定语言版本或 all 版本(推荐 all,包含所有语言支持)。
    请添加图片描述
1.2 安装依赖

在下载 Protobuf 前,必须安装以下依赖库,否则编译会失败:

sudo apt-get install autoconf automake libtool curl make g++ unzip -y
  • 依赖说明
    • autoconfautomakelibtool:生成配置脚本。
    • curl:下载工具。
    • makeg++:编译工具。
    • unzip:解压工具。
    • -y:自动确认安装。
1.3 下载与解压
  1. 访问 GitHub releases 页面,找到 protobuf-all-21.11.zip,右键复制下载链接。
    • 示例链接:
      https://github.com/protocolbuffers/protobuf/releases/download/v21.11/protobuf-all-21.11.zip
      
  2. 下载:
    wget https://github.com/protocolbuffers/protobuf/releases/download/v21.11/protobuf-all-21.11.zip
    
  3. 解压:
    unzip protobuf-all-21.11.zip
    
    • 解压后生成目录 protobuf-21.11
  4. 进入目录:
    cd protobuf-21.11
    
    请添加图片描述

2. 安装 Protobuf

2.1 配置与编译

进入解压后的目录,执行以下步骤:

  1. 生成配置(若下载 all 版本)
    ./autogen.sh
    
    • 注意:若下载的是特定语言版本(如 protobuf-cpp-21.11),无需执行此步。
  2. 配置安装路径(任选其一):
    • 默认安装:安装到 /usr/local,库文件和二进制文件分散:
      ./configure
      
    • 自定义安装:统一安装到 /usr/local/protobuf
      ./configure --prefix=/usr/local/protobuf
      
  3. 编译与测试
    make         # 编译,约 15-30 分钟
    make check   # 测试,约 15-30 分钟
    sudo make install  # 安装
    
    • 编译时间:视机器性能而定。
    • 测试失败处理:若 make check 报错(如 protobuf-test 失败),需增大 swap 分区。
2.2 增大 Swap 分区(若需要)

make check 出错,建议将 swap 分区调整到 5G:

  1. 检查当前 swap:
    sudo swapon --show
    
  2. 创建 5G swap 文件:
    sudo fallocate -l 5G /swapfile
    
    • 若报错 fallocate failed: Text file busy,执行:
      sudo swapoff -a
      sudo fallocate -l 5G /swapfile
      
  3. 设置权限并激活:
    sudo chmod 600 /swapfile
    sudo mkswap /swapfile
    sudo swapon /swapfile
    
  4. 永久生效:
    sudo cp /etc/fstab /etc/fstab.bak
    echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
    
2.3 配置环境变量(若自定义安装)

若选择自定义路径(如 /usr/local/protobuf),需编辑 /etc/profile,添加:

# 动态库搜索路径
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/protobuf/lib/
# 静态库搜索路径
export LIBRARY_PATH=$LIBRARY_PATH:/usr/local/protobuf/lib/
# 可执行文件路径
export PATH=$PATH:/usr/local/protobuf/bin/
# C 头文件路径
export C_INCLUDE_PATH=$C_INCLUDE_PATH:/usr/local/protobuf/include/
# C++ 头文件路径
export CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:/usr/local/protobuf/include/
# pkg-config 路径
export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/protobuf/lib/pkgconfig/

生效:

source /etc/profile

3. 检查安装是否成功

运行:

protoc --version
  • 若输出类似 libprotoc 3.21.11,则安装成功。
    请添加图片描述

二、什么是 Protobuf?如何入手学习?

1. 序列化概念

  • 序列化:将对象转换为字节序列,用于存储或传输。
  • 反序列化:将字节序列还原为对象。

2. 什么情况下需要序列化?

  • 存储数据:将内存中的对象状态保存到文件或数据库。
  • 网络传输:通过网络发送数据,例如 Socket 编程中的数据交换。

3. 有哪些序列化工具?

  • XML:标记语言,结构清晰但冗余。
  • JSON:轻量级,易读且广泛支持。
  • Protobuf:二进制格式,高效紧凑。

4. Protobuf 是什么?

4.1 官方定义
  • 官网: https://protobuf.dev/
  • 原文
    请添加图片描述
  • 翻译
    • Protocol Buffers 是 Google 开发的一种语言无关、平台无关、可扩展的结构化数据序列化方法。
    • 相比 XML,它更小、更快、更简单,适用于通信协议和数据存储。
    • 支持定义数据结构并生成代码,便于在多种语言和数据流中读写数据,且更新结构时不会破坏旧程序。
4.2 特点
  • 语言与平台无关:支持 Java、C++、Python 等多种语言,跨平台使用。
  • 高效:体积小、速度快、解析简单。
  • 扩展性与兼容性:可更新数据结构而不影响旧程序。

5. Protobuf 使用特点

  • 流程
    请添加图片描述
    1. 编写 .proto 文件,定义消息(message)及字段。
    2. 使用 protoc 编译器生成接口代码(头文件和源文件)。
    3. 在代码中包含生成的头文件,实现字段操作和序列化/反序列化。
  • 优势:通过代码生成机制,开发者无需手动编写协议解析代码,省时高效。

6. 如何入手学习 Protobuf?

  • 推荐方式:通过项目驱动学习。
  • 示例项目:实现一个通讯录应用,逐步升级版本,深入掌握 Protobuf。
    • 通讯录内容:包含联系人列表,每个联系人包括姓名、电话等属性。

三、Protobuf 快速上手指南

1. 目标

通过实现通讯录 1.0 版本,掌握 Protobuf 基本语法和序列化/反序列化流程。
请添加图片描述

2. 需求

  • 使用 Protobuf 序列化一个联系人(姓名、年龄)并打印结果。
  • 反序列化并解析联系人信息,打印输出。
  • 联系人信息:姓名(string)、年龄(int32)。

3. 实现步骤

3.1 创建 .proto 文件

文件:contacts.proto

syntax = "proto3";
package contacts;

message PeopleInfo {
  string name = 1;  // 姓名
  int32 age = 2;    // 年龄
}
  • 文件命名规范:全小写 + 下划线(如 contacts.proto)。
  • 注释:支持 ///* ... */
  • 语法syntax = "proto3",最新版本,支持多种语言。
  • 包名package contacts,避免命名冲突。
  • 字段类型
    • string:UTF-8 或 ASCII 字符串,长度不超过 2^32。
    • int32:32 位整数,负值编码效率较低。
  • 字段编号:1536,870,911,115 编码效率最高,19000~19999 预留。
.proto 类型C++ 类型说明
doubledouble双精度浮点数
floatfloat单精度浮点数
int32int32使用变长编码的32位整数。负数的编码效率较低,如果字段可能为负值,应使用 sint32
int64int64使用变长编码的64位整数。负数的编码效率较低,如果字段可能为负值,应使用 sint64
uint32uint32使用变长编码的无符号32位整数
uint64uint64使用变长编码的无符号64位整数
sint32int32使用变长编码的符号整型32位整数。负值编码效率高于常规的 int32 类型
sint64int64使用变长编码的符号整型64位整数。负值编码效率高于常规的 int64 类型
fixed32uint32定长4字节的整数。若值常大于 2^28 则会比 uint32 更高效
fixed64uint64定长8字节的整数。若值常大于 2^56 则会比 uint64 更高效
sfixed32int32定长4字节的符号整数
sfixed64int64定长8字节的符号整数
boolbool布尔类型
stringstring包含 UTF-8 和 ASCII 编码的字符串,长度不能超过 2^32
bytesstring可包含任意的字节序列,长度不能超过 2^32
3.2 编译 .proto 文件

使用 protoc 编译:

protoc --cpp_out=. contacts.proto
  • 可选参数-I 指定源目录,例如:
    protoc -I fast_start/ --cpp_out=fast_start/ contacts.proto
    
  • 输出
    • contacts.pb.h:消息类声明。
    • contacts.pb.cc:消息类实现。
3.3 序列化与反序列化

文件:main.cc

#include <iostream>
#include "contacts.pb.h"

int main() {
  std::string people_str;

  // 序列化
  {
    contacts::PeopleInfo people;
    people.set_name("张珊");
    people.set_age(20);
    if (!people.SerializeToString(&people_str)) {
      std::cerr << "序列化联系人失败!" << std::endl;
      return -1;
    }
    std::cout << "序列化成功,结果:" << people_str << std::endl;
  }

  // 反序列化
  {
    contacts::PeopleInfo people;
    if (!people.ParseFromString(people_str)) {
      std::cerr << "反序列化联系人失败!" << std::endl;
      return -1;
    }
    std::cout << "反序列化成功!\n"
              << "姓名:" << people.name() << "\n"
              << "年龄:" << people.age() << std::endl;
  }

  return 0;
}
3.4 编译与运行
g++ main.cc contacts.pb.cc -o TestProtoBuf -std=c++11 -lprotobuf
./TestProtoBuf
  • 参数
    • -std=c++11:使用 C++11 标准。
    • -lprotobuf:链接 Protobuf 库。
  • 运行结果示例
    请添加图片描述

四、Proto3 语法详解

请添加图片描述

1. 字段规则

  • singular:字段出现 0 或 1 次(默认)。
  • repeated:字段可出现任意次(类似数组),顺序保留。
  • 示例:升级 contacts.proto,添加多个电话号码:
    syntax = "proto3";
    package contacts;
    
    message PeopleInfo {
      string name = 1;
      int32 age = 2;
      repeated string phone_numbers = 3;
    }
    

2. 消息类型的定义与使用

2.1 定义

支持嵌套和非嵌套消息:

  • 嵌套写法
    syntax = "proto3";
    package contacts;
    
    message PeopleInfo {
      string name = 1;
      int32 age = 2;
      message Phone {
        string number = 1;
      }
    }
    
  • 非嵌套写法
    syntax = "proto3";
    package contacts;
    
    message Phone {
      string number = 1;
    }
    message PeopleInfo {
      string name = 1;
      int32 age = 2;
    }
    
2.2 使用
  • 作为字段类型
    syntax = "proto3";
    package contacts;
    
    message PeopleInfo {
      string name = 1;
      int32 age = 2;
      message Phone {
        string number = 1;
      }
      repeated Phone phone = 3;
    }
    
  • 导入其他文件
    // phone.proto
    syntax = "proto3";
    package phone;
    
    message Phone {
      string number = 1;
    }
    
    // contacts.proto
    syntax = "proto3";
    package contacts;
    import "phone.proto";
    
    message PeopleInfo {
      string name = 1;
      int32 age = 2;
      repeated phone.Phone phone = 3;
    }
    
    • 注意proto3proto2 可互导消息类型,但 proto3 不支持字段删除和自定义默认值。
2.3 升级通讯录

文件:contacts.proto

syntax = "proto3";
package contacts2;

message PeopleInfo {
  string name = 1;
  int32 age = 2;
  message Phone {
    string number = 1;
  }
  repeated Phone phone = 3;
}
message Contacts {
  repeated PeopleInfo contacts = 1;
}
  • 写入文件
    #include <iostream>
    #include <fstream>
    #include "contacts.pb.h"
    
    using namespace std;
    
    void AddPeopleInfo(contacts2::PeopleInfo* people) {
      cout << "---------------- 新增联系人-----------------\n";
      cout << "请输入联系人姓名:";
      string name;
      getline(cin, name);
      people->set_name(name);
    
      cout << "请输入联系人年龄:";
      int age;
      cin >> age;
      people->set_age(age);
      cin.ignore(256, '\n');
    
      for (int i = 0;; i++) {
        cout << "请输入联系人电话" << i + 1 << "(回车结束):";
        string number;
        getline(cin, number);
        if (number.empty()) break;
        contacts2::PeopleInfo_Phone* phone = people->add_phone();
        phone->set_number(number);
      }
      cout << "---------------- 添加联系人成功 ----------------\n";
    }
    
    int main() {
      GOOGLE_PROTOBUF_VERIFY_VERSION;
      contacts2::Contacts contacts;
    
      fstream input("contacts.bin", ios::in | ios::binary);
      if (!input) {
        cout << "文件打开失败\n";
      } else if (!contacts.ParseFromIstream(&input)) {
        cerr << "文件解析失败\n";
        input.close();
        return -1;
      }
    
      AddPeopleInfo(contacts.add_contacts());
    
      fstream output("contacts.bin", ios::out | ios::trunc | ios::binary);
      if (!contacts.SerializeToOstream(&output)) {
        cerr << "文件写入失败\n";
        input.close();
        output.close();
        return -1;
      }
      cout << "文件写入成功\n";
      input.close();
      output.close();
    
      google::protobuf::ShutdownProtobufLibrary();
      return 0;
    }
    
  • 读取文件
    #include <iostream>
    #include <fstream>
    #include "contacts.pb.h"
    
    using namespace std;
    
    void PrintContacts(const contacts2::Contacts& contacts) {
      for (int i = 0; i < contacts.contacts_size(); i++) {
        cout << "---------------联系人" << i + 1 << "---------------\n";
        const contacts2::PeopleInfo& people = contacts.contacts(i);
        cout << "姓名:" << people.name() << "\n";
        cout << "年龄:" << people.age() << "\n";
        for (int j = 0; j < people.phone_size(); j++) {
          const contacts2::PeopleInfo_Phone& phone = people.phone(j);
          cout << "电话:" << j + 1 << phone.number() << "\n";
        }
      }
    }
    
    int main() {
      contacts2::Contacts contacts;
    
      fstream input("contacts.bin", ios::in | ios::binary);
      if (!input) {
        cout << "文件打开失败\n";
      } else if (!contacts.ParseFromIstream(&input)) {
        cerr << "文件解析失败\n";
        input.close();
        return -1;
      }
    
      PrintContacts(contacts);
      input.close();
      return 0;
    }
    
  • Makefile
    all: write read
    
    write: write.cc contacts.pb.cc
        g++ -o write write.cc contacts.pb.cc -lprotobuf
    read: read.cc contacts.pb.cc
        g++ -o read read.cc contacts.pb.cc -lprotobuf
    
    .PHONY: clean
    clean:
        rm -f write read
    
  • 查看二进制文件
    hexdump -C contacts.bin #把二进制文件转换成十六进制
    protoc --decode=contacts2.Contacts contacts.proto < contacts.bin #查看二进制文件
    
    请添加图片描述

3. Enum 类型

3.1 定义规则
  • 命名
    • 枚举类型:驼峰命名(如 PhoneType)。
    • 常量值:全大写 + 下划线(如 MP)。
  • 规则
    1. 第一个值必须为 0,作为默认值。
    2. 可在消息体内或外定义。
    3. 值范围:32 位整数,避免负值。
  • 示例:
    enum PhoneType {
      MP = 0;  // 移动电话
      TEL = 1; // 固定电话
    }
    
3.2 注意事项
  • 同级枚举常量不能重名。
  • 不同 .proto 文件需用 package 区分。
3.3 加入通讯录
syntax = "proto3";
package contacts2;

message PeopleInfo {
  string name = 1;
  int32 age = 2;
  message Phone {
    string number = 1;
    enum PhoneType {
      MP = 0;
      TEL = 1;
    }
    PhoneType type = 2;
  }
  repeated Phone phone = 3;
}
message Contacts {
  repeated PeopleInfo contacts = 1;
}
  • 修改 AddPeopleInfo
    void AddPeopleInfo(contacts2::PeopleInfo* people) {
      // ... 姓名和年龄输入省略
      for (int i = 0;; i++) {
        cout << "请输入联系人电话" << i + 1 << "(回车结束):";
        string number;
        getline(cin, number);
        if (number.empty()) break;
        contacts2::PeopleInfo_Phone* phone = people->add_phone();
        phone->set_number(number);
    
        cout << "请输入电话类型(1、移动电话 2、固定电话):";
        int type;
        cin >> type;
        cin.ignore(256, '\n');
        switch (type) {
          case 1:
            phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);
            break;
          case 2:
            phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);
            break;
          default:
            cout << "输入错误,请重新输入\n";
            break;
        }
      }
    }
    
  • 修改 PrintContacts
    void PrintContacts(const contacts2::Contacts& contacts) {
      for (int i = 0; i < contacts.contacts_size(); i++) {
        // ... 姓名和年龄打印省略
        for (int j = 0; j < people.phone_size(); j++) {
          const contacts2::PeopleInfo_Phone& phone = people.phone(j);
          cout << "电话:" << j + 1 << phone.number()
               << "   (" << phone.PhoneType_Name(phone.type()) << ")\n";
        }
      }
    }
    

4. Any 类型

4.1 定义
  • 用途:存储任意消息类型,类似泛型。
  • 引入import "google/protobuf/any.proto"
  • 示例:
    syntax = "proto3";
    package contacts2;
    import "google/protobuf/any.proto";
    
    message Address {
      string home_address = 1;
      string unit_address = 2;
    }
    message PeopleInfo {
      string name = 1;
      int32 age = 2;
      google.protobuf.Any data = 4;
    }
    
4.2 加入通讯录
  • 修改 AddPeopleInfo
    void AddPeopleInfo(contacts2::PeopleInfo* people) {
      // ... 其他输入省略
      contacts2::Address address;
      cout << "请输入家庭地址:";
      string home_address;
      getline(cin, home_address);
      address.set_home_address(home_address);
      cout << "请输入公司地址:";
      string unit_address;
      getline(cin, unit_address);
      address.set_unit_address(unit_address);
      people->mutable_data()->PackFrom(address);
    }
    
  • 修改 PrintContacts
    void PrintContacts(const contacts2::Contacts& contacts) {
      // ... 其他打印省略
      if (people.has_data() && people.data().Is<contacts2::Address>()) {
        contacts2::Address address;
        people.data().UnpackTo(&address);
        if (!address.home_address().empty()) {
          cout << "家庭地址:" << address.home_address() << "\n";
        }
        if (!address.unit_address().empty()) {
          cout << "公司地址:" << address.unit_address() << "\n";
        }
      }
    }
    

5. Oneof 类型

5.1 定义
  • 用途:表示多选一字段。
  • 格式oneof 字段名 { 字段1; 字段2; ... }
  • 示例:
    message PeopleInfo {
      oneof other_contact {
        string qq = 5;
        string wechat = 6;
      }
    }
    
  • 注意
    • 不可用 repeated
    • 多次设置只保留最后值。
5.2 加入通讯录
syntax = "proto3";
package contacts2;
import "google/protobuf/any.proto";

message Address {
  string home_address = 1;
  string unit_address = 2;
}
message PeopleInfo {
  string name = 1;
  int32 age = 2;
  message Phone {
    string number = 1;
    enum PhoneType {
      MP = 0;
      TEL = 1;
    }
    PhoneType type = 2;
  }
  repeated Phone phone = 3;
  google.protobuf.Any data = 4;
  oneof other_contact {
    string qq = 5;
    string wechat = 6;
  }
}
  • 修改 AddPeopleInfo
    void AddPeopleInfo(contacts2::PeopleInfo* people) {
      // ... 其他输入省略
      cout << "请选择其他联系方式(1、qq,2、wechat):";
      int other_contact;
      cin >> other_contact;
      cin.ignore(256, '\n');
      if (1 == other_contact) {
        cout << "请输入qq号:";
        string qq;
        getline(cin, qq);
        people->set_qq(qq);
      } else if (2 == other_contact) {
        cout << "请输入wechat号:";
        string wechat;
        getline(cin, wechat);
        people->set_wechat(wechat);
      } else {
        cout << "输入错误,未成功设置\n";
      }
    }
    
  • 修改 PrintContacts
    void PrintContacts(const contacts2::Contacts& contacts) {
      // ... 其他打印省略
      switch (people.other_contact_case()) {
        case contacts2::PeopleInfo::OtherContactCase::kQq:
          cout << "联系人qq: " << people.qq() << "\n";
          break;
        case contacts2::PeopleInfo::OtherContactCase::kWechat:
          cout << "联系人微信号: " << people.wechat() << "\n";
          break;
        default:
          break;
      }
    }
    

6. Map 类型

6.1 定义
  • 格式map<key_type, value_type> map_field = N;
  • 限制
    • key_type:除 floatbytes 外的标量类型。
    • value_type:任意类型。
    • 不可用 repeated
  • 示例:
    message PeopleInfo {
      map<string, string> remark = 7;
    }
    
6.2 加入通讯录
syntax = "proto3";
package contacts2;
import "google/protobuf/any.proto";

message Address {
  string home_address = 1;
  string unit_address = 2;
}
message PeopleInfo {
  string name = 1;
  int32 age = 2;
  message Phone {
    string number = 1;
    enum PhoneType {
      MP = 0;
      TEL = 1;
    }
    PhoneType type = 2;
  }
  repeated Phone phone = 3;
  google.protobuf.Any data = 4;
  oneof other_contact {
    string qq = 5;
    string wechat = 6;
  }
  map<string, string> remark = 7;
}
  • 修改 AddPeopleInfo
    void AddPeopleInfo(contacts2::PeopleInfo* people) {
      // ... 其他输入省略
      for (int i = 0;; i++) {
        cout << "请输入备注" << i + 1 << "(回车结束):";
        string remark_key;
        getline(cin, remark_key);
        if (remark_key.empty()) break;
        cout << "请输入备注" << i + 1 << "内容:";
        string remark_value;
        getline(cin, remark_value);
        people->mutable_remark()->insert({remark_key, remark_value});
      }
    }
    
  • 修改 PrintContacts
    void PrintContacts(const contacts2::Contacts& contacts) {
      // ... 其他打印省略
      if (people.remark_size()) {
        cout << "备注信息:\n";
      }
      for (auto it = people.remark().cbegin(); it != people.remark().cend(); it++) {
        cout << "   " << it->first << ": " << it->second << "\n";
      }
    }
    

7. 默认值

反序列化消息时,如果被反序列化的⼆进制序列中不包含某个字段,反序列化对象中相应字段时,就 会设置为该字段的默认值。不同的类型对应的默认值不同:

  • 字符串:空字符串。
  • 字节:空字节。
  • 布尔值false
  • 数值:0。
  • 枚举:第一个值(必须为 0)。
  • 消息字段:未设置,具体值语言相关。
  • repeated:空列表。

8. 更新消息

8.1 更新规则

如果现有的消息类型已经不再满⾜我们的需求,例如需要扩展⼀个字段,在不破坏任何现有代码的情 况下更新消息类型⾮常简单。遵循如下规则即可:

  • 禁⽌修改任何已有字段的字段编号。
  • 若是移除⽼字段,要保证不再使⽤移除字段的字段编号。正确的做法是保留字段编号(reserved),以确保该编号将不能被重复使⽤。不建议直接删除或注释掉字段。
  • int32, uint32, int64, uint64 和 bool 是完全兼容的。可以从这些类型中的⼀个改为另⼀个, ⽽不破坏前后兼容性。若解析出来的数值与相应的类型不匹配,会采⽤与 C++ ⼀致的处理⽅案 (例如,若将 64 位整数当做 32 位进⾏读取,它将被截断为 32 位)。
  • sint32 和 sint64 相互兼容但不与其他的整型兼容。
  • string 和 bytes 在合法 UTF-8 字节前提下也是兼容的。
  • bytes 包含消息编码版本的情况下,嵌套消息与 bytes 也是兼容的。
  • fixed32 与 sfixed32 兼容, fixed64 与 sfixed64兼容。
  • enum 与 int32,uint32, int64 和 uint64 兼容(注意若值不匹配会被截断)。但要注意当反序 列化消息时会根据语⾔采⽤不同的处理⽅案:例如,未识别的 proto3 枚举类型会被保存在消息中,但是当消息反序列化时如何表⽰是依赖于编程语⾔的。整型字段总是会保持其的值。
  • oneof:
    • 将⼀个单独的值更改为 新 oneof 类型成员之⼀是安全和⼆进制兼容的。
    • 若确定没有代码⼀次性设置多个值那么将多个字段移⼊⼀个新 oneof 类型也是可⾏的。
    • 将任何字段移⼊已存在的 oneof 类型是不安全的。
8.2 保留字段 (Reserved)

如果通过 删除 或 注释掉 字段来更新消息类型,未来的⽤⼾在添加新字段时,有可能会使⽤以前已经 存在,但已经被删除或注释掉的字段编号。将来使⽤该 .proto 的旧版本时的程序会引发很多问题:数 据损坏、隐私错误等等。
确保不会发⽣这种情况的⼀种⽅法是:使⽤ reserved 将指定字段的编号或名称设置为保留项 。当 我们再使⽤这些编号或名称时,protocol buffer 的编译器将会警告这些编号或名称不可⽤。举个例 ⼦:

message Message {
  // 设置保留项
  reserved 100, 101, 200 to 299;
  reserved "field3", "field4";
  // 注意:不要在⼀⾏ reserved 声明中同时声明字段编号和名称。
  // reserved 102, "field5";
  // 设置保留项之后,下⾯代码会告警
  int32 field1 = 100; //告警:Field 'field1' uses reserved number 100
  int32 field2 = 101; //告警:Field 'field2' uses reserved number 101
  int32 field3 = 102; //告警:Field name 'field3' is reserved
  int32 field4 = 103; //告警:Field name 'field4' is reserved
}
8.3.1 验证错误删除字段造成的数据损坏

现模拟有两个服务,他们各⾃使⽤⼀份通讯录 .proto ⽂件,内容约定好了是⼀模⼀样的。

  • 服务1(service):负责序列化通讯录对象,并写⼊⽂件中。
  • 服务2(client):负责读取⽂件中的数据,解析并打印出来。

⼀段时间后,service 更新了⾃⼰的 .proto ⽂件,更新内容为:删除了某个字段,并新增了⼀个字段, 新增的字段使⽤了被删除字段的字段编号。并将新的序列化对象写进了⽂件。 但 client 并没有更新⾃⼰的 .proto ⽂件。根据结论,可能会出现数据损坏的现象,接下来就让我们来 验证下这个结论。 新建两个⽬录:service、client。分别存放两个服务的代码。

  • service

    syntax = "proto3";
    package s_contacts;
    
    // 联系⼈
    message PeopleInfo {
      reserved 2, 10, 11, 100 to 200;
      reserved "age";
    
      string name = 1; // 姓名
      int32 age = 2; // 年龄
    
      message Phone {
        string number = 1; // 电话号码
      }
      repeated Phone phone = 3; // 电话
    }
    
    // 通讯录
    message Contacts {
      repeated PeopleInfo contacts = 1;
    }
    
    #include <iostream>
    #include <fstream>
    #include "contacts.pb.h"
    using namespace std;
    using namespace s_contacts;
    
    /**
     * 新增联系⼈
     */
    void AddPeopleInfo(PeopleInfo *people_info_ptr) {
        cout << "-------------新增联系⼈-------------" << endl;
        cout << "请输⼊联系⼈姓名: ";
        string name;
        getline(cin, name);
        people_info_ptr->set_name(name);
    
        cout << "请输⼊联系⼈年龄: ";
        int age;
        cin >> age;
        people_info_ptr->set_age(age);
        cin.ignore(256, '\n');
    
        for(int i = 1; ; i++) {
            cout << "请输⼊联系⼈电话" << i << "(只输⼊回⻋完成电话新增): ";
            string number;
            getline(cin, number);
            if (number.empty()) {
                break;
            }
    
            PeopleInfo_Phone* phone = people_info_ptr->add_phone();
            phone->set_number(number);
        }
        cout << "-----------添加联系⼈成功-----------" << endl;
    }
    
    int main() {
        Contacts contacts;
        // 先读取已存在的 contacts
        fstream input("../contacts.bin", ios::in | ios::binary);
        if (!input) {
            cout << "contacts.bin not found. Creating a new file." << endl;
        } else if (!contacts.ParseFromIstream(&input)) {
            cerr << "Failed to parse contacts." << endl;
            input.close();
            return -1;
        }
    
        // 新增⼀个联系⼈
        AddPeopleInfo(contacts.add_contacts());
    
        // 向磁盘⽂件写⼊新的 contacts
        fstream output("../contacts.bin", ios::out | ios::trunc | ios::binary);
        if (!contacts.SerializeToOstream(&output)) {
            cerr << "Failed to write contacts." << endl;
            input.close();
            output.close();
            return -1;
        }
    
        input.close();
        output.close();
        return 0;
    }
    
    service:service.cc contacts.pb.cc
        g++ -o $@ $^ -std=c++11 -lprotobuf
    .PHONY:clean
    clean:
        rm -f service
    
  • client

    syntax = "proto3";
    package c_contacts;
    
    // 联系⼈
    message PeopleInfo {
      string name = 1; // 姓名
      int32 age = 2; // 年龄
    
      message Phone {
        string number = 1; // 电话号码
      }
      repeated Phone phone = 3; // 电话
    }
    
    // 通讯录
    message Contacts {
      repeated PeopleInfo contacts = 1;
    }
    
    #include <iostream>
    #include <fstream>
    #include "contacts.pb.h"
    using namespace std;
    using namespace c_contacts;
    using namespace google::protobuf;
    
    /**
     * 打印联系⼈列表
     */
    void PrintfContacts(const Contacts& contacts) {
        for (int i = 0; i < contacts.contacts_size(); ++i) {
            const PeopleInfo& people = contacts.contacts(i);
            cout << "------------联系⼈" << i+1 << "------------" << endl;
            cout << "联系⼈姓名:" << people.name() << endl;
            cout << "联系⼈年龄:" << people.age() << endl;
            int j = 1;
            for (const PeopleInfo_Phone& phone : people.phone()) {
                cout << "联系⼈电话" << j++ << ": " << phone.number() << endl;
            }
    
            const Reflection* reflection = PeopleInfo::GetReflection();
            const UnknownFieldSet& set = reflection->GetUnknownFields(people);
            for (int j = 0; j < set.field_count(); j++) {
                const UnknownField& unknown_field = set.field(j);
                cout << "未知字段" << j+1 << ":  "
                     << "  编号:" << unknown_field.number();
                switch(unknown_field.type()) {
                    case UnknownField::Type::TYPE_VARINT:
                        cout << "  值:" << unknown_field.varint() << endl;
                        break;
                    case UnknownField::Type::TYPE_LENGTH_DELIMITED:
                        cout << "  值:" << unknown_field.length_delimited() << endl;
                        break;
                    // case ...
                }
            }
        }
    }
    
    int main() {
        Contacts contacts;
        // 先读取已存在的 contacts
        fstream input("../contacts.bin", ios::in | ios::binary);
        if (!contacts.ParseFromIstream(&input)) {
            cerr << "Failed to parse contacts." << endl;
            input.close();
            return -1;
        }
    
        // 打印 contacts
        PrintfContacts(contacts);
        input.close();
        return 0;
    }
    
    client:client.cc contacts.pb.cc
        g++ -o $@ $^ -std=c++11 -lprotobuf
    .PHONY:clean
    clean:
        rm -f client
    

编译运行后,添加并且读取内容:
请添加图片描述

然后我们更新一下service目录的.proto文件,删除 age 字段,新增 birthday 字段,新增的字段使⽤被删除字段的字段编号

// 删除年龄字段
// int32 age = 2; // 年龄
int32 birthday = 2;// ⽣⽇

并且更新一下service.cc:

    // cout << "请输⼊联系⼈年龄: ";
    // int age;
    // cin >> age;
    // people_info_ptr->set_age(age);
    // cin.ignore(256, '\n');

    cout << "请输⼊联系⼈生日: ";
    int birthday;
    cin >> birthday;
    people_info_ptr->set_birthday(birthday);
    cin.ignore(256, '\n');

这时问题便出现了,我们发现输⼊的⽣⽇,在反序列化时,被设置到了使⽤了相同字段编号的年龄 上!!所以得出结论:若是移除⽼字段,要保证不再使⽤移除字段的字段编号,不建议直接删除或注 释掉字段。
那么正确的做法是保留字段编号(reserved),以确保该编号将不能被重复使⽤。
请添加图片描述

然后我们更新一下service目录的.proto文件,保留age 和字段编号2

  reserved 2, 10, 11, 100 to 200;
  reserved "age";

  string name = 1; // 姓名
  //int32 age = 2; // 年龄
  int32 birthday = 4;

根据实验结果,发现 ‘王五’ 的年龄为 0,这是由于新增时未设置年龄,通过 client 程序反序列化 时,给年龄字段设置了默认值 0。这个结果显然是我们想看到的。
还要解释⼀下 ‘李四’ 的年龄依旧使⽤了之前设置的⽣⽇字段 ‘1221’,这是因为在新增 ‘李四’ 的时候,⽣⽇字段的字段编号依旧为 2,并且已经被序列化到⽂件中了。最后再读取的时候,字段编号 依旧为 2。
还要再说⼀下的是:因为使⽤了 reserved 关键字,ProtoBuf在编译阶段就拒绝了我们使⽤已经保留 的字段编号。到此实验结束,也印证了我们的结论。
根据以上的例⼦,有的同学可能还有⼀个疑问:如果使⽤了 reserved 2 了,那么 service 给 ‘王五’ 设置的⽣⽇ ‘1112’,client 就没法读到了吗? 答案是可以的。继续学习下⾯的未知字段即可揭 晓答案。
请添加图片描述

8.3 未知字段

我们向 service ⽬录下的 contacts.proto 新增了‘⽣⽇’字段,但对于 client 相 关的代码并没有任何改动。验证后发现 新代码序列化的消息(service)也可以被旧代码(client)解 析。并且这⾥要说的是,新增的 ‘⽣⽇’字段在旧程序(client)中其实并没有丢失,⽽是会作为旧程 序的未知字段。

  • 未知字段:解析结构良好的 protocol buffer 已序列化数据中的未识别字段的表⽰⽅式。例如,当旧程序解析带有新字段的数据时,这些新字段就会成为旧程序的未知字段。
  • 本来,proto3 在解析消息时总是会丢弃未知字段,但在 3.5 版本中重新引⼊了对未知字段的保留机制。所以在 3.5 或更⾼版本中,未知字段在反序列化时会被保留,同时也会包含在序列化的结果中。
8.3.1 未知字段从哪获取

了解相关类关系图:
请添加图片描述


MessageLite 类
  • 定义MessageLite 是一个轻量级的消息类,仅提供序列化和反序列化功能。
  • 特点:从名字上看是“轻量级”的 message,功能专注于核心序列化操作,不包含额外的反射或描述功能。
  • 位置:类定义在 Google 提供的 message_lite.h 文件中。
  • 作用:适用于资源受限的场景(如移动设备),提供基本的序列化/反序列化功能。
  • 局限:不具备动态访问字段或元数据的功能,相比 Message 类更简单高效。

Message 类
  • 定义:我们自定义的 message 类都继承自 Message 类。
  • 核心接口
    • GetDescriptor():返回该消息类型的 Descriptor 对象指针,用于描述消息的元数据。
    • GetReflection():返回 Reflection 对象指针,用于动态操作消息字段。
  • 位置:类定义在 Google 提供的 message.h 文件中。
  • 部分代码展示
    const Descriptor* GetDescriptor() const;
    const Reflection* GetReflection() const;
    
  • 作用:作为所有自定义消息的基类,提供元数据描述和动态操作的桥梁。
  • 特点:通过 DescriptorReflection,支持对消息结构的静态描述和动态读写。
  • 使用场景:需要访问消息定义或动态操作字段时使用。

Descriptor 类
  • 定义Descriptor 是对消息类型定义的描述,包含消息的名字、所有字段的描述、原始 .proto 文件内容等。
  • 位置:类定义在 Google 提供的 descriptor.h 文件中。
  • 部分代码展示
    class PROTOBUF_EXPORT Descriptor : private internal::SymbolBase {
      string& name() const;
      int field_count() const;
      const FieldDescriptor* field(int index) const;
      const FieldDescriptor* FindFieldByNumber(int number) const;
      const FieldDescriptor* FindFieldByName(const std::string& name) const;
      const FieldDescriptor* FindFieldByLowercaseName(const std::string& lowercase_name) const;
      const FieldDescriptor* FindFieldByCamelcaseName(const std::string& camelcase_name) const;
      int enum_type_count() const;
      const EnumDescriptor* enum_type(int index) const;
      const EnumDescriptor* FindEnumTypeByName(const std::string& name) const;
      const EnumValueDescriptor* FindEnumValueByName(const std::string& name) const;
    }
    
  • 作用:提供消息的元数据信息,如字段数量、字段描述、枚举类型等。
  • 特点:支持按编号或名称查找字段,支持枚举类型的访问和管理。
  • 使用场景:需要静态分析消息结构或元数据时使用。

Reflection 类
  • 定义Reflection 是一个接口类,主要提供动态读写消息字段的功能。
  • 特点
    • 动态访问和修改消息字段,提供针对不同类型字段的专用接口(如 GetInt32SetString)。
    • 支持 singularrepeated 字段的操作,但接口不能混用。
    • 只能通过消息自身的 Reflection 对象操作(message.GetReflection())。
    • 包含访问和修改未知字段的方法。
  • 位置:类定义在 Google 提供的 message.h 文件中。
  • 部分代码展示
    class PROTOBUF_EXPORT Reflection final {
      const UnknownFieldSet& GetUnknownFields(const Message& message) const;
      UnknownFieldSet* MutableUnknownFields(Message* message) const;
      bool HasField(const Message& message, const FieldDescriptor* field) const;
      int FieldSize(const Message& message, const FieldDescriptor* field) const;
      void ClearField(Message* message, const FieldDescriptor* field) const;
      bool HasOneof(const Message& message, const OneofDescriptor* oneof_descriptor) const;
      void ClearOneof(Message* message, const OneofDescriptor* oneof_descriptor) const;
      const FieldDescriptor* GetOneofFieldDescriptor(const Message& message, const OneofDescriptor* oneof_descriptor) const;
      int32_t GetInt32(const Message& message, const FieldDescriptor* field) const;
      void SetInt32(Message* message, const FieldDescriptor* field, int32_t value) const;
      int32_t GetRepeatedInt32(const Message& message, const FieldDescriptor* field, int index) const;
      void SetRepeatedInt32(Message* message, const FieldDescriptor* field, int index, int32_t value) const;
      void AddInt32(Message* message, const FieldDescriptor* field, int32_t value) const;
      // ... 其他类型和方法的接口
    };
    
  • 作用:实现消息字段的动态读写,支持所有字段类型(singularrepeatedoneof 等)。
  • 特点:为每种字段类型提供专用接口,支持 repeated 字段操作,提供未知字段访问。
  • 使用场景:需要在运行时动态操作消息字段时使用。

UnknownFieldSet 类
  • 定义UnknownFieldSet 包含在解析消息时遇到的、未在类型定义中声明的字段。
  • 访问方式:通过 Reflection::GetUnknownFields() 获取消息的未知字段集。
  • 位置:类定义在 unknown_field_set.h 文件中。
  • 部分代码展示
    class PROTOBUF_EXPORT UnknownFieldSet {
      inline void Clear();
      void ClearAndFreeMemory();
      inline bool empty() const;
      inline int field_count() const;
      inline const UnknownField& field(int index) const;
      inline UnknownField* mutable_field(int index);
      void AddVarint(int number, uint64_t value);
      void AddFixed32(int number, uint32_t value);
      void AddLengthDelimited(int number, const std::string& value);
      UnknownFieldSet* AddGroup(int number);
      bool MergeFromCodedStream(io::CodedInputStream* input);
      bool ParseFromString(const std::string& data);
      bool SerializeToString(std::string* output) const;
      static const UnknownFieldSet& default_instance();
    };
    
  • 作用:存储和管理消息中的未知字段,支持解析和序列化。
  • 特点:提供添加不同类型未知字段的方法,支持从流或字符串解析。
  • 使用场景:处理向后兼容或向前兼容时,保留和操作未识别的字段。

UnknownField 类
  • 定义UnknownField 表示 UnknownFieldSet 中的单个未知字段。
  • 位置:类定义在 unknown_field_set.h 文件中。
  • 部分代码展示
    class PROTOBUF_EXPORT UnknownField {
    public:
      enum Type {
        TYPE_VARINT,
        TYPE_FIXED32,
        TYPE_FIXED64,
        TYPE_LENGTH_DELIMITED,
        TYPE_GROUP
      };
      inline int number() const;
      inline Type type() const;
      inline uint64_t varint() const;
      inline uint32_t fixed32() const;
      inline uint64_t fixed64() const;
      inline const std::string& length_delimited() const;
      inline const UnknownFieldSet& group() const;
      inline void set_varint(uint64_t value);
      inline void set_length_delimited(const std::string& value);
      inline UnknownFieldSet* mutable_group();
    };
    
  • 作用:表示单个未知字段的具体数据和类型。
  • 特点:支持多种类型,提供针对每种类型的读写接口。
  • 使用场景:在处理未知字段时,访问或修改具体字段的值。

类之间的关系

以下是这些类之间的关系分析,结合功能和依赖明确其联系:

  1. MessageLiteMessage 的关系

    • 继承关系Message 继承自 MessageLite,扩展了元数据和动态操作功能。
    • 功能差异MessageLite 是基础类,仅提供序列化功能;Message 在此基础上增加了 GetDescriptor()GetReflection() 接口,连接到元数据和动态操作。
  2. MessageDescriptor 的关系

    • 依赖关系Message 通过 GetDescriptor() 获取 Descriptor 对象,依赖其提供消息的元数据描述。
    • 协作Descriptor 是静态的结构信息来源,Message 用它来暴露类型定义。
  3. MessageReflection 的关系

    • 依赖关系Message 通过 GetReflection() 获取 Reflection 对象,依赖其实现字段的动态读写。
    • 协作ReflectionMessage 的动态操作工具,二者紧密配合完成运行时字段访问。
  4. DescriptorReflection 的关系

    • 协作关系Reflection 的操作(如 GetInt32SetString)依赖 Descriptor 提供的字段描述(如 FieldDescriptor),以确定字段类型和编号。
    • 分工Descriptor 提供静态元数据,Reflection 利用这些元数据执行动态操作。
  5. ReflectionUnknownFieldSet 的关系

    • 依赖关系Reflection 通过 GetUnknownFields()MutableUnknownFields() 访问和修改消息的 UnknownFieldSet
    • 协作UnknownFieldSetReflection 处理未知字段的载体,Reflection 提供接口桥接。
  6. UnknownFieldSetUnknownField 的关系

    • 包含关系UnknownFieldSet 是一个集合类,包含多个 UnknownField 对象。
    • 协作UnknownFieldSet 管理未知字段的集合,UnknownField 表示单个未知字段的具体数据。
关系图示

以下是简化的类关系图(文字形式,基于依赖和协作):

MessageLite
   ↑ (继承)
Message
   ├── GetDescriptor() → Descriptor (元数据描述)
   └── GetReflection() → Reflection (动态操作)
                         ├── 需要 Descriptor (字段描述)
                         └── GetUnknownFields() → UnknownFieldSet (未知字段集)
                                                      ↓ (包含)
                                                   UnknownField (单个未知字段)

综合分析与总结
  1. 层级与功能

    • MessageLite:最基础,提供序列化核心。
    • Message:核心枢纽,连接元数据 (Descriptor) 和动态操作 (Reflection)。
    • Descriptor:静态元数据,描述消息结构。
    • Reflection:动态操作,依赖 Descriptor 执行字段读写。
    • UnknownFieldSet:未知字段管理,依赖 Reflection 获取。
    • UnknownField:未知字段的具体表示,隶属 UnknownFieldSet
  2. 协作机制

    • Message 作为桥梁,通过 Descriptor 获取结构信息,通过 Reflection 操作字段和未知字段。
    • ReflectionDescriptor 互补,前者动态,后者静态。
    • UnknownFieldSetUnknownField 专注于兼容性支持。
  3. 使用场景

    • 轻量需求MessageLite
    • 复杂操作Message + Descriptor + Reflection
    • 兼容性需求Reflection + UnknownFieldSet + UnknownField
  4. 设计理念

    • 分层设计:从轻量 (MessageLite) 到功能丰富 (Message),再到动态操作和兼容性支持。
    • 模块化:元数据 (Descriptor) 和操作 (Reflection) 分离,未知字段独立处理。

8.3.2 验证未知字段

在client.cc中的函数中加入以下代码:

const Reflection* reflection = PeopleInfo::GetReflection();
        const UnknownFieldSet& set = reflection->GetUnknownFields(people);
        for (int j = 0; j < set.field_count(); j++) {
            const UnknownField& unknown_field = set.field(j);
            cout << "未知字段" << j+1 << ":  "
                 << "  编号:" << unknown_field.number();
            switch(unknown_field.type()) {
                case UnknownField::Type::TYPE_VARINT:
                    cout << "  值:" << unknown_field.varint() << endl;
                    break;
                case UnknownField::Type::TYPE_LENGTH_DELIMITED:
                    cout << "  值:" << unknown_field.length_delimited() << endl;
                    break;
                // case ...
            }
        }

编译后得到如下结果:
请添加图片描述

类型为何为 0 ?在介绍 UnknownField 类中讲到了类中包含了未知字段的⼏种类型:
enum Type {
  TYPE_VARINT,
  TYPE_FIXED32,
  TYPE_FIXED64,
  TYPE_LENGTH_DELIMITED,
  TYPE_GROUP
};
类型为 0,即为 TYPE_VARINT。
8.4 前后兼容性

根据上述的例⼦可以得出,pb是具有向前兼容的。为了叙述⽅便,把增加了“⽣⽇”属性的 service 称为“新模块”;未做变动的 client 称为 “⽼模块”。

  • 向前兼容:⽼模块能够正确识别新模块⽣成或发出的协议。这时新增加的“⽣⽇”属性会被当作未知字段(pb 3.5版本及之后)。
  • 向后兼容:新模块也能够正确识别⽼模块⽣成或发出的协议。前后兼容的作⽤:当我们维护⼀个很庞⼤的分布式系统时,由于你⽆法同时升级所有模块,为了保证在升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的“向后兼容”或“向前兼容”。

9. 选项 option

9.1 选项分类

选项的完整列表在google/protobuf/descriptor.proto中定义。部分代码:

syntax = "proto2"; // descriptor.proto 使⽤ proto2 语法版本
message FileOptions { ... } // ⽂件选项 定义在 FileOptions 消息中
message MessageOptions { ... } // 消息类型选项 定义在 MessageOptions 消息中
message FieldOptions { ... } // 消息字段选项 定义在 FieldOptions 消息中
message OneofOptions { ... } // oneof字段选项 定义在 OneofOptions 消息中
message EnumOptions { ... } // 枚举类型选项 定义在 EnumOptions 消息中
message EnumValueOptions { .. } // 枚举值选项 定义在 EnumValueOptions 消息中
message ServiceOptions { ... } // 服务选项 定义在 ServiceOptions 消息中
message MethodOptions { ... } // 服务⽅法选项 定义在 MethodOptions 消息中
...

由此可⻅,选项分为 ⽂件级、消息级、字段级等等,但并没有⼀种选项能作⽤于所有的类型。

9.2 常⽤选项列举
  • optimize_for : 该选项为⽂件选项,可以设置 protoc 编译器的优化级别,分别为 SPEED 、CODE_SIZE 、 LITE_RUNTIME 。受该选项影响,设置不同的优化级别,编译 .proto ⽂件后⽣成的代码内容不同。
    • SPEED : protoc 编译器将⽣成的代码是⾼度优化的,代码运⾏效率⾼,但是由此⽣成的代码 编译后会占⽤更多的空间。 SPEED 是默认选项。
    • CODE_SIZE : proto 编译器将⽣成最少的类,会占⽤更少的空间,是依赖基于反射的代码来 实现序列化、反序列化和各种其他操作。但和 SPEED 恰恰相反,它的代码运⾏效率较低。这 种⽅式适合⽤在包含⼤量的.proto⽂件,但并不盲⽬追求速度的应⽤中。
    • LITE_RUNTIME : ⽣成的代码执⾏效率⾼,同时⽣成代码编译后的所占⽤的空间也是⾮常少。这是以牺牲Protocol Buffer提供的反射功能为代价的,仅仅提供 encoding+序列化 功能,所以我们在链接 BP 库时仅需链接libprotobuf-lite,⽽⾮libprotobuf。这种模式通常⽤于资源有限的平台,例如移动⼿机平台中。
    option optimize_for = LITE_RUNTIME;
    
  • allow_alias : 允许将相同的常量值分配给不同的枚举常量,⽤来定义别名。该选项为枚举选项。
    举个例⼦:
    enum PhoneType {
        option allow_alias = true;
        MP = 0;
        TEL = 1;
        LANDLINE = 1; // 若不加 option allow_alias = true; 这⼀⾏会编译报错
    }
    
9.3 设置自定义选项

ProtoBuf 允许⾃定义选项并使⽤。该功能⼤部分场景⽤不到,在这⾥不拓展讲解。有兴趣可以参考: ProtoBuf官网

五、通讯录网络版实现

Protobuf 还常⽤于通讯协议、服务端数据交换场景。那么在这个⽰例中,我们将实现⼀个⽹络版本的通讯录,模拟实现客⼾端与服务端的交互,通过 Protobuf 来实现各端之间的协议序列化。
需求如下:

  • 客⼾端可以选择对通讯录进⾏以下操作:
    • 新增⼀个联系⼈
    • 删除⼀个联系⼈
    • 查询通讯录列表
    • 查询⼀个联系⼈的详细信息
  • 服务端提供 增 删 查 能⼒,并需要持久化通讯录。
  • 客⼾端、服务端间的交互数据使⽤ Protobuf 来完成。

1. 环境搭建

Httplib 库:cpp-httplib 是个开源的库,是⼀个c++封装的http库,使⽤这个库可以在linux、 windows平台下完成http客⼾端、http服务端的搭建。使⽤起来⾮常⽅便,只需要包含头⽂件 httplib.h 即可。编译程序时,需要带上 -lpthread 选项。

  • 源码库地址:https://github.com/yhirose/cpp-httplib
  • 镜像仓库:https://gitcode.net/mirrors/yhirose/cpp-httplib?utm_source=csdn_github_accelerator

2. 约定双端约定

新增一个联系人:

[请求]
Post /contacts/add AddContactRequest
Content-Type: application/protobuf
[响应]
AddContactResponse
Content-Type: application/protobuf

删除一个联系人:

[请求]
Post /contacts/del DelContactRequest
Content-Type: application/protobuf
[响应]
DelContactResponse
Content-Type: application/protobuf

查询通讯录列表:

[请求]
GET /contacts/find-all
[响应]
FindAllContactsResponse
Content-Type: application/protobuf

查询一个联系人的详细信息:

[请求]
Post /contacts/find-one FindOneContactRequest
Content-Type: application/protobuf
[响应]
FindOneContactResponse
Content-Type: application/protobuf

3. 约定双端交互req/resp

base_response.proto

syntax = "proto3";
package base_response;
message BaseResponse {
  bool success = 1; // 返回结果
  string error_desc = 2; // 错误描述
}

add_contact_request.proto

syntax = "proto3";
package add_contact_req;
// 新增联系⼈ req
message AddContactRequest {
  string name = 1; // 姓名
  int32 age = 2;  // 年龄
  message Phone {
    string number = 1; // 电话号码

    enum PhoneType {
      MP = 0; // 移动电话
      TEL = 1;// 固定电话
    }
    PhoneType type = 2;//类型

  }
  repeated Phone phone = 3; // 电话
  map<string, string> remark = 4; // 备注
}

add_contact_response.proto

syntax = "proto3";
package add_contact_resp;
import "base_response.proto"; // 引⼊base_response
message AddContactResponse {
  base_response.BaseResponse base_resp = 1;
  string uid = 2;
}

del_contact_request.proto

syntax = "proto3";
package del_contact_req;
// 删除⼀个联系⼈ req
message DelContactRequest {
  string uid = 1; // 联系⼈ID
}

del_contact_response.proto

syntax = "proto3";
package del_contact_resp;
import "base_response.proto"; // 引⼊base_response
// 删除⼀个联系⼈ resp
message DelContactResponse {
  base_response.BaseResponse base_resp = 1;
  string uid = 2;
}

find_one_contact_request.proto

syntax = "proto3";
package find_one_contact_req;
// 查询⼀个联系⼈ req
message FindOneContactRequest {
  string uid = 1; // 联系⼈ID
}

find_one_contact_response.proto

syntax = "proto3";
package find_one_contact_resp;
import "base_response.proto"; // 引⼊base_response
// 查询⼀个联系⼈ resp
message FindOneContactResponse {
  base_response.BaseResponse base_resp = 1;
  string uid = 2;  // 联系⼈ID
  string name = 3;// 姓名
  int32 age = 4;  // 年龄
  message Phone {
    string number = 1;   // 电话号码
    enum PhoneType {
      MP = 0;   // 移动电话
      TEL = 1;  // 固定电话
    }
    PhoneType type = 2;// 类型
  }
  repeated Phone phone = 5;     // 电话
  map<string, string> remark = 6;   // 备注
}

find_all_contacts_response.proto

syntax = "proto3";
package find_all_contacts_resp;
import "base_response.proto"; // 引⼊base_response
// 联系⼈摘要信息
message PeopleInfo {
  string uid = 1;   // 联系⼈ID
  string name = 2;   // 姓名
}
// 查询所有联系⼈ resp
message FindAllContactsResponse {
  base_response.BaseResponse base_resp = 1;
  repeated PeopleInfo contacts = 2;
}

完整代码:https://gitee.com/hu_yuchen

六、性能测试

测试代码:

syntax = "proto3";
package compare_serialization;
import "google/protobuf/any.proto"; // 引⼊ any.proto ⽂件

// 地址
message Address{
  string home_address = 1; // 家庭地址
  string unit_address = 2; // 单位地址
}

// 联系⼈
message PeopleInfo {
  string name = 1; // 姓名
  int32 age = 2; // 年龄
  message Phone {
    string number = 1; // 电话号码
    enum PhoneType {
      MP = 0; // 移动电话
      TEL = 1; // 固定电话
    }
    PhoneType type = 2; // 类型
  }
  repeated Phone phone = 3; // 电话
  google.protobuf.Any data = 4;
  oneof other_contact { // 其他联系⽅式:多选⼀
    string qq = 5;
    string weixin = 6;
  }
  map<string, string> remark = 7; // 备注
}

json库下载地址 https://github.com/open-source-parsers/jsoncpp/

#include <iostream>
#include <sys/time.h>
#include <jsoncpp-master/dist/json/json.h>
#include "contacts.pb.h"

using namespace std;
using namespace compare_serialization;
using namespace google::protobuf;

#define TEST_COUNT 100000

void createPeopleInfoFromPb(PeopleInfo *people_info_ptr);
void createPeopleInfoFromJson(Json::Value& root);

int main(int argc, char *argv[])
{
  struct timeval t_start,t_end;
  double time_used;
  int count;
  string pb_str, json_str;

  // ------------------------------Protobuf 序列化------------------------------------
  {
    PeopleInfo pb_people;
    createPeopleInfoFromPb(&pb_people);
    count = TEST_COUNT;
    gettimeofday(&t_start, NULL);
    // 序列化count次
    while ((count--) > 0) {
      pb_people.SerializeToString(&pb_str);
    }
    gettimeofday(&t_end, NULL);
    time_used=1000000*(t_end.tv_sec - t_start.tv_sec) + t_end.tv_usec - t_start.tv_usec;
    cout << TEST_COUNT << "次 [pb序列化]耗时:" << time_used/1000 << "ms."
         << " 序列化后的大小:" << pb_str.length() << endl;
  }

  // ------------------------------Protobuf 反序列化------------------------------------
  {
    PeopleInfo pb_people;
    count = TEST_COUNT;
    gettimeofday(&t_start, NULL);
    // 反序列化count次
    while ((count--) > 0) {
      pb_people.ParseFromString(pb_str);
    }
    gettimeofday(&t_end, NULL);
    time_used=1000000*(t_end.tv_sec - t_start.tv_sec) + t_end.tv_usec - t_start.tv_usec;
    cout << TEST_COUNT << "次 [pb反序列化]耗时:" << time_used / 1000 << "ms." << endl;
  }

  // ------------------------------JSON 序列化------------------------------------
  {
    Json::Value json_people;
    createPeopleInfoFromJson(json_people);
    Json::StreamWriterBuilder builder;
    count = TEST_COUNT;
    gettimeofday(&t_start, NULL);
    // 序列化count次
    while ((count--) > 0) {
      json_str = Json::writeString(builder, json_people);
    }
    gettimeofday(&t_end, NULL);
    // 打印序列化结果
    // cout << "json: " << endl << json_str << endl;
    time_used=1000000*(t_end.tv_sec - t_start.tv_sec) + t_end.tv_usec - t_start.tv_usec;
    cout << TEST_COUNT << "次 [json序列化]耗时:" << time_used/1000 << "ms."
         << " 序列化后的大小:" << json_str.length() << endl;

  }

  // ------------------------------JSON 反序列化------------------------------------
  {
    Json::CharReaderBuilder builder;
    unique_ptr<Json::CharReader> reader(builder.newCharReader());
    Json::Value json_people;
    count = TEST_COUNT;
    gettimeofday(&t_start, NULL);
    // 反序列化count次
    while ((count--) > 0) {
      reader->parse(json_str.c_str(), json_str.c_str() + json_str.length(), &json_people, nullptr);
    }
    gettimeofday(&t_end, NULL);
    time_used=1000000*(t_end.tv_sec - t_start.tv_sec) + t_end.tv_usec - t_start.tv_usec;
    cout << TEST_COUNT << "次 [json反序列化]耗时:" << time_used/1000 << "ms." << endl;
  }

  return 0;
}

/**
 * 构造pb对象
 */
void createPeopleInfoFromPb(PeopleInfo *people_info_ptr)
{
  people_info_ptr->set_name("张珊");
  people_info_ptr->set_age(20);
  people_info_ptr->set_qq("95991122");

  for(int i = 0; i < 5; i++) {
    PeopleInfo_Phone* phone = people_info_ptr->add_phone();
    phone->set_number("110112119");
    phone->set_type(PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);
  }

  Address address;
  address.set_home_address("陕西省西安市长安区");
  address.set_unit_address("陕西省西安市雁塔区");
  google::protobuf::Any * data = people_info_ptr->mutable_data();
  data->PackFrom(address);

  people_info_ptr->mutable_remark()->insert({"key1", "value1"});
  people_info_ptr->mutable_remark()->insert({"key2", "value2"});
  people_info_ptr->mutable_remark()->insert({"key3", "value3"});
  people_info_ptr->mutable_remark()->insert({"key4", "value4"});
  people_info_ptr->mutable_remark()->insert({"key5", "value5"});
}

/**
 * 构造json对象
 */
void createPeopleInfoFromJson(Json::Value& root) {
  root["name"] = "张珊";
  root["age"] = 20;
  root["qq"] = "95991122";

  for(int i = 0; i < 5; i++) {
    Json::Value phone;
    phone["number"] = "110112119";
    phone["type"] = 0;
    root["phone"].append(phone);
  }

  Json::Value address;
  address["home_address"] = "陕西省西安市长安区";
  address["unit_address"] = "陕西省西安市雁塔区";
  root["address"] = address;

  Json::Value remark;
  remark["key1"] = "value1";
  remark["key2"] = "value2";
  remark["key3"] = "value3";
  remark["key4"] = "value4";
  remark["key5"] = "value5";
  root["remark"] = remark;
}

由实验结果可得:

  • 编解码性能:ProtoBuf 的编码解码性能,⽐ JSON ⾼出 2-4 倍。
  • 内存占⽤:ProtoBuf 的内存278,⽽JSON到达567,ProtoBuf的内存占⽤只有JSON的1/2。
    注:以上结论的数据只是根据该项实验得出。因为受不同的字段类型、字段个数等影响,测出的数据 会有所差异。
    该实验有很多可待优化的地⽅。但其实这种粗略的测试,也能看出来ProtoBuf的优势。

1. ProtoBuf、XML和JSON对比

1.1 XML(eXtensible Markup Language)
  • 格式:标记语言,类似于 HTML,数据结构是基于标签的层次结构。
  • 优点
    • 可读性强,人类容易理解。
    • 支持复杂的数据结构,可以包含属性、命名空间等。
    • 广泛应用于配置文件和文档传输(如 SOAP)。
  • 缺点
    • 冗余,格式占用的字节多,传输和解析效率低。
    • 需要更复杂的解析器。
  • 使用场景:适用于数据格式需要有可读性或存在复杂嵌套结构的场合,如文档处理和配置文件。
1.2 JSON(JavaScript Object Notation)
  • 格式:轻量级数据交换格式,基于键值对和数组的结构,类似于 JavaScript 的对象。
  • 优点
    • 简洁,结构简单,体积较小。
    • 易于解析,支持大多数编程语言的内置或第三方解析库。
    • 可读性较好,适合人类和机器同时读取。
  • 缺点
    • 不支持自定义数据类型,结构较 XML 简单,适合相对简单的嵌套。
    • 数据格式容易出错(如缺少逗号或引号)。
  • 使用场景:适用于网络数据传输(如 RESTful API),特别是 Web 应用程序中数据交互。
1.3 Protobuf(Protocol Buffers)
  • 格式:二进制序列化格式,由 Google 开发。
  • 优点
    • 高效紧凑,体积小,传输速度快。
    • 支持静态类型定义,有强类型验证。
    • 能够定义复杂的嵌套结构,支持枚举、嵌套消息等数据类型。
  • 缺点
    • 可读性差,人类难以直接理解(因使用二进制格式)。
    • 需要先定义 .proto 文件,序列化和反序列化时必须依赖定义的结构。
  • 使用场景:适用于内部服务间的高效通信、低带宽传输和移动设备上资源受限的应用场合。
1.4 选择建议
  • XML:适用于需要复杂结构、强数据描述和高可读性的场合。
  • JSON:适合轻量级数据交换和需要兼容性的场合,如 Web 服务。
  • Protobuf:适合高性能、高效传输的需求,如服务端间的通信或需要节省带宽的应用。


http://www.kler.cn/a/586644.html

相关文章:

  • 医院手术麻醉信息系统是如何为医院提质增效的?
  • spring boot实现程序运行过程中数据源动态切换
  • Linux文件管理练习
  • k8s调度机制:亲和性,污点,容忍
  • 4.0 相机引导XY轴控制螺丝枪打螺丝
  • /etc/sysconfig/jenkins 没有这个文件
  • ffmpeg 添加毫秒时间戳
  • Logo语言的移动应用安全
  • Ansible 自动化运维
  • java手机号、邮箱、日期正则表达式
  • 向量数据库对比以及Chroma操作
  • 数据分布偏移检测:保障模型在生产环境中的稳定性
  • 鸿蒙 @ohos.arkui.observer (无感监听)
  • vue3:八、登录界面实现-忘记密码
  • DeepSeek与人工智能:技术演进、架构解析与未来展望
  • Hive SQL 精进系列:REGEXP_COUNT 函数的用法
  • 函数指针/逗号表达式/不用if语句完成的字母输出题
  • React 和 Vue 框架设计原理对比分析
  • 乐观锁VS分布式锁实现抢单服务
  • windows安装两个或多个JDK,并实现自由切换