应用层自定义协议

应用层自定义协议

粘包问题

TCP是面向字节流的协议,本身没有"包"的概念,所谓的"粘包"实际上是以下两种现象的统称:

  1. 发送方粘包:发送方应用层多次写入的数据被TCP合并为一个TCP段发送
  2. 接收方拆包:接收方一次读取操作可能包含多个应用层消息或不完整的消息

序列化与反序列化的含义

基本概念

序列化(Serialization)

定义:将数据结构或对象状态转换为可以存储或传输的格式(通常是字节流)的过程。

反序列化(Deserialization)

定义:将序列化后的数据重新构造为原始数据结构或对象的过程。

解决的问题

  1. 跨平台数据交换

    • 不同系统(不同字节序、不同语言)间的数据交换
    • 示例:C++服务与Java服务通信
  2. 持久化存储

    • 将内存中的对象保存到文件或数据库
    • 示例:游戏存档、应用配置保存
  3. 网络传输

    • 将复杂数据结构转换为适合网络传输的格式
    • 解决TCP粘包问题的基础
  4. 分布式计算

    • 在进程间或机器间传递复杂数据结构
    • 示例:MapReduce中的中间结果传递

技术实现对比

特性 二进制序列化 文本序列化
效率 高(体积小,处理快) 低(体积大,解析慢)
可读性 不可读 可读
跨语言支持 通常需要相同实现 通用性好(如JSON/XML)
典型协议 Protobuf, FlatBuffers JSON, XML, YAML
版本兼容性 需要显式处理 相对灵活

C++序列化示例

  1. 简单二进制序列化
cpp 复制代码
// 序列化结构体到二进制
struct Person {
    int id;
    char name[50];
    double salary;
};

std::vector<char> SerializePerson(const Person& p) {
    std::vector<char> buffer(sizeof(Person));
    memcpy(buffer.data(), &p, sizeof(Person));
    return buffer;
}

Person DeserializePerson(const std::vector<char>& data) {
    Person p;
    memcpy(&p, data.data(), sizeof(Person));
    return p;
}

// 注意:此方法有字节序和内存对齐问题,仅适用于同构系统
  1. 带长度前缀的字符串序列化
cpp 复制代码
// 序列化字符串(解决定长数组浪费空间问题)
std::vector<char> SerializeString(const std::string& str) {
    std::vector<char> buffer(sizeof(uint32_t) + str.size());
    uint32_t len = str.size();
    memcpy(buffer.data(), &len, sizeof(uint32_t));
    memcpy(buffer.data() + sizeof(uint32_t), str.data(), str.size());
    return buffer;
}

std::string DeserializeString(const std::vector<char>& data) {
    if (data.size() < sizeof(uint32_t)) return "";
    
    uint32_t len;
    memcpy(&len, data.data(), sizeof(uint32_t));
    
    if (data.size() < sizeof(uint32_t) + len) return "";
    
    return std::string(data.data() + sizeof(uint32_t), len);
}
  1. 使用Protobuf(跨语言解决方案)
proto 复制代码
// person.proto
syntax = "proto3";

message Person {
    int32 id = 1;
    string name = 2;
    double salary = 3;
}
cpp 复制代码
// C++使用
Person person;
person.set_id(123);
person.set_name("John Doe");
person.set_salary(5000.0);

// 序列化
std::string serialized = person.SerializeAsString();

// 反序列化
Person new_person;
new_person.ParseFromString(serialized);

序列化中的关键问题

  1. 字节序问题

    cpp 复制代码
    // 网络字节序转换
    uint32_t host_to_network(uint32_t value) {
        return htonl(value);
    }
    
    uint32_t network_to_host(uint32_t value) {
        return ntohl(value);
    }
  2. 版本兼容性

    • 向后兼容:新代码能读旧数据
    • 向前兼容:旧代码能忽略新字段
  3. 安全考虑

    • 反序列化时验证数据完整性
    • 防止缓冲区溢出攻击
    cpp 复制代码
    // 安全的反序列化检查
    bool SafeDeserialize(const char* data, size_t size, Person& out) {
        if (size < sizeof(Person)) return false;
        memcpy(&out, data, sizeof(Person));
        return true;
    }
  4. 性能优化

    • 零拷贝序列化(如FlatBuffers)
    • 内存池管理

现代序列化方案对比

  1. Protocol Buffers

    • Google开发,二进制格式
    • 支持多语言,紧凑高效
    • 需要预定义schema
  2. FlatBuffers

    • Google开发,零拷贝反序列化
    • 游戏开发常用,访问速度快
    • 内存占用相对较大
  3. JSON

    • 文本格式,人类可读
    • 无schema要求,灵活
    • 解析性能较差
  4. MessagePack

    • 二进制JSON
    • 比JSON紧凑,仍保持简单性
  5. Boost.Serialization

    • C++专用,支持复杂对象图
    • 与Boost深度集成
    • 仅适用于C++系统

实际应用建议

  1. 选择标准

    • 跨语言需求 → Protobuf/JSON
    • 极致性能 → FlatBuffers/Cap'n Proto
    • 配置/日志 → JSON/YAML
    • 纯C++环境 → Boost.Serialization
  2. 最佳实践

    cpp 复制代码
    // 版本化序列化示例
    struct Header {
        uint32_t magic;
        uint16_t version;
        uint16_t reserved;
    };
    
    void SerializeV2(std::ostream& os, const Data& data) {
        Header hdr{0xABCD, 2, 0};
        os.write(reinterpret_cast<char*>(&hdr), sizeof(hdr));
        // 写入V2特有字段...
    }
    
    Data Deserialize(std::istream& is) {
        Header hdr;
        is.read(reinterpret_cast<char*>(&hdr), sizeof(hdr));
        
        switch (hdr.version) {
            case 1: return DeserializeV1(is);
            case 2: return DeserializeV2(is);
            default: throw std::runtime_error("Unsupported version");
        }
    }
  3. 调试技巧

    • 实现ToDebugString()方法
    • 二进制数据转换为hex dump
    cpp 复制代码
    std::string HexDump(const void* data, size_t size) {
        static const char hex[] = "0123456789ABCDEF";
        std::string result;
        const uint8_t* p = reinterpret_cast<const uint8_t*>(data);
        
        for (size_t i = 0; i < size; ++i) {
            result += hex[(p[i] >> 4) & 0xF];
            result += hex[p[i] & 0xF];
            if ((i + 1) % 16 == 0) result += '\n';
            else result += ' ';
        }
        
        return result;
    }

序列化与反序列化是分布式系统和数据持久化的基础技术,合理选择方案能显著影响系统性能、可维护性和扩展性。

客户端,服务端设计

Protocol.hpp

C++ 复制代码
#pragma once
#include <iostream>
#include <string>
#include <memory>
#include "Socket.hpp"
#include <jsoncpp/json/json.h>
#include <functional>
using namespace SocketModule;
class Request
{
    public:
        Request(int x,int y,char oper):_x(x),_y(y),_oper(oper){}
        Request(){}
        std::string Serialize()
        {
            std::string s;
            Json::Value root;
            root["x"]=_x;
            root["y"]=_y;
            root["oper"]=_oper;

            Json::FastWriter writer;
            std::string s=writer.write(root);
            return s;
        }
        bool Deserialize(std::string&in)
        {
            Json::Value root;
            Json::Reader reader;
            bool ok=reader.parse(in,root);
            if(ok)
            {
                _x=root["x"].asInt();
                _y=root["y"].asInt();
                _oper=root["oper"].asInt();
            }
            return ok;
        }
        ~Request(){}
        int X(){return _x;}
        int Y(){return _y;}
        char Oper(){return _oper;}
    private:
        int _x;
        int _y;
        char _oper;
};

class Response
{
    public:
    Response(){}
    Response(int result,int code):_result(result),_code(code){}
    std::string Serialize()
    {
        Json::Value root;
        root["result"]=_result;
        root["code"]=_code;

        Json::FastWriter writer;
        return writer.write(root);
    }
    bool Deserialize(std::string &in)
    {
        Json::Value root;
        Json::Reader reader;
        bool ok=reader.parse(in,root);
        if(ok)
        {
            _result=root["result"].asInt();
            _code=root["code"].asInt();
        }
        return ok;
    }
    ~Response(){}
    void SetResult(int res)
    {
        _result=res;
    }
    void SetCode(int code)
    {
        _code=code;
    }
     void ShowResult()
    {
        std::cout << "计算结果是: " << _result << "[" << _code << "]" << std::endl;
    }
    private:
        int _result;
        int _code;
};
const std::string sep="\r\n";
using func_t=std::function<Response(Request&)>;
class Protocol
{
public:
    Protocol(){}
    Protocol(func_t func):_func(func)
    {}
    std::string Encode(const std::string jsonstr)
    {
        std::string len=std::to_string(jsonstr.size());
        return len+sep+jsonstr+sep;
    }
    bool Decode(std::string &buffer,std::string *package)
    {
        ssize_t pos=buffer.find(sep);
        if(pos==std::string::npos)
        {
            return false;
        }
        std::string package_len_str=buffer.substr(0,pos);
        int package_len_int=std::stoi(package_len_str);
        int target_len=package_len_str.size()+package_len_int+2*sep.size();
        if(buffer.size()<target_len)
            return false;
        *package=buffer.substr(pos+sep.size(),package_len_int);
        buffer.erase(0,target_len);
        return true;
    }
    void GetRequest(std::shared_ptr<Socket> &sock, InetAddr &client)
    {
        std::string buffer_queue;
        while(true)
        {
        int n=sock->Recv(&buffer_queue);
        if(n>0)
        {
            std::string json_package;
            bool ret=Decode(buffer_queue,&json_package);
            if(!ret)
                continue;
            Request req;
            bool ok=req.Deserialize(json_package);
            if(!ok)
                continue;
            Response resp = _func(req);

                // 4. 序列化
                std::string json_str = resp.Serialize();

                // 5. 添加自定义长度
                std::string send_str = Encode(json_str); // 携带长度的应答报文了"len\r\n{result:XXX,code:XX}\r\n"

                // 6. 直接发送
                sock->Send(send_str);

        }
        else if(n==0)
        {
            LOG(LogLevel::INFO)<<"client"<<client.StringAddr()<<"Quit";
            break;
        }
        else
        {
            LOG(LogLevel::WARNING)<<"client:"<<client.StringAddr()<<",recv error";
            break;
        }
       
        }
        sock->Close();
    }
     bool GetResponse(std::shared_ptr<Socket> &client, std::string &resp_buff, Response *resp)
    {
        // 面向字节流,你怎么保证,你的client读到的 一个网络字符串,就一定是一个完整的请求呢??
        while (true)
        {
            int n = client->Recv(&resp_buff);
            if (n > 0)
            {
                std::string json_package;

                while (Decode(resp_buff, &json_package))
                {
                    resp->Deserialize(json_package);
                }
                return true;
            }
            else if (n == 0)
            {
                std::cout << "server quit " << std::endl;
                return false;
            }
            else
            {
                std::cout << "recv error" << std::endl;
                return false;
            }
        }
    }
    std::string BuildRequestString(int x, int y, char oper)
    {
        // 1. 构建一个完整的请求
        Request req(x, y, oper);
        // 2. 序列化
        std::string json_req = req.Serialize();
        // 3. 添加长度报头
        return Encode(json_req);
    }
    ~Protocol()
    {}
private:
    func_t _func;
};

这段代码实现了一个基于JSON和自定义协议的客户端-服务器通信框架,主要用于处理数学运算请求和响应。下面我将详细解释代码的各个部分及其功能:

  1. 核心类结构

Request类

  • 功能:表示客户端发送的数学运算请求
  • 数据成员
    • _x, _y:运算的操作数
    • _oper:运算符(如'+', '-', '*', '/')
  • 关键方法
    • Serialize():将请求对象序列化为JSON字符串
    • Deserialize():从JSON字符串反序列化为请求对象
    • 访问器方法:X(), Y(), Oper()

Response类

  • 功能:表示服务器返回的运算结果
  • 数据成员
    • _result:运算结果
    • _code:状态码(可用于表示运算是否成功)
  • 关键方法
    • Serialize():将响应对象序列化为JSON字符串
    • Deserialize():从JSON字符串反序列化为响应对象
    • SetResult(), SetCode():设置结果和状态码
    • ShowResult():显示结果信息

Protocol类

  • 功能:处理协议编码/解码和通信逻辑
  • 关键组件
    • sep:分隔符("\r\n")
    • func_t:函数对象类型,用于处理请求并生成响应
  • 核心方法
    • Encode():为JSON字符串添加长度前缀
    • Decode():从接收缓冲区解析出完整JSON包
    • GetRequest():服务器端处理请求的完整流程
    • GetResponse():客户端处理响应的完整流程
    • BuildRequestString():构建完整的请求字符串
  1. 协议格式

该实现使用了自定义的应用层协议,格式为:

复制代码
长度\r\n
JSON数据\r\n

示例:

复制代码
15\r\n
{"x":10,"y":20,"oper":"+"}\r\n
  1. 工作流程

服务器端流程

  1. 接收客户端数据到缓冲区
  2. 使用Decode()尝试解析出完整请求包
  3. 反序列化JSON字符串为Request对象
  4. 调用注册的处理函数(_func)生成Response
  5. 序列化Response并编码后发送回客户端

客户端流程

  1. 使用BuildRequestString()构建请求字符串

  2. 发送请求到服务器

  3. 使用GetResponse()接收并解析响应

  4. 处理响应结果

  5. 关键设计点

  6. 粘包处理

    • 通过长度前缀+分隔符的方式解决TCP粘包问题
    • Decode()方法会检查缓冲区中是否有完整消息
  7. JSON序列化

    • 使用JsonCpp库进行序列化/反序列化
    • 文本格式便于调试和跨语言兼容
  8. 函数对象设计

    • 使用std::function允许灵活注册请求处理逻辑
    • 服务器可以自定义不同的业务处理函数
  9. 错误处理

    • 检查反序列化结果
    • 处理连接断开等网络异常
  10. 使用示例

服务器端

cpp 复制代码
Response Calculate(Request& req) {
    int result = 0;
    int code = 200;
    switch(req.Oper()) {
        case '+': result = req.X() + req.Y(); break;
        case '-': result = req.X() - req.Y(); break;
        // 其他运算...
        default: code = 400; // 错误码
    }
    return Response(result, code);
}

int main() {
    Protocol protocol(Calculate);
    // 创建服务器socket并接受连接...
    protocol.GetRequest(client_sock, client_addr);
}

客户端

cpp 复制代码
int main() {
    Protocol protocol;
    auto sock = /* 创建并连接服务器 */;
    
    std::string req_str = protocol.BuildRequestString(10, 20, '+');
    sock->Send(req_str);
    
    Response resp;
    std::string buffer;
    if(protocol.GetResponse(sock, buffer, &resp)) {
        resp.ShowResult();
    }
}

这段代码实现了一个完整的网络通信框架,展示了如何设计自定义应用层协议来处理TCP粘包问题,并通过JSON实现数据的序列化和反序列化。

这段代码实现了一个基于TCP协议的简单计算器客户端程序,它通过Socket与服务器通信,发送数学运算请求并接收计算结果。下面是对代码的详细解析:

  1. 主要功能
  • 这是一个命令行客户端程序,连接指定的服务器IP和端口
  • 用户可以输入两个数字和运算符(如+,-,*,/)
  • 将运算请求发送到服务器
  • 接收并显示服务器返回的计算结果
  1. 代码结构解析

2.1 头文件包含

cpp 复制代码
#include "Socket.hpp"       // 自定义Socket封装
#include "Common.hpp"       // 公共定义(如错误码)
#include <iostream>         // 标准输入输出
#include <string>           // 字符串处理
#include <memory>           // 智能指针
#include "Protocol.hpp"     // 自定义协议处理

2.2 辅助函数

Usage函数

cpp 复制代码
void Usage(std::string proc) {
    std::cerr<<"Usage: "<<proc<<"server_ip server_port"<<std::endl;
}
  • 显示程序用法提示
  • 参数proc是程序名(argv[0])

GetDataFromStdin函数

cpp 复制代码
void GetDataFromStdin(int *x,int *y,char *oper) {
    std::cout<<"Please Enter x: ";
    std::cin>>*x;
    std::cout<<"Please Enter y: ";
    std::cin>>*y;
    std::cout<<"Please Enter oper: ";
    std::cin>>oper;
}
  • 从标准输入获取用户输入的运算数(x,y)和运算符(oper)
  • 通过指针参数返回结果

2.3 主函数逻辑

参数检查

cpp 复制代码
if(argc!=3) {
    Usage(argv[0]);
    exit(USAGE_ERR);
}
  • 检查命令行参数数量是否正确(需要服务器IP和端口)
  • 不正确则显示用法并退出

初始化连接

cpp 复制代码
std::string server_ip=argv[1];
uint16_t server_port=std::stoi(argv[2]);

std::shared_ptr<Socket> client=std::make_shared<TcpSocket>();
client->BuildTcpClientSocketMethod();

if(client->Connect(server_ip,server_port)!=0) {
    std::cerr<<"connect error"<<std::endl;
    exit(CONNECT_ERR);
}
  1. 解析服务器IP和端口参数
  2. 创建TCP Socket客户端
  3. 尝试连接服务器,失败则退出

主循环

cpp 复制代码
std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>();
std::string resp_buffer;

while(true) {
    // 获取用户输入
    int x,y;
    char oper;
    GetDataFromStdin(&x,&y,&oper);
    
    // 构建并发送请求
    std::string req_str=protocol->BuildRequestString(x,y,oper);
    client->Send(req_str);
    
    // 获取并显示响应
    Response resp;
    bool res = protocol->GetResponse(client, resp_buffer, &resp);
    if(res == false) break;
    
    resp.ShowResult();
}
  1. 创建Protocol对象处理通信协议
  2. 进入无限循环:
    • 获取用户输入
    • 构建请求字符串(自动添加协议头)
    • 发送请求到服务器
    • 接收并解析服务器响应
    • 显示计算结果

资源清理

cpp 复制代码
client->Close();
  • 退出时关闭Socket连接
  1. 协议工作流程

  2. 请求构建

    cpp 复制代码
    protocol->BuildRequestString(x,y,oper)
    • 创建Request对象
    • 序列化为JSON(如{"x":5,"y":3,"oper":"+"}
    • 添加长度前缀和分隔符(如15\r\n{"x":5,"y":3,"oper":"+"}\r\n
  3. 响应处理

    cpp 复制代码
    protocol->GetResponse(client, resp_buffer, &resp)
    • 从Socket读取数据到缓冲区
    • 使用分隔符解析完整响应
    • 反序列化JSON到Response对象
    • 返回解析结果
  4. 关键设计点

  5. 智能指针管理资源

    • shared_ptr管理Socket生命周期
    • unique_ptr管理Protocol对象
  6. 错误处理

    • 定义了错误码(USAGE_ERR, CONNECT_ERR等)
    • 检查关键操作返回值
  7. 用户交互

    • 简单的命令行界面
    • 支持连续多次计算
  8. 协议封装

    • 协议细节(如JSON格式、长度前缀)对主程序透明
    • 便于修改协议实现而不影响主逻辑
  9. 使用示例

编译运行:

bash 复制代码
./client 127.0.0.1 8080

交互示例:

复制代码
Please Enter x: 10
Please Enter y: 20
Please Enter oper: +
计算结果是: 30[200]

这段代码展示了一个结构清晰、模块化的网络客户端实现,核心业务逻辑与网络通信细节良好分离,便于维护和扩展。

反向理解OSI七层模型与自定义协议实践

一、反向视角看OSI七层模型

传统OSI模型是从底层到应用层(1-7层)的抽象,我们反向从应用层出发理解:

  1. 应用层(7):用户直接交互的协议和数据(HTTP/FTP等)

    • 思考:我的业务需要传输什么数据?
  2. 表示层(6):数据格式转换、加密解密

    • 思考:我的数据需要特殊编码或加密吗?
  3. 会话层(5):建立和管理会话

    • 思考:需要保持长时间连接还是短连接?
  4. 传输层(4):端到端传输(TCP/UDP)

    • 思考:需要可靠传输(TCP)还是快速传输(UDP)?
  5. 网络层(3):路由和寻址(IP)

    • 思考:数据要如何跨网络到达目标?
  6. 数据链路层(2):相邻节点间帧传输

    • 思考:数据在本地网络如何传递?
  7. 物理层(1):比特流传输

    • 思考:使用什么物理介质传输?

反向设计启示:从业务需求出发,自上而下选择每层的最适技术。

二、自定义协议的常见实践

  1. 协议设计要素

协议头 魔数/版本 消息类型 序列号 时间戳 协议体 业务数据 协议尾 校验和

  1. 典型实现方案

方案A:文本协议(如HTTP)

python 复制代码
# 示例:简单文本协议
"GET /data?id=123 HTTP/1.1\r\n"
"Host: example.com\r\n"
"Content-Type: text/json\r\n"
"\r\n"
"{'key':'value'}"

方案B:二进制协议(推荐)

cpp 复制代码
// C++二进制协议头示例
#pragma pack(push, 1)  // 1字节对齐
struct ProtocolHeader {
    uint32_t magic;     // 0xABCD1234
    uint16_t version;   // 协议版本
    uint8_t  type;      // 消息类型
    uint32_t length;    // 数据长度
    uint64_t timestamp; // 时间戳
    uint32_t checksum;  // 头部校验
};
#pragma pack(pop)
  1. 现代序列化方案对比
方案 优点 缺点 适用场景
Protocol Buffers 高效/跨语言/向后兼容 需要预编译 复杂业务/多语言系统
FlatBuffers 零拷贝/极高性能 内存占用稍大 游戏/高性能场景
JSON 易读/无需schema 体积大/解析慢 配置/简单RPC
MessagePack 比JSON紧凑/支持多语言 无schema验证 移动设备/简单通信