protobuf: 网络版通讯录
1.客户端
1.协议约定
2..proto文件编写
syntax = "proto3";
package Add_http_contacts;
message AddContactRequest
{
string name = 1;
int32 age = 2;
//里面是电话的字段和类型
message Phone
{
string num = 1;
enum PhoneType
{
MP = 0; // 移动电话
TEL = 1; // 固定电话
}
PhoneType type = 2;
}
repeated Phone phone = 3;
}
message AddContactResponse
{
bool success = 1;
string error_desc = 2;
string uuid = 3;
}
3.Makefile
server:*.cc
g++ -o $@ $^ -std=c++11 -lpthread -lprotobuf //-lpthread是因为引入了第三方 httplib库
.PHONY:clean
clean:
rm -rf server
4.异常类
#include <iostream>
#include <string>
class ContactException
{
private:
std::string _error;
public:
ContactException(std::string err = "a problem")
:_error(err)
{}
std::string What() const
{
return _error;
}
};
5.client.cc
#include <iostream>
#include "httplib.h"
#include "ContactException.hpp"
#include "add_contact.pb.h"
using std::cerr;
using std::cin;
using std::cout;
using std::endl;
using namespace httplib;
#define CONTACT_HOST "82.157.3.118"
#define CONTACT_PORT 8888
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(Add_http_contacts::AddContactRequest *req)
{
std::cout << "--------------新增联系人--------------" << std::endl;
std::cout << "请输入联系人的姓名: ";
std::string name;
getline(std::cin, name);
req->set_name(name);
std::cout << "请输入联系人的年龄: ";
int age;
std::cin >> age;
req->set_age(age);
std::cin.ignore(256, '\n'); // 清除缓冲区所有内容,直到'\n',把'\n'删除后结束,或者清除256个字节直接结束
for (int i = 0;; i++)
{
std::cout << "请输入联系人的电话: " << i + 1 << "(只输入回车完成电话新增): ";
std::string number;
getline(std::cin, number);
if (number.empty())
break;
// 接收add_phone返回的phone对象
Add_http_contacts::AddContactRequest_Phone *phone = req->add_phone();
phone->set_num(number);
std::cout << "请输入联系人的电话类型:(0.移动电话, 1.固定电话): ";
int type;
std::cin >> type;
std::cin.ignore(256, '\n');
switch (type)
{
case 0:
phone->set_type(Add_http_contacts::AddContactRequest_Phone_PhoneType::AddContactRequest_Phone_PhoneType_MP);
break;
case 1:
phone->set_type(Add_http_contacts::AddContactRequest_Phone_PhoneType::AddContactRequest_Phone_PhoneType_TEL);
break;
default:
std::cout << "输入有误, 请重新根据提示输入" << std::endl;
break;
}
}
}
void AddContact()
{
Client client(CONTACT_HOST, CONTACT_PORT);
// 构建req对象
Add_http_contacts::AddContactRequest req;
BuildAddContactRequest(&req);
// 序列化
std::string req_str;
if (!req.SerializeToString(&req_str))
{
std::string errstr = "AddContact时,序列化失败";
throw ContactException(errstr);
}
// 发起post请求
auto post = client.Post("/contacts/add", req_str, "application/protobuf");
if (!post)
{
std::string err_str = "/contacts/add 链接失败,错误原因: ";
err_str.append(httplib::to_string(post.error()));
throw ContactException(err_str);
}
// 反序列化rsp
Add_http_contacts::AddContactResponse resp;
bool parse = resp.ParseFromString(post->body);
if (post->status != 200 && !parse)
{
std::string err_str = "调用 /contact/add 失败 ";
err_str.append(std::to_string(post->status))
.append("(")
.append(post->reason)
.append(")");
throw ContactException(err_str);
}
else if (post->status != 200)
{
std::string err_str = "调用 /contact/add 失败 ";
err_str.append(std::to_string(post->status))
.append("(")
.append(post->reason)
.append(")")
.append("错误原因: ")
.append(resp.error_desc());
throw ContactException(err_str);
}
else if (!resp.success())
{
std::string err_str = "调用 /contact/add 结果异常 ";
err_str.append("异常原因: ")
.append(resp.error_desc());
throw ContactException(err_str);
}
// 打印结果
cout << "新增联系人成功" << "联系人id: " << resp.uuid() << endl;
}
int main()
{
enum Option
{
Quit = 0,
Add,
Del,
Findone,
Findall
};
menu();
cout << "---->请选择: ";
int choose;
cin >> choose;
cin.ignore(256, '\n');
try
{
switch (choose)
{
case Option::Quit:
cout << "---->退出" << endl;
return 1;
case Option::Add:
cout << "---->新增联系人" << endl;
AddContact();
break;
case Option::Del:
cout << "---->暂未实现" << endl;
break;
case Option::Findone:
cout << "---->暂未实现" << endl;
break;
case Option::Findall:
cout << "---->暂未实现" << endl;
break;
default:
cout << "---->选择有误,请重新选择" << endl;
break;
};
}
catch (const ContactException &e)
{
cout << "--->出现错误, 错误原因是: " << e.What() << endl;
}
}
2.服务端
1.UUIDUtil类
#pragma once
#include <iostream>
#include <sstream>
#include <string>
#include <random>
#include <iomanip>
#include <atomic>
namespace ns_uuid
{
class UUIDUtil
{
public:
static std::string uuid()
{
// 生成机器随机数对象
std::random_device rd;
// 用mt算法,以机器随机数对象为种子生成伪随机数对象
std::mt19937 generator(rd());
// 构造限定数据范围的伪随机数
std::uniform_int_distribution<int> distribution(0, 255);
// ⽣成8个随机数,按照特定格式组织成为16进制数字字符的字符串
std::stringstream ss;
for (int i = 0; i < 8; i++)
{
if(i == 4 || i == 6) ss << "-";
ss << std::setw(2) << std::setfill('0') << std::hex << distribution(generator);
}
ss << "-";
// 定义⼀个8字节序号,逐字节组织成为16进制数字字符的字符串
static std::atomic<int> a(0);
// size_t 64个bit位
size_t cur = a.fetch_add(1);
for (int i = 7; i >= 0; i--)
{
if(i == 5) ss << "-";
ss << std::setw(2) << std::setfill('0') << std::hex << ((cur >> (i*8)) & 0xFF);
}
return ss.str();
}
};
}
2.server.cc
#include <iostream>
#include "add_contact.pb.h"
#include "ContactException.hpp"
#include <string>
#include "httplib.h"
#include "UUIDUtil.hpp"
using namespace httplib;
using namespace ns_uuid;
using std::cout;
using std::endl;
void PrintContacts(Add_http_contacts::AddContactRequest& request)
{
std::cout << "联系人姓名:" << request.name() << std::endl;
std::cout << "联系人年龄:" << request.age() << std::endl;
for(int j = 0;j<request.phone_size();j++)
{
const Add_http_contacts::AddContactRequest_Phone phone = request.phone(j);
std::cout << "联系人电话" << j+1 << ": "<< phone.num() << " ";
std::cout << "电话类型" << ": ("<< AddContactRequest_Phone_PhoneType_Name(phone.type()) << ")" << std::endl;
}
}
int main()
{
Server server;
server.Post("/contacts/add", [](const Request &req, Response &resp)
{
// 反序列化request : req.body
Add_http_contacts::AddContactRequest request;
Add_http_contacts::AddContactResponse response;
try
{
if (!request.ParseFromString(req.body))
{
throw ContactException("request反序列化失败");
}
//新增联系人,持久化存储——————这里只是实现打印
PrintContacts(request);
// 序列化response
response.set_success(true);
response.set_uuid(UUIDUtil::uuid());
std::string resp_str;
if (!response.SerializeToString(&resp_str))
{
throw ContactException("response序列化失败");
}
resp.status = 200;
resp.body = resp_str;
resp.set_header("Content-Type", "application/protobuf");
}
catch (const ContactException &e)
{
resp.status = 500;
response.set_success(false);
response.set_error_desc(e.What());
std::string resp_str;
if (response.SerializeToString(&resp_str))
{
resp.body = resp_str;
resp.set_header("Content-Type", "application/protobuf");
}
}
});
server.listen("0.0.0.0", 8888);
return 0;
}
3.总结
序列化协议 | 通⽤性 | 格式 | 可读性 | 序列化⼤ ⼩ | 序列化性能 |
JSON | 通⽤(json、 xml已成为多种 ⾏业标准的编 写⼯具 | ⽂本格式 | 好 | 轻量(使 ⽤键值对 ⽅式,压 缩了⼀定 的数据空 间 | 中 |
XML | 通用 | ⽂本格式 | 好 | 重量(数 据冗余, 因为需要 成对的闭 合标签 | 低 |
ProtoBuf | 独⽴ (Protobuf只 是Google公司 内部的⼯具) | ⼆进制格式 | 差(只能 反序列化 后得到真 正可读的 数据) | 轻量(⽐ JSON更轻 量,传输 起来带宽 和速度会 有优化 | 高 |
⼩结:
- XML、JSON、ProtoBuf都具有数据结构化和数据序列化的能⼒。
- XML、JSON更注重数据结构化,关注可读性和语义表达能⼒。ProtoBuf更注重数据序列化,关注
效率、空间、速度,可读性差,语义表达能⼒不⾜,为保证极致的效率,会舍弃⼀部分元信息。
3. ProtoBuf的应⽤
场景更为明确,XML、JSON的应⽤
场景更为丰富。
protobuf 和 Json 的实验对比:
- 编解码性能:ProtoBuf的编码解码性能,⽐JSON⾼出2-4倍。
- 内存占⽤:ProtoBuf的内存278,⽽JSON到达567,ProtoBuf的内存占⽤只有JSON的1/2。
注:以上结论的数据只是根据该项实验得出。因为受不同的字段类型、字段个数等影响,测出的数据会有所差异。