Protobuf 还常⽤于通讯协议、服务端数据交换场景。那么在这个⽰例中,我们将实现⼀个⽹络版本的通讯录,模拟实现客⼾端与服务端的交互,通过 Protobuf 来实现各端之间的协议序列化。需求如下:
客⼾端可以选择对通讯录进⾏以下操作:
- 新增⼀个联系⼈
- 删除⼀个联系⼈
- 查询通讯录列表
- 查询⼀个联系⼈的详细信息
为了将代码简化,我们只具体实现新增联系人的功能, 服务端则相应提供增删查能⼒,并需要持久化通讯录(为了将代码简化, 这里只是将收到的需求序列化后打印出来),客⼾端、服务端间的交互数据使⽤ Protobuf 来完成。
搭建 Httplib 库
Httplib 库:cpp-httplib 是个开源库,是⼀个c++封装的http库,使⽤这个库可以在linux、 windows平台下完成http客⼾端、http服务端的搭建。使⽤起来⾮常⽅便,只需要包含头⽂件 httplib.h 即可。编译程序时,需要带上 -lpthread 选项。
源码库地址: https://github.com/yhirose/cpp-httplib
完整代码
protocol.proto
syntax="proto3";
package protocol;
message Response
{
string uid = 1;
bool success = 2;// 返回结果
string error_desc = 3;// 错误描述
}
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;
}
将代码补充好后编译一下
protoc --cpp_out=. protocol.proto
之后就在当前文件夹出现 protocol.pb.h 和 protocol.pb.cc 文件,还需要记得添加下载的 httplib.h 文件。
ContactsException.h
定义抛异常的类
cpp
#include <string>
class ContactsException
{
public:
ContactsException(std::string str = "problem")
: message(str)
{
}
std::string what() const
{
return message;
}
private:
std::string message;
};
client.cc
cpp
#include <iostream>
#include "httplib.h"
#include "ContactsException.h"
#include "protocol.pb.h"
using namespace std;
const string server_ip = "127.0.0.1";
const int server_port = 8080;
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;
}
void buildAddContactRequest(protocol::AddContactRequest *req)
{
cout << "-------------新增联系⼈-------------" << endl;
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 = 1;; i++)
{
cout << "请输⼊联系⼈电话" << i << "(只输⼊回⻋完成电话新增):";
string numbers;
getline(cin, numbers);
if (numbers.empty())
break;
auto phone = req->add_phone();
phone->set_number(numbers);
cout << "选择此电话类型(1.移动电话 2.固定电话):";
int type;
cin >> type;
cin.ignore(256, '\n');
switch (type)
{
case 1:
phone->set_type(protocol::AddContactRequest_Phone_PhoneType::AddContactRequest_Phone_PhoneType_MP);
break;
case 2:
phone->set_type(protocol::AddContactRequest_Phone_PhoneType::AddContactRequest_Phone_PhoneType_TEL);
break;
default:
cout << "非法选择,使用默认值!" << endl;
break;
}
}
}
void addContact()
{
httplib::Client client(server_ip, server_port);
// 构造request
protocol::AddContactRequest req;
buildAddContactRequest(&req);
// 序列化request
string req_str;
if (!req.SerializeToString(&req_str))
{
throw ContactsException("AddContactRequest序列化失败!");
}
// 发起post调用
httplib::Result res = client.Post("/contacts/add", req_str, "application/protobuf");
if (!res)
{
string err_desc = "/contacts/add 链接失败!错误信息: " + httplib::to_string(res.error());
throw ContactsException(err_desc);
}
// 反序列化response
protocol::Response rsp;
bool parse = rsp.ParseFromString(res->body);
if (res->status != 200 && parse == false)
{
string err_desc = "/contacts/add 调用失败,返回状态码为: " +
to_string(res->status) + "(" + res->reason + ")";
throw ContactsException(err_desc);
}
else if (res->status != 200)
{
string err_desc = "/contacts/add 调用失败,返回状态码为: " +
to_string(res->status) + "(" + res->reason + "),错误原因: " + rsp.error_desc();
throw ContactsException(err_desc);
}
else if (rsp.success() == false)
{
string err_desc = "/contacts/add 结果异常,异常原因: " + rsp.error_desc();
throw ContactsException(err_desc);
}
// 结果打印
cout << "新增联系人成功,联系人ID: " << rsp.uid() << 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:
cout << "新增联系人--->" << endl;
addContact();
break;
case OPTION::DEL:
cout << "删除联系人--->" << endl;
break;
case OPTION::FIND_ALL:
cout << "查看联系⼈列表--->" << endl;
break;
case OPTION::FIND_ONE:
cout << "查看联系⼈详细信息--->" << endl;
break;
default:
cout << "选择错误,请重新选择!" << endl;
break;
}
}
catch (const ContactsException &e)
{
cout << "操作通讯录时发生异常,异常信息为: " << e.what() << endl;
}
}
return 0;
}
server.cc
cpp
#include <iostream>
#include "httplib.h"
#include "ContactsException.h"
#include "protocol.pb.h"
using namespace std;
unsigned int random_char()
{
// ⽤于随机数引擎获得随机种⼦
random_device rd;
// mt19937是c++11新特性,它是⼀种随机数算法,⽤法与rand()函数类似,但是mt19937具有速度快,周期⻓的特点
// 作⽤是⽣成伪随机数
mt19937 gen(rd());
// 随机⽣成⼀个整数i 范围[0, 255]
uniform_int_distribution<> dis(0, 255);
return dis(gen);
}
// ⽣成 UUID (通⽤唯⼀标识符)
string generate_hex(const unsigned int len)
{
stringstream ss;
// ⽣成 len 个16进制随机数,将其拼接⽽成
for (auto i = 0; i < len; i++)
{
const auto rc = random_char();
stringstream hexstream;
hexstream << hex << rc;
auto hex = hexstream.str();
ss << (hex.length() < 2 ? '0' + hex : hex);
}
return ss.str();
}
void printContact(const protocol::AddContactRequest &proto_req)
{
cout << "联系⼈姓名: " << proto_req.name() << endl;
cout << "联系⼈年龄: " << proto_req.age() << endl;
int j = 1;
for (auto phone : proto_req.phone())
{
cout << "联系⼈电话" << j++ << ": " << phone.number();
cout << " (" << phone.PhoneType_Name(phone.type()) << ")" << endl;
}
}
int main()
{
cout << "服务启动..." << endl;
httplib::Server server;
server.Post("/contacts/add", [](const httplib::Request &req, httplib::Response &rsp)
{
cout << "收到post请求!" << endl;
protocol::AddContactRequest proto_req;
protocol::Response proto_rsp;
try
{
// 反序列化req,取出req的body
if(!proto_req.ParseFromString(req.body))
{
throw ContactsException("AddContactRequest反序列化失败");
}
// 新增联系人,持久化到文件或打印
printContact(proto_req);
// 构造proto_rsp
proto_rsp.set_success(true);
proto_rsp.set_uid(generate_hex(10));
// 序列化proto_rsp,构造rsp的body
string rsp_str;
if(!proto_rsp.SerializeToString(&rsp_str))
{
throw ContactsException("Response序列化失败");
}
rsp.status=200;
rsp.body=rsp_str;
rsp.set_header("Content-Type","application/protobuf");
}
catch (const ContactsException &e)
{
//返回异常的rsp
rsp.status=500;
proto_rsp.set_success(false);
proto_rsp.set_error_desc(e.what());
string rsp_str;
if(proto_rsp.SerializeToString(&rsp_str))
{
rsp.body=rsp_str;
rsp.set_header("Content-Type","application/protobuf");
}
cout<<"/contacts/add 发生异常,异常信息: "<<e.what()<<endl;
} });
// 绑定端口
server.listen("0.0.0.0", 8080);
return 0;
}
makefile
cpp
.PHONY:all
all:server client
server:server.cc protocol.pb.cc
g++ -o $@ $^ -std=c++17 -lpthread -lprotobuf
client:client.cc protocol.pb.cc
g++ -o $@ $^ -std=c++17 -lpthread -lprotobuf
.PHONY:clean
clean:
rm -f server client