环境搭建
Protobuf 还常⽤于通讯协议、服务端数据交换场景。
因为我们主要目的只是为了学习protobuf,因此对于客户端,原本应该具备:
新增⼀个联系⼈
◦
删除⼀个联系⼈
◦
查询通讯录列表
◦
查询⼀个联系⼈的详细信息
这样四个功能。
但是在这里我们只实现新增联系人的功能。
对于服务器端,我们同样应该具备增删改查的功能,这里我们也只实现增的功能。
这里我们需要用到一个Httplib库:
cpp-httplib 是个开源的库,是⼀个c++封装的http库,使⽤这个库可以在linux、
windows平台下完成http客⼾端、http服务端的搭建。使⽤起来⾮常⽅便,只需要包含头⽂件
httplib.h 即可。编译程序时,需要带上 -lpthread 选项。
双方通信的流程
约定双方交互req/rsp
base_response.proto
syntax = "proto3";
package base_response;message BaseResponse
{bool succes = 1; // 返回结果string error_desc = 2; // 状态码
}
add_contact_request.proto
syntax = "proto3";
package add_contact_request;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_numbers = 3; // 电话号码map<string,string> remark = 4; // 备注
}
add_contact_response.proto
syntax = "proto3";
package add_contact_response;
import "base_response.proto"; // 引入base_responsemessage AddContactResponse
{base_response.BaseResponse base_resp = 1;string uid = 2; // 联系人的uid
}
客户端部分:
定义一个异常类,用于抛异常:
ContactException.h
#pragma once#include <iostream>class ContactException
{
private:std::string message;
public:ContactException(std::string str = "A Problem") : message(str) {}std::string What() const { return message; }
};
client.cc
#include <iostream>
#include "../add_contact_request.pb.h"
#include "../add_contact_response.pb.h"
#include "../../cpp-httplib/httplib.h"
#include "ContactException.h"using namespace std;
using namespace httplib;
using namespace add_contact_request;
using namespace add_contact_response;const string IP = "127.0.0.1";
const uint16_t PORT = 8080;void addContact();void menu()
{cout << "----------------网络通讯录------------------" << endl;cout << "选择: 1.添加联系人 2. 删除联系人 3.查找所有联系人 4. 查找一个联系人 0.退出" << endl;
}void addContact()
{httplib::Client cli(IP, PORT);AddContactRequest info;cout << "------------添加联系人-------------" << endl;cout << "请输入联系人姓名: " << endl;string name;getline(cin, name);info.set_name(name);cout << "请输入联系人年龄: " << endl;int32_t age;cin >> age;cin.ignore(256, '\n'); // 清空一下缓冲区// info->set_age(age);string numbers;for (int i = 1;; ++i){cout << "请输入联系人的第" << i << "个号码(直接回车终止输入)" << endl;getline(cin, numbers);if (numbers.empty())break;cout << "请输入这个电话的类型(1/移动电话 2/固定电话): " << endl;int num = 0;cin >> num;cin.ignore(256, '\n'); // 清空以下缓冲区add_contact_request::AddContactRequest_Phone *phone = info.add_phone_numbers();phone->set_number(numbers); // info->add_phones()返回的是一块开辟好空间的地址// 为了代码的可扩展性,这里使用switch而不是if语句switch (num){case 1:phone->set_type(add_contact_request::AddContactRequest_Phone_PhoneType::AddContactRequest_Phone_PhoneType_MP);break;case 2:phone->set_type(add_contact_request::AddContactRequest_Phone_PhoneType::AddContactRequest_Phone_PhoneType_TEL);break;default:cout << "选择有误,取默认类型" << endl;break;}}for (int i = 1;; ++i){cout << "请输入备注" << i << "标题(直接回车退出输入)" << endl;string remark_key;getline(cin, remark_key);if (remark_key.empty())break;cout << "请输入备注" << i << "内容" << endl;string remark_val;getline(cin, remark_val);info.mutable_remark()->insert({remark_key, remark_val});}string str_req;if (!info.SerializePartialToString(&str_req)){// 失败抛异常ContactException e("str_req序列化失败!");throw(e);}// 发起Post请求auto res = cli.Post("/contacts/add", str_req, "application/protobuf");if (!res){// 失败抛异常string str_error = "发起post请求失败,错误类型: ";str_error += httplib::to_string(res.error());ContactException e(str_error);throw(e);}// 反序列化responseAddContactResponse resp;bool parse = resp.ParseFromString(res->body);if(res->status != 200 && !parse){string err_desc;err_desc += "post contact/add 失败: ";err_desc += std::to_string(res->status);err_desc += "(";err_desc += res->reason;err_desc += ")";throw ContactException(err_desc);}else if(res->status != 200){string err_desc;err_desc += "post contact/add 失败: ";err_desc += std::to_string(res->status);err_desc += "(";err_desc += res->reason;err_desc += ")";err_desc += resp.base_resp().error_desc();throw ContactException(err_desc);}else if(!parse){string err_desc;err_desc += "post contact/add 反序列化失败: ";err_desc += resp.base_resp().error_desc();throw ContactException(err_desc);}cout << "新增联系人成功,uid = " << resp.uid() << endl;
}int main()
{while (true){menu();int choose;cin >> choose;cin.ignore(256, '\n');try{switch (choose){case 1:addContact();break;case 2:case 3:case 4:break;case 0:cout << "程序退出" << endl;exit(0);default:cout << "选择有误" << endl;exit(-1);break;}}catch (const ContactException &e){cerr << "捕获一个异常:操作通讯录时!" << endl;cerr << "信息: " << e.What() << endl;}}return 0;
}
makefile:
all:client
client:g++ client.cc ../add_contact_request.pb.cc ../add_contact_response.pb.cc ../base_response.pb.cc -o client -std=c++11 -lpthread -lprotobuf.PHONY:clean
clean:rm -f client
服务器部分:
这里为了简单,偷个懒,没有使用异常了,程序出错就直接退出
server.cc
#include <iostream>
#include <random>
#include "../../cpp-httplib/httplib.h"
#include "../add_contact_request.pb.h"
#include "../add_contact_response.pb.h"using namespace std;
using namespace httplib;
using namespace add_contact_request;
using namespace add_contact_response;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();
}void PrintPeopelInfo(const AddContactRequest &people_info)
{cout << "-----------新增联系人信息---------------" << endl;cout << "姓名: " << people_info.name() << endl;// cout << "年龄: " << people_info.age() << endl;// 打印电话for (int j = 0; j < people_info.phone_numbers().size(); ++j){const add_contact_request::AddContactRequest_Phone &phones = people_info.phone_numbers(j);cout << "第" << j + 1 << "个电话: " << phones.number();int num = phones.type();switch (num){case add_contact_request::AddContactRequest_Phone::PhoneType::AddContactRequest_Phone_PhoneType_MP:cout << "(移动电话MP)" << endl;break;case add_contact_request::AddContactRequest_Phone::PhoneType::AddContactRequest_Phone_PhoneType_TEL:cout << "(固定电话TEL)" << endl;break;default:cerr << "未知类型" << endl;break;}}// 打印备注信息int count = 1;for (auto &pair : people_info.remark()){cout << "备注信息" << count++ << ": ";cout << "(" << pair.first << ") : " << pair.second << endl;;}// 打印未知字段const google::protobuf::Reflection *reflection = people_info.GetReflection();const google::protobuf::UnknownFieldSet &unknown_field_set = reflection->GetUnknownFields(people_info);for (int j = 0; j < unknown_field_set.field_count(); ++j){const google::protobuf::UnknownField &unknown_field = unknown_field_set.field(j);cout << "未知字段" << j + 1<< " 字段编号: " << unknown_field.number()<< " 字段类型: " << unknown_field.type();switch (unknown_field.type()){case google::protobuf::UnknownField::Type::TYPE_VARINT:cout << " 值: " << unknown_field.varint() << endl;break;case google::protobuf::UnknownField::Type::TYPE_LENGTH_DELIMITED:cout << " 值: " << unknown_field.length_delimited() << endl;break;default:cout << "未知" << endl;break;}}cout << "新增联系人成功" << endl;
}int main()
{cout << "----------服务器启动!----------" << endl;httplib::Server svr;// 先设置方法,再进行监听svr.Post("/contacts/add", [](const Request &req, Response &resp){AddContactRequest contact_req;string str_req;if (!contact_req.ParseFromString(req.body)){cerr << "/contacts/add Post,反序列化失败!" << endl;exit(-1);}PrintPeopelInfo(contact_req); // 打印新增联系人信息// 准备响应信息// 这里为了简单就硬编码设置了AddContactResponse contact_resp;contact_resp.set_uid(generate_hex(8));string str_resp;if(!contact_resp.SerializePartialToString(&str_resp)){cerr << "序列化响应信息失败,程序退出!" << endl;exit(-1);}resp.status = 200;resp.body = str_resp;resp.set_header("Content-Type", "application/protobuf");cout << "响应构建成功,准备发送!" << endl;});svr.listen("0.0.0.0",8080);return 0;
}
在上述过程中,设计了一个能随机生成uid的函数,可以了解一下。
makefile:
all:server
server:g++ server.cc ../add_contact_request.pb.cc ../add_contact_response.pb.cc ../base_response.pb.cc -o server -std=c++11 -lpthread -lprotobuf.PHONY:clean
clean:rm -f server
演示效果:
客户端的新增联系人功能:
这里也简化了对服务器发送过来的响应的处理,我们只打印了服务器给我们返回的联系人的uid
服务器端的新增联系人功能:
服务器受到信息后,就会对客户端那边设置的信息进行一个打印,然后把生成的uid发送给客户端
总结:ProtoBuf的性能与使用场景
1. XML、JSON、ProtoBuf 都具有数据结构化和数据序列化的能⼒。
2. XML、JSON 更注重数据结构化,关注可读性和语义表达能⼒。ProtoBuf 更注重数据序列化,关注 效率、空间、速度,可读性差,语义表达能⼒不⾜,为保证极致的效率,会舍弃⼀部分元信息。
3. ProtoBuf 的应⽤场景更为明确, 适合⾼性能,对响应 速度有要求的数据传 输场景。
XML、JSON 的应⽤场景更为丰富。