【ProtoBuf 实战训练】网络版通讯录
文章目录1. 通讯录 4.0 实现网络版2. 环境搭建2.1 搭建服务端2.2 搭建客户端2.3 运行结果3. 新增联系人功能3.1 协议约定3.2 协议接口定义 (.proto)3.2.1 AddContactRequest请求消息3.2.2 AddContactResponse响应消息3.3 完成客户端代码3.3.1 菜单函数实现3.3.2 main 函数实现3.3.3 addContact 函数3.3.4 buildAddContactRequest 函数3.4 完成服务端代码3.4.1 main 函数实现3.4.2 生成 uid3.4.3 辅助功能说明4. 功能演示5. 总结1. 通讯录 4.0 实现网络版Protobuf 还常用于通讯协议、服务端数据交换场景。那么在这个示例中我们将实现一个网络版本的通讯录模拟实现客户端与服务端的交互通过 Protobuf 来实现各端之间的协议序列化。需求如下客户端可以选择对通讯录进行以下操作新增一个联系人删除一个联系人查询通讯录列表查询一个联系人的详细信息服务端提供增删查能力并需要持久化通讯录。客户端、服务端间的交互数据使用 Protobuf 来完成。2. 环境搭建Httplib 库cpp-httplib 是个开源的库并且是用 C 封装的 http 库使用这个库可以在 linux、windows 平台下完成 http 客户端、http 服务端的搭建。使用起来非常方便只需要包含头文件httplib.h即可。编译程序时需要带上-lpthread选项。源码库地址cpp-httplib直接把 httplib.h 这个文件复制到服务器上即可。另外如果使用 centOS 环境yum 源带的 g 最新版本是 4.8.5发布于 2015 年年代久远。那么使用最新版本的 cpp-httplib 编译该项目会出现异常。所以必须将 gcc/g 升级为更高版本可解决问题。# 安装gcc 8版本yuminstall-ydevtoolset-8-gcc devtoolset-8-gcc-c# 启用版本source/opt/rh/devtoolset-8/enable# 查看版本已经变成gcc 8.3.1gcc-v下面是一个简单的服务端和客户端 Demo 示例主要是演示一下 cpp-httplib 库是如何使用的。代码结构如下2.1 搭建服务端先编写服务端的主函数代码#includeiostream#includehttplib.husingstd::cout;usingstd::endl;usingstd::cerr;usingnamespacehttplib;intmain(){cout-----------服务启动----------endl;Server server;server.Post(/test-post,[](constRequestreq,Responseres){cout接收到post请求!endl;res.status200;});server.Get(/test-get,[](constRequestreq,Responseres){cout接收到get请求!endl;res.status200;});// 绑定 8123 端口并且将端口号对外开放server.listen(0.0.0.0,8123);return0;}再编写 makefileservice:*.cc g-o$$^-stdc11-lpthread-lprotobuf.PHONY:clean clean:rm-fservice2.2 搭建客户端先编写客户端的主函数代码#includeiostream#includehttplib.husingnamespacestd;usingnamespacehttplib;#defineCONTACTS_HOST172.19.61.204#defineCONTACTS_PORT8123intmain(){Clientcli(CONTACTS_HOST,CONTACTS_PORT);Result res1cli.Post(/test-post);if(res1-status200){cout调用post成功!endl;}Result res2cli.Get(/test-get);if(res2-status200){cout调用get成功!endl;}return0;}再编写 makefileservice:*.cc g-o$$^-stdc11-lpthread-lprotobuf.PHONY:clean clean:rm-fservice2.3 运行结果编译运行的结果如下以上就是 httplib 库的一个基本用法。3. 新增联系人功能在本篇文章中我们只针对【新增一个联系人】这个功能进行实现完整代码可以移步到代码仓库。3.1 协议约定对于新增一个联系人我们在 Client 客户端需要发起 POST 请求调用。格式如下[请求]Post/contacts/add AddContactRequest Content-Type:application/protobuf同时在服务端需要进行响应格式如下[响应]AddContactResponse Content-Type:application/protobuf3.2 协议接口定义 (.proto)接着开始编写 .proto 文件客户端和服务器一样这份.proto文件是定义客户端和服务端通信协议的 “合同”。它规定了数据将以什么样的格式进行打包和传输。3.2.1 AddContactRequest请求消息用于封装新增联系人的完整信息name (string)映射为 C 中的std::string。age (int32)映射为 C 中的int。Phone (嵌套消息)定义了电话详情包含号码number和枚举type。PhoneType (枚举)包含MP(0, 移动电话) 和TEL(1, 固定电话)。repeated Phone表示一个动态数组列表在 C 代码中通过add_phone()动态增加条目通过phone_size()获取数量。代码如下所示syntax proto3; package add_contact; 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; // 电话信息 }3.2.2 AddContactResponse响应消息用于服务端向客户端反馈执行结果success (bool)标识服务器业务处理是否成功。error_desc (string)当success为false时存储具体的报错信息如反序列化失败。uid (string)业务成功后返回服务端生成的唯一标识符。代码如下syntax proto3; package add_contact; message AddContactResponse { bool success 1; // 服务调用是否成功 string error_desc 2; // 错误原因 string uid 3; }3.3 完成客户端代码3.3.1 菜单函数实现功能描述该函数是一个纯显示型的辅助函数负责在终端界面输出美化后的操作菜单。实现要点用户引导清晰地列出了当前通讯录系统支持的五大功能选项新增、删除、查看列表、查看详情、退出。交互边界定义了合法输入的范围0-4为 main 函数中的逻辑判断提供视觉依据。代码如下所示voidmenu(){std::cout-----------------------------------------------------std::endl--------------- 请选择对通讯录的操作 ----------------std::endl------------------ 1、新增联系人 --------------------std::endl------------------ 2、删除联系人 --------------------std::endl------------------ 3、查看联系人列表 ----------------std::endl------------------ 4、查看联系人详细信息 ------------std::endl------------------ 0、退出 --------------------------std::endl-----------------------------------------------------std::endl;}3.3.2 main 函数实现功能描述客户端的逻辑控制中心负责驱动主循环、分发用户指令并处理全局异常。核心逻辑语义化选项通过枚举 OPTION 将数字输入0 - 4映射为具体业务如 ADD、QUIT提高代码可读性。交互循环使用 while(true) 配合 menu() 持续显示菜单直到用户输入 0 触发 return 0 退出。缓冲区清理在 cin choose 后立即执行 cin.ignore()防止残留的换行符干扰后续的字符串输入。统一容错通过 try-catch 捕获整个业务流中的 ContactsException。这样即便网络或序列化出错程序也能打印错误并安全返回主菜单不会直接崩溃。代码如下// 自定义异常类classContactsException{private:std::string message;public:ContactsException(std::string strA problem):message(str){}std::stringwhat()const{returnmessage;}};intmain(){enumOPTION{QUIT0,ADD,DEL,FIND_ALL,FIND_ONE};while(true){menu();cout---请选择;intchoose;cinchoose;cin.ignore(256,\n);try{switch(choose){caseOPTION::QUIT:cout---程序退出endl;return0;caseOPTION::ADD:addContact();break;caseOPTION::DEL:caseOPTION::FIND_ALL:caseOPTION::FIND_ONE:break;default:cout选择有误请重新选择!endl;break;}}catch(constContactsExceptione){cout---操作通讯录时发生异常endl---异常信息e.what()endl;}}return0;}3.3.3 addContact 函数功能描述该函数负责执行添加联系人的完整业务流。它是一个上层调用函数串联了本地交互、数据转换、网络通信及错误处理。核心逻辑步骤对象初始化创建 HTTP 客户端实例和空的 AddContactRequest 请求对象。调用构造逻辑调用 buildAddContactRequest 函数填充数据。序列化使用 SerializeToString() 将结构化对象转为二进制字节流以满足 Protobuf 传输要求。同步请求向服务器路径/contacts/add发起 POST 请求。特别指定了application/protobuf的 Content-Type告知服务端数据格式。多层级校验传输层检查 HTTP 请求是否送达if (!res)。协议层检查 HTTP 状态码是否为 200并尝试解析响应体。业务层解析成功后检查resp.success()字段确认服务端业务逻辑是否执行成功。结果反馈业务成功后从响应对象中获取并显示服务端分配的uid。代码如下voidaddContact(){// 定义客户端Clientcli(CONTACTS_HOST,CONTACTS_PORT);// 构造 reqAddContactRequest req;buildAddContactRequest(req);// 序列化 reqstring req_str;if(!req.SerializeToString(req_str)){throwContactsException(AddContactRequest序列化失败!);}// 发起 post 调用autorescli.Post(/contacts/add,req_str,application/protobuf);if(!res){string err_desc;err_desc.append(/contacts/add 链接失败错误信息).append(httplib::to_string(res.error()));throwContactsException(err_desc);}// 反序列化 respAddContactResponse resp;boolparseresp.ParseFromString(res-body);if(res-status!200!parse){string err_desc;err_desc.append(/contacts/add 调用失败).append(std::to_string(res-status)).append(().append(res-reason).append());throwContactsException(err_desc);}elseif(res-status!200){string err_desc;err_desc.append(/contacts/add 调用失败).append(std::to_string(res-status)).append(().append(res-reason).append() 错误原因).append(resp.error_desc());throwContactsException(err_desc);}elseif(!resp.success()){string err_desc;err_desc.append(/contacts/add 结果异常).append(异常原因).append(resp.error_desc());throwContactsException(err_desc);}// 结果打印cout新增联系人成功,联系人ID: resp.uid()endl;}3.3.4 buildAddContactRequest 函数功能描述该函数负责构造请求对象。它通过标准的输入输出流cin/cout与用户进行交互将用户输入的原始数据填充到 Protobuf 协议定义的结构体中。实现细节字段填充逐个提示用户输入 “姓名” 和 “年龄”并调用 Protobuf 自动生成的set_方法完成赋值。流清理在读取整数年龄后使用cin.ignore()清除缓冲区中的换行符确保后续getline能够正常读取字符串。重复字段处理利用for循环支持输入多个电话号码。每增加一个号码通过add_phone()动态创建一个电话子消息对象。输入校验通过if (number.empty())判断用户是否直接按下回车以此作为结束电话输入录入的信号。代码如下voidbuildAddContactRequest(AddContactRequest*req){cout请输入联系人姓名;string name;getline(cin,name);req-set_name(name);cout请输入联系人年龄;intage;cinage;req-set_age(age);cin.ignore(256,\n);for(inti0;;i){cout请输入联系人电话i1(只输入回车完成电话新增);string number;getline(cin,number);if(number.empty()){break;}AddContactRequest_Phone*phonereq-add_phone();phone-set_number(number);cout请输入该电话类型(1、移动电话 2、固定电话);inttype;cintype;cin.ignore(256,\n);switch(type){case1:phone-set_type(AddContactRequest_Phone_PhoneType::AddContactRequest_Phone_PhoneType_MP);break;case2:phone-set_type(AddContactRequest_Phone_PhoneType::AddContactRequest_Phone_PhoneType_TEL);break;default:cout选择有误endl;break;}}}3.4 完成服务端代码服务端采用了 cpp-httplib 作为 HTTP 框架结合 Protocol Buffers 处理数据的接收与响应并实现了简单的模拟持久化控制台打印以及 uuid 生成逻辑。3.4.1 main 函数实现功能描述main 函数是服务端的入口负责初始化 HTTP 服务器、注册路由回调逻辑并启动监听。实现逻辑解析路由注册通过server.Post(/contacts/add, ...)监听特定的 POST 请求路径。数据反序列化从req.body中提取二进制数据并使用 request.ParseFromString() 将其还原为 AddContactRequest 对象。业务处理调用 printContact() 函数将接收到的联系人姓名、年龄及电话列表打印到控制台模拟数据持久化过程。设置响应状态将 success 标记为 true。生成响应调用 generate_hex() 为新联系人生成一个唯一的 uid并将整个 AddContactResponse 对象序列化为二进制流。异常处理使用 try-catch 块捕获自定义异常 ContactsException。若发生错误如解析失败将 HTTP 状态码设为500并在响应体中封装具体的错误描述error_desc返回给客户端。服务启动调用server.listen(0.0.0.0, 8123)使服务在 8123 端口上运行并接受任何 IP 的访问。代码如下intmain(){cout-----------服务启动----------endl;Server server;server.Post(/contacts/add,[](constRequestreq,Responseres){cout接收到post请求!endl;// 反序列化 request: req.bodyadd_contact::AddContactRequest request;add_contact::AddContactResponse response;try{if(!request.ParseFromString(req.body)){throwContactsException(AddContactRequest反序列化失败!);}// 新增联系人持久化存储通讯录----》 打印新增的联系人信息printContact(request);// 构造 responseres.bodyresponse.set_success(true);response.set_uid(generate_hex(10));// res.body (序列化response)string response_str;if(!response.SerializeToString(response_str)){throwContactsException(AddContactResponse序列化失败!);}res.status200;res.bodyresponse_str;res.set_header(Content-Type,application/protobuf);}catch(constContactsExceptione){res.status500;response.set_success(false);response.set_error_desc(e.what());string response_str;if(response.SerializeToString(response_str)){res.bodyresponse_str;res.set_header(Content-Type,application/protobuf);}cout/contacts/add 发生异常异常信息e.what()endl;}});// 绑定 8123 端口并且将端口号对外开放server.listen(0.0.0.0,8123);return0;}3.4.2 生成 uid功能描述由于本示例未接入数据库服务端通过generate_hex函数手动生成一个 10 字节20 个字符的十六进制字符串作为联系人的唯一标识符UID。随机数引擎使用了 C11 标准的random库。std::random_device用于获取硬件随机种子。std::mt19937梅森旋转算法引擎具有随机性好、速度快的特点。std::uniform_int_distribution确保生成的随机整数均匀分布在[0, 255]范围内。十六进制转换利用std::stringstream和std::hex操纵符将随机生成的字节值转换为十六进制字符串。补位处理通过(hex.length() 2 ? 0 hex : hex)确保每个字节都占据两位字符例如将0x5转换为05从而保证生成的 uid 长度固定。代码如下voidprintContact(add_contact::AddContactRequestreq){cout联系人姓名req.name()endl;cout联系人年龄req.age()endl;for(intj0;jreq.phone_size();j){constadd_contact::AddContactRequest_Phonephonereq.phone(j);cout联系人电话j1phone.number();// 联系人电话1:1311111 (MP)cout (phone.PhoneType_Name(phone.type()))endl;}}staticunsignedintrandom_char(){// ⽤于随机数引擎获得随机种⼦std::random_device rd;// mt19937是c11新特性它是⼀种随机数算法⽤法与rand()函数类似但是mt19937具有速度快周期⻓的特点// 作⽤是⽣成伪随机数std::mt19937gen(rd());// 随机⽣成⼀个整数i 范围[0, 255]std::uniform_int_distributiondis(0,255);returndis(gen);}3.4.3 辅助功能说明ContactsException 类自定义异常类专门用于捕获并传递 Protobuf 处理过程中的序列化/反序列化错误。代码如下// 自定义异常类classContactsException{private:std::string message;public:ContactsException(std::string strA problem):message(str){}std::stringwhat()const{returnmessage;}};printContact 函数该函数展示了如何遍历 Protobuf 中的 repeated 字段电话列表并利用 PhoneType_Name() 接口将枚举数值转换为易读的字符串标签。代码如下voidprintContact(add_contact::AddContactRequestreq){cout联系人姓名req.name()endl;cout联系人年龄req.age()endl;for(intj0;jreq.phone_size();j){constadd_contact::AddContactRequest_Phonephonereq.phone(j);cout联系人电话j1phone.number();// 联系人电话1:1311111 (MP)cout (phone.PhoneType_Name(phone.type()))endl;}}4. 功能演示对于客户端和服务器都需要先编译 proto 文件protoc--cpp_out. add_contact.proto接着再进行 make 编译 cpp 代码文件即可。make新增一个联系人的结果如下所示5. 总结下面将展示网络版通讯录的完整功能。新增第一个联系人如下图所示新增第二个联系人如下图所示查看联系人列表如下图所示查看联系人详细信息如下图所示删除联系人 张三如下图所示此时通讯录只剩李四这一个联系人了对于完整的通讯录代码可以移步到我的代码仓库Online-Address-Book
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2481291.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!