应用层自定义协议
应用层自定义协议
粘包问题
TCP是面向字节流的协议,本身没有"包"的概念,所谓的"粘包"实际上是以下两种现象的统称:
- 发送方粘包:发送方应用层多次写入的数据被TCP合并为一个TCP段发送
- 接收方拆包:接收方一次读取操作可能包含多个应用层消息或不完整的消息
序列化与反序列化的含义
基本概念
序列化(Serialization)
定义:将数据结构或对象状态转换为可以存储或传输的格式(通常是字节流)的过程。
反序列化(Deserialization)
定义:将序列化后的数据重新构造为原始数据结构或对象的过程。
解决的问题
-
跨平台数据交换:
- 不同系统(不同字节序、不同语言)间的数据交换
- 示例:C++服务与Java服务通信
-
持久化存储:
- 将内存中的对象保存到文件或数据库
- 示例:游戏存档、应用配置保存
-
网络传输:
- 将复杂数据结构转换为适合网络传输的格式
- 解决TCP粘包问题的基础
-
分布式计算:
- 在进程间或机器间传递复杂数据结构
- 示例:MapReduce中的中间结果传递
技术实现对比
特性 | 二进制序列化 | 文本序列化 |
---|---|---|
效率 | 高(体积小,处理快) | 低(体积大,解析慢) |
可读性 | 不可读 | 可读 |
跨语言支持 | 通常需要相同实现 | 通用性好(如JSON/XML) |
典型协议 | Protobuf, FlatBuffers | JSON, XML, YAML |
版本兼容性 | 需要显式处理 | 相对灵活 |
C++序列化示例
- 简单二进制序列化
// 序列化结构体到二进制
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;
}// 注意:此方法有字节序和内存对齐问题,仅适用于同构系统
- 带长度前缀的字符串序列化
// 序列化字符串(解决定长数组浪费空间问题)
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);
}
- 使用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);
序列化中的关键问题
-
字节序问题:
// 网络字节序转换 uint32_t host_to_network(uint32_t value) {return htonl(value); }uint32_t network_to_host(uint32_t value) {return ntohl(value); }
-
版本兼容性:
- 向后兼容:新代码能读旧数据
- 向前兼容:旧代码能忽略新字段
-
安全考虑:
- 反序列化时验证数据完整性
- 防止缓冲区溢出攻击
// 安全的反序列化检查 bool SafeDeserialize(const char* data, size_t size, Person& out) {if (size < sizeof(Person)) return false;memcpy(&out, data, sizeof(Person));return true; }
-
性能优化:
- 零拷贝序列化(如FlatBuffers)
- 内存池管理
现代序列化方案对比
-
Protocol Buffers:
- Google开发,二进制格式
- 支持多语言,紧凑高效
- 需要预定义schema
-
FlatBuffers:
- Google开发,零拷贝反序列化
- 游戏开发常用,访问速度快
- 内存占用相对较大
-
JSON:
- 文本格式,人类可读
- 无schema要求,灵活
- 解析性能较差
-
MessagePack:
- 二进制JSON
- 比JSON紧凑,仍保持简单性
-
Boost.Serialization:
- C++专用,支持复杂对象图
- 与Boost深度集成
- 仅适用于C++系统
实际应用建议
-
选择标准:
- 跨语言需求 → Protobuf/JSON
- 极致性能 → FlatBuffers/Cap’n Proto
- 配置/日志 → JSON/YAML
- 纯C++环境 → Boost.Serialization
-
最佳实践:
// 版本化序列化示例 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");} }
-
调试技巧:
- 实现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和自定义协议的客户端-服务器通信框架,主要用于处理数学运算请求和响应。下面我将详细解释代码的各个部分及其功能:
- 核心类结构
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()
:构建完整的请求字符串
- 协议格式
该实现使用了自定义的应用层协议,格式为:
长度\r\n
JSON数据\r\n
示例:
15\r\n
{"x":10,"y":20,"oper":"+"}\r\n
- 工作流程
服务器端流程
- 接收客户端数据到缓冲区
- 使用
Decode()
尝试解析出完整请求包 - 反序列化JSON字符串为Request对象
- 调用注册的处理函数(
_func
)生成Response - 序列化Response并编码后发送回客户端
客户端流程
-
使用
BuildRequestString()
构建请求字符串 -
发送请求到服务器
-
使用
GetResponse()
接收并解析响应 -
处理响应结果
-
关键设计点
-
粘包处理:
- 通过长度前缀+分隔符的方式解决TCP粘包问题
Decode()
方法会检查缓冲区中是否有完整消息
-
JSON序列化:
- 使用JsonCpp库进行序列化/反序列化
- 文本格式便于调试和跨语言兼容
-
函数对象设计:
- 使用
std::function
允许灵活注册请求处理逻辑 - 服务器可以自定义不同的业务处理函数
- 使用
-
错误处理:
- 检查反序列化结果
- 处理连接断开等网络异常
-
使用示例
服务器端
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与服务器通信,发送数学运算请求并接收计算结果。下面是对代码的详细解析:
- 主要功能
- 这是一个命令行客户端程序,连接指定的服务器IP和端口
- 用户可以输入两个数字和运算符(如+,-,*,/)
- 将运算请求发送到服务器
- 接收并显示服务器返回的计算结果
- 代码结构解析
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);
}
- 解析服务器IP和端口参数
- 创建TCP Socket客户端
- 尝试连接服务器,失败则退出
主循环
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();
}
- 创建Protocol对象处理通信协议
- 进入无限循环:
- 获取用户输入
- 构建请求字符串(自动添加协议头)
- 发送请求到服务器
- 接收并解析服务器响应
- 显示计算结果
资源清理
client->Close();
- 退出时关闭Socket连接
-
协议工作流程
-
请求构建:
protocol->BuildRequestString(x,y,oper)
- 创建Request对象
- 序列化为JSON(如
{"x":5,"y":3,"oper":"+"}
) - 添加长度前缀和分隔符(如
15\r\n{"x":5,"y":3,"oper":"+"}\r\n
)
-
响应处理:
protocol->GetResponse(client, resp_buffer, &resp)
- 从Socket读取数据到缓冲区
- 使用分隔符解析完整响应
- 反序列化JSON到Response对象
- 返回解析结果
-
关键设计点
-
智能指针管理资源:
shared_ptr
管理Socket生命周期unique_ptr
管理Protocol对象
-
错误处理:
- 定义了错误码(USAGE_ERR, CONNECT_ERR等)
- 检查关键操作返回值
-
用户交互:
- 简单的命令行界面
- 支持连续多次计算
-
协议封装:
- 协议细节(如JSON格式、长度前缀)对主程序透明
- 便于修改协议实现而不影响主逻辑
-
使用示例
编译运行:
./client 127.0.0.1 8080
交互示例:
Please Enter x: 10
Please Enter y: 20
Please Enter oper: +
计算结果是: 30[200]
这段代码展示了一个结构清晰、模块化的网络客户端实现,核心业务逻辑与网络通信细节良好分离,便于维护和扩展。
反向理解OSI七层模型与自定义协议实践
一、反向视角看OSI七层模型
传统OSI模型是从底层到应用层(1-7层)的抽象,我们反向从应用层出发理解:
-
应用层(7):用户直接交互的协议和数据(HTTP/FTP等)
- 思考:我的业务需要传输什么数据?
-
表示层(6):数据格式转换、加密解密
- 思考:我的数据需要特殊编码或加密吗?
-
会话层(5):建立和管理会话
- 思考:需要保持长时间连接还是短连接?
-
传输层(4):端到端传输(TCP/UDP)
- 思考:需要可靠传输(TCP)还是快速传输(UDP)?
-
网络层(3):路由和寻址(IP)
- 思考:数据要如何跨网络到达目标?
-
数据链路层(2):相邻节点间帧传输
- 思考:数据在本地网络如何传递?
-
物理层(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)
- 现代序列化方案对比
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Protocol Buffers | 高效/跨语言/向后兼容 | 需要预编译 | 复杂业务/多语言系统 |
FlatBuffers | 零拷贝/极高性能 | 内存占用稍大 | 游戏/高性能场景 |
JSON | 易读/无需schema | 体积大/解析慢 | 配置/简单RPC |
MessagePack | 比JSON紧凑/支持多语言 | 无schema验证 | 移动设备/简单通信 |