【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 的应用场景更为丰富。 比特就业
相关推荐
win x10 小时前
深入理解HTTPS协议加密流程
网络协议·http·https
仙俊红10 小时前
从 Filter / Interceptor 到 HTTPS
网络协议·http·https
艾莉丝努力练剑10 小时前
【Linux:文件】Ext系列文件系统(初阶)
大数据·linux·运维·服务器·c++·人工智能·算法
Once_day10 小时前
C++之《程序员自我修养》读书总结(1)
c语言·开发语言·c++·程序员自我修养
Trouvaille ~10 小时前
【Linux】TCP Socket编程实战(一):API详解与单连接Echo Server
linux·运维·服务器·网络·c++·tcp/ip·socket
liann11910 小时前
3.1_网络——基础
网络·安全·web安全·http·网络安全
坚果派·白晓明10 小时前
在鸿蒙设备上快速验证由lycium工具快速交叉编译的C/C++三方库
c语言·c++·harmonyos·鸿蒙·编程语言·openharmony·三方库
独行soc11 小时前
2026年渗透测试面试题总结-17(题目+回答)
android·网络·安全·web安全·渗透测试·安全狮
辣香牛肉面11 小时前
Wireshark v4.6.2 开源免费网络嗅探抓包工具中文便携版
网络·测试工具·wireshark
全栈工程师修炼指南11 小时前
Nginx | stream 四层反向代理:SSL、PREREAD 阶段模块指令浅析与实践
运维·网络·网络协议·nginx·ssl