自定义协议 && 序列化与反序列化

自定义协议 && 序列化与反序列化

七月 31, 2025 次阅读

网络通信中的自定义协议

什么是自定义协议

自定义协议是指在网络通信中,开发者根据特定应用需求自行设计的数据交换规则和格式。与HTTP、FTP等标准协议不同,自定义协议通常针对特定场景优化,具有更高的灵活性和效率。

自定义协议的主要特点包括:

  1. 专为特定应用场景设计
  2. 通常比通用协议更高效
  3. 可以根据需求自由定义数据格式
  4. 需要通信双方都实现协议解析逻辑

自定义协议的设计要素

设计一个良好的自定义协议需要考虑以下要素:

  1. 消息边界:如何确定一个完整消息的开始和结束
  2. 消息格式:消息头(Header)和消息体(Body)的结构
  3. 编解码方式:数据如何编码传输和解码读取
  4. 错误处理:如何处理不完整或错误的消息
  5. 扩展性:如何支持未来可能的协议升级

自定义协议的实现方式

常见的自定义协议实现方式包括:

  1. 基于TCP的二进制协议:如游戏、即时通讯等高性能场景
  2. 基于文本的协议:如SMTP等人类可读的协议
  3. 混合型协议:头部使用二进制,正文使用文本或二进制

自定义协议中的序列化和反序列化

序列化的概念与作用

序列化(Serialization)是指将数据结构或对象状态转换为可以存储或传输的格式的过程。在网络通信中,序列化主要用于:

  1. 将内存中的对象转换为字节流以便网络传输
  2. 保持数据的完整性和结构
  3. 实现跨平台、跨语言的数据交换

反序列化的概念与作用

反序列化(Deserialization)是序列化的逆过程,将字节流转换回原始的数据结构或对象。其主要作用包括:

  1. 重建发送端的数据结构
  2. 验证数据的完整性和正确性
  3. 恢复对象的完整状态

常见的序列化方式

自定义协议中常用的序列化方式包括:

  1. 二进制序列化

    • 直接内存拷贝
    • Protocol Buffers
    • Thrift
    • FlatBuffers
  2. 文本序列化

    • JSON
    • XML
    • YAML
  3. 混合序列化

    • MessagePack
    • BSON

简单自定义协议实现

该自定义协议中同样采用了序列化和反序列化的手段处理数据,实现了远程计算器功能,我们首先使用纯手搓的序列化及反序列化手段来实现:

首先我们实现一个 TCP 接口包装:

tcp.hpp

// tcp.hpp
#ifndef _TCP_HPP_
#define _TCP_HPP_ 1

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>  // sockaddr_in结构体定义
#include <arpa/inet.h>   // 网络地址转换函数
#include <cstring>       // bzero函数
#include "log.hpp"       // 自定义日志头文件

// 错误码枚举定义
enum
{
    socket_error = 1,  // socket创建失败
    bind_error,       // bind失败
    listen_error,      // listen失败
    connect_error,     // connect失败
    accept_error,      // accept失败
};

// 线程本地存储的地址缓冲区,用于IP地址转换
inline thread_local char addr_buffer[1024];

// TCP网络操作命名空间
namespace tcp
{
    // 创建TCP socket
    // 返回值: 成功返回socket文件描述符,失败退出程序
    int Socket()
    {
        // 创建IPv4 TCP socket
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd == -1)
        {
            perror("socket");
            exit(socket_error);  // 创建失败退出程序
        }
        return sockfd;
    }

    // 绑定socket到指定端口
    // 参数: sockfd - socket文件描述符
    //       port - 要绑定的端口号
    void Bind(int sockfd, int port)
    {
        struct sockaddr_in local;  // IPv4地址结构
        bzero(&local, sizeof(local));  // 清空结构体
        
        // 设置地址族、IP地址和端口
        local.sin_family = AF_INET;           // IPv4
        local.sin_addr.s_addr = INADDR_ANY;   // 监听所有网络接口
        local.sin_port = htons(port);         // 主机字节序转网络字节序
        
        // 绑定socket
        int n = bind(sockfd, (struct sockaddr *)(&local), sizeof(local));
        if (n != 0)
        {
            perror("bind");
            exit(bind_error);  // 绑定失败退出程序
        }
    }

    // 从sockaddr_in结构体中提取IP和端口
    // 参数: addr_in - 网络地址结构体
    //       ip - 输出参数,存储IP地址字符串
    //       port - 输出参数,存储端口号
    void GetAddrAndPort(struct sockaddr_in &addr_in, std::string &ip, uint16_t &port)
    {
        port = ntohs(addr_in.sin_port);  // 网络字节序转主机字节序
        // 将IP地址转换为点分十进制字符串
        inet_ntop(AF_INET, &addr_in.sin_addr, addr_buffer, sizeof(addr_buffer) - 1);
        ip = addr_buffer;  // 存储到输出参数
    }

    // 开始监听socket连接
    // 参数: listen_sock - 监听socket
    //       backlog - 最大挂起连接数,默认为10
    void Listen(int listen_sock, int backlog = 10)
    {
        int n = listen(listen_sock, backlog);
        if (n == -1)
        {
            perror("listen");
            exit(listen_error);  // 监听失败退出程序
        }
    }

    // 连接到服务器
    // 参数: sockfd - 客户端socket
    //       server_ip - 服务器IP地址
    //       server_port - 服务器端口号
    void Connect(int sockfd, const std::string &server_ip, const u_int16_t &server_port)
    {
        struct sockaddr_in server;  // 服务器地址结构
        bzero(&server, sizeof(server));  // 清空结构体
        
        // 设置服务器地址信息
        server.sin_port = htons(server_port);        // 端口号转网络字节序
        server.sin_family = AF_INET;                  // IPv4
        server.sin_addr.s_addr = inet_addr(server_ip.c_str());  // IP地址转换
        
        // 发起连接
        int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
        if (n == -1)
        {
            perror("connect");
            exit(connect_error);  // 连接失败退出程序
        }
    }

    // 接受客户端连接
    // 参数: listen_sockfd - 监听socket
    //       client_addr - 输出参数,存储客户端地址信息
    // 返回值: 成功返回连接socket文件描述符,失败退出程序
    int Accept(int listen_sockfd, sockaddr_in &client_addr)
    {
        socklen_t len = sizeof(client_addr);  // 地址结构长度
        
        // 接受连接
        int n = accept(listen_sockfd, (struct sockaddr *)&client_addr, &len);
        if (n == -1)
        {
            perror("accept");
            exit(accept_error);  // 接受失败退出程序
        }
        return n;  // 返回新连接的socket描述符
    }
};

#endif

然后是我们的重头戏:自定义协议实现:

protocol.hpp

#ifndef _PROTOCOL_HPP_
#define _PROTOCOL_HPP_ 1

#include <iostream>

// 协议分隔符定义
const std::string blank_space_sep = " ";  // 字段间分隔符(空格)
const std::string protocol_sep = "\n";    // 协议分隔符(换行)

// 错误码枚举
enum {
    divide_by_zero_error = 1,  // 除零错误
    operator_identify,         // 操作符识别错误
};

// 协议解码函数
// 格式: "长度\n内容\n"
// 参数: package - 输入的网络数据包
//       content - 输出的解码后内容
// 返回值: 解码成功返回true,失败返回false
bool Decode(std::string &package, std::string &content)
{
    // 查找第一个分隔符位置
    size_t pos = package.find(protocol_sep);
    if(pos == std::string::npos) return false;
    
    // 获取内容长度
    size_t size = std::stoi(package.substr(0, pos));
    
    // 查找第二个分隔符位置
    size_t n_pos = package.find(protocol_sep, pos + 1);
    size_t total_len = pos + size + 2;  // 计算完整消息长度
    
    // 验证消息格式是否正确
    if(n_pos + 1 != total_len) {
        // 格式错误,清空数据包
        package.clear();
        return false;
    }
    
    // 提取消息内容
    content = package.substr(pos + 1, size);
    
    // 删除已处理的部分
    package.erase(0, total_len);
    return true;
}

// 协议编码函数
// 格式: "长度\n内容\n"
// 参数: content - 要编码的内容
// 返回值: 编码后的字符串
std::string Encode(std::string &content)
{
    std::string ret = std::to_string(content.size());  // 添加长度
    ret += protocol_sep;                               // 添加分隔符
    ret += content;                                    // 添加内容
    ret += protocol_sep;                               // 添加结束分隔符
    return ret;
}

// 请求类(客户端→服务端)
class Request{
public:
    // 构造函数,默认值表示无效请求
    Request(int x = 0, int y = 0, char op = '?')
    :x_(x),
    y_(y),
    op_(op){}
    
    // 序列化方法:将对象转换为字符串
    // 格式: "x op y"(如 "10 + 20")
    std::string Serialize()
    {
        std::string ret = std::to_string(x_);
        ret += blank_space_sep;
        ret += op_;
        ret += blank_space_sep;
        ret += std::to_string(y_);
        return ret;
    }
    
    // 反序列化方法:从字符串解析对象
    // 参数: in - 输入字符串(格式必须为 "x op y")
    // 返回值: 解析成功返回true,失败返回false
    bool Deserialize(std::string &in)
    {
        // 查找第一个空格位置
        size_t pos = in.find(blank_space_sep);
        if(pos == std::string::npos) return false;
        
        // 解析第一个操作数
        x_ = std::stoi(in.substr(0, pos));
        
        // 查找第二个空格位置
        size_t npos = in.find(blank_space_sep, pos + 1);
        if(npos == std::string::npos) return false;
        
        // 解析操作符
        op_ = in[pos + 1];
        
        // 解析第二个操作数
        y_ = std::stoi(in.substr(npos + 1));
        return true;
    }
public:
    int x_;     // 第一个操作数
    int y_;     // 第二个操作数
    char op_;   // 操作符(+,-,*,/等)
};

// 响应类(服务端→客户端)
class Response{
public:
    // 构造函数
    Response(int res = 0, int code = 0)
    :res_(res),
    code_(code){}
    
    // 序列化方法:将对象转换为字符串
    // 格式: "result code"(如 "30 0")
    std::string Serialize()
    {
        std::string ret = std::to_string(res_);
        ret += blank_space_sep;
        ret += std::to_string(code_);
        return ret;
    }
    
    // 反序列化方法:从字符串解析对象
    // 参数: in - 输入字符串(格式必须为 "result code")
    // 返回值: 解析成功返回true,失败返回false
    bool Deserialize(std::string &in)
    {
        // 查找空格位置
        size_t pos = in.find(blank_space_sep);
        if(pos == std::string::npos) return false;
        
        // 解析计算结果
        res_ = std::stoi(in.substr(0, pos));
        
        // 解析状态码
        code_ = std::stoi(in.substr(pos + 1));
        return true;
    }
public:
    int res_;    // 计算结果
    int code_;   // 状态码(0表示成功,非0表示错误)
};

#endif

代码概述

  1. 协议格式

    • 外层协议格式:"长度\n内容\n"(如 "7\n10 + 20\n"
    • 请求格式:"x op y"(如 "10 + 20"
    • 响应格式:"result code"(如 "30 0"
  2. 主要功能

    • Encode/Decode:处理网络传输层面的封包/解包
    • Request:表示客户端请求,包含两个操作数和一个运算符
    • Response:表示服务端响应,包含计算结果和状态码
  3. 特点

    • 使用简单的文本协议,便于调试
    • 包含基本的错误检测机制
    • 支持序列化和反序列化操作
    • 使用空格和换行符作为分隔符

server_cal.hpp

以下是添加了详细注释的代码:

#ifndef _SERVER_CAL_HPP_
#define _SERVER_CAL_HPP_ 1;

#include <iostream>
#include "protocol.hpp"  // 包含之前定义的自定义协议头文件

// 计算器服务端类
class ServerCal
{
public:
    // 辅助计算函数,处理具体运算逻辑
    // 参数: req - 包含计算请求的Request对象
    // 返回值: 包含计算结果和状态的Response对象
    Response CalculatorHelper(const Request &req)
    {
        Response resp(0, 0);  // 初始化响应对象,默认结果0,状态码0(成功)
        
        // 根据操作符执行不同运算
        switch (req.op_)
        {
        case '+':
            resp.res_ = req.x_ + req.y_;  // 加法运算
            break;
        case '-':
            resp.res_ = req.x_ - req.y_;  // 减法运算
            break;
        case '*':
            resp.res_ = req.x_ * req.y_;  // 乘法运算
            break;
        case '/':
        {
            // 除法运算,检查除数是否为0
            if (req.y_ == 0)
                resp.code_ = divide_by_zero_error;  // 除零错误
            else
                resp.res_ = req.x_ / req.y_;  // 正常除法
        }
        break;
        case '%':
        {
            // 取模运算,检查模数是否为0
            if (req.y_ == 0)
                resp.code_ = divide_by_zero_error;  // 除零错误
            else
                resp.res_ = req.x_ % req.y_;  // 正常取模
        }
        break;
        default:
            resp.code_ = operator_identify;  // 未知操作符错误
            break;
        }
        return resp;
    }

    // 主计算函数,处理协议解码和编码
    // 参数: package - 网络接收到的原始数据包
    // 返回值: 编码后的响应字符串,出错返回空字符串
    std::string Calculator(std::string &package)
    {
        std::string content;
        
        // 1. 解码网络数据包
        if (!Decode(package, content)){
            return "";  // 解码失败返回空字符串
        }
        
        // 2. 反序列化请求内容
        Request req;
        if(!req.Deserialize(content)) return "";  // 反序列化失败返回空字符串
        
        // 3. 执行计算逻辑
        Response resp = CalculatorHelper(req);

        // 4. 序列化响应结果
        content = resp.Serialize();
        
        // 5. 编码为网络协议格式
        content = Encode(content);
        
        return content;
    }

};

#endif

代码功能概述

这段代码实现了一个简单的网络计算器服务端,主要功能包括:

  1. 核心计算功能

    • 支持加(+)、减(-)、乘(*)、除(/)、取模(%)五种运算
    • 处理除零错误和无效操作符等异常情况
  2. 协议处理流程

    • 接收网络数据包 → 解码 → 反序列化为请求对象 → 执行计算 → 序列化响应 → 编码为网络格式
    • 使用之前定义的protocol.hpp中的协议格式
  3. 错误处理

    • 解码失败时返回空字符串
    • 反序列化失败时返回空字符串
    • 运算错误通过Response的状态码返回
  4. 设计特点

    • 将协议处理和业务逻辑分离(Calculator和CalculatorHelper分工明确)
    • 使用状态码表示运算结果状态
    • 严格遵循自定义协议的格式要求

这样后,我们就可以通过将 std::string ServerCal::Calculator(std::string &package) 包装起来传递给 TCP 服务器从而使得服务器与协议功能解耦合

添加注释后的代码

#ifndef _TCPSERVERHPP_
#define _TCPSERVERHPP_ 1

#include "tcp.hpp"    // TCP基础操作封装
#include "log.hpp"    // 日志模块
#include <functional> // 函数对象支持

const u_int16_t default_port = 7777; // 默认服务端口

// 定义协议处理函数类型(输入请求包,返回响应包)
using func_t = std::function<std::string (std::string &)>;

class TcpServer{
public:
    // 构造函数:初始化端口和协议处理回调
    TcpServer(int port = default_port, func_t cal = nullptr)
    :port_(port),
    cal_(cal)
    {}

    // 初始化服务器:创建监听socket
    void Init()
    {
        listen_sockfd_ = tcp::Socket();  // 创建socket
        tcp::Bind(listen_sockfd_, port_); // 绑定端口
        tcp::Listen(listen_sockfd_);      // 开始监听
    }

    // 启动服务主循环
    void Start()
    {
        struct sockaddr_in client; // 客户端地址信息
        int client_sockfd = tcp::Accept(listen_sockfd_, client); // 接受连接
        
        char buffer[1024]; // 接收缓冲区
        std::string ip;    // 客户端IP
        u_int16_t port;    // 客户端端口
        tcp::GetAddrAndPort(client, ip, port); // 获取客户端地址
        
        std::string package; // 累积接收的数据包
        while(true)
        {
            // 接收客户端数据
            int n = recv(client_sockfd, buffer, sizeof(buffer) - 1, 0);
            if(n == -1) {
                lg(Error, "recv data failed. client [%s: %d]", ip.c_str(), port);
            }
            else if(n == 0){ // 客户端断开连接
                lg(Info, "client [%s: %d] quit", ip.c_str(), port);
                close(client_sockfd);
                break;
            }else{
                buffer[n] = 0; // 添加字符串结束符
                package += buffer; // 累积数据
                
                // 处理完整数据包(可能包含多个请求)
                while(true)
                {
                    std::string resp = cal_(package); // 调用协议处理回调
                    if(resp.empty()) break; // 无完整包可处理时退出
                    
                    // 发送响应
                    if(send(client_sockfd, resp.c_str(), resp.size(), 0) == -1){
                        lg(Error, "send failed. client [%s: %d]", ip.c_str(), port);
                    }
                }
            }
        }
    }
private:
    int listen_sockfd_; // 监听socket描述符
    u_int16_t port_;    // 服务端口
    func_t cal_;        // 协议处理回调函数
};

#endif

服务器解耦合设计特点

  1. 回调机制设计

    • 通过func_t函数对象类型定义协议处理接口
    • 构造函数接收外部传入的std::string (std::string&)类型函数
    • 实际处理时调用cal_(package)触发业务逻辑
  2. 与ServerCal的协作

    • 使用时将ServerCal::Calculator方法绑定给cal_成员
    • 服务器仅负责:接收原始数据 → 累积数据包 → 调用回调 → 返回响应
    • 协议解析和业务处理完全由外部Calculator实现
  3. 优势体现

    • 服务器不感知具体协议格式(如换行分隔符等)
    • 可灵活更换不同的协议处理器(只需符合函数签名)
    • 业务逻辑变更不影响网络通信层
    • 便于单元测试(可mock协议处理器)
  4. 数据处理流程

       flowchart TD
     A[网络层 TcpServer] -->|接收原始字节流| B[协议层 Decode]
     B -->|完整数据包| C[协议解析 Deserialize]
     C -->|Request对象| D[业务逻辑 CalculatorHelper]
     D -->|Response对象| E[协议组装 Serialize]
     E -->|响应字节流| F[协议层 Encode]
     F -->|发送数据| A

这种设计遵循了单一职责原则,使网络IO处理与协议解析分离,提高了代码的可维护性和扩展性。

客户端

以下是添加了详细注释的代码:

#include <iostream>
#include "protocol.hpp"  // 自定义协议头文件
#include "tcp.hpp"      // TCP网络操作头文件

using namespace std;

int main(int argc, char *argv[])
{
    // 参数校验:需要服务器IP和端口
    if(argc != 3)
    {
        cerr << "\nUsage: " << argv[0] << " server_ip server_port\n"
              << endl;
        return 1;
    }

    // 解析命令行参数
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 创建TCP socket并连接服务器
    int sockfd = tcp::Socket();
    tcp::Connect(sockfd, serverip, serverport);

    std::string recv_str;  // 接收数据缓冲区
    char buffer[1024];     // 临时接收缓冲区

    // 主循环:持续与服务器交互
    while(true)
    {
        // 1. 获取用户输入
        int x, y; 
        char op;
        cout << "Please Enter x: ";
        cin >> x;
        cout << "Please Enter y: ";
        cin >> y;
        cout << "Please Enter op: ";
        cin >> op;

        // 2. 构造请求并序列化
        Request req(x, y, op);
        string content = req.Serialize();  // 序列化为"x op y"格式
        content = Encode(content);        // 添加协议头尾

        // 3. 发送请求到服务器
        send(sockfd, content.c_str(), content.size(), 0);

        // 4. 接收服务器响应
        int n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
        if(n == -1){
            perror("recv");
            exit(1);
        }
        buffer[n] = 0;          // 添加字符串结束符
        recv_str += buffer;      // 累积接收的数据

        // 5. 解析并处理响应
        Response resp;
        while(true)
        {
            std::string content;
            if(!Decode(recv_str, content)) break;  // 解码协议
            
            resp.Deserialize(content);  // 反序列化响应数据
            cout << "result: " << resp.res_ << ", code: " << resp.code_ << endl;
        }
    }
    return 0;
}

代码实现思路说明

  1. 基本流程

    • 接收用户输入的两个数字和运算符
    • 将输入序列化为协议格式并发送给服务器
    • 接收服务器响应并解析显示结果
    • 循环这个过程实现持续计算
  2. 协议处理

    • 使用Request类序列化用户输入(格式:”x op y”)
    • 通过Encode添加协议头尾(长度前缀和分隔符)
    • 接收时使用Decode解析完整数据包
    • Response类解析服务器返回的结果和状态码
  3. 网络通信

    • 使用TCP socket连接服务器
    • 发送序列化后的请求数据
    • 接收响应数据并累积(处理可能的分包情况)
  4. 特点

    • 遵循严格的协议格式
    • 支持持续交互(不退出)
    • 基本的错误处理(接收错误退出)
    • 显示完整的计算结果和状态码

服务端

#include <iostream>
#include <memory>           // 智能指针头文件
#include "server_cal.hpp"   // 计算器服务实现
#include "tcpserver.hpp"    // TCP服务器实现
using namespace std;

int main(int argc, char *argv[])
{
    // 参数检查:需要指定服务端口
    if(argc != 2){
        cerr << "\nUsage: " << argv[0] << " server_port\n"
              << endl;
        exit(1);
    }

    // 创建计算器服务实例
    ServerCal sc;

    // 创建TCP服务器实例:
    // 1. 使用unique_ptr管理资源
    // 2. 端口号从命令行参数获取
    // 3. 绑定ServerCal::Calculator方法作为协议处理器
    unique_ptr<TcpServer> ts(new TcpServer(
        stoi(argv[1]),                      // 服务端口
        bind(&ServerCal::Calculator,        // 绑定成员函数
             &sc,                          // 对象指针
             placeholders::_1)             // 占位符表示参数
    ));

    // 初始化服务器(创建socket+绑定端口+监听)
    ts->Init();

    // 启动服务器主循环
    ts->Start();

    return 0;
}

至此,我们就讲自定义协议的计算服务实现了,下面我们来测试一下这段代码:

╭─ljx@VM-16-15-debian ~/linux_review/protocol
╰─➤  ./client_cal.o 82.156.255.140 7788
Please Enter x: 1
Please Enter y: 2
Please Enter op: +
result: 3, code: 0
Please Enter x: 3
Please Enter y: 2
Please Enter op: -
result: 1, code: 0
Please Enter x: 1
Please Enter y: 4
Please Enter op: /
result: 0, code: 0
Please Enter x: 3
Please Enter y: 0
Please Enter op: /
result: 0, code: 1
Please Enter x: 2
Please Enter y: 3
Please Enter op: %
result: 2, code: 0
Please Enter x: 2
Please Enter y: 0
Please Enter op: %
result: 0, code: 1
Please Enter x: 2
Please Enter y: 1
Please Enter op: &
result: 0, code: 2

客户端若输入的不是数字,就会导致未定义行为,因为我们对 xy 的类型定义的是 int 类型,因此我们可以将 xy 的类型定义为 string 类型,然后通过 std::stoi() 函数来转换为 int 类型,如果转换失败,就说明输入的不是数字,我们就可以提示用户重新输入:

string x_str, y_str;
char op;
int x = 0, y = 0;
// 输入x并验证
while (true)
{
    cout << "Please Enter x: ";
    cin >> x_str;
    try
    {
        x = stoi(x_str);
                break; // 转换成功则退出循环
    }
    catch (const exception &e)
    {
       cout << "Invalid number! Please enter an integer." << endl;
    }
}

// 输入y并验证
while (true)
{
    cout << "Please Enter y: ";
    cin >> y_str;
    try
    {
        y = stoi(y_str);
        break; // 转换成功则退出循环
    }
    catch (const exception &e)
    {
        cout << "Invalid number! Please enter an integer." << endl;
    }
}

而后,我们就可以正确处理客户端的输入错误了,而且这些检查都是在客户端做的,而不是让数据传递带服务端再做检查,一举两得

测试结果:

╭─ljx@VM-16-15-debian ~/linux_review/protocol
╰─➤  ./client_cal.o 82.156.255.140 7788
Please Enter x: 1
Please Enter y: +
Invalid number! Please enter an integer.
Please Enter y: %
Invalid number! Please enter an integer.
Please Enter y: 3
Please Enter op: (
result: 0, code: 2
Please Enter x: 3
Please Enter y: 2
Please Enter op: +
result: 5, code: 0

当然,序列化和反序列化是可以通过更简单的方式来处理的,我们可以利用一些比较成熟的序列化技术来更简单地实现序列化:

下面我们使用 JSON 来对数据进行序列化和反序列化:

JSON格式化实现

以下是添加了详细注释的代码:

#ifndef _PROTOCOL_HPP_
#define _PROTOCOL_HPP_ 1

#include <iostream>
#include <jsoncpp/json/json.h>  // JSON库头文件

// 分隔符定义(虽然改用JSON后部分不再需要,但保留协议兼容性)
const std::string blank_space_sep = " ";  // 空格分隔符
const std::string protocol_sep = "\n";    // 协议分隔符

// 错误码枚举
enum {
    divide_by_zero_error = 1,  // 除零错误
    operator_identify,         // 操作符识别错误
};

/* 协议解码函数(兼容旧协议)
 * 参数: package - 输入的数据包
 *       content - 输出的解码后内容
 * 返回值: 解码成功返回true,失败返回false
 * 注意: 改用JSON后不再严格检查格式,但仍保留长度前缀机制
 */
bool Decode(std::string &package, std::string &content)
{
    // 查找第一个分隔符位置
    size_t pos = package.find(protocol_sep);
    if(pos == std::string::npos) return false;
    
    // 获取JSON内容长度
    size_t size = std::stoi(package.substr(0, pos));
    size_t total_len = pos + size + 2;  // 计算总长度(长度+分隔符+内容+分隔符)
    
    // 提取JSON内容
    content = package.substr(pos + 1, size);
    
    // 删除已处理的数据
    package.erase(0, total_len);
    return true;
}

/* 协议编码函数
 * 参数: content - 要编码的JSON内容
 * 返回值: 添加长度前缀的协议字符串
 */
std::string Encode(std::string &content)
{
    std::string ret = std::to_string(content.size());  // 添加长度前缀
    ret += protocol_sep;                               // 添加分隔符
    ret += content;                                    // 添加JSON内容
    ret += protocol_sep;                               // 添加结束分隔符
    return ret;
}

// 请求类(客户端→服务端)
class Request{
public:
    // 构造函数(默认值表示无效请求)
    Request(int x = 0, int y = 0, char op = '?')
    :x_(x), y_(y), op_(op){}
    
    /* 序列化为JSON字符串
     * 格式: {"x":数值, "y":数值, "op":操作符}
     */
    std::string Serialize()
    {
        Json::Value root;          // 创建JSON根节点
        root["x"] = x_;           // 添加x字段
        root["y"] = y_;           // 添加y字段
        root["op"] = op_;         // 添加op字段
        Json::StyledWriter w;      // 使用格式化写入器
        return w.write(root);      // 生成JSON字符串
    }

    /* 从JSON字符串反序列化
     * 参数: in - 输入的JSON字符串
     * 返回值: 解析成功返回true
     */
    bool Deserialize(std::string &in)
    {
        Json::Value root;         // 创建JSON根节点
        Json::Reader r;          // JSON解析器
        if(!r.parse(in, root))   // 解析JSON
            return false;
            
        x_ = root["x"].asInt();   // 读取x值
        y_ = root["y"].asInt();   // 读取y值
        op_ = root["op"].asInt(); // 读取op值(注意: 这里可能有类型问题)
        return true;
    }
    
public:
    int x_;     // 第一个操作数
    int y_;     // 第二个操作数
    char op_;   // 操作符
};

// 响应类(服务端→客户端)
class Response{
public:
    // 构造函数
    Response(int res = 0, int code = 0)
    :res_(res), code_(code){}
    
    /* 序列化为JSON字符串
     * 格式: {"res":结果, "code":状态码}
     */
    std::string Serialize()
    {
        Json::Value root;
        root["res"] = res_;       // 添加结果字段
        root["code"] = code_;     // 添加状态码字段
        Json::StyledWriter w;
        return w.write(root);
    }

    /* 从JSON字符串反序列化
     * 参数: in - 输入的JSON字符串
     * 返回值: 解析成功返回true
     */
    bool Deserialize(std::string &in)
    {
        Json::Value root;
        Json::Reader r;
        if(!r.parse(in, root))
            return false;
            
        res_ = root["res"].asInt();    // 读取结果值
        code_ = root["code"].asInt();  // 读取状态码
        return true;
    }
    
public:
    int res_;    // 计算结果
    int code_;   // 状态码
};

#endif
  1. JSON序列化

    • 使用Json::Value作为数据容器
    • StyledWriter生成格式化的JSON字符串
    • Reader解析JSON字符串
  2. 协议兼容性

    • 保留原有的长度前缀机制
    • 外层协议格式不变,仅内容改为JSON
  3. 优势

    • 更易扩展新字段
    • 数据结构更清晰
    • 支持嵌套复杂数据结构
    • 跨语言兼容性更好

学会合理地使用外部工具,是一个程序员的必备技能

下面是整个服务器客户端的数据处理流程图,希望对你有帮助:

flowchart LR
    subgraph 客户端
        A[启动客户端] --> B[建立TCP连接]
        B --> C[输入x,y,op]
        subgraph 接收循环[接收响应循环]
        C --> D[序列化Request为JSON]
        D --> E[添加长度前缀协议封装]
        E --> F[发送数据]
        F --> G{接收数据}
        G -->|成功| H[累积到recv_buffer]
        
            H --> I{尝试解码}
            I -->|完整包| J[解析JSON响应]
            I -->|不完整| C
            J --> K[显示结果]
            K --> I
        end
    end

    subgraph 服务端
        M[启动服务] --> N[监听端口]
        N --> O[接受连接]
        
        subgraph 处理循环[请求处理循环]
            O --> P{接收数据}
            P -->|成功| Q[累积到recv_buffer]
            Q --> R{尝试解码}
            R -->|完整包| S[解析JSON请求]
            R -->|不完整| P
            S --> T[执行计算逻辑]
            T --> U[序列化Response为JSON]
            U --> V[添加长度前缀协议封装]
            V --> W[发送响应]
            W --> R
            P -->|失败| X[关闭连接]
        end
    end

    客户端 -- 请求 --> 服务端
    服务端 -- 响应 --> 客户端