
🌈 个人主页:Zfox_
🔥 系列专栏:ProtoBuf

🔥 ProtoBuf:通讯录4.0实现
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_accelerator
🦋 约定双端交互接⼝
新增⼀个联系⼈:
[请求]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
🦋 约定双端交互 req / resp
base_response.proto
syntax = "proto3";
package base_response;message BaseResponse {bool success = 1; // 返回结果string error_desc = 2; // 错误描述
}
add_contact_request.proto
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;
}message AddContactResponse {bool success = 1; // 服务调用是否成功string error_desc = 2; // 错误原因string uid = 3;
}
add_contact_response.proto
message AddContactResponse {bool success = 1; // 服务调用是否成功string error_desc = 2; // 错误原因string uid = 3;
}
del_contact_request.proto
syntax = "proto3";
package del_contact_req;// 删除⼀个联系⼈ req
message DelContactRequest {string uid = 1; // 联系⼈ID
}
del_contact_response.proto
syntax = "proto3";
package del_contact_resp;
import "base_response.proto"; // 引⼊base_response// 删除⼀个联系⼈ resp
message DelContactResponse {base_response.BaseResponse base_resp = 1;string uid = 2;
}
find_one_contact_request.proto
syntax = "proto3";
package find_one_contact_req;// 查询⼀个联系⼈ req
message FindOneContactRequest {string uid = 1; // 联系⼈ID
}
find_one_contact_response.proto
syntax = "proto3";
package find_one_contact_resp;
import "base_response.proto"; // 引⼊base_response// 查询⼀个联系⼈ resp
message FindOneContactResponse {base_response.BaseResponse base_resp = 1;string uid = 2; // 联系⼈IDstring name = 3; // 姓名int32 age = 4; // 年龄message Phone {string number = 1; // 电话号码enum PhoneType {MP = 0; // 移动电话TEL = 1; // 固定电话}PhoneType type = 2; // 类型}repeated Phone phone = 5; // 电话map<string, string> remark = 6; // 备注
}
find_all_contacts_response.proto
syntax = "proto3";
package find_all_contacts_resp;
import "base_response.proto"; // 引⼊base_response// 联系⼈摘要信息
message PeopleInfo {string uid = 1; // 联系⼈IDstring name = 2; // 姓名
}
// 查询所有联系⼈ resp
message FindAllContactsResponse {base_response.BaseResponse base_resp = 1;repeated PeopleInfo contacts = 2;
}
🦋 客户端代码实现
🎀 main.cc
#include <iostream>
#include "httplib.h"
#include "ContactsException.h"
#include "add_contact.pb.h"using namespace httplib;#define CONTACTS_HOST "127.0.0.1"
#define CONTACTS_POST 8080void menu()
{std::cout << "-----------------------------------------------------" << std::endl<< "--------------- 请选择对通讯录的操作 ----------------" << std::endl<< "------------------ 1、新增联系⼈ --------------------" << std::endl<< "------------------ 2、删除联系⼈ --------------------" << std::endl<< "------------------ 3、查看联系⼈列表 ----------------" << std::endl<< "------------------ 4、查看联系⼈详细信息 ------------" << std::endl<< "------------------ 0、退出 --------------------------" << std::endl<< "-----------------------------------------------------" << std::endl;
}void addContact();int main()
{enum OPTION{QUIT = 0,ADD,DEL,FIND_ALL,FIND_ONE};while (true){menu();std::cout << "--->请选择: ";int choose;std::cin >> choose;std::cin.ignore(256, '\n');try{switch (choose){case OPTION::QUIT:std::cout << "--->程序退出" << std::endl;return 0;case OPTION::ADD:addContact();break;case OPTION::DEL:break;case OPTION::FIND_ALL:break;case OPTION::FIND_ONE:break;default:std::cout << "选择有误,请重新选择!" << std::endl;break;}}catch (const ContactsException &e){std::cout << "--->操作通讯录时发生异常" << std::endl;std::cout << "--->异常信息: " << e.what() << std::endl;}}return 0;
}void buildAddContactRequest(add_contact::AddContactRequest *req)
{std::cout << "--------------------新增联系人--------------------" << std::endl;std::cout << "请输入联系人的姓名: ";std::string name;std::getline(std::cin, name);req->set_name(name);std::cout << "请输入联系人的年龄: ";int age;std::cin >> age;req->set_age(age);std::cin.ignore(256, '\n');for (int i = 0;; i++){std::cout << "请输入联系人的电话 " << i + 1 << " " << "(只输⼊回⻋完成电话新增): ";std::string number;std::getline(std::cin, number);if (number.empty())break;add_contact::AddContactRequest_Phone *phone = req->add_phone();phone->set_number(number);std::cout << "请输入该电话的类型 (1. 移动电话 2. 固定电话): ";int type;std::cin >> type;std::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:std::cout << "选择有误! " << std::endl;break;}}std::cout << "-------------------添加联系人成功------------------" << std::endl;
}void addContact()
{Client client(CONTACTS_HOST, CONTACTS_POST);// 构造 reqadd_contact::AddContactRequest req;buildAddContactRequest(&req);// 序列化 reqstd::string req_str;if (!req.SerializeToString(&req_str)){throw ContactsException("AddContactRequest 序列化失败! ");}// 发起 post 调用Result res = client.Post("/contacts/add", req_str, "application/protobuf");if (!res){std::string err_desc;err_desc.append("/contacts/add 链接失败!错误信息:").append(httplib::to_string(res.error()));throw ContactsException(err_desc);}// 反序列化 respadd_contact::AddContactResponse resp;bool parse = resp.ParseFromString(res->body);// 反序列化也失败了if (res->status != 200 && !parse){std::string err_desc;err_desc.append("/contacts/add 调用失败! ").append(std::to_string(res->status)).append("(" + res->reason + ")");throw ContactsException(err_desc);}else if (res->status != 200){std::string err_desc;err_desc.append("/contacts/add 调用失败! ").append(std::to_string(res->status)).append("(" + res->reason + ")").append("错误原因:" + resp.error_desc());throw ContactsException(err_desc);}else if (!resp.success()){std::string err_desc;err_desc.append("/contacts/add 结果异常! ").append(std::to_string(res->status)).append("(" + res->reason + ")").append("异常原因:" + resp.error_desc());throw ContactsException(err_desc);}// 结果打印std::cout << "新增联系人成功,联系人id:" << resp.uid() << std::endl;
}
🎀 ContactException.h:定义异常类
#include <iostream>
#include <string>class ContactsException
{
public:ContactsException(const std::string &str = "A problem") : message(str) {}~ContactsException() {}std::string what() const{return message;}
private:std::string message;
};
🦋 服务端代码实现
🎀 main.cc
#include <iostream>
#include <string>
#include <sstream>
#include <random>
#include <google/protobuf/map.h>
#include "httplib.h"
#include "add_contact.pb.h"using namespace httplib;class ContactsException
{
public:ContactsException(const std::string &str = "A problem") : message(str) {}~ContactsException() {}std::string what() const{return message;}private:std::string message;
};void printContact(add_contact::AddContactRequest &req)
{std::cout << "联系人姓名:" << req.name() << std::endl;std::cout << "联系人年龄: " << req.age() << std::endl;for (int j = 0; j < req.phone_size(); j++){const add_contact::AddContactRequest_Phone &phone = req.phone(j);std::cout << "联系人电话: " << j + 1 << " " << phone.number();std::cout << " (" << phone.PhoneType_Name(phone.type()) << ")" << std::endl;}
}class Utils
{
public: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();}static void map_copy(google::protobuf::Map<std::string, std::string> *target, const google::protobuf::Map<std::string, std::string> &source){if (nullptr == target){std::cout << "map_copy warning, target is nullptr!" << std::endl;return;}for (auto it = source.cbegin(); it != source.cend(); ++it){target->insert({it->first, it->second});}}
};int main()
{std::cout << "-------------服务启动-------------" << std::endl;Server server;server.Post("/contacts/add", [](const Request &req, Response &resp) {std::cout << "接收到post请求!" << std::endl;// 反序列化 request req.bodyadd_contact::AddContactRequest request;add_contact::AddContactResponse response;try {if(!request.ParseFromString(req.body)) {throw ContactsException("AddContactRequest 反序列化失败! ");}// 新增联系人,持久化存储通讯录 ----> 打印新增的联系人信息printContact(request);// 构造 response resp.bodyresponse.set_success(true);response.set_uid(Utils::generate_hex(10));// resp.body(序列化 response)std::string response_str;if(!response.SerializeToString(&response_str)) {throw ContactsException("AddContactResponse 序列化失败");}resp.status = 200;resp.body = response_str;resp.set_header("Content-Type", "application/protobuf");} catch (const ContactsException &e) {resp.status = 500;response.set_success(false);response.set_error_desc(e.what());std::string response_str;if(response.SerializeToString(&response_str)) {resp.body = response_str;resp.set_header("Content-Type", "application/protobuf");}std::cout << "/contacts/add 发生异常,异常信息:" << e.what() << std::endl;} });// 绑定 8080 端口,并且将端口对外开放server.listen("0.0.0.0", 8080);return 0;
}
🔥 序列化能⼒对⽐验证
在这⾥让我们分别使⽤PB与JSON的序列化与反序列化能⼒,对值完全相同的⼀份结构化数据进⾏不同次数的性能测试。
为了可读性,下⾯这⼀份⽂本使⽤JSON格式展⽰了需要被进⾏测试的结构化数据内容:
{"age" : 20,"name" : "张珊","phone" :{{"number" : "110112119","type" : 0},{"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;oneof other_contact { // 其他联系⽅式:多选⼀string qq = 5;string weixin = 6;}map<string, string> remark = 7; // 备注
}
使⽤protoc命令编译⽂件后,新建性能测试⽂件compare.cc,我们分别对相同的结构化数据进⾏ 100 、 1000 、 10000 、 100000 次的序列化与反序列化,分别获取其耗时与序列化后的⼤⼩。
内容如下:
#include <iostream>
#include <sys/time.h>
#include <jsoncpp/json/json.h>
#include "contacts.pb.h"using namespace std;
using namespace compare_serialization;
using namespace google::protobuf;#define TEST_COUNT 100void createPeopleInfoFromPb(PeopleInfo *people_info_ptr);
void createPeopleInfoFromJson(Json::Value &root);int main(int argc, char *argv[])
{struct timeval t_start, t_end;double time_used;int count;string pb_str, json_str;// ------------------------------Protobuf 序列化------------------------------------{PeopleInfo pb_people;createPeopleInfoFromPb(&pb_people);count = TEST_COUNT;gettimeofday(&t_start, NULL);// 序列化count次while ((count--) > 0){pb_people.SerializeToString(&pb_str);}gettimeofday(&t_end, NULL);time_used = 1000000 * (t_end.tv_sec - t_start.tv_sec) + t_end.tv_usec -t_start.tv_usec;cout << TEST_COUNT << "次 [pb序列化]耗时:" << time_used / 1000 << "ms."<< " 序列化后的⼤⼩:" << pb_str.length() << endl;}// ------------------------------Protobuf 反序列化------------------------------------{PeopleInfo pb_people;count = TEST_COUNT;gettimeofday(&t_start, NULL);// 反序列化count次while ((count--) > 0){pb_people.ParseFromString(pb_str);}gettimeofday(&t_end, NULL);time_used = 1000000 * (t_end.tv_sec - t_start.tv_sec) + t_end.tv_usec -t_start.tv_usec;cout << TEST_COUNT << "次 [pb反序列化]耗时:" << time_used / 1000 << "ms."<< endl;}// ------------------------------JSON 序列化------------------------------------{Json::Value json_people;createPeopleInfoFromJson(json_people);Json::StreamWriterBuilder builder;count = TEST_COUNT;gettimeofday(&t_start, NULL);// 序列化count次while ((count--) > 0){json_str = Json::writeString(builder, json_people);}gettimeofday(&t_end, NULL);// 打印序列化结果// cout << "json: " << endl << json_str << endl;time_used = 1000000 * (t_end.tv_sec - t_start.tv_sec) + t_end.tv_usec -t_start.tv_usec;cout << TEST_COUNT << "次 [json序列化]耗时:" << time_used / 1000 << "ms."<< " 序列化后的⼤⼩:" << json_str.length() << endl;}// ------------------------------JSON 反序列化------------------------------------{Json::CharReaderBuilder builder;unique_ptr<Json::CharReader> reader(builder.newCharReader());Json::Value json_people;count = TEST_COUNT;gettimeofday(&t_start, NULL);// 反序列化count次while ((count--) > 0){reader->parse(json_str.c_str(), json_str.c_str() + json_str.length(),&json_people, nullptr);}gettimeofday(&t_end, NULL);time_used = 1000000 * (t_end.tv_sec - t_start.tv_sec) + t_end.tv_usec -t_start.tv_usec;cout << TEST_COUNT << "次 [json反序列化]耗时:" << time_used / 1000 << "ms."<< endl;}return 0;
}/*** 构造pb对象*/
void createPeopleInfoFromPb(PeopleInfo *people_info_ptr)
{people_info_ptr->set_name("张珊");people_info_ptr->set_age(20);people_info_ptr->set_qq("95991122");for (int i = 0; i < 5; i++){PeopleInfo_Phone *phone = people_info_ptr->add_phone();phone->set_number("110112119");phone->set_type(PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);}Address address;address.set_home_address("陕西省西安市⻓安区");address.set_unit_address("陕西省西安市雁塔区");google::protobuf::Any *data = people_info_ptr->mutable_data();data->PackFrom(address);people_info_ptr->mutable_remark()->insert({"key1", "value1"});people_info_ptr->mutable_remark()->insert({"key2", "value2"});people_info_ptr->mutable_remark()->insert({"key3", "value3"});people_info_ptr->mutable_remark()->insert({"key4", "value4"});people_info_ptr->mutable_remark()->insert({"key5", "value5"});
}/*** 构造json对象*/
void createPeopleInfoFromJson(Json::Value &root)
{root["name"] = "张珊";root["age"] = 20;root["qq"] = "95991122";for (int i = 0; i < 5; i++){Json::Value phone;phone["number"] = "110112119";phone["type"] = 0;root["phone"].append(phone);}Json::Value address;address["home_address"] = "陕西省西安市⻓安区";address["unit_address"] = "陕西省西安市雁塔区";root["address"] = address;Json::Value remark;remark["key1"] = "value1";remark["key2"] = "value2";remark["key3"] = "value3";remark["key4"] = "value4";remark["key5"] = "value5";root["remark"] = remark;
}
测试结果如下:
100次 [pb序列化]耗时:0.342ms. 序列化后的⼤⼩:278
100次 [pb反序列化]耗时:0.435ms.
100次 [json序列化]耗时:1.306ms. 序列化后的⼤⼩:567
100次 [json反序列化]耗时:0.926ms.1000次 [pb序列化]耗时:3.59ms. 序列化后的⼤⼩:278
1000次 [pb反序列化]耗时:5.069ms.
1000次 [json序列化]耗时:11.582ms. 序列化后的⼤⼩:567
1000次 [json反序列化]耗时:9.289ms.10000次 [pb序列化]耗时:34.386ms. 序列化后的⼤⼩:278
10000次 [pb反序列化]耗时:45.96ms.
10000次 [json序列化]耗时:115.76ms. 序列化后的⼤⼩:567
10000次 [json反序列化]耗时:91.046ms.100000次 [pb序列化]耗时:349.937ms. 序列化后的⼤⼩:278
100000次 [pb反序列化]耗时:428.366ms.
100000次 [json序列化]耗时:1150.54ms. 序列化后的⼤⼩:567
100000次 [json反序列化]耗时:904.58ms.
由实验结果可得:
- 编解码性能:ProtoBuf的编码解码性能,⽐JSON⾼出2-4倍。
- 内存占⽤:ProtoBuf的内存278,⽽JSON到达567,ProtoBuf的内存占⽤只有JSON的1/2。
注:以上结论的数据只是根据该项实验得出。因为受不同的字段类型、字段个数等影响,测出的数据会有所差异。
该实验有很多可待优化的地⽅。但其实这种粗略的测试,也能看出来ProtoBuf的优势。
🦋 总结
⼩结:
- XML、JSON、ProtoBuf都具有数据结构化和数据序列化的能⼒。
- XML、JSON更注重数据结构化,关注可读性和语义表达能⼒。ProtoBuf更注重数据序列化,关注效率、空间、速度,可读性差,语义表达能⼒不⾜,为保证极致的效率,会舍弃⼀部分元信息。
- ProtoBuf的应⽤场景更为明确,XML、JSON的应⽤场景更为丰富。
🔥 共勉
😋 以上就是我对 ProtoBuf:通讯录4.0实现 & 序列化能⼒对⽐验证
的理解, 觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~ 😉