当前位置: 首页 > news >正文

【ProtoBuf 】C++ 网络通讯录开发实战:ProtoBuf 协议设计与 HTTP 服务实现

文章目录

  • 一、环境搭建
  • 二、约定双端交互接口
  • 三、客户端
    • 3.1 实现客户端菜单
    • 3.2 实现异常类
    • 3.3 实现客户端代码
  • 四、服务端
    • 4.1 生成UID
    • 4.2 打印数据
    • 4.3 实现服务端代码
  • 五、总结
    • 5.1 序列化能力对比验证
    • 5.2 各类序列化协议总结

ProtoBuf语法相关知识点可以通过点击以下链接进行学习一起加油!
ProtoBuf入门与安装ProtoBuf快速上手Proto3 语法与类型实战ProtoBuf 进阶实战:默认值、消息更新与兼容性最佳实践

在现代软件开发中,数据序列化是系统间通信的核心技术之一。随着分布式架构和微服务的普及,如何高效、可靠地在客户端与服务端之间传输结构化数据成为了关键问题。Protocol Buffers(protobuf)作为 Google 开源的序列化框架,以其高性能、跨语言支持和强类型约束等特性,在众多序列化方案中脱颖而出。本文将通过实现一个完整的网络版通讯录系统,深入探讨 protobuf 在实际项目中的应用,并与 JSON、XML 等传统序列化方案进行全面对比。

Protobuf 还常用于通讯协议、服务端数据交换场景。那么在这个示例中,我们将实现一个网络版本的
通讯录,模拟实现客户端与服务端的交互,通过 Protobuf 来实现各端之间的协议序列化。
需求如下:
客户端可以选择对通讯录进行以下操作:

  • 新增一个联系人

    • 删除一个联系人

    • 查询通讯录列表

    • 查询一个联系人的详细信息

  • 服务端提供 增 删 查 能力,并需要持久化通讯录。

  • 客户端、服务端间的交互数据来使用Protobuf来完成

一、环境搭建

Httplib 库:cpp-httplib 是个开源的库,是一个c++封装的http库,使用这个库可以在linux、windows平台下完成http客户端、http服务端的搭建。使用起来非常方便,只需要包含头文件httplib.h 即可。编译程序时,需要带上 -lpthread 选项。

  • 源码库地址:https://github.com/yhirose/cpp-httplib
  • 镜像仓库:https://gitcode.net/mirrors/yhirose/cpp-httplib?utm_source=csdn_github_acceleratore

image-20250902182100242

将这个httplib.h头文件拿过来使用。

二、约定双端交互接口

image-20250902185707953image-20250830235017906

新增一个联系人:

[请求]Post /contacts/add AddContactRequestContent-Type:application/protobuf[响应]AddContactResponseContent-Type:application/protobuf

删除一个联系人

[请求]Post /contacts/del DelContactRequestContent-Type:application/protobuf[响应]DelContactResponseContent-Type:application/protobuf

查询通讯录列表:

[请求]GET /contacts/find-all
[响应]FindAllContactsResponseContent-Type: application/protobuf

查询一个联系人的详细信息

[请求]Post /contacts/find-one FindOneContactRequestContent-Type: application/protobuf
[响应]FindOneContactResponseContent-Type: application/protobuf

三、客户端

3.1 实现客户端菜单

image-20250902183219546

可能在调用对应函数中,会出现错误,我们可以实现自定义异常类,打印异常的原因。

3.2 实现异常类

image-20250902183547682

#include <string>
class ContactsException{private:std::string message;public:ContactsException(std::string str = "A problem") : message(str){}std::string what() const{return message;}
}

3.3 实现客户端代码

#include <iostream>
#include "httplib.h"
#include "ContactsException.h"
#include "add_contact.pb.h"using namespace std;
using namespace httplib;#define CONTACTS_HOST "139.159.150.152"
#define CONTACTS_PORT 8123void addContact();void menu() {std::cout << "-----------------------------------------------------" << std::endl<< "--------------- 请选择对通讯录的操作 ----------------" << std::endl<< "------------------ 1、新增联系⼈ --------------------" << std::endl << "------------------ 2、删除联系⼈ --------------------" << std::endl<< "------------------ 3、查看联系⼈列表 ----------------" << std::endl << "------------------ 4、查看联系⼈详细信息 ------------" << std::endl<< "------------------ 0、退出 --------------------------" << std::endl<< "-----------------------------------------------------" << std::endl;
}int main(){enum OPTION {QUIT = 0, ADD, DEL, FIND_ALL, FIND_ONE};while(true){    //手动退出menu();cout << "--->请选择: ";int choose;cin >> choose;cin.ignore(256,'\n');try{switch(choose){case OPTION:QUIT:cout <<"--->程序退出" << endl;return 0;case OPTION:ADD:addContact();break;//下面的就不实现了case OPTION::DEL:case OPTION::FIND_ALL:case OPTION::FIND_ONE:break;default:cout << "选择有误,请重新选择!" << endl;break;} catch(const ContactsException& e){cout << "--->操作通讯录时发生异常" << endl<< "--->异常信息:" << e.what() << endl;}}}return 0;void buildAddContactRequest(add_contact::AddContactRequest* req) {cout << "请输入联系人姓名:";string name;getline(cin, name);req->set_name(name);cout << "请输入联系人年龄:";int age;cin >> age;req->set_age(age);cin.ignore(256, '\n');for (int i = 0;; i++) {cout << "请输入联系人电话" << i+1 << "(只输⼊回⻋完成电话新增):";string number;getline(cin, number);if (number.empty()) {break;}add_contact::AddContactRequest_Phone* phone = req->add_phone();phone->set_number(number);cout << "请输入该电话类型(1、移动电话   2、固定电话): ";int type;cin >> type;cin.ignore(256, '\n');switch (type) {case 1:phone->set_type(add_contact::AddContactRequest_Phone_PhoneType::AddContactRequest_Phone_PhoneType_MP);break;case 2:phone->set_type(add_contact::AddContactRequest_Phone_PhoneType::AddContactRequest_Phone_PhoneType_TEL);break;default:cout << "选择有误!" << endl;break;}}
}void addContact(){Client cli(CONTACTS_HOST,CONTACTS_PORT);//构建 reqadd_contact::AddContactRequest req;buildAddContactRequest(&req);//序列化 reqstring req_str;if (!req.SerializeToString(&req_str)) {throw ContactsException("AddContactRequest序列化失败!");}//发起post调用auto res = cil.Post("/contacts/add", req_str, "application/protobuf");if (!res) {string err_desc;err_desc.append("/contacts/add 链接失败!错误信息:").append(httplib::to_string(res.error()));throw ContactsException(err_desc);} //反序列化add_contact::AddContactRequest resp;bool parse = resp.ParseFromStirng(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(")");throw ContactsException(err_desc);}else if (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());throw ContactsException(err_desc);} else if (!resp.success()) {string err_desc;err_desc.append("/contacts/add 结果异常") .append("异常原因:").append(resp.error_desc());throw ContactsException(err_desc);}// 结果打印cout << "新增联系人成功,联系人ID: " << resp.uid() << endl;
}

四、服务端

这个resp和这个response不是一个东西。

image-20250831091151622

image-20250831091224670

4.1 生成UID

image-20250903102911654

static unsigned int random_char(){//用于随机数引擎获得随机种子std::random_device rd;// mt19937是c++11新特性,它是⼀种随机数算法,⽤法与rand()函数类似,但是mt19937具有速度快,周期⻓的特点// 作⽤是⽣成伪随机数std::mt19937 gen(rd());// 随机⽣成⼀个整数i 范围[0, 255]std::uniform_int_distribution<> dis(0, 255);return dis(gen);// ⽣成 UUID (通⽤唯⼀标识符)static std::string generate_hex(const unsigned int len) {std::stringstream ss;// ⽣成 len 个16进制随机数,将其拼接⽽成for (auto i = 0; i < len; i++) {const auto rc = random_char();std::stringstream hexstream;hexstream << std::hex << rc;auto hex = hexstream.str();ss << (hex.length() < 2 ? '0' + hex : hex);}return ss.str();}

4.2 打印数据

image-20250903103014835

void printContact(add_contact::AddContactRequest &req){cout << "联系人姓名: " << req.name() << endl;cout << "联系人年龄:" << req.age() << endl;for(int j = 0; j < req.phone_size(); j++){const add_contact::AddContactRequest_Phone& phone = req.phone(j);cout << "联系人电话" << j+1 << ":" << phone.number();// 联系人电话1:1311111  (MP)cout << "   (" << phone.PhoneType_Name(phone.type()) << ")" <<endl;}
}

4.3 实现服务端代码

#include <iostram>
#include "httplib.h"
#include "add_contact.pb.h"using namespace std;
using namespace httplibclass ContactsException
{
private:std::string message;
public:ContactsException(std::string str = "A problem") : message(str) {}std::string what() const {return message;}
};void printContact(add_contact::AddContactRequest &req){cout << "联系人姓名: " << req.name() << endl;cout << "联系人年龄:" << req.age() << endl;for(int j = 0; j < req.phone_size(); j++){const add_contact::AddContactRequest_Phone& phone = req.phone(j);cout << "联系人电话" << j+1 << ":" << phone.number();// 联系人电话1:1311111  (MP)cout << "   (" << phone.PhoneType_Name(phone.type()) << ")" <<endl;}}static unsigned int random_char(){//用于随机数引擎获得随机种子std::random_device rd;// mt19937是c++11新特性,它是⼀种随机数算法,⽤法与rand()函数类似,但是mt19937具有速度快,周期⻓的特点// 作⽤是⽣成伪随机数std::mt19937 gen(rd());// 随机⽣成⼀个整数i 范围[0, 255]std::uniform_int_distribution<> dis(0, 255);return dis(gen);// ⽣成 UUID (通⽤唯⼀标识符)static std::string generate_hex(const unsigned int len) {std::stringstream ss;// ⽣成 len 个16进制随机数,将其拼接⽽成for (auto i = 0; i < len; i++) {const auto rc = random_char();std::stringstream hexstream;hexstream << std::hex << rc;auto hex = hexstream.str();ss << (hex.length() < 2 ? '0' + hex : hex);}return ss.str();}int main() {cout << "-----------服务启动----------" << endl;Server server;server.Post("/contacts/add", [](const Request& req, Response& res) {cout << "接收到post请求!" << endl; // 反序列化 request: req.bodyadd_contact::AddContactRequest request;add_contact::AddContactResponse response;try {if (!request.ParseFromString(req.body)) {throw ContactsException("AddContactRequest反序列化失败!");}// 新增联系人,持久化存储通讯录----》 打印新增的联系人信息printContact(request);// 构造 response:res.bodyresponse.set_success(true);response.set_uid(generate_hex(10));// res.body (序列化response)string response_str;if (!response.SerializeToString(&response_str)) {throw ContactsException("AddContactResponse序列化失败!");}res.status = 200;res.body = response_str;res.set_header("Content-Type", "application/protobuf");} catch (const ContactsException& e) {res.status = 500;response.set_success(false);response.set_error_desc(e.what());string response_str;if (response.SerializeToString(&response_str)) {res.body = response_str;res.set_header("Content-Type", "application/protobuf");}cout << "/contacts/add 发生异常,异常信息:" << e.what() << endl;}});//绑定 8123 端口,并且将端口号对外开放server.listen("0.0.0.0",8123);return 0;}

五、总结

5.1 序列化能力对比验证

在这里让我们分别使用 PB 与 JSON 的序列化与反序列化能力, 对值完全相同的一份结构化数据进行不同次数的性能测试。
为了可读性,下面这一份文本使用 JSON 格式展示了需要被进行测试的结构化数据内容:

{"age": 20,"name": "张珊","phone": [{"number": "110112119","type": 0},{"number": "110112119","type": 0},{"number": "110112119","type": 0},{"number": "110112119","type": 0}],"qq": "95991122","address": {"home_address": "陕西省西安市长安区","unit_address": "陕西省西安市雁塔区"},"remark": {"key1": "value1","key2": "value2","key3": "value3","key4": "value4","key5": "value5"}
}

开始进行测试代码编写,我们在新的目录下新建 contacts.proto文件,内容如下:

syntax = "proto3";package compare_serialization;import "google/protobuf/any.proto"; // 引入 any.proto 文件// 地址
message Address {string home_address = 1; // 家庭地址string unit_address = 2; // 单位地址
}// 联系人
message PeopleInfo {string name = 1; // 姓名int32 age = 2; // 年龄message Phone {string number = 1; // 电话号码enum PhoneType {MP = 0;  // 移动电话TEL = 1; // 固定电话}PhoneType type = 2; // 类型}repeated Phone phone = 3; // 电话google.protobuf.Any data = 4; // 其他联系方式:多选一string qq = 5;string weixin = 6;map<string, string> remark = 7; // 备注
}

使用 protoc 命令编译文件后,新建性能测试文件 compare.cc,我们分别对相同的结构化数据进行100 、1000 、10000 、100000 次的序列化与反序列化,分别获取其耗时与序列化后的大小。

image-20250831092419399

5.2 各类序列化协议总结

序列化协议对比

序列化协议通用性格式可读性序列化大小序列化性能适用场景
JSON通用(多语言/多平台支持广)文本格式好(人类可读、易调试)轻量(结构紧凑;压缩后更小)Web/API、配置、日志、浏览器环境
XML通用(标准化良好,生态成熟)文本格式一般(标签冗长)重(标签开销大,体积通常较大)文档交换、需命名空间/Schema 验证、老系统互操作
ProtoBuf多语言但需 .proto/编译器支持二进制格式差(不适合人工直接阅读)最小(通常比 JSON/XML 更小)gRPC/RPC、微服务、移动弱网、IoT、高性能传输

优缺点摘要

协议优点缺点备注
JSON易读易写、生态广、与 JS 天然适配类型表达力一般(无强 Schema)、二进制/精度处理需额外方案业务可读性强、前后端交互常用
XML结构严谨、命名空间、XSD/DTD 校验、XPath/XSLT 转换能力冗长、解析/序列化较慢、传输开销大适合需要强规范与文档化的集成场景
ProtoBuf体积小、速度快、强 Schema、良好前后兼容(字段号)、跨语言性能稳定二进制不可读、需维护 .proto 并编译、对临时人工排错不友好常与 gRPC 配合,用于高性能与带宽敏感场景

小结

  1. XMLJSONProtoBuf 都具有数据结构化和数据序列化的能力。
  2. XMLJSON 更注重数据结构化,关注可读性和语义表达能力。ProtoBuf 更注重数据序列化,关注
    效率、空间、速度,可读性差,语义表达能力不足,为保证极致的效率,会舍弃一部分元信息。
  3. ProtoBuf 的应用场景更为明确,XMLJSON 的应用场景更为丰富。 比特就业
http://www.xdnf.cn/news/1439983.html

相关文章:

  • 构建下一代互联网:解码Web3、区块链、协议与云计算的协同演进
  • 【微信小程序预览文件】(PDF、DOC、DOCX、XLS、XLSX、PPT、PPTX)
  • 机器学习进阶,一文搞定模型选型!
  • 智能高效内存分配器测试报告
  • 根据fullcalendar实现企业微信的拖动式预约会议
  • Linux 用户的 Windows 改造之旅
  • Web端最强中继器表格元件库来了!55页高保真交互案例,Axure 9/10/11通用
  • 使用langgraph创建工作流系列3:增加记忆
  • 100种高级数据结构 (速查表)
  • 【NVIDIA B200】1.alltoall_perf 单机性能深度分析:基于 alltoall_perf 测试数据
  • 如何评价2025年数学建模国赛?
  • Debezium系列之:Flink SQL消费Debezium数据,只消费新增数据,过滤掉更新、删除数据
  • 计算机毕业设计选题推荐:基于Python+Django的新能源汽车数据分析系统
  • AI随笔番外 · 猫猫狐狐的尾巴式技术分享
  • Networking Concepts
  • 超越马力欧:如何为经典2D平台游戏注入全新灵魂
  • vue 手动书写步骤条
  • 用Blender制作Rat Rod风格汽车
  • MySQL 8.0.40 主从复制完整实验总结(基础搭建 + 进阶延时同步与误操作恢复)
  • 智能电视小米电视浏览器兼容性踩坑电视黑屏或者电视白屏,Vue项目从Axios到Fetch的避坑指南
  • GitHub每日最火火火项目(9.3)
  • 演员-评论员算法有何优点?
  • 《探索C++11:现代语法的性能优化策略(中篇)》
  • 从公共形象到专属定制,井云交互数字人满足金融/政务多元需求
  • etcd对比redis
  • MySQL--CRUD
  • Oracle 10g 安装教程(详解,从exe安装到数据库配置,附安装包)​
  • 食物分类案例优化改进 (数据增强,最优模型保存和使用)
  • oracle 从一张表更新到另外一张表的方法(MERGE)
  • IO进程线程;进程,发送信号;进程,消息队列通信;0903