🔥个人主页: Milestone-里程碑
❄️个人专栏: <<力扣hot100>> <<C++>><<Linux>>
🌟心向往之行必能至
目录
[一、Any 类型:泛型字段,存储任意消息体](#一、Any 类型:泛型字段,存储任意消息体)
[1. Any 类型的核心规则](#1. Any 类型的核心规则)
[2. Any 类型的使用步骤](#2. Any 类型的使用步骤)
[步骤 1:导入 any.proto 文件(必选)](#步骤 1:导入 any.proto 文件(必选))
[步骤 2:定义需要存储的任意消息体](#步骤 2:定义需要存储的任意消息体)
[步骤 3:定义 Any 类型字段](#步骤 3:定义 Any 类型字段)
[3. 实战:为联系人添加任意类型的地址信息](#3. 实战:为联系人添加任意类型的地址信息)
[4. Any 类型的核心方法(C++ 为例)](#4. Any 类型的核心方法(C++ 为例))
[二、Oneof 类型:多选一字段,仅一个字段生效](#二、Oneof 类型:多选一字段,仅一个字段生效)
[1. Oneof 类型的核心规则](#1. Oneof 类型的核心规则)
[2. Oneof 类型的定义格式](#2. Oneof 类型的定义格式)
[3. 实战:为联系人添加二选一的其他联系方式](#3. 实战:为联系人添加二选一的其他联系方式)
[4. Oneof 类型的核心方法(C++ 为例)](#4. Oneof 类型的核心方法(C++ 为例))
[三、Map 类型:键值对字段,存储关联数据](#三、Map 类型:键值对字段,存储关联数据)
[1. Map 类型的核心规则](#1. Map 类型的核心规则)
[2. Map 类型的定义格式](#2. Map 类型的定义格式)
[3. 实战:为联系人添加键值对格式的备注信息](#3. 实战:为联系人添加键值对格式的备注信息)
[4. Map 类型的核心方法(C++ 为例)](#4. Map 类型的核心方法(C++ 为例))
在前两篇博客中,我们掌握了 proto3 的基础语法和高级语法,能定义包含基础字段、重复字段、嵌套消息和枚举的结构化数据,但实际业务中还有更复杂的需求:
- 字段需要存储任意类型的对象(如联系人的附加信息可能是地址、身份证、银行卡等);
- 多个字段只能有一个生效(如联系人的其他联系方式:QQ / 微信二选一);
- 字段需要键值对格式(如联系人的备注信息:key = 备注标题,value = 备注内容)。
proto3 提供了Any 、Oneof 、Map三大高级类型,专门解决以上场景,让协议定义更灵活、更贴合实际业务。本文将详细讲解这三种类型的定义、使用规则和实战场景,结合通讯录案例完成协议升级。
一、Any 类型:泛型字段,存储任意消息体
Any 类型 相当于编程语言中的泛型 ,可以在字段中存储任意类型的 Protobuf 消息体 ,无需提前指定具体类型,实现了字段的 "动态类型",适合存储不确定类型的附加信息。
1. Any 类型的核心规则
- Any 类型是 Google 预定义的类型,需要导入官方的 any.proto 文件才能使用;
- Any 类型的字段可以存储任意自定义的消息体,支持嵌套和复杂结构;
- 编译后,会生成PackFrom() 和UnpackTo() 方法,用于消息体与 Any 类型的互转;
- 提供Is<T>() 方法,用于判断 Any 字段中存储的消息体类型。
2. Any 类型的使用步骤
步骤 1:导入 any.proto 文件(必选)
proto
// 导入Google官方的any.proto,必须写在包名声明后
import "google/protobuf/any.proto";
步骤 2:定义需要存储的任意消息体
proto
// 示例:定义地址消息体,作为Any字段的存储内容
message Address {
string home_address = 1; // 家庭地址
string unit_address = 2; // 单位地址
}
步骤 3:定义 Any 类型字段
proto
message 消息体名称 {
// 使用google.protobuf.Any作为字段类型
google.protobuf.Any 字段名 = 唯一编号;
}
3. 实战:为联系人添加任意类型的地址信息
在通讯录的PeopleInfo消息体中,添加 Any 类型的data字段,用于存储联系人的地址信息(Address 消息体),实现 "附加信息的动态存储":
proto
syntax = "proto3";
package contacts;
// 导入Any类型的官方proto文件
import "google/protobuf/any.proto";
// 地址消息体:可作为Any字段的存储内容
message Address {
string home_address = 1; // 家庭地址
string unit_address = 2; // 单位地址
}
// 联系人消息体
message PeopleInfo {
string name = 1; // 姓名
sint32 age = 2; // 年龄
// 嵌套:电话号码(含枚举)
message Phone {
enum PhoneType {
MP = 0; // 移动电话
TEL = 1; // 固定电话
}
string number = 1;
PhoneType type = 2;
}
repeated Phone phone = 3; // 多个电话号码
// Any类型:存储任意消息体(此处存储Address),编号4
google.protobuf.Any data = 4;
}
// 通讯录消息体
message Contacts {
repeated PeopleInfo contacts = 1;
}
4. Any 类型的核心方法(C++ 为例)
编译后,C++ 会生成以下核心方法,实现消息体与 Any 类型的互转:
-
PackFrom() :将自定义消息体(如 Address)打包为 Any 类型;
cpp
运行
// 将Address对象打包到Any字段中 Address address; people_info.mutable_data()->PackFrom(address); -
UnpackTo() :将 Any 类型解包为自定义消息体;
cpp
运行
// 将Any字段解包为Address对象 Address address; people_info.data().UnpackTo(&address); -
Is<T>() :判断 Any 字段中存储的是否为指定类型的消息体;
cpp
运行
// 判断data字段是否存储的是Address对象 if (people_info.data().Is<Address>()) { // 解包并处理 } -
has_data():判断 Any 字段是否被赋值。
二、Oneof 类型:多选一字段,仅一个字段生效
Oneof 类型 用于定义一组互斥的字段 ,即这组字段中只能有一个字段被赋值,赋值新的字段会自动清除之前赋值的字段,适合 "多选一" 的业务场景(如联系方式:QQ / 微信二选一、支付方式:微信 / 支付宝 / 银行卡三选一)。
1. Oneof 类型的核心规则
- Oneof 字段的定义格式为
oneof 字段名 { 子字段1; 子字段2; ... }; - 子字段的编号不可重复,且不可与消息体中的其他字段编号冲突;
- 不能在 Oneof 中使用 repeated 字段(重复字段与互斥规则冲突);
- 赋值 Oneof 中的一个子字段,会自动清除其他子字段的值;
- 编译后,会生成**_case()** 方法,用于判断当前哪个子字段被赋值。
2. Oneof 类型的定义格式
proto
message 消息体名称 {
// 其他普通字段
字段类型 字段名 = 唯一编号;
// Oneof类型:定义互斥的子字段
oneof oneof字段名 {
字段类型 子字段1 = 唯一编号;
字段类型 子字段2 = 唯一编号;
// ... 更多子字段
}
}
3. 实战:为联系人添加二选一的其他联系方式
在PeopleInfo消息体中,添加 Oneof 类型的other_contact字段,包含qq 和weixin两个子字段,实现 "QQ / 微信二选一" 的联系方式:
proto
syntax = "proto3";
package contacts;
import "google/protobuf/any.proto";
message Address {
string home_address = 1;
string unit_address = 2;
}
message PeopleInfo {
string name = 1;
sint32 age = 2;
message Phone {
enum PhoneType { MP = 0; TEL = 1; }
string number = 1;
PhoneType type = 2;
}
repeated Phone phone = 3;
google.protobuf.Any data = 4;
// Oneof类型:其他联系方式(QQ/微信二选一),编号5、6
oneof other_contact {
string qq = 5;
string weixin = 6;
}
}
message Contacts {
repeated PeopleInfo contacts = 1;
}
4. Oneof 类型的核心方法(C++ 为例)
编译后,C++ 会生成以下核心方法,用于 Oneof 字段的操作和判断:
-
普通字段操作方法 :
set_qq()、set_weixin()、qq()、weixin(); -
判断赋值状态 :
other_contact_case(),返回当前生效的子字段枚举值;cpp
运行
// 判断哪个联系方式被赋值 switch (people_info.other_contact_case()) { case PeopleInfo::kQq: // QQ被赋值 cout << "QQ: " << people_info.qq() << endl; break; case PeopleInfo::kWeixin: // 微信被赋值 cout << "微信: " << people_info.weixin() << endl; break; case PeopleInfo::OTHER_CONTACT_NOT_SET: // 未赋值 break; } -
清空 Oneof 字段 :
clear_other_contact(),清除所有子字段的值。
三、Map 类型:键值对字段,存储关联数据
Map 类型 用于定义键值对格式的字段 ,相当于编程语言中的Map/Dictionary,适合存储备注信息、扩展属性、键值对配置等场景,无需定义额外的消息体。
1. Map 类型的核心规则
- Map 类型的定义格式为
map<key_type, value_type> 字段名 = 唯一编号;; - key_type(键类型) :只能是除
float和bytes外的标量类型(如 int32、string、uint64 等); - value_type(值类型) :可以是任意类型(标量类型、消息体、枚举、重复字段等);
- 不能用 repeated 修饰 Map 字段(Map 本身支持多组键值对);
- Map 中存储的元素是无序的,序列化和反序列化后的顺序可能不一致;
- 编译后,会生成与普通字段类似的操作方法,支持键值对的增删改查。
2. Map 类型的定义格式
proto
message 消息体名称 {
// 其他字段
字段类型 字段名 = 唯一编号;
// Map类型:键类型key_type,值类型value_type
map<key_type, value_type> 字段名 = 唯一编号;
}
3. 实战:为联系人添加键值对格式的备注信息
在PeopleInfo消息体中,添加 Map 类型的remark字段,键类型为string(备注标题),值类型为string(备注内容),实现联系人备注的灵活存储:
proto
syntax = "proto3";
package contacts;
import "google/protobuf/any.proto";
message Address {
string home_address = 1;
string unit_address = 2;
}
message PeopleInfo {
string name = 1;
sint32 age = 2;
message Phone {
enum PhoneType { MP = 0; TEL = 1; }
string number = 1;
PhoneType type = 2;
}
repeated Phone phone = 3;
google.protobuf.Any data = 4;
oneof other_contact {
string qq = 5;
string weixin = 6;
}
// Map类型:备注信息(键:备注标题,值:备注内容),编号7
map<string, string> remark = 7;
}
message Contacts {
repeated PeopleInfo contacts = 1;
}
4. Map 类型的核心方法(C++ 为例)
编译后,C++ 会生成以下核心方法,用于 Map 字段的键值对操作:
-
获取 Map 对象 :
mutable_remark(),返回 Map 对象的指针,用于增删改查;cpp
运行
// 向备注中添加键值对 people_info.mutable_remark()->insert({"日程", "10月1日出游"}); people_info.mutable_remark()->insert({"备注", "重要联系人"}); -
获取 Map 大小 :
remark_size(),返回键值对的个数; -
遍历 Map :通过迭代器遍历所有键值对;
cpp
运行
for (auto it = people_info.remark().cbegin(); it != people_info.remark().cend(); ++it) { cout << it->first << ": " << it->second << endl; } -
清空 Map :
clear_remark(),清除所有键值对。
四、三大高级类型的综合实战:完整的通讯录协议
结合 Any、Oneof、Map 三大高级类型,定义一个完整的通讯录.proto 文件 (contacts.proto),包含联系人的所有核心信息:姓名、年龄、多个电话号码(含类型)、任意类型的地址、二选一的联系方式、键值对备注,以及多个联系人的通讯录:
proto
syntax = "proto3";
package contacts;
// 导入Any类型依赖
import "google/protobuf/any.proto";
// 地址消息体:供Any类型存储
message Address {
string home_address = 1; // 家庭地址
string unit_address = 2; // 单位地址
}
// 联系人消息体
message PeopleInfo {
string name = 1; // 姓名
sint32 age = 2; // 年龄
// 电话号码(嵌套+枚举)
message Phone {
enum PhoneType {
MP = 0; // 移动电话
TEL = 1; // 固定电话
}
string number = 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; // 键值对备注
}
// 通讯录消息体:多个联系人
message Contacts {
repeated PeopleInfo contacts = 1;
}
五、高级类型注意事项
- Any 类型必须导入 any.proto,否则编译报错,且仅能存储 Protobuf 消息体;
- Oneof 的子字段不可重复、不可用 repeated,赋值互斥,适合多选一场景;
- Map 的键类型不支持 float 和 bytes,元素无序,适合键值对存储;
- 三大高级类型的字段编号仍需遵循唯一规则,为后续扩展预留;
- 编译后,所有高级类型都会生成对应的操作方法,无需手动编写解析代码。
下一篇博客,我们将讲解 Protobuf 的默认值规则 和消息体更新规则 ,这是保证 Protobuf版本兼容性的核心,也是分布式系统中协议迭代的关键,让大家能在项目迭代中无缝扩展协议,不破坏旧版本程序。