目录
网络版通讯录需求
实现网络版通讯录
搭建服务端客户端
协议约定
客户端菜单功能
服务端代码
Protobuf 还常用于 通讯协议、服务端数据交换 的场景,接下来,我们将实现一个网络版本的通讯录,模拟实现客户端与服务端的交互,通过 Protobuf 来实现各端之间的协议序列化
网络版通讯录需求
客户端可以选择对通讯录进行以下操作:
-
新增一个联系人
-
删除一个联系人
-
查询通讯录列表
-
查询一个联系人的详细信息
服务端:
提供:增、删、查能力,并需要持久化通讯录
客户端、服务端间的交互数据使用 Protobuf 来完成
下面分析具体需要做的事情:
-
因为是网络传输,所以需要 req和resp,首先给客户端和服务端的 req和resp 创建对应的 message
-
客户端选择需要做的事情后,放入 req 中,进行网络传输前 序列化req
-
调用接口 req
-
在 服务端 反序列化req
-
接着调用对应的代码,并保证持久化存储 通讯录
-
完成后 序列化resp,进行网络传输
-
客户端接收到 resp 后,反序列化 resp,整个网络通信结束
实现网络版通讯录
由于新增、删除、查询联系人,只是实现逻辑不同,而网络通信的步骤是相同的,所以下面只实现 新增联系人 的功能,帮我们更好的了解网络通信
搭建服务端客户端
首先需要使用 cpp-httplib 库,只需要克隆到服务器上后,将 httplib.h 放入项目路径下,包含即可使用
点击进入对应gitee
搭建服务端
服务端有以下文件:
makefile:
server:*.cc
g++ -o $@ $^ -std=c++11 -lpthread -lprotobuf
.PHNOY:clean
clean:
rm -f server
main.cc:
#include <iostream>
#include "httplib.h"
using namespace std;
using namespace httplib;
int main()
{
cout << "-----------服务启动-----------";
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;
});
// 绑定8081端口,并将端口对外开放
server.listen("0.0.0.0", 8081);
return 0;
}
搭建客户端
此时再另一台主机上搭建客户端,客户端同样是这三个文件:
makefile:
client:*.cc
g++ -o $@ $^ -std=c++11 -lpthread -lprotobuf
.PHNOY:clean
clean:
rm -f client
main.cc:
#include <iostream>
#include "httplib.h"
using namespace std;
using namespace httplib;
int main()
{
Client cli("127.0.0.1", 8081);
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;
}
此时服务端和客户端都编译后启动,初步验证了当前的服务端和客户端是可以跨网络传输的
协议约定
在服务端和客户端中,各自创建一个 add_contact.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; // 代表唯一的一个联系人
}
客户端菜单功能
在某些方法中可能出现一些错误的行为,所以我们可以自定义一个异常类,如果在客户端序列化 req 时,失败了,我们就可以直接抛一个异常
在 main函数 中接收到异常后,将异常捕获,再进行错误信息的打印
所以这里重新写一个文件:ContactsException.h
#pragma once
#include <iostream>
#include <string>
class ContactsException
{
private:
std::string message;
public:
ContactsException(std::string str = "A problem") : message(str)
{}
std::string what() const
{
return message;
}
};
客户端首先需要一个菜单:
在 main函数 中肯定需要使用 Switch case,不想 case 1 这样使用,所以创建一个枚举类型:
main函数 的整体框架是:
其中 2/3/4 都不是实现,只实现 新增
main.cc代码:
#include <iostream>
#include "httplib.h"
#include "ContactsException.h"
#include "add_contact.pb.h"
using namespace std;
using namespace httplib;
void addContact();
void menu()
{
cout << "------------------------------------" << endl;
cout << "---------请选择对通讯录的操作---------" << endl;
cout << "------------1. 新增联系人------------" << endl;
cout << "------------2. 删除联系人------------" << endl;
cout << "------------3. 查看联系人列表--------" << endl;
cout << "------------4. 查看联系人详细信息----" << endl;
cout << "------------0. 退出-----------------" << endl;
cout << "------------------------------------" << endl;
}
enum OPTION
{
QUIT = 0,
NEW,
DEL,
FIND_ALL,
FIND_ONE
};
int main()
{
while (true)
{
menu();
cout << "--->请选择: ";
int choose;
cin >> choose;
cin.ignore(256, '\n');
try
{
switch (choose)
{
case OPTION::QUIT:
cout << "程序退出" << endl;
return 0;
case OPTION::NEW:
addContact();
break;
case OPTION::DEL:
case OPTION::FIND_ALL:
case OPTION::FIND_ONE:
break;
default:
cout << "选择有误, 请重新选择!" << endl;
break;
}
}
catch (const ContactsException &e)
{
cout << "--->操作通讯录时发送异常" << endl;
cout << "--->异常信息: " << e.what() << endl;
}
}
return 0;
}
// 从前面实现的通讯录版本的 write.cc 中拷贝过来即可, 大体逻辑类似
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 = 1; ; i++)
{
cout << "请输入联系人电话" << i << "(只输入回车表示输入完毕): ";
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("0.0.0.0", 8080);
// 构造 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)
{
// httplib::to_string() 可以将错误的枚举类型转化了枚举对应的描述
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;
}
服务端代码
main.cc完整代码:
#include <iostream>
#include <string>
#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:123456 (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;
}
});
// 绑定8080端口,并将端口对外开放
server.listen("0.0.0.0", 8080);
return 0;
}
网络版通讯录实现完毕