Protocol Buffers C++ 进阶数据类型与应用逻辑深度解析

目录

    • 前言
    • 一、枚举类型(Enum)的定义与命名空间管理
      • 1.1 枚举冲突与命名空间隔离
      • 1.2 通讯录中的枚举实践
      • 1.3 写入与读取枚举类型
    • 二、Any 类型的应用机制
      • 2.1 引入 Any 类型与定义地址信息
      • 2.2 Any 类型数据的封装(Pack)与解包(Unpack)
    • 三、Oneof 类型的排他性字段
      • 3.1 定义 Oneof 字段
      • 3.2 Oneof 字段的设置与读取
    • 四、Map 类型的键值对存储
      • 4.1 定义 Map 字段
      • 4.2 Map 数据的插入与遍历

前言

在构建高效、可扩展的数据序列化系统时,Protocol Buffers 提供了丰富的数据类型以应对复杂的业务需求。除了基础的整型和字符串类型外,掌握枚举、泛型、联合体及哈希映射的使用对于设计健壮的通信协议至关重要。本文将通过构建一个功能完善的通讯录系统,逐步引入并解析这些高级特性的实现细节。

一、枚举类型(Enum)的定义与命名空间管理

在定义协议文件时,枚举类型用于表示一组预定义的常量。然而,在大型项目中,不同模块可能会定义同名的枚举常量,导致 C++ 编译层面的符号冲突。

1.1 枚举冲突与命名空间隔离

首先创建一个名为test_enum.proto的文件进行测试。在此阶段,若未指定包名(Package),直接定义枚举可能会引发全局命名空间的污染。

使用 Protocol Buffers 编译器对该文件进行编译:

protoc --cpp_out=. test_enum.proto

在 Protobuf 的语义中,如果在同一个.proto文件内定义的两个message中包含完全相同的枚举元素名称,或者在不同的.proto文件中定义了重复的枚举常量(例如均为MP),编译器会抛出重定义错误。这是因为生成的 C++ 代码会将枚举常量映射到外层作用域,导致符号冲突。

下图展示了当两个 message 中存在相同元素时出现的编译报错信息,提示符号被重复定义:

此外,若在另一个独立的 proto 文件中也定义了相同的枚举常量MP,当当前文件使用import引入该文件时,同样会触发跨文件的符号冲突,导致编译失败。下图展示了引入外部文件导致重复定义的错误场景:

解决此类冲突的标准方案是使用package关键字声明包名。包名在生成的 C++ 代码中会被映射为命名空间(namespace),从而实现符号隔离。

package test_enum;

通过指定package test_enum;,生成的枚举类型将被封装在test_enum命名空间下,有效避免了全局冲突。下图展示了添加 package 声明后,编译顺利通过的结果:

1.2 通讯录中的枚举实践

contacts.proto文件中,通过引入枚举类型来标识电话号码的类别(移动电话或固定电话)。同时,该文件通过importpackage指令构建了完整的协议结构。

syntax="proto3";//首行:语法指定行package contacts2;//命令空间//联系人message的定义message PeopleInfo//定义一个联系人的message{//定义一个message的变量一定要加上编号//姓名string name=1;//年龄int32 age=2;message Phone{//电话号码的类string number=1;enumPhoneType//枚举类型{//移动电话类型MP=0;//固定电话类型TEL=1;}PhoneType type=2;}//repeated string phone_numbers=3;//repeated这个关键字可以达到修饰数组的操作repeated Phone phone=3;//电话信息}//定义一个通讯录的messagemessage Contacts2{repeated PeopleInfo contacts=1;//类型是PeopleInfo}

执行编译命令生成对应的 C++ 源文件与头文件:

protoc --cpp_out=. contacts.proto

编译操作顺利完成,生成了contacts.pb.hcontacts.pb.cc文件。

打开生成的.h头文件,可以观察到PhoneType枚举类型的定义及其相关的辅助函数。Protobuf 自动生成了枚举值到字符串名称的映射方法(如PhoneType_Name)以及有效性检查方法(如PhoneType_IsValid)。下图展示了头文件中生成的枚举类型相关代码结构:

由于在Phone消息中新增了PhoneType type=2;字段,生成的类中也相应增加了type()(获取值)和set_type()(设置值)等成员函数。

1.3 写入与读取枚举类型

在数据写入阶段(write.cc),通过AddPeopleInfo函数实现联系人信息的录入。对于枚举类型,通常结合switch语句将用户的输入(整数)映射为具体的枚举常量。

voidAddPeopleInfo(contacts2::PeopleInfo*people){cout<<"--------------新增联系人--------------";cout<<"请输入联系人姓名";string name;getline(cin,name);people->set_name(name);cout<<"请输入联系人年龄";intage;cin>>age;people->set_age(age);cin.ignore(256,'\n');//清空输入缓冲区的内容,for(inti=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、固定电话)";inttype;cin>>type;cin.ignore(256,'\n');//忽略回车switch(type){case1:phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);break;case2:phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);break;default:cout<<"选择有误"<<endl;break;}}cout<<"------------添加联系人成功------------";}

在读取阶段(read.cc),为了直观显示枚举值的含义,利用 Protobuf 提供的反射机制或辅助函数PhoneType_Name将枚举的整数值转换为对应的字符串标识(如 “MP” 或 “TEL”)。

voidPrintContacts(contacts2::Contacts2&contacts){for(inti=0;i<contacts.contacts_size();i++){cout<<"-------------联系人"<<i+1<<"-------------"<<endl;//取出对应下标的元素const::contacts2::PeopleInfo&people=contacts.contacts(i);//获取到这个联系人cout<<"联系人的姓名"<<people.name()<<endl;cout<<"联系人的年龄"<<people.age()<<endl;for(intj=0;j<people.phone_size();j++){constcontacts2::PeopleInfo_Phone&phone=people.phone(j);cout<<"联系人的电话信息"<<j+1<<":"<<phone.number();cout<<" ("<<phone.PhoneType_Name(phone.type())<<")"<<endl;//phone.PhoneType_Name可以打印出声明的枚举常量的名称}}}

执行make编译并运行程序。首先进行联系人新增操作,输入姓名、年龄及电话类型。下图展示了程序运行时的交互界面,用户成功添加了联系人信息:

随后执行读取程序./read。可以看到输出结果中,电话类型被正确解析并打印为字符串 “TEL”。需要注意的是,对于未显式设置类型的记录,Protobuf 会使用默认值(枚举的第一个元素,即数值 0),因此序号为 0 的枚举常量(此处为MP)即为默认类型。下图展示了读取到的通讯录内容,包含了解析后的电话类型:

二、Any 类型的应用机制

Any类型是 Protobuf 提供的泛型解决方案,允许在 message 字段中嵌入任意类型的其他 message,而无需在定义阶段确定具体的类型。这在处理多态数据或动态内容时极为有用。

2.1 引入 Any 类型与定义地址信息

要使用Any类型,必须在.proto文件中导入定义文件google/protobuf/any.proto。Any 类型本质上是一个包含了序列化数据和类型 URL 的 message。

contacts.proto中添加Any类型的字段data,用于存储扩展信息:

import"google/protobuf/any.proto";
google.protobuf.Any data=4;

下图展示了在 proto 文件中引入 Any 定义及添加 data 字段后的文件结构:

为了演示 Any 的存储能力,定义一个新的Addressmessage,包含家庭地址和单位地址:

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

下图展示了 Address 消息的定义:

再次编译.proto文件:

protoc --cpp_out=. contacts.proto

编译完成后,查看生成的头文件,可以确认 Address 类及其相关的方法已经生成。

2.2 Any 类型数据的封装(Pack)与解包(Unpack)

在 C++ 代码中操作 Any 字段时,不能直接赋值,而是需要使用PackFrom方法将具体的 message 对象序列化并存入 Any 字段,以及使用UnpackTo方法将数据还原。

write.ccAddPeopleInfo函数中,首先创建并填充Address对象,随后将其封装进people消息的data字段。

//定义一个地址对象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);//将address对象转换为anypeople->mutable_data()->PackFrom(address);//帮我们开辟一块空间,其实就是any的那个空间,我们就拿到了这个any对象了,然后对any对象中的PackFrom方法进行调用,就能将我们的address对象转换为any对象并且存储到data中,这个方法成功返回true,失败返回false

mutable_data()方法返回指向Any字段的指针,调用PackFrom(address)会自动处理序列化,并设置正确的 Type URL,以便后续识别。下图展示了写入代码的逻辑片段:

在读取逻辑read.cc中,解析 Any 数据前需进行类型检查。使用Is<T>()方法判断data字段中存储的是否为预期类型,确认为真后,使用UnpackTo提取数据。

if(people.has_data()&&people.data().Is<contacts2::Address>())//查看是否为data字段设置内容,是不是Address类型的{contacts2::Address address;people.data().UnpackTo(&address);//UnpackTo是any中的方法,将我们people中的data数据转换为Addree类型的对象,转换的值就放到address中//然后现在我们就拿到了我们address中的地址了if(!address.home_address().empty())//如果家庭地址为空的话{cout<<"联系人家庭地址"<<address.home_address()<<endl;}if(!address.unit_address().empty())//如果单位地址为空的话{cout<<"联系人单位地址"<<address.unit_address()<<endl;}}}}

这段代码确保了只有当Any字段确实包含Address类型数据时才进行反序列化,保证了程序的安全性。下图为读取代码的更新内容:

清理旧的构建并重新编译:

makecleanmake

运行程序新增联系人,此时会提示输入家庭和单位地址。下图展示了新增联系人时包含地址信息的交互过程:

最后运行./read读取通讯录,可以看到程序成功解析了 Any 字段中的地址信息并打印出来。

三、Oneof 类型的排他性字段

在某些业务场景下,多个字段中同时只能有一个生效。例如,联系人可能通过 QQ 或微信联系,但记录中仅保留其中一种。oneof关键字提供了这种“联合体”特性,能够节省存储空间并强制字段的互斥性。

3.1 定义 Oneof 字段

contacts.proto中,新增other_contact组,包含qqwechat两个字段。

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

这种定义意味着qqwechat共享内存空间(逻辑上),设置其中一个会自动清除另一个。下图展示了 proto 文件中 oneof 字段的定义:

需要特别注意的是,oneof内部的字段不能使用repeated修饰符。编译该文件:

protoc --cpp_out=. contacts.proto

生成的 C++ 代码中包含用于查询当前设置了哪个字段的方法,如has_qq()has_wechat()以及获取当前字段类型的other_contact_case()方法。下图展示了头文件中生成的 oneof 相关方法:

3.2 Oneof 字段的设置与读取

write.cc中,根据用户的选择设置 QQ 或微信。由于oneof的特性,无需手动清除旧值,最后一次set操作将决定最终保留的数据。

cout<<"请选择要添加的其他联系方式(1、QQ 2、wechat):";intother_contact;cin>>other_contact;cin.ignore(256,'\n');//忽略换行草操作if(1==other_contact)//如果选择的是1的话{cout<<"请输入联系人QQ号码";string qq;getline(cin,qq);people->set_qq(qq);}elseif(2==other_contact){cout<<"请输入联系人wechat号码";string wechat;getline(cin,wechat);people->set_wechat(wechat);}else{cout<<"选择有误,未成功设置其他联系方式";}

下图展示了写入 oneof 字段的代码逻辑:

read.cc中,使用other_contact_case()方法判断当前存储的是哪种数据,并据此进行相应的输出。该方法返回一个枚举值,对应 proto 中定义的字段。

switch(people.other_contact_case())//返回当时设置的到底是QQ还是wechat{casecontacts2::PeopleInfo::OtherContactCase::kQq:cout<<"联系人qq:"<<people.qq()<<endl;break;casecontacts2::PeopleInfo::OtherContactCase::kWechat:cout<<"联系人wechat:"<<people.wechat()<<endl;break;default:break;}

重新编译并运行,测试 Oneof 字段的写入与读取。下图展示了程序正确处理了 QQ 或微信的选择逻辑,并在读取时准确显示了结果:

四、Map 类型的键值对存储

Map 类型用于存储键值对数据,非常适合表示属性、备注或配置信息。Protobuf 中的 map 类型是无序的,且不能使用repeated修饰。

4.1 定义 Map 字段

contacts.proto中新增remark字段,类型为map<string, string>,用于存储联系人的备注信息。

map<string,string>remark=7;//备注信息

下图展示了 map 类型字段的定义:

执行编译命令:

protoc --cpp_out=. contacts.proto

查看生成的头文件,Protobuf 为 map 字段生成了类似 C++ STL map 的接口,包括mutable_remark()用于获取可修改的 map 对象。

4.2 Map 数据的插入与遍历

write.cc中,通过循环让用户输入多条备注信息。调用mutable_remark()获取 map 指针后,使用insert方法插入键值对。

for(inti=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});//mutable_remark会返回map的那一块空间,然后我们进行插入键值对的操作}

下图展示了 map 数据插入的代码实现:

read.cc中,遍历 map 字段需要使用迭代器。cbegin()cend()提供了常量迭代器,确保在读取过程中数据不会被意外修改。

if(people.remark_size())//存在备注信息{cout<<"备注信息:"<<endl;}for(autoit=people.remark().cbegin();it!=people.remark().cend();it++)//将remark字段拿出来{cout<<" "<<it->first<<":"<<it->second<<endl;}

再次执行make并运行程序,测试备注信息的添加与展示。可以看到,系统成功存储并打印了多条备注键值对。

通过以上步骤,通讯录系统集成了 Enum、Any、Oneof 和 Map 等高级数据类型,极大地增强了数据模型的表达能力和系统的灵活性。这些特性的正确使用,是构建高效 Protobuf 应用程序的基础。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1184744.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python基于vue的仓库综合管理与数据可视化分析平台 仓库火灾监测预警系统 仓库销售数据可视化分析系统

目录Python基于Vue的仓库综合管理与数据可视化分析平台仓库火灾监测预警系统仓库销售数据可视化分析系统开发技术路线相关技术介绍核心代码参考示例结论源码lw获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01;Python基于Vue的仓库综合管理与数据可…

提示工程架构师实战:用AI上下文工程优化社交媒体内容创作的3个技巧

提示工程架构师实战&#xff1a;用AI上下文工程优化社交媒体内容创作的3个核心技巧 一、引言&#xff1a;为什么你的AI内容总像“没睡醒”&#xff1f; 凌晨1点&#xff0c;你盯着电脑屏幕上的AI输出&#xff0c;揉了揉发涩的眼睛—— “这款面膜超好用&#xff0c;大家一定要试…

写论文软件哪个好?实测封神!虎贲等考 AI 凭真材实料成毕业党首选

毕业季的论文战场&#xff0c;“写论文软件哪个好” 的灵魂拷问&#xff0c;每天都在各大高校的班级群、学术交流群刷屏。有人踩坑 “AI 生成内容被标红”&#xff0c;有人吐槽 “文献引用查无此文”&#xff0c;还有人困在 “数据图表不规范” 的死循环里。作为深耕论文写作科…

python基于vue的摄影跟拍预约系统

目录摘要开发技术路线相关技术介绍核心代码参考示例结论源码lw获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01;摘要 该系统基于Python后端与Vue.js前端技术栈&#xff0c;构建了一个高效、用户友好的摄影跟拍预约平台。后端采用Django框架实现R…

深入解析:为何虚拟机/服务器中MySQL优先选择Docker安装?

在云计算与容器化技术普及的今天&#xff0c;MySQL作为全球最流行的开源关系型数据库&#xff0c;其部署方式也在不断迭代。从早期的源码编译、RPM/YUM包安装&#xff0c;到如今的Docker容器化部署&#xff0c;每一种方式都适配了不同的技术场景。尤其在虚拟机或物理服务器环境…

深度对比:PostgreSQL与MySQL的核心差异及选型指南

开发者必须知道的开源资讯网页OSCHINA - 开源 AI 开发者生态社区在开源关系型数据库领域&#xff0c;PostgreSQL&#xff08;简称Pgsql&#xff09;与MySQL无疑是两大主流选择。前者以“功能全面、扩展性强”著称&#xff0c;后者以“轻量高效、易于运维”立足&#xff0c;二…

day152—回溯—电话号码的字母组合(LeetCode-17)

题目描述 给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下&#xff08;与电话按键相同&#xff09;。注意 1 不对应任何字母。 示例 1&#xff1a; 输入&#xff1a;digits "23" …

python基于vue的电力集团企业员工职称评定系统

目录摘要开发技术路线相关技术介绍核心代码参考示例结论源码lw获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01;摘要 电力集团企业员工职称评定系统基于Python与Vue.js技术栈开发&#xff0c;旨在实现职称评定的数字化、标准化与高效化管理。系统…

AI 写论文哪个软件最好?实测封神!虎贲等考 AI 成毕业通关 “学术引擎”

毕业季的论文战场&#xff0c;“AI 写论文哪个软件最好” 的灵魂拷问&#xff0c;成了无数学子的热议焦点。市面上的 AI 论文工具五花八门&#xff0c;却大多难逃 “文献造假”“数据空洞”“逻辑断层” 的三大魔咒。作为深耕论文写作科普的测评博主&#xff0c;我以计算机、汉…

上海靠谱嵌入式开发怎么选,实邦电子值得考虑吗?

上海靠谱嵌入式开发怎么选&#xff0c;实邦电子值得考虑吗&#xff1f; 实邦电子&#xff1a;十六年行业沉淀的科技企业 上海实邦电子科技有限公司成立于2009年&#xff0c;至今已在行业内深耕16年。这16年的时间里&#xff0c;实邦电子不断积累经验&#xff0c;在电子科技领…

python基于vue美剧观影点评网站的设计与实现

目录摘要开发技术路线相关技术介绍核心代码参考示例结论源码lw获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01;摘要 设计并实现了一个基于Python和Vue的美剧观影点评网站&#xff0c;旨在为用户提供剧集评分、评论交流及个性化推荐功能。后端采…

9 款 AI 写论文哪个好?实测虎贲等考 AI:毕业论文的学术通关全能王

毕业季的论文战场上&#xff0c;“9 款 AI 写论文哪个好” 的灵魂拷问&#xff0c;总能在高校互助群里刷屏。不少同学踩坑无数&#xff1a;有的工具生成内容充斥 “文献幻觉”&#xff0c;有的查重结果与学校标准脱节&#xff0c;有的 AI 痕迹明显被系统预警。作为深耕论文写作…

魔果云课|寒假录课变现密码[特殊字符]

教培老师集合&#xff01;寒假想靠录课变现又怕麻烦&#xff1f;是不是总被这些问题卡壳&#x1f92f;手写圈题手酸到抬不起来&#xff0c;字迹模糊学员看不清重点&#xff1f;同一道题重复讲N遍&#xff0c;嗓子喊哑还总有人说没听懂&#xff1f;录课素材杂乱没法复用&#xf…

单片机毕业论文(毕设)易上手开题报告推荐

【单片机毕业设计项目分享系列】 &#x1f525; 这里是DD学长&#xff0c;单片机毕业设计及享100例系列的第一篇&#xff0c;目的是分享高质量的毕设作品给大家。 &#x1f525; 这两年开始毕业设计和毕业答辩的要求和难度不断提升&#xff0c;传统的单片机项目缺少创新和亮点…

【上海大学主办|应用数学会议】第六届应用数学、建模与智能计算国际学术会议(CAMMIC 2026)

第六届应用数学、建模与智能计算国际学术会议&#xff08;CAMMIC 2026&#xff09; 2026 6th International Conference on Applied Mathematics, Modelling and Intelligent Computing 2026年3月27-29日|中国-上海 第六届应用数学、建模与智能计算国际学术会议&#xff08;C…

探秘无锡大厂成熟Foc电机控制代码,解锁电动车控制新高度

无锡某大厂成熟Foc电机控制 代码&#xff0c;有原理图&#xff0c;用于很多电动车含高端电动自行车厂在用。 直接可用&#xff0c;不是一般的普通代码可比的。 有上位机用于调试和显示波形&#xff0c;直观调试。 代码基于Stm32F030&#xff0c;国产很多芯片可以通用。 本产品包…

深度测评8个AI论文软件,专科生搞定毕业论文+格式规范!

深度测评8个AI论文软件&#xff0c;专科生搞定毕业论文格式规范&#xff01; AI 工具如何成为专科生论文写作的“秘密武器” 在当今学术环境中&#xff0c;AI 工具正逐渐成为学生撰写论文的重要助手。尤其是对于专科生而言&#xff0c;面对繁重的论文任务和严格的格式要求&…

BCL-XL/CRBN PROTAC试剂盒:推动肿瘤选择性蛋白降解疗法开发的标准化工具

一、PROTAC技术的发展现状与核心价值 靶向蛋白降解嵌合体技术&#xff08;PROTAC&#xff09;是一种革命性的药物开发平台&#xff0c;能够利用细胞自身的泛素-蛋白酶体系统来选择性降解致病蛋白质。与传统抑制剂相比&#xff0c;PROTAC技术通过事件驱动的催化降解模式&#x…

第4章:开源模型全景图:如何选择你的技术底座

第4章:开源模型全景图:如何选择你的技术底座 引言 开源大模型生态正在经历爆炸式增长。截至2024年6月,HuggingFace平台托管的模型数量已超过50万个,每月新增数千个模型。面对如此庞杂的选择,技术决策者往往陷入两难:是选择规模最大、性能最强的模型,还是选择更符合实际…

一个网安老炮的私活生存指南:6年赚够4倍工资,这些野路子你敢试吗?

一个网安老炮的私活生存指南&#xff1a;6年赚够4倍工资&#xff0c;这些野路子你敢试吗&#xff1f; 前言 今天是我蹲在网络安全这个坑里的第9个年头。回想起刚入行时&#xff0c;怎么也没想到&#xff0c;这个让我熬夜扒漏洞、跟防火墙较劲的领域&#xff0c;竟成了我人生的…