【ProtoBuf 】C++ 网络通讯录开发实战:ProtoBuf 协议设计与 HTTP 服务实现

文章目录

  • 一、环境搭建
  • 二、约定双端交互接口
  • 三、客户端
    • [3.1 实现客户端菜单](#3.1 实现客户端菜单)
    • [3.2 实现异常类](#3.2 实现异常类)
    • [3.3 实现客户端代码](#3.3 实现客户端代码)
  • 四、服务端
    • [4.1 生成UID](#4.1 生成UID)
    • [4.2 打印数据](#4.2 打印数据)
    • [4.3 实现服务端代码](#4.3 实现服务端代码)
  • 五、总结
    • [5.1 序列化能力对比验证](#5.1 序列化能力对比验证)
    • [5.2 各类序列化协议总结](#5.2 各类序列化协议总结)
ProtoBuf语法 相关知识点 可以通过点击 以下链接进行学习 一起加油!
ProtoBuf入门与安装 ProtoBuf快速上手 Proto3 语法与类型实战 ProtoBuf 进阶实战:默认值、消息更新与兼容性最佳实践

在现代软件开发中,数据序列化是系统间通信的核心技术之一。随着分布式架构和微服务的普及,如何高效、可靠地在客户端与服务端之间传输结构化数据成为了关键问题。Protocol Buffers(protobuf)作为 Google 开源的序列化框架,以其高性能、跨语言支持和强类型约束等特性,在众多序列化方案中脱颖而出。本文将通过实现一个完整的网络版通讯录系统,深入探讨 protobuf 在实际项目中的应用,并与 JSON、XML 等传统序列化方案进行全面对比。

Protobuf 还常用于通讯协议、服务端数据交换场景。那么在这个示例中,我们将实现一个网络版本的
通讯录,模拟实现客户端与服务端的交互,通过 Protobuf 来实现各端之间的协议序列化。

需求如下:

客户端可以选择对通讯录进行以下操作:

  • 新增一个联系人

    • 删除一个联系人

    • 查询通讯录列表

    • 查询一个联系人的详细信息

  • 服务端提供 增 删 查 能力,并需要持久化通讯录。

  • 客户端、服务端间的交互数据来使用Protobuf来完成

一、环境搭建

Httplib 库 :cpp-httplib 是个开源的库,是一个c++封装的http库,使用这个库可以在linux、windows平台下完成http客户端、http服务端的搭建。使用起来非常方便,只需要包含头文件httplib.h 即可。编译程序时,需要带上 -lpthread 选项。

将这个httplib.h头文件拿过来使用。

二、约定双端交互接口

新增一个联系人:

cpp 复制代码
[请求]
	Post /contacts/add AddContactRequest
	Content-Type:application/protobuf

[响应]
    AddContactResponse
    Content-Type:application/protobuf

删除一个联系人

cpp 复制代码
[请求]
	Post /contacts/del DelContactRequest
	Content-Type:application/protobuf

[响应]
    DelContactResponse
    Content-Type:application/protobuf

查询通讯录列表:

cpp 复制代码
[请求]
	GET /contacts/find-all
[响应]
    FindAllContactsResponse
	Content-Type: application/protobuf

查询一个联系人的详细信息

cpp 复制代码
[请求]
	Post /contacts/find-one FindOneContactRequest
    Content-Type: application/protobuf
[响应]
	FindOneContactResponse
	Content-Type: application/protobuf

三、客户端

3.1 实现客户端菜单

可能在调用对应函数中,会出现错误,我们可以实现自定义异常类,打印异常的原因。

3.2 实现异常类

cpp 复制代码
#include <string>
class ContactsException{
    private:
        std::string message;
    public:
    ContactsException(std::string str = "A problem") : message(str){}
    std::string what() const{
        return message;
    }
}

3.3 实现客户端代码

cpp 复制代码
#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 = cil.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);
        } 

        //反序列化
        add_contact::AddContactRequest resp;
        bool parse = resp.ParseFromStirng(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;
}

四、服务端

这个resp和这个response不是一个东西。

4.1 生成UID

cpp 复制代码
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();
 }

4.2 打印数据

cpp 复制代码
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;
    }
}

4.3 实现服务端代码

cpp 复制代码
#include <iostram>
#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;
 }

五、总结

5.1 序列化能力对比验证

在这里让我们分别使用 PB 与 JSON 的序列化与反序列化能力, 对值完全相同的一份结构化数据进行不同次数的性能测试。

为了可读性,下面这一份文本使用 JSON 格式展示了需要被进行测试的结构化数据内容:

cpp 复制代码
{
  "age": 20,
  "name": "张珊",
  "phone": [
    {
      "number": "110112119",
      "type": 0
    },
    {
      "number": "110112119",
      "type": 0
    },
    {
      "number": "110112119",
      "type": 0
    },
    {
      "number": "110112119",
      "type": 0
    }
  ],
  "qq": "95991122",
  "address": {
    "home_address": "陕西省西安市长安区",
    "unit_address": "陕西省西安市雁塔区"
  },
  "remark": {
    "key1": "value1",
    "key2": "value2",
    "key3": "value3",
    "key4": "value4",
    "key5": "value5"
  }
}

开始进行测试代码编写,我们在新的目录下新建 contacts.proto文件,内容如下:

protobuf 复制代码
syntax = "proto3";

package compare_serialization;

import "google/protobuf/any.proto"; // 引入 any.proto 文件

// 地址
message Address {
  string home_address = 1; // 家庭地址
  string unit_address = 2; // 单位地址
}

// 联系人
message PeopleInfo {
  string name = 1; // 姓名
  int32 age = 2; // 年龄
  
  message Phone {
    string number = 1; // 电话号码
    enum PhoneType {
      MP = 0;  // 移动电话
      TEL = 1; // 固定电话
    }
    PhoneType type = 2; // 类型
  }
  
  repeated Phone phone = 3; // 电话
  google.protobuf.Any data = 4; // 其他联系方式:多选一
  string qq = 5;
  string weixin = 6;
  map<string, string> remark = 7; // 备注
}

使用 protoc 命令编译文件后,新建性能测试文件 compare.cc,我们分别对相同的结构化数据进行100 、1000 、10000 、100000 次的序列化与反序列化,分别获取其耗时与序列化后的大小。

5.2 各类序列化协议总结

序列化协议对比

序列化协议 通用性 格式 可读性 序列化大小 序列化性能 适用场景
JSON 通用(多语言/多平台支持广) 文本格式 好(人类可读、易调试) 轻量(结构紧凑;压缩后更小) Web/API、配置、日志、浏览器环境
XML 通用(标准化良好,生态成熟) 文本格式 一般(标签冗长) 重(标签开销大,体积通常较大) 文档交换、需命名空间/Schema 验证、老系统互操作
ProtoBuf 多语言但需 .proto/编译器支持 二进制格式 差(不适合人工直接阅读) 最小(通常比 JSON/XML 更小) gRPC/RPC、微服务、移动弱网、IoT、高性能传输

优缺点摘要

协议 优点 缺点 备注
JSON 易读易写、生态广、与 JS 天然适配 类型表达力一般(无强 Schema)、二进制/精度处理需额外方案 业务可读性强、前后端交互常用
XML 结构严谨、命名空间、XSD/DTD 校验、XPath/XSLT 转换能力 冗长、解析/序列化较慢、传输开销大 适合需要强规范与文档化的集成场景
ProtoBuf 体积小、速度快、强 Schema、良好前后兼容(字段号)、跨语言性能稳定 二进制不可读、需维护 .proto 并编译、对临时人工排错不友好 常与 gRPC 配合,用于高性能与带宽敏感场景

小结

  1. XMLJSONProtoBuf 都具有数据结构化和数据序列化的能力。
  2. XMLJSON 更注重数据结构化,关注可读性和语义表达能力。ProtoBuf 更注重数据序列化,关注
    效率、空间、速度,可读性差,语义表达能力不足,为保证极致的效率,会舍弃一部分元信息。
  3. ProtoBuf 的应用场景更为明确,XMLJSON 的应用场景更为丰富。 比特就业
相关推荐
long_run6 小时前
C++之箭头操作符
c++
广药门徒6 小时前
开发板直连电脑的搭建网络环境(以正点原子阿尔法imx6ull开发板为讲解)
网络
URBBRGROUN4676 小时前
Streamable HTTP
网络·网络协议·http
心想事成的幸运大王6 小时前
HTTP 协议核心组件与安全扩展深度解析
网络·网络协议·http
啊?啊?6 小时前
13 选 list 还是 vector?C++ STL list 扩容 / 迭代器失效问题 + 模拟实现,对比后再做选择
c++·stl容器·模拟实现
MoloXuanhe7 小时前
[TryHackMe]Oh My WebServer(nday漏洞+容器逃逸)
运维·网络·tryhackme·thm
MSTcheng.7 小时前
【C++】C++入门—(中)
开发语言·c++
wheeldown10 小时前
【Linux】为什么死循环卡不死 Linux?3 个核心逻辑看懂进程优先级与 CPU 调度密码
linux·运维·服务器·开发语言·c++·unix·进程
Want59511 小时前
C/C++哆啦A梦
c语言·开发语言·c++