Protobuf还常⽤于通讯协议、服务端数据交换场景。那么在这个⽰例中,我们将实现⼀个⽹络版本的通讯录,模拟实现客⼾端与服务端的交互,通过Protobuf来实现各端之间的协议序列化。
需求如下:
- 客⼾端可以选择对通讯录进⾏以下操作:
- 新增⼀个联系⼈
- 删除⼀个联系⼈
- 查询通讯录列表
- 查询⼀个联系⼈的详细信息
- 服务端提供增删查能⼒,并需要持久化通讯录。
- 客⼾端、服务端间的交互数据使⽤Protobuf来完成
环境搭建
Httplib库:cpp-httplib是个开源的库,是⼀个c++封装的http库,使⽤这个库可以在linux、windows平台下完成http客⼾端、http服务端的搭建。使⽤起来⾮常⽅便,只需要包含头⽂件httplib.h即可。编译程序时,需要带上-lpthread选项
centos下编写的注意事项
如果使⽤centOS环境,yum源带的g++最新版本是4.8.5,发布于2015年,年代久远。编译该项⽬会出现异常。将gcc/g++升级为更⾼版本可解决问题
c
# 升级参考:https://juejin.cn/post/6844903873111392263
# 安装gcc 8版本
yum install -y devtoolset-8-gcc devtoolset-8-gcc-c++
# 启⽤版本
source /opt/rh/devtoolset-8/enable
# 查看版本已经变成gcc 8.3.1
gcc -v
约定双端交互接⼝
新增⼀个联系⼈
c
[请求]
Post /contacts/add AddContactRequest
Content-Type: application/protobuf
[响应]
AddContactResponse
Content-Type: application/protobuf
删除⼀个联系⼈
c
[请求]
Post /contacts/del DelContactRequest
Content-Type: application/protobuf
[响应]
DelContactResponse
Content-Type: application/protobuf
查询通讯录列表
c
[请求]
GET /contacts/find-all
[响应]
FindAllContactsResponse
Content-Type: application/protobuf
查询⼀个联系⼈的详细信息:
c
[请求]
Post /contacts/find-one FindOneContactRequest
Content-Type: application/protobuf
[响应]
FindOneContactResponse
Content-Type: application/protobuf
约定双端交互req/resp
base_response.proto
c
syntax = "proto3";
package base_response;
message BaseResponse {
bool success = 1; // 返回结果
string error_desc = 2; // 错误描述
}
add_contact_request.proto
c
syntax = "proto3";
package add_contact_req;
// 新增联系⼈ req
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; // 电话
map<string, string> remark = 4; // 备注
}
add_contact_response.proto
c
syntax = "proto3";
package add_contact_resp;
import "base_response.proto"; // 引⼊base_response
message AddContactResponse {
base_response.BaseResponse base_resp = 1;
string uid = 2;
}
del_contact_request.proto
c
syntax = "proto3";
package del_contact_req;
// 删除⼀个联系⼈ req
message DelContactRequest {
string uid = 1; // 联系⼈ID
}
del_contact_response.proto
c
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
c
syntax = "proto3";
package find_one_contact_req;
// 查询⼀个联系⼈ req
message FindOneContactRequest {
string uid = 1; // 联系⼈ID
}
find_one_contact_response.proto
c
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; // 联系⼈ID
string 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
c
syntax = "proto3";
package find_all_contacts_resp;
import "base_response.proto"; // 引⼊base_response
// 联系⼈摘要信息
message PeopleInfo {
string uid = 1; // 联系⼈ID
string name = 2; // 姓名
}
// 查询所有联系⼈ resp
message FindAllContactsResponse {
base_response.BaseResponse base_resp = 1;
repeated PeopleInfo contacts = 2;
}
httplib

客户端




协议约定

实现菜单功能


ContactException.h:定义异常类





c++
#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 8123
void 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);
// 构造 req
add_contact::AddContactRequest req;
buildAddContactRequest(&req);
// 序列化 req
string req_str;
if (!req.SerializeToString(&req_str)) {
throw ContactsException("AddContactRequest序列化失败!");
}
// 发起post调用
auto res = cli.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);
}
// 反序列化 resp
add_contact::AddContactResponse resp;
bool parse = resp.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(")");
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;
}
// int main() {
// Client cli(CONTACTS_HOST, CONTACTS_PORT);
// Result res1 = cli.Post("/test-post");
// if (res1->status == 200) {
// cout << "调用post成功!" << endl;
// }
// Result res2 = cli.Get("/test-get");
// if (res2->status == 200) {
// cout << "调用get成功!" << endl;
// }
// return 0;
// }


服务端代码
c++
#include <iostream>
#include "httplib.h"
#include "add_contact.pb.h"
using namespace std;
using namespace httplib;
class 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.body
add_contact::AddContactRequest request;
add_contact::AddContactResponse response;
try {
if (!request.ParseFromString(req.body)) {
throw ContactsException("AddContactRequest反序列化失败!");
}
// 新增联系人,持久化存储通讯录----》 打印新增的联系人信息
printContact(request);
// 构造 response:res.body
response.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;
}
// int main() {
// cout << "-----------服务启动----------" << endl;
// Server server;
// server.Post("/test-post", [](const Request& req, Response& res) {
// cout << "接收到post请求!" << endl;
// res.status = 200;
// });
// server.Get("/test-get", [](const Request& req, Response& res) {
// cout << "接收到get请求!" << endl;
// res.status = 200;
// });
// // 绑定 8123 端口,并且将端口号对外开放
// server.listen("0.0.0.0", 8123);
// return 0;
// }




总结




- 编解码性能:ProtoBuf的编码解码性能,⽐JSON⾼出2-4倍。
- 内存占⽤:ProtoBuf的内存278,⽽JSON到达567,ProtoBuf的内存占⽤只有JSON的1/2。
注:以上结论的数据只是根据该项实验得出。因为受不同的字段类型、字段个数等影响,测出的数据会有所差异
| 序列化协议 | 通用性 | 格式 | 可读性 | 序列化大小 | 序列化性能 | 适用场景 |
|---|---|---|---|---|---|---|
| JSON | 通用(json、xml 已成为多种行业标准的编写工具) | 文本格式 | 好 | 轻量(使用键值对方式,压缩了一定的数据空间) | 中 | web 项目。因为浏览器对于 json 数据支持非常好,有很多内建的函数支持。 |
| XML | 通用 | 文本格式 | 好 | 重量(数据冗余,因为需要成对的闭合标签) | 低 | XML 作为一种扩展标记语言,衍生出了 HTML、RDF/RDFS,它强调数据结构化的能力和可读性。 |
| ProtoBuf | 独立(Protobuf 只是 Google 公司内部的工具) | 二进制格式 | 差(只能反序列化后得到真正可读的数据) | 轻量(比 JSON 更轻量,传输起来带宽和速度会有优化) | 高 | 适合高性能,对响应速度有要求的数据传输场景。Protobuf 比 XML、JSON 更小、更快。 |
- XML、JSON、ProtoBuf都具有数据结构化和数据序列化的能⼒。
- XML、JSON更注重数据结构化,关注可读性和语义表达能⼒。ProtoBuf更注重数据序列化,关注效率、空间、速度,可读性差,语义表达能⼒不⾜,为保证极致的效率,会舍弃⼀部分元信息。
- ProtoBuf的应⽤场景更为明确,XML、JSON的应⽤场景更为丰富