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