当前位置: 首页 > news >正文

应用层自定义协议

应用层自定义协议

粘包问题

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

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

序列化与反序列化的含义

基本概念

序列化(Serialization)

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

反序列化(Deserialization)

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

解决的问题

  1. 跨平台数据交换

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

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

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

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

技术实现对比

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

C++序列化示例

  1. 简单二进制序列化
// 序列化结构体到二进制
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. 带长度前缀的字符串序列化
// 序列化字符串(解决定长数组浪费空间问题)
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(跨语言解决方案)
// person.proto
syntax = "proto3";message Person {int32 id = 1;string name = 2;double salary = 3;
}
// 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. 字节序问题

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

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

    • 反序列化时验证数据完整性
    • 防止缓冲区溢出攻击
    // 安全的反序列化检查
    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. 最佳实践

    // 版本化序列化示例
    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
    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

#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. 使用示例

服务器端

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

客户端

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 头文件包含

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

2.2 辅助函数

Usage函数

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

GetDataFromStdin函数

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 主函数逻辑

参数检查

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

初始化连接

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. 尝试连接服务器,失败则退出

主循环

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. 进入无限循环:
    • 获取用户输入
    • 构建请求字符串(自动添加协议头)
    • 发送请求到服务器
    • 接收并解析服务器响应
    • 显示计算结果

资源清理

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

  2. 请求构建

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

    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. 使用示例

编译运行:

./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)

# 示例:简单文本协议
"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:二进制协议(推荐)

// C++二进制协议头示例
#pragma pack(push, 1)  // 1字节对齐
struct ProtocolHeader {uint32_t magic;     // 0xABCD1234uint16_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验证移动设备/简单通信
http://www.lryc.cn/news/613872.html

相关文章:

  • 相机Camera日志实例分析之十:相机Camx【萌拍调节AE/AF拍照】单帧流程日志详解
  • LeetCode 面试经典 150_数组/字符串_加油站(14_134_C++_中等)(贪心算法)
  • 力扣 hot100 Day69
  • GISBox私有云+SaaS:安全协同的地理智能平台
  • C++-->stl: list的使用
  • 《Java枚举类深度解析:定义与实战应用》
  • 一洽客服系统:APP路由等级与路由条件设置
  • SITIME汽车时钟发生器Chorus保障智能汽车安全
  • 【MATLAB技巧】打开脚本(m文件)后,中文乱码的解决方案
  • TensorFlow深度学习实战(29)——自监督学习(Self-Supervised Learning)
  • element plus table 表格操作列根据按钮数量自适应宽度
  • 宝龙地产债务化解解决方案二:基于资产代币化与轻资产转型的战略重构
  • (1-9-1) Maven 特性、安装、配置、打包
  • Electron——窗口
  • linux mysql 8.X主从复制
  • 【Linux】从零开始:RPM 打包全流程实战万字指南(含目录结构、spec 编写、分步调试)
  • 避免“卡脖子”!如何减少内存I/O延迟对程序的影响?
  • Function + 异常策略链:构建可组合的异常封装工具类
  • 二叉树、算法
  • 防火墙概述
  • React 原生部落的生存现状:观察“Hooks 猎人“如何用useEffect设陷阱反被依赖项追杀
  • 【Unity3D实例-功能-跳跃】角色跳跃
  • Rocky Linux 10.0下安装使用KVM虚拟机
  • 破界之光:DeepSeek 如何重构AI搜索引擎的文明坐标 || #AIcoding·八月创作之星挑战赛#
  • Mac上安装和配置MySQL(使用Homebrew安装MySQL 8.0)
  • [202403-E]春日
  • 等保测评-Nginx中间件
  • DM8数据库服务正常,但是登录报错 [-70019]:没有匹配的可登录服务器
  • cAdvisor 容器监控软件学习
  • docker下载安装和使用(Hyper-V方式)