什么是粘包问题?
在使用 TCP 协议发送数据时,经常会遇到:明明分两次发了两条消息,结果对方一次就全收到了,两条消息粘在一起,分不清边界。这就叫粘包。
举个生活例子: 你分两句话说:
本文介绍了 TCP 粘包问题的成因及解决方案,重点讲解了基于 Boost.Asio 的 C++ 网络编程中如何处理粘包与拆包。文章详细阐述了使用“消息头 + 消息体”协议(先读固定长度头部获取消息体长度,再读指定长度内容)的实现逻辑。内容包括数据节点设计、Session 类状态管理、异步接收逻辑的循环处理机制,以及针对内存越界、裸指针泄漏、同步阻塞等潜在问题的优化建议。最后提供了客户端示例代码,并指出了跨平台大小端模式的遗留问题。

在使用 TCP 协议发送数据时,经常会遇到:明明分两次发了两条消息,结果对方一次就全收到了,两条消息粘在一起,分不清边界。这就叫粘包。
举个生活例子: 你分两句话说:
TCP 为了效率,会把它们打包一起发: 对方收到:'我是 A 我是 B' ——不知道哪里是第一句、哪里是第二句。
所以:发送方分几次发 ≠ 接收方分几次收
所有网络编程(C++、Java、Go、Python)都用这几种:
TCP 只保证字节流可靠,不保证消息边界,边界要你自己定义。用消息头 + 消息体的形式来接收。
// 数据节点来存储数据
class MsgNode {
friend class Session;
public:
MsgNode(char* msg, int max_len) : _total_len(max_len + HEAD_LENGTH), _cur_len(0) {
_data = new char[_total_len + 1]; // 消息长度 + 消息内容
memcpy(_data, &max_len, HEAD_LENGTH);
memcpy(_data + HEAD_LENGTH, msg, max_len);
_data[_total_len] = '\0';
}
MsgNode(short max_len) : _total_len(max_len), _cur_len(0) {
_data = new char[_total_len + 1]();
}
~MsgNode() {
delete[] _data;
}
void clear() {
memset(_data, 0, _total_len);
_cur_len = 0;
}
private:
int _cur_len;
int _total_len;
char* _data;
};
// 收到的消息结构
std::shared_ptr<MsgNode> _recv_msg_node;
bool _b_head_parse; // 收到的头部结构
std::shared_ptr<MsgNode> _recv_head_node;
_recv_msg_node 用来存储接收的消息体信息
_recv_head_node 用来存储接收的头部信息
_b_head_parse 表示是否处理完头部信息
#define MAX_LENGTH 1024 * 2
#define HEAD_LENGTH 2
void Session::handle_read(const boost::system::error_code& error, size_t bytes_transferred, std::shared_ptr<Session> shared_self) {
if (!error) {
// 已经移动的字符数
int copy_len = 0;
while (bytes_transferred > 0) {
if (!_b_head_parse) {
// 收到的数据不足头部大小
if (bytes_transferred + _recv_head_node->_cur_len < HEAD_LENGTH) {
memcpy(_recv_head_node->_data + _recv_head_node->_cur_len, _data + copy_len, bytes_transferred);
_recv_head_node->_cur_len += bytes_transferred;
memset(_data, 0, MAX_LENGTH);
_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&Session::handle_read, this, std::placeholders::_1, std::placeholders::_2, shared_self));
return;
}
// 收到的数据比头部多
// 头部剩余未复制的长度
int head_remain = HEAD_LENGTH - _recv_head_node->_cur_len;
memcpy(_recv_head_node->_data + _recv_head_node->_cur_len, _data + copy_len, head_remain);
// 更新已处理的 data 长度和剩余未处理的长度
copy_len += head_remain;
bytes_transferred -= head_remain;
// 获取头部数据
short data_len = 0;
memcpy(&data_len, _recv_head_node->_data, HEAD_LENGTH);
cout << "data_len is " << data_len << endl;
// 头部长度非法
if (data_len > MAX_LENGTH) {
std::cout << "invalid data length is " << data_len << endl;
_server->ClearSession(_uuid);
return;
}
_recv_msg_node = make_shared<MsgNode>(data_len);
// 消息的长度小于头部规定的长度,说明数据未收全,则先将部分消息放到接收节点里
if (bytes_transferred < data_len) {
memcpy(_recv_msg_node->_data + _recv_msg_node->_cur_len, _data + copy_len, bytes_transferred);
_recv_msg_node->_cur_len += bytes_transferred;
::memset(_data, 0, MAX_LENGTH);
_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&Session::handle_read, this, std::placeholders::_1, std::placeholders::_2, shared_self));
// 头部处理完成
_b_head_parse = true;
return;
}
memcpy(_recv_msg_node->_data + _recv_msg_node->_cur_len, _data + copy_len, data_len);
_recv_msg_node->_cur_len += data_len;
copy_len += data_len;
bytes_transferred -= data_len;
_recv_msg_node->_data[_recv_msg_node->_total_len] = '\0';
cout << "receive data is " << _recv_msg_node->_data << endl;
// 此处可以调用 Send 发送测试
Send(_recv_msg_node->_data, _recv_msg_node->_total_len);
// 继续轮询剩余未处理数据
_b_head_parse = false;
_recv_head_node->clear();
if (bytes_transferred <= 0) {
::memset(_data, 0, MAX_LENGTH);
_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&Session::handle_read, this, std::placeholders::_1, std::placeholders::_2, shared_self));
return;
}
continue;
} else {
// 已经处理完头部,处理上次未接受完的消息数据
// 接收的数据仍不足剩余未处理的
int remain_msg = _recv_msg_node->_total_len - _recv_msg_node->_cur_len;
if (bytes_transferred < remain_msg) {
memcpy(_recv_msg_node->_data + _recv_msg_node->_cur_len, _data + copy_len, bytes_transferred);
_recv_msg_node->_cur_len += bytes_transferred;
::memset(_data, 0, MAX_LENGTH);
_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&Session::handle_read, this, std::placeholders::_1, std::placeholders::_2, shared_self));
return;
}
memcpy(_recv_msg_node->_data + _recv_msg_node->_cur_len, _data + copy_len, remain_msg);
_recv_msg_node->_cur_len += remain_msg;
bytes_transferred -= remain_msg;
copy_len += remain_msg;
_recv_msg_node->_data[_recv_msg_node->_total_len] = '\0';
cout << "receive data is " << _recv_msg_node->_data << endl;
// 此处可以调用 Send 发送测试
Send(_recv_msg_node->_data, _recv_msg_node->_total_len);
// 继续轮询剩余未处理数据
_b_head_parse = false;
_recv_head_node->clear();
if (bytes_transferred <= 0) {
::memset(_data, 0, MAX_LENGTH);
_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&Session::handle_read, this, std::placeholders::_1, std::placeholders::_2, shared_self));
return;
}
continue;
}
}
} else {
std::cout << "handle read failed, error is " << error.what() << endl;
close();
_server->ClearSession(_uuid);
}
}
这段代码是 Boost.Asio 异步 TCP 通信中基于'头部 + 消息体'协议的粘包/拆包处理逻辑,是网络编程中处理不定长消息的核心实现。
这段 handle_read 是 Session 类的异步读回调函数,核心目标是:
read_some 可能只收到部分消息、或多个消息粘在一起,代码通过'先读固定长度头部(存消息体长度)→ 再读指定长度消息体'的方式,精准解析完整消息;_b_head_parse=false):先读取并解析固定长度的消息头部(HEAD_LENGTH),从头部中提取消息体的总长度;_b_head_parse=true):根据头部解析出的长度,读取完整的消息体数据;| 变量/结构体 | 含义 |
|---|---|
HEAD_LENGTH | 消息头部固定长度(比如 2 字节),头部仅存储'消息体的长度'(short 类型); |
MAX_LENGTH | 单条消息体的最大长度(防止恶意数据导致内存越界); |
_b_head_parse | 标记位:false=未解析头部/正在解析头部;true=头部已解析、正在解析消息体; |
_recv_head_node | 头部缓存节点(智能指针),存储'正在接收的不完整头部数据'; |
_recv_msg_node | 消息体缓存节点(智能指针),存储'正在接收的不完整消息体数据'; |
MsgNode | 自定义消息节点结构体,包含 _data(数据缓冲区)、_total_len(该节点要接收的总长度)、_cur_len(已接收的长度)、clear()(清空节点的重置方法); |
_data | Session 类的读缓冲区(char[MAX_LENGTH]),存储 async_read_some 读到的原始数据; |
shared_self | shared_from_this() 传递的智能指针,保证 Session 生命周期安全; |
if (!error) { /* 核心解析逻辑... */ }
else {
std::cout << "handle read failed, error is " << error.what() << endl;
close(); // 关闭 socket
_server->ClearSession(_uuid); // 通知服务器清理当前 Session
}
error 非空:说明读取失败(比如客户端断开连接、网络错误),直接关闭连接并通知服务器清理当前 Session;error 为空:开始处理读到的有效数据(bytes_transferred 是本次 read_some 读到的字节数)。int copy_len = 0; // 记录本次已处理的_data 偏移量
while (bytes_transferred > 0) {
// 只要还有未处理的数据,就循环解析
// 分支 1:未解析头部(_b_head_parse=false)
// 分支 2:已解析头部,处理消息体(_b_head_parse=true)
}
copy_len:标记 _data 中'已处理的字节偏移量';while (bytes_transferred > 0):核心设计——一次 read_some 可能读到多个粘包的消息,循环解析直到所有数据处理完毕。这是'先读头部'的核心逻辑,目标是凑齐 HEAD_LENGTH 字节的完整头部。
关键逻辑梳理:
这是'补全消息体'的逻辑,目标是凑齐头部指定长度的完整消息体。
data_len,计算消息体还缺多少字节(remain_msg);while 循环解析,一次回调处理多条消息;shared_self 保证 Session 在异步操作期间不被销毁;data_len > MAX_LENGTH,防止恶意客户端发送超大长度导致内存越界;_recv_head_node->clear() 重置头部节点,避免频繁创建/销毁对象。_recv_msg_node->_data[_recv_msg_node->_total_len] = '\0';
_recv_msg_node->_data 的长度是 _total_len,_data[_total_len] 是缓冲区外的字节,直接赋值会导致内存越界;MsgNode 时,缓冲区长度设为 _total_len + 1,预留字符串结束符位置。_recv_head_node/_recv_msg_node 的 _data 是 char*,若未手动释放会导致内存泄漏;
std::vector<char> 替代裸指针,自动管理内存。代码中未体现 _recv_head_node 的初始化,若未初始化直接使用会崩溃;
Send 是同步写,会阻塞 Asio 事件循环,导致服务器性能下降;async_write 异步发送,并通过消息队列保证写操作串行执行。std::vector 替代裸指针、异步发送消息、补充初始化逻辑,提升代码健壮性。整个逻辑是工业级 TCP 服务器处理不定长消息的标准实现,理解透这段代码,就能掌握 Boost.Asio 异步通信中粘包/拆包的核心解法。
int main() {
try {
// 创建上下文服务
boost::asio::io_context ioc;
// 构造 endpoint
tcp::endpoint remote_ep(address::from_string("127.0.0.1"), 10086);
tcp::socket sock(ioc);
boost::system::error_code error = boost::asio::error::host_not_found;
sock.connect(remote_ep, error);
if (error) {
cout << "connect failed, code is " << error.value() << " error msg is " << error.message();
return 0;
}
thread send_thread([&sock] {
for (;;) {
this_thread::sleep_for(std::chrono::milliseconds(2));
const char* request = "hello world!";
size_t request_length = strlen(request);
char send_data[MAX_LENGTH] = {0};
memcpy(send_data, &request_length, 2);
memcpy(send_data + 2, request, request_length);
boost::asio::write(sock, boost::asio::buffer(send_data, request_length + 2));
}
});
thread recv_thread([&sock] {
for (;;) {
this_thread::sleep_for(std::chrono::milliseconds(2));
cout << "begin to receive..." << endl;
char reply_head[HEAD_LENGTH];
size_t reply_length = boost::asio::read(sock, boost::asio::buffer(reply_head, HEAD_LENGTH));
short msglen = 0;
memcpy(&msglen, reply_head, HEAD_LENGTH);
char msg[MAX_LENGTH] = {0};
size_t msg_length = boost::asio::read(sock, boost::asio::buffer(msg, msglen));
std::cout << "Reply is: ";
std::cout.write(msg, msglen) << endl;
std::cout << "Reply len is " << msglen;
std::cout << "\n";
}
});
send_thread.join();
recv_thread.join();
} catch (std::exception& e) {
std::cerr << "Exception: " << e.what() << endl;
}
return 0;
}
该服务虽然实现了粘包处理,但是服务器仍存在不足,比如当客户端和服务器处于不同平台时收发数据会出现异常,根本原因是未处理大小端模式的问题,这个留给下节处理。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online