ProtoBuf 的使用特性 在使用 ProtoBuf 时,必须依赖由编译器生成的头文件和源文件。其核心结构类似于一个类(class),通常包含以下内容: - 定义一系列属性字段 - 提供用于操作字段的方法:get 和 set - 提供用于序列化与反序列化的类方法,实现结构化数据与二进制字节流之间的相互转换 在 ProtoBuf 中,这种“类”并不称为 class,而是被称为 message,定义在 `.proto` 文件中。用户只需在该文件中声明所需字段,再通过 PB 编译器自动生成对应的 C++ 类代码。.proto 文件的基本结构 每份 `.proto` 文件通常以语法声明行开头。若未显式指定,则默认采用 proto2 语法。 - 使用 `package` 关键字定义命名空间,避免名称冲突 - 定义 message 时建议采用大驼峰命名法 - 每个字段都必须分配唯一的字段编号,这是编译机制的要求 - 字段编号不可使用 19000 至 19999 范围内的数值,该区间被 ProtoBuf 内部保留 关于整型类型的选择: - `int32` 与 `sint32` 均采用变长编码(Varint),但对负数推荐使用 `sint32`,因其编码效率更高 - `fixed32` 和 `fixed64` 采用固定长度编码,适用于大小确定的场景 性能优化建议: 对于频繁访问的字段,建议将其字段编号设置在 1~15 范围内。因为此范围内的编号在序列化后仅占用一个字节,有助于减少数据体积。 .proto 文件的编译流程 使用如下命令进行编译:syntax = "proto3"; package contacts; message PeopleInfo{ string name = 1; int32 age = 2; }protoc -I path/to/proto --cpp_out=target_dir name.proto其中: - `protoc` 是 Protocol Buffers 的编译工具 - `-I` 指定 `.proto` 文件的搜索路径 - `--cpp_out` 指定生成的 C++ 代码输出目录 执行后,将在目标路径下生成对应的头文件(.h)和源文件(.cc),其中包含字段的 get/set 方法,以及继承自 `MessageLite` 父类的序列化与反序列化功能。 注意:所有序列化函数均为 const 成员函数,不会修改对象状态,仅将序列化结果写入指定缓冲区。 生成的输出为二进制格式,提升了数据的安全性,同时也增加了逆向解析的难度。 编译链接使用了 ProtoBuf 的程序时,需使用如下命令:g++ -o TestPb main.cc contacts.pb.cc -std=c++11 -lprotobuf说明: - 必须包含 PB 编译生成的源文件 - 需启用 C++11 标准,因生成代码依赖该标准语法 - 必须链接 protobuf 库(-lprotobuf),否则链接阶段会报错 proto3 语法特点 在 proto3 中,字段规则和消息定义方式有所简化: - 不再需要显式声明字段规则(如 required/optional) - 支持嵌套定义 message,也可在一个 `.proto` 文件中定义多个 message - 可通过 import 导入其他 `.proto` 文件中定义的 message,但使用时需加上被导入文件的 package 名称作为前缀 访问嵌套成员(如 message 或 enum)时,采用 `BaseName_TargetName` 的命名格式。 例如,在以下示例中,访问方式为:contacts2.PeopleInfo_Address//通讯录.proto文件 syntax="proto3"; package contacts2; import "school.proto"; message Phone{ string number = 1; } message PeopleInfo{ string name = 1; int32 age = 2; message Address{ string number = 1; } Address address = 3; repeated Phone phone = 4; school.School school = 5; } message Contacts{ repeated PeopleInfo contacts = 1; }数组的定义与操作 使用 `repeated` 关键字可定义数组类型字段。 - 对于普通字段,直接调用 `set_fieldname(value)` 设置值 - 对于 repeated 字段,应先调用 `add_fieldname()` 获取指向新元素的指针,然后通过该指针设置具体值 - 若该元素内部仍含有 repeated 字段,则继续通过获取子指针的方式逐层插入 示例代码:向通讯录添加姓名、年龄、电话号码 #include <iostream> #include <fstream> #include "./pb_files/contacts.pb.h" void AddContacts(contacts2::PeopleInfo* people) { std::cout << "----------新增联系人----------" << std::endl; std::string name; std::cout << "请输入姓名:"; std::getline(std::cin, name); people->set_name(name); int age; std::cout << "请输入年龄:"; std::cin >> age; people->set_age(age); std::cin.ignore(256, '\n'); for(int i = 1;; i++){ std::cout << "请输入电话" << i << ":"; std::string number; std::getline(std::cin, number); if(number.empty()){ break; } contacts2::Phone* phone = people->add_phone();//用来引入的school.proto文件 syntax="proto3"; package school; message School{ string name = 1; }
#include <iostream>
#include <fstream>
#include "./pb_files/contacts.pb.h"
// 添加联系人信息的函数实现
void AddContacts(contacts2::PeopleInfo* people) {
std::string name;
int age;
std::cout << "请输入姓名:" << std::endl;
std::cin >> name;
people->set_name(name);
std::cout << "请输入年龄:" << std::endl;
std::cin >> age;
people->set_age(age);
std::string number;
std::cout << "请输入电话号码:" << std::endl;
std::cin >> number;
contacts2::Phone* phone = people->add_phone();
phone->set_number(number);
}
std::cout << "----------新增成功----------" << std::endl;
}
// 主函数:用于序列化并保存联系人数据到文件
int main() {
contacts2::Contacts contacts;
std::ifstream input("contacts.bin", std::ios::in | std::ios::binary);
if (!input) {
std::cout << "----------file not exists, create one----------" << std::endl;
}
else if (!contacts.ParseFromIstream(&input)) {
std::cout << "Parse from file fail" << std::endl;
return -1;
}
std::cout << "----------Parse success----------" << std::endl;
AddContacts(contacts.add_contacts());
std::ofstream write("contacts.bin", std::ios::out | std::ios::trunc | std::ios::binary);
if (!contacts.SerializeToOstream(&write)) {
std::cout << "write fail" << std::endl;
return -1;
}
std::cout << "----------write success----------" << std::endl;
return 0;
}
在处理数组或重复字段的数据读取时,首先需要获取对应元素的指针。通过调用相应成员的 get 方法来提取数据。 需要注意的是,在 Protocol Buffers 生成的 C++ 代码中,setter 方法通常采用 set_ 加上成员变量名的形式(如 set_name),而 getter 方法则省略了“get”前缀,直接使用属性名称作为方法名(如 name())进行访问。
#include <iostream>
#include <fstream>
#include "./pb_files/contacts.pb.h"
// 打印所有联系人信息的函数
void PrintContacts(contacts2::Contacts& contacts) {
for (int i = 0; i < contacts.contacts_size(); ++i) {
std::cout << "----------联系人信息 " << i + 1 << "----------" << std::endl;
contacts2::PeopleInfo people = contacts.contacts(i);
std::cout << "姓名:" << people.name() << std::endl;
std::cout << "年龄:" << people.age() << std::endl;
for (int j = 0; j < people.phone_size(); ++j) {
contacts2::Phone phone = people.phone(j); // 注意此处索引应为 j 而非 i
std::cout << "联系人电话" << j + 1 << ": " << phone.number() << std::endl;
}
}
}
// 主函数:从文件反序列化并打印联系人列表
int main() {
contacts2::Contacts contacts;
std::fstream input("contacts.bin", std::ios::in | std::ios::binary);
if (!contacts.ParseFromIstream(&input)) {
std::cout << "Parse from contacts.bin fail" << std::endl;
input.close();
return -1;
}
PrintContacts(contacts);
input.close();
return 0;
}
decode 是 Protocol Buffers 提供的一个命令行工具指令,可用于快速解析和查看二进制格式的 .bin 文件内容。 默认情况下,该指令从标准输入读取数据,但可通过输入重定向的方式将文件内容传入。
wangjiale@ubuntu:~/protobuf/proto3$ protoc --decode=contacts2.Contacts contacts.proto < contacts.bin
使用格式示例:
protoc --decode=package.MessageName path/to/file.proto < binary_file.bin
其中需指定目标 package 和 message 名称,并提供对应的 .proto 定义文件路径,最后通过重定向输入二进制数据。
在 .proto 文件中可以定义枚举类型(enum),用于限制某个字段的取值范围。生成的代码会将枚举映射为整型常量, 支持在序列化与反序列化过程中正确处理这些预定义值。使用时只需赋值相应的枚举标识符即可,无需手动管理整数值。
在.proto文件中,枚举类型通过enum关键字定义,且枚举的字段编号必须从0开始。其中,编号为0的枚举值会被默认作为该枚举类型的默认值。在同一层级的枚举定义中,不允许存在同名的枚举成员。若引入外部的.proto文件且未声明package名称,则这些枚举也将被视为同一命名空间下的内容,因此同样禁止重名。
要获取枚举值对应的名称(即字符串形式而非数字),可使用EnumName_Name(target)方法进行转换。
对于Any类型,它是一种泛型容器,能够封装任意其他Protocol Buffer消息类型。使用前需引入google/protobuf/any.proto文件。
在写入数据时,首先实例化目标message对象,然后通过mutable_name()方法获取Any字段的可变引用指针,并调用其PackFrom()函数将目标消息打包存储为Any类型。
读取时,则使用UnpackTo()方法将Any中封装的消息解包至指定的message实例中进行后续处理。
import "google/protobuf/any.proto";
message PeopleInfo{
string name = 1;
int32 age = 2;
repeated Phone phone = 3;
google.protobuf.Any data = 4;
}
// 示例:写入操作
contacts2::Address address;
people->mutable_data()->PackFrom(address);
// 示例:读取操作
if (people.has_data() && people.data().Is<contacts2::Address>()) {
contacts2::Address address;
people.data().UnpackTo(&address);
}
oneof用于表示一组互斥字段,这些字段属于同一个message层级,因此其字段编号和名称在整个message范围内都必须唯一,不可与其他字段重复。设置值时直接使用set_name()方法即可。
需要注意的是,oneof不支持repeated修饰符。其内部字段以类似枚举的方式管理,但额外包含一个“not known”状态,用于表示当前无有效字段被设置。
读取时,可通过oneof_name_case()方法判断当前激活的是哪一个字段;若需获取对应的整型编号,则调用OneofNameCase()函数。
枚举成员的常量命名通常采用kName的形式生成。
map类型以键值对方式组织数据。key支持除float和bytes之外的所有标量类型,value则可以是任意类型。与Any一样,map也不允许使用repeated修饰符。
插入数据时,可通过mutable_前缀的方法获得可变引用指针,再利用insert函数构造std::pair完成插入操作。读取时,结合_size字段与迭代器遍历整个map结构,通过first访问key,second访问value。
关于字段的删除:Protocol Buffers不推荐直接移除字段。一旦某个字段被删除,应确保其字段编号不再被新字段复用,因为PB编译器依据字段编号进行序列化数据的解析。若重复使用可能导致数据错乱或误读。为避免此类问题,建议使用reserved关键字标记已被废弃的字段编号或名称,这样编译器会在后续使用中发出警告,提示该字段不可用。
未知字段方面,在Protobuf 3.5版本之后,无法识别的字段会被保留在消息中的“未知字段集合”里,不会被丢弃。
可通过Message对象的GetReflection()方法获取指向google::protobuf::Reflection的指针,进而调用GetUnknownFields(const Message& message)获取UnknownFieldSet对象,随后遍历其中的所有未知字段。
获取未知字段的具体信息时,先通过number()得到字段编号,再通过type()获取其类型枚举值,最后根据类型选择对应的方法提取实际值。
例如,若类型为TYPE_VARINT,表明该字段是int32或int64等变长整数类型,此时可通过相应的varint解析方法读取具体数值。
关于前后兼容性:只要不违反字段规则(如更改字段类型、删除仍在使用的字段编号等),一般不会破坏兼容性。
向前兼容指旧版本程序能正确处理新版本生成的数据。自3.5版本起,未识别的字段会被保留于未知字段中,从而实现一定程度的向前兼容。
向后兼容则是指新版本程序能正常解析旧版本生成的数据,这一点在Protobuf设计中天然支持,因为新增字段默认有合理缺省值,且旧字段仍可被识别。
通过option选项可配置.proto文件的行为特性:
SPEED:默认选项,生成高度优化的代码,运行速度快,但生成的类体积较大。CODE_SIZE:优先减少生成代码的大小,适用于对二进制尺寸敏感的场景,但运行效率较低。LITE_RUNTIME:兼顾速度与空间,生成轻量级运行时代码,但牺牲了反射功能(如不再继承自Message,而是MessageLite),仅支持序列化与反序列化操作。
扫码加好友,拉您进群



收藏
