Bootstrap

【Protobuf】基本使用总结+项目实践

概述

序列化与反序列化

网络传输中使用,可以实现将对象转换为二进制序列,然后将二进制序列转换为对象,这一个交互的过程就是序列化。生成的数据,持久化存储到磁盘上的过程,也需要经过序列化和反序列化才可以实现。

  • 序列化:把对象转换为字节序列的过程,序列化就像把一堆不同形状的物品压缩打包成一个标准的箱子,这样就可以轻松的放入仓库或者运输到其他地方
  • 反序列化:把字节序列解压成原来的对象,恢复成可以正常使用的数据,类似于拆开箱子,取出原来的物品

序列化场景分析

  • 存储数据:将某个对象存储到文件或者数据库的时候,需要将其转化为序列化格式,因为我们是不可以直接将内存中的对象直接存储的,必须将其打包后再进行存储
  • 网络传输:网络只可以传输字节,而无法直接传输数据对象,因此在发送对象之前,必须将对象序列化为字节序列,传输完毕后,接收端再将字节序列反序列成原来的对象。这就好似如果想要向网络发送一组数据,需要将其整理成一个标准的数据包,传输后再进行还原

实现序列化的方法

目前主流方法,Protobuf、XML、JSON ,这些都是将数据进行序列化的一种方式

  • XML:早起常用的标记语言格式,相对于比较笨重,传输效率不高
  • JSON :轻量级的文本数据格式,适合网络传输和人类阅读,但是因为其是文本格式,在性能和效率上是比不过二进制的

Protobuf特点

  • 语言、平台无关:支持多种编程语言,以及多个平台
  • 高效:比XML更小、更快、更简单
  • 拓展性与兼容性好:支持更新数据结构,不影响和破坏原有的旧程序

网络不直接传输对象的原因

网络传输的本质是比特和字节,这些才是网络传输中可以识别的“语言”。但是计算机中有些结构并不是简单的比特或者字节,例如对象、数组等。

因此想要通过网络传输这些负责的数据时,必须先转换成网络能够理解的比特和字节,这也就是的需要序列化的根本原因。

Protobuf的使用过程

编写.proto文件

.proto文件是用于定义数据结构的协议文件,在这个文件中的,可以定义消息类型以及每个消息属性,message可以理解为数据结构,也就是要传递的对象。

syntax = "proto3";

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

编译.proto文件

使用编译器编译.proto文件后,这些源文件包含了对象序列化和反序列化的方法。

例如编译后代码会生成用来处理Person对象的各种方法,例如序列化(转成二进制)和反序列化(二进制还原成对象)的方法。

生成的代码用于业务逻辑

具体业务中,业务代码可以直接编译生成的接口,比如序列化和反序列化的方法,处理从.proto文件定义的消息。所以在具体使用中,只需调用对应的接口即可,不需要自己实现序列化和反序列化的逻辑。

项目实践

1.0 版本

指定使用proto的版本

syntax = "proto3";

package声明符

表明.proto文件的命名空间,也就是在项目中具有唯一性。其作用就是要避免我们定义的消息出现冲突。 该声明符就是为了防止在多人合作场景下,不同人定义了相同的名称消息类型,从而导致命名冲突,所以使用该声明符的目的就是避免该冲突。

syntax = "proto3";   // 指定使用 proto3 语法
package contacts;    // 声明 package 名称为 contacts

 消息的定义格式

message PeopleInfo {
    int32 id = 1;
    string name = 2;
    string email = 3;
}
  • 定义消息格式是Protobuf实现的关键部分,用于定义数据结构
  • 每个message包含多个字段,字段类型和编号是必须唯一的
  • 通过Protobuf,数据可以被高效序列化并在不同系统中进行传输

 字段基本格式

  • 字段类型:例如int32表示的是字段数据类型
  • 字段名称:给该字段起一个别名
  • 字段唯一编号:字段的标识符、必须是正整数而且是唯一的,只要设定了就不可以改变
int32 id = 1;
string name = 2;

字段类型 字段名称 = 字段唯一编号;

字段类型

  • 标准数据类型:也就是常见的一些数据类型,包括整数、浮点数、布尔值等
    • int32,int64:32位或者64位整数
    • float,double:单精度和双精度浮点数
    • bool :布尔值
  • 特殊类型
    • 枚举类型:一般用于定义固定值的集合,适用于状态、类型等字段
    • 嵌套message类型:可以将message作为另一个message的字段,从而表示复杂的嵌套结构
message Address {
    string street = 1;
    string city = 2;
}

message Person {
    string name = 1;
    Address address = 2; // 嵌套的 Address message
}

字段编号

  • 每个字段必须分配一个唯一的正整数编号,字段编号一旦使用就不可以更改,因为Protobuf在序列化和反序列化都是根据这些编号来映射字段的
  • 编号在1--15中间值最优,大于这个值也可以
message Person {
    int32 id = 1;         // 唯一标识
    string name = 2;      // 名字
    int32 age = 3;        // 年龄
    bool is_active = 4;   // 活跃状态
}

编译格式 

protoc [--proto_path=IMPORT_PATH] --cpp_out=DST_DIR path/to/file.proto
  •  protoc:指定编译工具
  • --proto_path = IMPORT_PATH:指定.proto文件的导入路径,其中IMPORT_PATH就是需要编译的.proto文件所在的路径(如果不指定参数的话,则会在当前目录中寻找相应文件)
  • --cpp_out=DST_DIR:指定生成C++代码的输出目录
  • path...:需要编译.proto文件的路径,可以是相对路径也可以是绝对路径
//具体事例
protoc --cpp_out=. contacts.proto

 生成文件分析

  • .pb.h文件:头文件,其中包含消息类型的声明与Protobuf相关的序列化、反序列化方法
  • .pb.cc文件:源文件,包含消息类型的实现和相关序列化、反序列化代码
  • 生成的文件中包含序列化和反序列化方法

整合测试

// .proto文件

syntax = "proto3";

package example;

message Person {
  string name = 1;
  int32 id = 2;
  string email = 3;
}
// test.cc

#include <iostream>
#include <fstream>
#include "example.pb.h"

using namespace example;

int main() {
    // 创建一个 Person 消息实例
    Person person;
    person.set_name("张三");
    person.set_id(1234);
    person.set_email("[email protected]");

    // 序列化后的数据以二进制的方式写入到文件
    std::ofstream output("person.dat", std::ios::binary);
    if (!person.SerializeToOstream(&output)) {
        std::cerr << "Failed to write serialized message." << std::endl;
        return -1;
    }

    // 清空输出流
    output.close();

    // 从文件中反序列化消息
    Person restored_person;
    std::ifstream input("person.dat", std::ios::binary);
    if (!restored_person.ParseFromIstream(&input)) {
        std::cerr << "Failed to parse message from file." << std::endl;
        return -1;
    }

    // 输出反序列化后的消息
    std::cout << "Restored Person:" << std::endl;
    std::cout << "Name: " << restored_person.name() << std::endl;
    std::cout << "ID: " << restored_person.id() << std::endl;
    std::cout << "Email: " << restored_person.email() << std::endl;

    return 0;
}

2.0 版本

重新编译一次.proto文件

此处对文件进行重新编译后,编译生成的文件会直接将contacts.pb.h和contacts.pb.cc文件覆盖掉 

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;
}
// 编译命令
protoc --cpp_out=.contacts.proto

 Protobuf常见操作方法

 Protobuf会为每一个字段生成一个API接口

Protobuf生成的代码为每个字段都提供了常用的操作方法

  • clear_:重置字段
  • set_和getter:设置和获取字段值
  • mutable_:修改嵌套消息对象
  • add_和_size:用于操作repeated字段

清除字段

每个字段都有一个以clear_开头的方法,这个方法会将字段的值重置为空

person.clear_name();  // 将 person 对象的 name 字段重置为空字符串

字段设置和获取方法

set_开头的方法就是用于设置字段数值的,获取某些数值则用小写方式获取

person.set_name("张三");
std::string name = person.name();  // 获取 name 字段的值

 获取嵌套消息指针mutable_方法

mutable_方法可以用来获取嵌套消息指针并允许直接修改嵌套的消息对象

PeopleInfo::Phone* phone = person.mutable_phone();
phone->set_number("123-4567");

 处理repeated字段

add_方法

通过该方法可以向repeated字段中添加新元素,即每次调用该元素的时候,都会向数组中添加一个新元素

person.add_phone()->set_number("123-4567");

_size方法

用于获取repeated字段的元素个数,用于判断当前数组中包含多少个元素

int phone_count = person.phone_size();  // 获取 phone 字段的元素个数

数据输入与持久化

实现逻辑梳理

  • 检测输入参数,可以通过命令行指定通讯录文件,如果没有正确的文件路径会输出错误信息提
  • 读取或创建通讯录文件,如果文件不存在就会创建一个新文件,如果文件存在则会反序列化已经有的通讯录数据
  • 添加新的联系人,通过AddPeopleInfo函数去获取输入联系人的信息,然后将其添加到contacts中,简单来说就是添加联系人追加到现有的通讯录中
  • 将联系人保存到文件中,文件保存为二进制的形式,下一次程序运行的时候可以重新读取并继续操作

具体实现

设计并编译.proto文件

syntax = "proto3";

package contacts;

message PeopleInfo {
    string name = 1;
    int32 age = 2;
    repeated PhoneInfo phones = 3;
}

message PhoneInfo {
    string number = 1;
}

message Contacts {
    repeated PeopleInfo contacts = 1;
}

创建write.cc

#include <iostream>
#include <fstream>
#include <string>
#include "contacts.pb.h"  // 引入生成的 Protobuf 头文件

using namespace std;
using namespace contacts;  // 使用 contacts 命名空间

// 添加联系人信息
void AddPeopleInfo(PeopleInfo* people_info_ptr) {
    cout << "请输入联系人姓名: ";
    string name;
    getline(cin, name);
    people_info_ptr->set_name(name);

    cout << "请输入联系人年龄: ";
    int age;
    cin >> age;
    cin.ignore();  // 忽略换行符

    people_info_ptr->set_age(age);

    cout << "请输入电话号码: ";
    string number;
    while (true) {
        cout << "输入号码(按回车键结束): ";
        getline(cin, number);
        if (number.empty()) {
            break;
        }
        PhoneInfo* phone = people_info_ptr->add_phones();
        phone->set_number(number);
    }
}

int main(int argc, char* argv[]) {
    GOOGLE_PROTOBUF_VERIFY_VERSION;

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

    Contacts contacts;

    // 读取现有的通讯录数据
    fstream input(argv[1], ios::in | ios::binary);
    if (!input) {
        cout << argv[1] << ": File not found. Creating a new file." << endl;
    } else if (!contacts.ParseFromIstream(&input)) {
        cerr << "Failed to parse contacts." << endl;
        return -1;
    }

    // 添加新的联系人
    AddPeopleInfo(contacts.add_contacts());

    // 将联系人信息序列化到文件中
    fstream output(argv[1], ios::out | ios::trunc | ios::binary);
    if (!contacts.SerializeToOstream(&output)) {
        cerr << "Failed to write contacts." << endl;
        return -1;
    }

    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}

Makefile

write: write.cc contacts.pb.cc
	g++ -o write write.cc contacts.pb.cc -std=c++11 -lprotobuf

.PHONY: clean
clean:
	rm -f write

2.1版本

基于上述版本,在用户信息中添加了输入多个用户电话的选项

更新联系人的Proto

 修改write.cc文件,在输入的时候设置可以输入多个用户信息的选项

修改read.cc文件 

整合编译

源码归纳

// contact.proto

syntax = "proto3";

package contacts;

// 定义人员信息的message
message PeopleInfo {
    string name = 1;   // 姓名
    int32 age = 2;     // 年龄

    // 定义电话号码的message
    message Phone {
        string number = 1;  // 电话号码

        // 定义枚举类型,表示电话号码类型
        enum PhoneType {
            MP = 0;  // 移动电话
            TEL = 1; // 固定电话
        }

        PhoneType type = 2;  // 电话类型字段
    }

    repeated Phone phone = 3;  // 人员信息可以有多个电话
}

// 定义通讯录
message Contacts {
    repeated PeopleInfo contacts = 1;
}
// write.cc

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

using namespace std;
using namespace 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 << "请输入联系人电话(只按回车跳过添加新电话): ";
        string number;
        getline(cin, number);
        if (number.empty()) break;

        PeopleInfo::Phone* phone = people_info_ptr->add_phone();
        phone->set_number(number);

        // 选择电话号码类型
        cout << "选择电话号码类型 (1: 移动电话, 2: 固定电话): ";
        int type;
        cin >> type;
        cin.ignore(256, '\n');
        switch (type) {
            case 1:
                phone->set_type(PeopleInfo::Phone::MP);  // 设置为移动电话
                break;
            case 2:
                phone->set_type(PeopleInfo::Phone::TEL); // 设置为固定电话
                break;
            default:
                cout << "无效选择,使用默认值" << endl;
        }
    }

    cout << "------添加联系人成功------" << endl;
}

int main(int argc, char* argv[]) {
    GOOGLE_PROTOBUF_VERIFY_VERSION;

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

    Contacts contacts;

    // 从文件读取已有的联系人信息
    fstream input(argv[1], ios::in | ios::binary);
    if (!input) {
        cout << argv[1] << ": File not found. Creating a new file." << endl;
    } else if (!contacts.ParseFromIstream(&input)) {
        cerr << "Failed to parse contacts." << endl;
        return -1;
    }

    // 新增联系人
    AddPeopleInfo(contacts.add_contacts());

    // 将联系人信息写回文件
    fstream output(argv[1], ios::out | ios::trunc | ios::binary);
    if (!contacts.SerializeToOstream(&output)) {
        cerr << "Failed to write contacts." << endl;
        return -1;
    }

    google::protobuf::ShutdownProtobufLibrary();

    return 0;
}
// read.cc
#include <iostream>
#include <fstream>
#include "contacts.pb.h"

using namespace std;
using namespace contacts;

// 打印联系人信息
void PrintContacts(const Contacts& contacts) {
    for (int i = 0; i < contacts.contacts_size(); i++) {
        const PeopleInfo& person = contacts.contacts(i);
        cout << "-----联系人 " << i+1 << " -----" << endl;
        cout << "姓名: " << person.name() << endl;
        cout << "年龄: " << person.age() << endl;
        
        for (int j = 0; j < person.phone_size(); j++) {
            const PeopleInfo::Phone& phone = person.phone(j);
            cout << "电话 " << j + 1 << ": " << phone.number();
            cout << " (" << PeopleInfo::Phone::PhoneType_Name(phone.type()) << ")" << endl;
        }
    }
}

int main(int argc, char* argv[]) {
    GOOGLE_PROTOBUF_VERIFY_VERSION;

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

    Contacts contacts;

    // 从文件中读取联系人信息
    fstream input(argv[1], ios::in | ios::binary);
    if (!input) {
        cerr << argv[1] << ": File not found." << endl;
        return -1;
    } else if (!contacts.ParseFromIstream(&input)) {
        cerr << "Failed to parse contacts." << endl;
        return -1;
    }

    // 打印联系人信息
    PrintContacts(contacts);

    google::protobuf::ShutdownProtobufLibrary();

    return 0;
}

2.2 版本

引入Any类型到通讯录版本中

源码省略,基于2.1代码更改即可 

2.3 版本

引入oneof 类型

 

修改核心代码 

完整代码 

// write.cc

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

using namespace std;
using namespace 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 << "请输入联系人电话(只按回车跳过添加新电话): ";
        string number;
        getline(cin, number);
        if (number.empty()) break;

        PeopleInfo::Phone* phone = people_info_ptr->add_phone();
        phone->set_number(number);

        // 选择电话号码类型
        cout << "选择电话号码类型 (1: 移动电话, 2: 固定电话): ";
        int type;
        cin >> type;
        cin.ignore(256, '\n');
        switch (type) {
            case 1:
                phone->set_type(PeopleInfo::Phone::MP);  // 设置为移动电话
                break;
            case 2:
                phone->set_type(PeopleInfo::Phone::TEL); // 设置为固定电话
                break;
            default:
                cout << "无效选择,使用默认值" << endl;
        }
    }

    //Any补充
    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);
    google::protobuf::Any * data = people_info_ptr->mutable_data();
    data->PackFrom(address);//address打包到any类型中

    //oneof类型添加内容2.3 
    cout<<"选择将要添加的其他联系方式(1.QQ号 2-微信号):";
    int other_contact;
    cin>>other_contact;
    cin.ignore(256,'\n');
    if(other_contact ==1)
    {
        cout<<"请输入QQ号:";
        string qq;
        getline(cin,qq);
        people_info_ptr->set_qq(qq);
    }else if(other_contact ==2){
        cout<<"请输入微信号的:";
        string weixin;
        getline(cin,weixin);
        people_info_ptr->set_weixin(weixin);
    }else{
        cout<<"非法选项!"<<endl;
    }

    cout << "------添加联系人成功------" << endl;
}

int main(int argc, char* argv[]) {
    GOOGLE_PROTOBUF_VERIFY_VERSION;

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

    Contacts contacts;

    // 从文件读取已有的联系人信息
    fstream input(argv[1], ios::in | ios::binary);
    if (!input) {
        cout << argv[1] << ": File not found. Creating a new file." << endl;
    } else if (!contacts.ParseFromIstream(&input)) {
        cerr << "Failed to parse contacts." << endl;
        return -1;
    }

    // 新增联系人
    AddPeopleInfo(contacts.add_contacts());

    // 将联系人信息写回文件
    fstream output(argv[1], ios::out | ios::trunc | ios::binary);
    if (!contacts.SerializeToOstream(&output)) {
        cerr << "Failed to write contacts." << endl;
        return -1;
    }

    google::protobuf::ShutdownProtobufLibrary();

    return 0;
}
// read.cc

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

using namespace std;
using namespace contacts;

// 打印联系人信息
void PrintContacts(const Contacts& contacts) {
    for (int i = 0; i < contacts.contacts_size(); ++i) {
        const PeopleInfo& person = contacts.contacts(i);

        // 打印基本信息
        cout << "姓名: " << person.name() << endl;
        cout << "年龄: " << person.age() << endl;

        // 打印电话信息
        for (int j = 0; j < person.phone_size(); ++j) {
            const PeopleInfo::Phone& phone = person.phone(j);
            cout << "电话 " << j + 1 << ": " << phone.number();
            cout << " (" << PeopleInfo::Phone::PhoneType_Name(phone.type()) << ")" << endl;
        }

        // 检查是否有 Address 信息,并解封 Address
        if (person.has_data() && person.data().Is<Address>()) {
            Address address;
            person.data().UnpackTo(&address);
            if(!address.home_address().empty())
            {
                cout<<"家庭地址:"<<address.home_address()<<endl;
            }
            if(!address.unit_address().empty())
            {
                cout<<"单位地址:"<< (address.unit_address()) <<endl;
            }
        }

        //oneof类型数据打印
        switch (person.other_contact_case()) {
        case PeopleInfo::OtherContactCase::kQq:  // 如果设置了 QQ 号
            cout << "qq号: " << person.qq() << endl;
            break;
        case PeopleInfo::OtherContactCase::kWeixin:  // 如果设置了微信号
            cout << "微信号: " << person.weixin() << endl;
            break;
        case PeopleInfo::OtherContactCase::OTHER_CONTACT_NOT_SET:  // 未设置
            cout << "没有设置其他联系信息。" << endl;
            break;
    }

        cout << "--------------------------------------" << endl;
    }
}

int main(int argc, char* argv[]) {
    GOOGLE_PROTOBUF_VERIFY_VERSION;

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

    Contacts contacts;

    // 从文件中读取联系人信息
    fstream input(argv[1], ios::in | ios::binary);
    if (!input) {
        cerr << argv[1] << ": File not found." << endl;
        return -1;
    } else if (!contacts.ParseFromIstream(&input)) {
        cerr << "Failed to parse contacts." << endl;
        return -1;
    }

    // 打印联系人信息
    PrintContacts(contacts);

    google::protobuf::ShutdownProtobufLibrary();

    return 0;
}
// contacts.proto

syntax = "proto3";

package contacts;

import"google/protobuf/any.proto";

//地址
message Address{
    string home_address = 1;
    string unit_address = 2;
}

// 定义人员信息的message
message PeopleInfo {
    string name = 1;   // 姓名
    int32 age = 2;     // 年龄

    // 定义电话号码的message
    message Phone {
        string number = 1;  // 电话号码

        // 定义枚举类型,表示电话号码类型
        enum PhoneType {
            MP = 0;  // 移动电话
            TEL = 1; // 固定电话
        }

        PhoneType type = 2;  // 电话类型字段
    }

    repeated Phone phone = 3;  // 人员信息可以有多个电话
    google.protobuf.Any data = 4;// Any类型数据

    oneof other_contact
    {
        string qq = 5;
        string weixin = 6;
    }
}



// 定义通讯录
message Contacts {
    repeated PeopleInfo contacts = 1;
}

2.4版本

使用map类型表示新增的联系人信息

 

3.0版本

未知字段+网络版本+option选项

 

 

Client代码 

// client.cc

#include <iostream>
#include <fstream>
#include<google/protobuf/unknown_field_set.h>
#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<<
        // }
    }
}



int main(int argc, char*argv[])
{
    GOOGLE_PROTOBUF_VERIFY_VERSION;

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

    Contacts contacts;
    fstream input(argv[1],ios::in | ios::binary);
    if(!contacts.ParseFromIstream(&input)){
        cerr<<"Failed to parse contacts."<<endl;
        input.close();
        return -1;
    }

    PrintfContacts(contacts);

    input.close();
    google::protobuf::ShutdownProtobufLibrary();

    return 0;
}
//contacts.proto

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;
}
// makefile (Server相似略)

client:client.cc contacts.pb.cc
	g++ -o $@ $^ -std=c++11 -lprotobuf
	
.PHONY:clean
clean:
	rm -f client

Server代码 

 

// contacts.proto

syntax = "proto3";
package s_contacts;

message PeopleInfo{
    reserved 2;

    string name =1;
    
    int32 birthday = 4;


    message Phone{
        string number =1;
    }
    repeated Phone phone =3;
}

message Contacts{
    repeated PeopleInfo contacts =1;
}
//server.cc

#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');

    cout<<"请输入联系人生日: ";
    int birthday;
    cin>>birthday;
    people_info_ptr->set_birthday(birthday);
    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(int argc,char*argv[])
{
    GOOGLE_PROTOBUF_VERIFY_VERSION;

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

    Contacts contacts;
    // 先读取已存在的 contacts
    fstream input(argv[1], ios::in | ios::binary);
    if (!input)
    {
        cout << argv[1] << ": File 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(argv[1], 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();
    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}

proto 3 语法

字段规则修饰符

singular(默认)

singular用于表示字段在消息中出现0次或者1次。也就是说该字段要么不存在,要么只出现一次。proto3中,singular是默认规则,因此字段声明的时候不需要显式添加修饰符。

message Person {
    string name = 1;  // 默认是 singular,最多出现一次
    int32 age = 2;    // 默认是 singular,最多出现一次
}

repeated

repeated表示该字段在消息中可以出现0次或者多次,可以简单理解成为数组,也就是说该字段可以保存多个数值。

下面的事例表示一个联系人可以有多个电话号码

message Person {
    repeated string phone_numbers = 3;  // 一个联系人可以有多个电话号码
}

Protobuf调用规则总结

完整事例理解

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

using namespace std;
using namespace contacts;

int main() {
    // 创建 PeopleInfo 实例
    PeopleInfo person;
    
    // 使用 set_ 方法设置 name 和 age
    person.set_name("Alice");
    person.set_age(30);
    
    // 获取并打印 name 和 age
    cout << "姓名: " << person.name() << endl;
    cout << "年龄: " << person.age() << endl;

    return 0;
}

Message的调用

set_和get_方法

message PeopleInfo {
    string name = 1;
    int32 age = 2;
}
  • set_方法:主要就是用于给某个字段赋值

  • get_方法:获取某个字段的数值

// 设置字段的值
PeopleInfo person;
person.set_name("Alice");
person.set_age(25);

// 获取字段的值
std::string name = person.name();
int age = person.age();

基于完整事例的设置和获取 

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

using namespace std;
using namespace contacts;

int main() {
    // 创建 PeopleInfo 实例
    PeopleInfo person;
    
    // 使用 set_ 方法设置 name 和 age
    person.set_name("Alice");
    person.set_age(30);
    
    // 获取并打印 name 和 age
    cout << "姓名: " << person.name() << endl;
    cout << "年龄: " << person.age() << endl;

    return 0;
}

repeated字段调用

类似于数组或者列表,Protobuf会给该字段生成方法来增加、获取以及遍历

message PeopleInfo {
    repeated Phone phone = 3;
}

添加元素:注意该处的使用方法,主要就是使用add_方法实现

PeopleInfo person;
PeopleInfo::Phone* phone = person.add_phone();
phone->set_number("123456789");
phone->set_type(PeopleInfo::Phone::MP);

获取元素:使用message.field_size()方法获取repeated字段的长度,然后通过下标访问元素(注意该处的使用方法与其他不同

for (int i = 0; i < person.phone_size(); i++) {
    std::string number = person.phone(i).number();
    PeopleInfo::Phone::PhoneType type = person.phone(i).type();
}

 完整事例代码

int main() {
    PeopleInfo person;
    person.set_name("Bob");
    person.set_age(25);

    // 使用 add_ 方法添加多个电话号码
    PeopleInfo::Phone* phone1 = person.add_phone();
    phone1->set_number("123456789");
    phone1->set_type(PeopleInfo::Phone::MP);

    PeopleInfo::Phone* phone2 = person.add_phone();
    phone2->set_number("987654321");
    phone2->set_type(PeopleInfo::Phone::TEL);

    // 遍历 phone 字段
    for (int i = 0; i < person.phone_size(); i++) {
        const PeopleInfo::Phone& phone = person.phone(i);
        cout << "电话 " << i + 1 << ": " << phone.number();
        cout << " (" << PeopleInfo::Phone::PhoneType_Name(phone.type()) << ")" << endl;
    }

    return 0;
}

 枚举类型的调用

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

 通过set_type()方法为phoneType字段赋值

PeopleInfo::Phone phone;
phone.set_type(PeopleInfo::Phone::MP);  // 设置电话类型为移动电话

获取枚举资源的类型,也就是Enum_Name()方法 

PeopleInfo::Phone::PhoneType type = phone.type();
std::cout << PeopleInfo::Phone::PhoneType_Name(type) << std::endl;  // 输出"MP"

完整代码事例 

int main() {
    PeopleInfo person;
    person.set_name("Charlie");
    person.set_age(40);

    // 添加电话号码并设置枚举类型
    PeopleInfo::Phone* phone = person.add_phone();
    phone->set_number("1357924680");
    phone->set_type(PeopleInfo::Phone::MP);  // 设置为移动电话

    // 输出枚举类型为可读的字符串
    cout << "电话号码: " << phone->number() << endl;
    cout << "电话类型: " << PeopleInfo::Phone::PhoneType_Name(phone->type()) << endl;

    return 0;
}

序列化与反序列化

序列化,将contacts数据写入到文件contacts.bin文件中

fstream output("contacts.bin", ios::out | ios::trunc | ios::binary);
if (!contacts.SerializeToOstream(&output)) {
    cerr << "Failed to write contacts." << endl;
}

反序列化:从文件contacts.bin中读取并解析数据,然后填充到contacts对象中 

fstream input("contacts.bin", ios::in | ios::binary);
if (!contacts.ParseFromIstream(&input)) {
    cerr << "Failed to parse contacts." << endl;
}

完整代码事例(结合开头代码理解) 

int main() {
    // 创建并填充 PeopleInfo 实例
    PeopleInfo person;
    person.set_name("Dave");
    person.set_age(22);
    PeopleInfo::Phone* phone = person.add_phone();
    phone->set_number("5551234");
    phone->set_type(PeopleInfo::Phone::TEL);

    // 序列化到文件
    fstream output("contacts.bin", ios::out | ios::trunc | ios::binary);
    if (!person.SerializeToOstream(&output)) {
        cerr << "Failed to write to file." << endl;
        return -1;
    }
    output.close();

    // 从文件反序列化
    PeopleInfo new_person;
    fstream input("contacts.bin", ios::in | ios::binary);
    if (!new_person.ParseFromIstream(&input)) {
        cerr << "Failed to read from file." << endl;
        return -1;
    }
    input.close();

    // 输出反序列化后的内容
    cout << "姓名: " << new_person.name() << endl;
    cout << "年龄: " << new_person.age() << endl;
    for (int i = 0; i < new_person.phone_size(); i++) {
        const PeopleInfo::Phone& phone = new_person.phone(i);
        cout << "电话 " << i + 1 << ": " << phone.number();
        cout << " (" << PeopleInfo::Phone::PhoneType_Name(phone.type()) << ")" << endl;
    }

    return 0;
}

消息类型的定义与使用

嵌套消息类型

一个消息体中嵌套着另一个消息体,通过这种方式将消息组织的更加紧密

syntax = "proto3";
package contacts;

message PeopleInfo {
    string name = 1;  // 联系人姓名
    int32 age = 2;    // 联系人年龄
    
    // 嵌套定义 phone_number
    message Phone {
        string number = 1;  // 电话号码
    }
}

非嵌套消息类型

syntax = "proto3";
package contacts;

// 独立定义 phone_number
message Phone {
    string number = 1;  // 电话号码
}

// 独立定义联系人信息
message PeopleInfo {
    string name = 1;  // 联系人姓名
    int32 age = 2;    // 联系人年龄
}

跨文件导入消息

phone.proto文件 

syntax = "proto3";
package phone;

message Phone {
    string number = 1;  // 电话号码
}

contacts.proto文件

通过import引入文件,然后通过phone.Phone来调用另一个文件中的消息类型

syntax = "proto3";
package contacts;

import "phone.proto";  // 使用 import 导入 phone.proto 文件

message PeopleInfo {
    string name = 1;  // 联系人姓名
    int32 age = 2;    // 联系人年龄

    repeated phone.Phone phone = 3;  // 使用导入的 Phone 消息
}

repeated类型字段

Protobuf会为repeated字段生成一组函访问器函数

add_contacts()

  • 作用:用于向contacts字段中追加一个新的peopleInfo对象(对应2.0版本)
  • 返回值:返回一个新添加的peopleInfo对象的指针,这样就可以对这个新对象设置各个字段的值
  • 实现机制:每次调用该函数,Protobuf底层会动态调整contacts数组的大小,并返回一个指向新分配的peopleInfo对象的指针
Contacts contacts;  // 定义一个 Contacts 对象

// 添加一个新的 PeopleInfo 对象到 contacts 中,并获取该对象的指针
PeopleInfo* new_contact = contacts.add_contacts();

// 设置新联系人信息
new_contact->set_name("John Doe");
new_contact->set_age(30);

// 添加电话号码
PhoneInfo* phone = new_contact->add_phones();
phone->set_number("1234567890");

// 再添加一个电话号码
phone = new_contact->add_phones();
phone->set_number("0987654321");

contacts_size()

  • 作用:返回contacts字段中的元素个数,也就是已经添加的peopleInfo对象的数量
int size = contacts.contacts_size();
std::cout << "Number of contacts: " << size << std::endl;

contacts(int index)

  • 作用:获取contacs中下标index个元素,可以用它来读取或者修改某个特定位置的peopleInfo
  • const版本只可以读,非const版本允许修改
// 获取第一个联系人
const PeopleInfo& first_contact = contacts.contacts(0);
std::cout << "First contact's name: " << first_contact.name() << std::endl;

mutable_contacts(int index)

  • 作用:返回该索引位置对象可变指针,同时允许直接修改该索引位置上的对象
PeopleInfo* mutable_contacts(int index);

PeopleInfo* contact = contacts.mutable_contacts(0);
contact->set_name("Jane Doe");

 clear_contacts()

  • 作用:清空该字段中的所以元素,然后重置初始状态
contacts.clear_contacts();

mutable_contacts()

  • 作用:返回整个contacts字段的指针,用于进行更加底层的操作
// 原型
::google::protobuf::RepeatedPtrField< PeopleInfo >* mutable_contacts();


// 使用
auto* contacts_field = contacts.mutable_contacts();
for (int i = 0; i < contacts_field->size(); ++i) {
    PeopleInfo* contact = contacts_field->Mutable(i);
    // Modify contact...
}

 contacts()

  • 返回整个contacts字段的不可变引用,可以用于遍历或者读取整个peopleInfo对象,知识返回只读引用,无法修改其内容
const ::google::protobuf::RepeatedPtrField< PeopleInfo >& contacts() const;


const auto& contacts_field = contacts.contacts();
for (int i = 0; i < contacts_field.size(); ++i) {
    const PeopleInfo& contact = contacts_field.Get(i);
    std::cout << "Contact name: " << contact.name() << std::endl;
}

 enum类型

定义和使用的基本规则

  • enum使用大写字母和下划线进行命名
  • 枚举值必须是整数,并且第一个枚举数值必须是0,Protobuf3不做要求,但是Protobuf2做要求
  • 每个枚举值必须是唯一的,只可以是32位有符号整数
enum EnumName {
    VALUE_NAME = 0;
    ANOTHER_VALUE = 1;
}

enum使用事例

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

通讯录2.1版本结合看

枚举类型与message共同使用,整体实现参考通讯录2.1

message Phone {
    string number = 1;  // 电话号码
    PhoneType type = 2; // 电话类型
}

定义中必须遵循的原则

  • 第一个枚举值必须是0
  • 重复枚举值问题,如果同一个Protobuf文件中定义两个相同名称的枚举值(即使他们都属于不同的枚举类型),编译器也同样会报错的。为避免该问题,可以使用不同的package或者明确的命名规则
  • 最大值范围,枚举值必须在32位有符号整数范围内

代码中的具体使用

注意要明确自己传输的是哪一个枚举类型数值,其中set_type()是一个setter方法,主要就是用于设置phoneType枚举的值

PeopleInfo person;
Phone* phone = person.add_phones();
phone->set_number("123456789");
phone->set_type(PhoneType::MOBILE);  // 设置电话类型为 MOBILE

 通讯录引入enum后逻辑梳理

写入联系人逻辑梳理

  • 获取用户输入:程序提示用户输入姓名、年龄以及电话号码
  • 电话号码类型的选择:根据用户选择,将电话类型设置成移动电话或者固定电话
  • 写入数据到文件:将联系人信息保存到文件中,以供后续读取使用

读取联系人的数据逻辑

  • 从文件读取数据:从二进制文件中反序列化出联系人信息
  • 打印联系人信息: 按照联系人枚举类型,将联系人信息打印出来

Any类型

Any的组成

Protobuf中的Any允许存储任意类型的Protobuf消息,这也就解决了动态类型需求,也就是允许一个字段在存储和传输过程中动态变化,而不是一经编译类型就定死了

message Any {
  string type_url = 1;
  bytes value = 2;
}
  • type_url:字符串类型,保存序列化消息的类型URL
  • value:字节类型,保存序列化的二进制数据

Any类型的使用

使用Any类型的时候,必须将消息序列化为二进制格式后存入Any类型中的value字段中,同时消息类型信息要存储在type_url中

基本流程总结

  • 将消息封装到Any类型中,使用pack方法
  • 从Any类型中解封消息,使用Unpack方法

具体使用理解

定义nay.proto文件并编译

syntax = "proto3";  // 使用 Protobuf 3 语法

package example;

import "google/protobuf/any.proto";  // 导入 Any 类型

// 用户信息
message UserInfo {
    string username = 1;
    int32 age = 2;
}

// 产品信息
message ProductInfo {
    string product_name = 1;
    float price = 2;
}

// 通用响应消息,包含 Any 类型
message Response {
    google.protobuf.Any data = 1;  // Any 类型
}

编写Any字段的主逻辑,封装和解封消息的逻辑

#include<iostream>
#include<fstream>
#include"any.pb.h"
#include"google/protobuf/any.pb.h"

using namespace std;
using namespace example;
using google::protobuf::Any;

int main() {
    // Step 1: 创建 Response 对象,用于封装不同的消息类型
    Response response;

    // Step 2: 创建 UserInfo 消息
    UserInfo user;
    user.set_username("Alice");
    user.set_age(28);

    // Step 3: 使用 Any::Pack() 方法将 UserInfo 封装到 Any 中
    Any* any_data = response.mutable_data();  // 获取可修改的 Any 字段
    any_data->PackFrom(user);  // 将 UserInfo 封装到 Any 类型中

    // Step 4: 将 Response 序列化到文件中
    fstream output("response.bin", ios::out | ios::trunc | ios::binary);
    if (!response.SerializeToOstream(&output)) {
        cerr << "Failed to write response." << endl;
        return -1;
    }
    output.close();

    // Step 5: 从文件反序列化并读取 Response
    Response new_response;
    fstream input("response.bin", ios::in | ios::binary);
    if (!new_response.ParseFromIstream(&input)) {
        cerr << "Failed to read response." << endl;
        return -1;
    }
    input.close();

    // Step 6: 解封 Any 中的消息,判断是否为 UserInfo 类型
    Any any_read = new_response.data();
    if (any_read.Is<UserInfo>()) {  // 检查 Any 中是否是 UserInfo 类型
        UserInfo user_info_read;
        if (any_read.UnpackTo(&user_info_read)) {  // 解封消息
            cout << "UserInfo 解封成功:" << endl;
            cout << "用户名: " << user_info_read.username() << endl;
            cout << "年龄: " << user_info_read.age() << endl;
        }
    } else {
        cout << "Any 中的消息不是 UserInfo 类型。" << endl;
    }

    return 0;
}

代码执行逻辑梳理 

oneof 类型

基本语法使用

同一个消息中有多个互斥字段,任何时刻只可以有一个字段的数值,也就类似于二选一,该类型必须设计到message类型中。

syntax = "proto3";

package contacts;

// 定义 PeopleInfo 消息
message PeopleInfo {
    string name = 1;  // 姓名
    int32 age = 2;    // 年龄

    // 定义 oneof 字段,用于表示互斥的联系信息
    oneof other_contact {
        string qq = 3;      // QQ 号
        string weixin = 4;  // 微信号
    }
}

设置与清除数据的方法

设置oneof字段的值

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

using namespace std;
using namespace contacts;

int main() {
    PeopleInfo person;  // 创建 PeopleInfo 实例

    // 设置基本信息
    person.set_name("Alice");
    person.set_age(30);

    // 设置 oneof 字段的 QQ 号
    person.set_qq("12345678");

    // 打印信息
    cout << "姓名: " << person.name() << endl;
    cout << "年龄: " << person.age() << endl;
    cout << "qq号: " << person.qq() << endl;

    return 0;
}

清除oneof字段的数值

person.clear_qq();  // 清除 QQ 号

设置微信字段

person.set_weixin("alice_wechat");  // 设置微信号,自动清除之前的 QQ 号

 检查oneof当前的状态

  • *_case()方法可以用于返回oneof中当前设置的字段
  • other_contact_case()返回一个枚举,表示的是oneof中哪个字段被设置了
  • kQq \ KWeixin是Protobuf自动生成的枚举类型,分别代表的是qq字段和weixin字段被设置
  • OTHER_CONTACT_NOT_SET用于标识oneof中没有任何字段被设置
switch (person.other_contact_case()) {
    case PeopleInfo::OtherContactCase::kQq:
        cout << "qq号: " << person.qq() << endl;
        break;
    case PeopleInfo::OtherContactCase::kWeixin:
        cout << "微信号: " << person.weixin() << endl;
        break;
    case PeopleInfo::OtherContactCase::OTHER_CONTACT_NOT_SET:
        cout << "未设置联系信息" << endl;
        break;
}

序列化与反序列化的使用 

// 序列化 PeopleInfo 对象到文件
fstream output("peopleinfo.bin", ios::out | ios::trunc | ios::binary);
if (!person.SerializeToOstream(&output)) {
    cerr << "Failed to write person data." << endl;
}
output.close();
// 反序列化 PeopleInfo 对象
fstream input("peopleinfo.bin", ios::in | ios::binary);
PeopleInfo person;
if (!person.ParseFromIstream(&input)) {
    cerr << "Failed to parse person data." << endl;
}
input.close();

map 类型

基本语法

map<key_type, value_type> map_field = N;
syntax = "proto3";

package example;

// 定义一个包含 map 的消息
message Person {
    string name = 1;   // 姓名
    int32 id = 2;      // 唯一标识符

    // 定义一个 map 类型,用于存储联系人名称到电话号码的映射
    map<string, string> phone_numbers = 3;
}

map键值支持的类型

  • map的键不可以是浮点类型或者bytes类型,因为Protobuf需要保证键的唯一性以及可排序性
  • map的值可以是标量类型、枚举类型、自定义message类型

具体使用参考(结合上文看)

#include <iostream>
#include "example.pb.h"  // 包含生成的 Protobuf 文件

using namespace std;
using namespace example;

int main() {
    // 创建 Person 对象
    Person person;
    person.set_name("Alice");
    person.set_id(12345);

    // 往 map 中添加联系人信息
    PhoneNumber* home_number = (*person.mutable_contacts())["home"];
    home_number->set_number("123-456-7890");
    home_number->set_type("home");

    PhoneNumber* mobile_number = (*person.mutable_contacts())["mobile"];
    mobile_number->set_number("098-765-4321");
    mobile_number->set_type("mobile");

    // 打印联系人信息
    for (const auto& contact : person.contacts()) {
        cout << "联系类型: " << contact.first << ", 电话号码: " << contact.second.number()
             << ", 类型: " << contact.second.type() << endl;
    }

    return 0;
}

map字段相应操作

插入和访问元素

mutable_contacts()会返回一个指向map<string,phoneNumber>的指针,可以通过类似于数组插入和访问的方式

(*person.mutable_contacts())["home"];  // 获取或创建 key 为 "home" 的值

遍历map的数值

for (const auto& contact : person.contacts()) {
    cout << "联系类型: " << contact.first << ", 电话号码: " << contact.second.number() << endl;
}

 检查元素是否存在

使用find()方法检查元素是否存在

auto it = person.contacts().find("home");
if (it != person.contacts().end()) {
    cout << "找到家庭电话: " << it->second.number() << endl;
} else {
    cout << "没有找到家庭电话" << endl;
}

清除map元素

person.mutable_contacts()->clear();  // 清除所有联系人

person.mutable_contacts()->erase("home");  // 删除 key 为 "home" 的联系人信息

默认值

默认值只出现在反序列化中的,只有当Protobuf反序列化一个消息的时候,如果某个字段的二进制数据并不存在,该字段会被设置为默认值。

  • 字符串string:默默认值就是空字符串
  • 字节类型bytes:默认值就是空字节数组
  • 布尔类型bool:默认值是false
  • 数值类型:0
  • 枚举类型:枚举类型定义的第一个枚举值

保留字段

 保留字段存在的原因

提前保留一些字段留作他用,主要就是防止未来可能在使用这些字段编号或者引发数据混淆或者兼容性问题,使用保留字段就是为了规避这些问题

  • 当不再需要某些字段,需要删除它们的时候
  • 如果想要避免某个字段编号或者字段名称被重新使用

保留字段使用简单说明

N表示保留字段的名称,name则是要保留字段的名称

reserved N[, N, ...];            // 保留字段编号
reserved "name"[, "name", ...];  // 保留字段名称
message Person {
    reserved 3, 4;                // 保留字段编号
    reserved "age", "address";    // 保留字段名称
    
    int32 id = 1;
    string name = 2;
}

只要设置了保留字段和保留编号,如果其他字段使用这些保留的编号或者名称就会导致编译器错误

未知字段

未知字段的理解

旧编译器遇到新编译器定义的字段时,此时就会将这些字段当做未知字段保留,注意旧版本只是对其保留,不会对其处理。

但是在Protobuf3.5以上的版本不仅有保留处理,还可以对其进行子弹处理,也就是说即使反序列化的时候没有识别这些字段,它们仍然会被存储

UnknownFieldSet

主要就是用来存储和操作消息的未知字段

  • GetUnknownFields():用于获取未知字段
  • MutabelUnknownFields():用于获取一个可变的未知字符集,从而可以对这些未知字段进行修改
  • AddVarint(),AddFixed32():可以将不同类型字段数值添加到未知字段中
const google::protobuf::UnknownFieldSet& unknown_fields = message.GetReflection()->GetUnknownFields(message);
for (int i = 0; i < unknown_fields.field_count(); ++i) {
    const google::protobuf::UnknownField& field = unknown_fields.field(i);
    if (field.type() == google::protobuf::UnknownField::TYPE_VARINT) {
        std::cout << "Varint value: " << field.varint() << std::endl;
    }
}

反序列化和序列化中的未知字段

如果接收到的消息中包含旧版本中未识别的字段,可以通过以下方式保留并重新序列化

Message message;
message.ParseFromIstream(&input_stream); // 解析包含未知字段的消息
message.SerializeToOstream(&output_stream); // 重新序列化,未知字段仍然会被保留

Reflection接口

Reflection是Protobuf提供的一种动态访问消息字段的方式,其中包含已知字段和未知字段,使用GetReflection接口可以动态访问消息的元数据,并对对消息进行操作

const google::protobuf::Reflection* reflection = message.GetReflection();
const google::protobuf::FieldDescriptor* field = descriptor->field(0);
int value = reflection->GetInt32(message, field);

选项option

Option可以定义多个影.proto文件处理方式的选项,必须改选项可以设置在文件级别、消息级别、字段级别等。这些都会影响Protobuf编译器的行为

选项分类

  • 文件选项(FileOptions):定义文件级别的选项,影响整个 .proto 文件的处理
  • 消息选项(MessageOptions):定义消息类型的选项,影响消息级别的处理
  • 字段选项(FieldOptions):定义字段级别的选项,影响字段的处理
  • 枚举选项(EnumOptions):定义枚举的处理选项
  • 服务选项(ServiceOptions):定义服务类型的处理
  • 方法选项(MethodOptions):定义服务方法的处理

常用选项

optimize_size文件级别选项

作用:Protobuf编译器对代码的优化方式,可以根据下面选项进行优化

  • SPEED:默认选项,如果在性能密集的系统中使用Protobuf使用这个选项,可以让生成的代码高度优化,运行效率高,但是占用空间会大
  • CODE_SIZE:生成占用空间较小的代码,如果想要减少程序的体积使用该选项
  • LITE_RUNTIME:轻量级设备使用,仅仅提供序列化和反序列化功能,省略了完整的反射功能,代码空间占用少

allow_alias(枚举常量选项

作用:允许不同管道枚举值共享共同的整数值,可以给同一个枚举值取多个名字

enum PhoneType {
    option allow_alias = true;
    MP = 0;
    TEL = 1;
    LANDLINE = 1;  // 若没有 `allow_alias = true`,这里会抛出错误
}

default 字段选项

作用:在Protobuf2中,允许为标量字段设置默认值,如果解码的数据中没有该字段的值,字段就会被设置成默认值

optional int32 id = 1 [default = 100];

deprecated 

标记一个弃用的字段,也就是表明这些字段不再使用

int32 old_field = 2 [deprecated = true];

json_name 

为这个字段指定JSON序列化的时候使用的字段名

string user_name = 1 [json_name = "userName"];

;