NO.4|protobuf网络版通讯录|httplib|JSON、XML、ProtoBuf对比

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:定义异常类

main.cc

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;
// }

服务端代码

main.cc

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 更小、更快。
  1. XML、JSON、ProtoBuf都具有数据结构化和数据序列化的能⼒。
  2. XML、JSON更注重数据结构化,关注可读性和语义表达能⼒。ProtoBuf更注重数据序列化,关注效率、空间、速度,可读性差,语义表达能⼒不⾜,为保证极致的效率,会舍弃⼀部分元信息。
  3. ProtoBuf的应⽤场景更为明确,XML、JSON的应⽤场景更为丰富
相关推荐
青衫客363 小时前
浅谈 Java 后端对象映射:从 JSON → VO → Entity 的原理与实践
java·json
qqxhb11 小时前
11|结构化输出:为什么 JSON 能让系统更稳定
json·ai编程·结构化·规范模板
弹简特20 小时前
【JavaEE19-后端部分】 MyBatis 入门第三篇:使用XML完成增删改查
xml·mybatis
小黑要努力21 小时前
json-c安装以及amixer使用
linux·运维·json
spencer_tseng1 天前
Tomcat server.xml <Connector> address=“0.0.0.0“
xml·tomcat
听风者一号1 天前
cssMoudle生成器
前端·javascript·json
ID_180079054732 天前
小红书笔记详情 API 接口系列 + 标准 JSON 返回参考(完整版)
数据库·笔记·json
常利兵2 天前
Android 字体字重设置:从XML到Kotlin的奇妙之旅
android·xml·kotlin
小狗丹尼4002 天前
JSON 基础认知、数据转换与 Flask 前后端交互全解
python·flask·json