跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

C++ Asio 网络编程处理 TCP 粘包问题

TCP 粘包问题的成因及解决方案,重点讲解了基于 Boost.Asio 的 C++ 网络编程中如何处理粘包与拆包。文章详细阐述了使用“消息头 + 消息体”协议(先读固定长度头部获取消息体长度,再读指定长度内容)的实现逻辑。内容包括数据节点设计、Session 类状态管理、异步接收逻辑的循环处理机制,以及针对内存越界、裸指针泄漏、同步阻塞等潜在问题的优化建议。最后提供了客户端示例代码,并指出了跨平台大小端模式的遗留问题。

未来可期发布于 2026/3/28更新于 2026/7/1949 浏览
C++ Asio 网络编程处理 TCP 粘包问题

什么是粘包问题?

在使用 TCP 协议发送数据时,经常会遇到:明明分两次发了两条消息,结果对方一次就全收到了,两条消息粘在一起,分不清边界。这就叫粘包。

举个生活例子: 你分两句话说:

  1. '我是 A'
  2. '我是 B'

TCP 为了效率,会把它们打包一起发: 对方收到:'我是 A 我是 B' ——不知道哪里是第一句、哪里是第二句。

为什么会粘包?(核心原因)

TCP 是面向字节流的协议
  • 它不认识你的'消息',只认一串连续的字节
  • 为了网络效率,底层会合并、缓存、分批发送

所以:发送方分几次发 ≠ 接收方分几次收

怎么解决?(通用 3 种方案)

所有网络编程(C++、Java、Go、Python)都用这几种:

  1. 固定长度消息:每条消息必须一样长,不够补空格。→ 不灵活,很少用。
  2. 分隔符协议:每条消息末尾加特殊符号,比如 \n、| 等。→ 简单,但内容里不能出现分隔符。
  3. 消息头 + 消息体(最常用、最标准)
    • 先发 4 字节:表示消息长度
    • 再发真正内容
    • 接收端先读长度,再读对应字节
    • → 工业级方案,Linux 网络编程、Qt 网络、RPC 都用这个。

一句话总结

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() {
        [] _data;
    }

    {
        (_data, , _total_len);
        _cur_len = ;
    }

:
     _cur_len;
     _total_len;
    * _data;
};
delete
void clear()
memset
0
0
private
int
int
char

完善 CSession 类

// 收到的消息结构
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 粘包/拆包处理解析

这段代码是 Boost.Asio 异步 TCP 通信中基于'头部 + 消息体'协议的粘包/拆包处理逻辑,是网络编程中处理不定长消息的核心实现。

一、整体功能总结

这段 handle_read 是 Session 类的异步读回调函数,核心目标是:

  1. 解决 TCP 粘包/拆包问题:TCP 是流式协议,一次 read_some 可能只收到部分消息、或多个消息粘在一起,代码通过'先读固定长度头部(存消息体长度)→ 再读指定长度消息体'的方式,精准解析完整消息;
  2. 分阶段处理:
    • 阶段 1(_b_head_parse=false):先读取并解析固定长度的消息头部(HEAD_LENGTH),从头部中提取消息体的总长度;
    • 阶段 2(_b_head_parse=true):根据头部解析出的长度,读取完整的消息体数据;
  3. 循环处理:一次回调中若收到多个粘包的消息,会循环解析直到所有数据处理完毕;若数据未收全(拆包),则缓存已收数据,继续异步读取剩余部分。
二、核心前置概念
变量/结构体含义
HEAD_LENGTH消息头部固定长度(比如 2 字节),头部仅存储'消息体的长度'(short 类型);
MAX_LENGTH单条消息体的最大长度(防止恶意数据导致内存越界);
_b_head_parse标记位:false=未解析头部/正在解析头部;true=头部已解析、正在解析消息体;
_recv_head_node头部缓存节点(智能指针),存储'正在接收的不完整头部数据';
_recv_msg_node消息体缓存节点(智能指针),存储'正在接收的不完整消息体数据';
MsgNode自定义消息节点结构体,包含 _data(数据缓冲区)、_total_len(该节点要接收的总长度)、_cur_len(已接收的长度)、clear()(清空节点的重置方法);
_dataSession 类的读缓冲区(char[MAX_LENGTH]),存储 async_read_some 读到的原始数据;
shared_selfshared_from_this() 传递的智能指针,保证 Session 生命周期安全;
三、代码逐模块解析
1. 入口错误处理
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 读到的字节数)。
2. 核心循环:处理本次读到的所有数据
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 可能读到多个粘包的消息,循环解析直到所有数据处理完毕。
3. 分支 1:未解析头部(_b_head_parse=false)

这是'先读头部'的核心逻辑,目标是凑齐 HEAD_LENGTH 字节的完整头部。

  • 子分支 1.1:已收头部 + 本次数据 < 头部总长度 → 数据未收全,缓存后继续读
  • 子分支 1.2:已收头部 + 本次数据 ≥ 头部总长度 → 能凑齐完整头部
  • 子分支 1.3:本次剩余数据 < 消息体总长度 → 消息体未收全,缓存后继续读
  • 子分支 1.4:本次剩余数据 ≥ 消息体总长度 → 能凑齐完整消息体
  • 子分支 1.5:本次数据已处理完 → 继续异步读新数据

关键逻辑梳理:

  • 先凑齐固定长度的头部 → 解析出消息体长度 → 再凑齐指定长度的消息体;
  • 若数据未收全(拆包),则缓存已收数据,继续异步读取;
  • 若数据足够(粘包),则解析完当前消息后,循环处理剩余数据。
4. 分支 2:已解析头部,处理消息体(_b_head_parse=true)

这是'补全消息体'的逻辑,目标是凑齐头部指定长度的完整消息体。

  • 基于头部解析出的 data_len,计算消息体还缺多少字节(remain_msg);
  • 若数据未收全,缓存后继续读;若收全,则处理消息体,并重置状态准备解析下一条(应对粘包)。
四、关键设计亮点
  1. 解决粘包/拆包的核心思路:拆包通过缓存未收全的头部/消息体继续异步读取;粘包通过 while 循环解析,一次回调处理多条消息;
  2. 生命周期安全:通过 shared_self 保证 Session 在异步操作期间不被销毁;
  3. 安全校验:检查 data_len > MAX_LENGTH,防止恶意客户端发送超大长度导致内存越界;
  4. 内存复用:通过 _recv_head_node->clear() 重置头部节点,避免频繁创建/销毁对象。
五、潜在问题与优化建议
1. 内存越界风险
_recv_msg_node->_data[_recv_msg_node->_total_len] = '\0';
  • 问题:_recv_msg_node->_data 的长度是 _total_len,_data[_total_len] 是缓冲区外的字节,直接赋值会导致内存越界;
  • 优化:创建 MsgNode 时,缓冲区长度设为 _total_len + 1,预留字符串结束符位置。
2. 裸指针内存泄漏

_recv_head_node/_recv_msg_node 的 _data 是 char*,若未手动释放会导致内存泄漏;

  • 优化:改用 std::vector<char> 替代裸指针,自动管理内存。
3. 缺少初始化逻辑

代码中未体现 _recv_head_node 的初始化,若未初始化直接使用会崩溃;

  • 优化:在 Session 构造函数中初始化。
4. 同步 Send 可能阻塞事件循环
  • 问题:若 Send 是同步写,会阻塞 Asio 事件循环,导致服务器性能下降;
  • 优化:改用 async_write 异步发送,并通过消息队列保证写操作串行执行。
六、总结
  1. 这段代码的核心是基于'固定头部 + 消息体'的 TCP 粘包/拆包处理,先读固定长度头部获取消息体长度,再读指定长度消息体,解决流式协议的消息边界问题;
  2. 核心逻辑:分阶段(解析头部→解析消息体)+ 缓存未收全数据 + 循环处理粘包数据;
  3. 关键优化点:修复内存越界、用 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;
}

总结

该服务虽然实现了粘包处理,但是服务器仍存在不足,比如当客户端和服务器处于不同平台时收发数据会出现异常,根本原因是未处理大小端模式的问题,这个留给下节处理。

目录

  1. 什么是粘包问题?
  2. 为什么会粘包?(核心原因)
  3. TCP 是面向字节流的协议
  4. 怎么解决?(通用 3 种方案)
  5. 一句话总结
  6. 完善数据节点
  7. 完善 CSession 类
  8. 新增变量
  9. 完善接收逻辑
  10. Boost Asio TCP 粘包/拆包处理解析
  11. 一、整体功能总结
  12. 二、核心前置概念
  13. 三、代码逐模块解析
  14. 1. 入口错误处理
  15. 2. 核心循环:处理本次读到的所有数据
  16. 3. 分支 1:未解析头部(bhead_parse=false)
  17. 4. 分支 2:已解析头部,处理消息体(bhead_parse=true)
  18. 四、关键设计亮点
  19. 五、潜在问题与优化建议
  20. 1. 内存越界风险
  21. 2. 裸指针内存泄漏
  22. 3. 缺少初始化逻辑
  23. 4. 同步 Send 可能阻塞事件循环
  24. 六、总结
  25. 客户端示例
  26. 总结
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 渗透测试基本流程与实战指南
  • Python 爬虫实战:常见验证码自动识别方案
  • 攻防世界 Web 题解:反序列化、RCE 及文件包含漏洞
  • Apache IoTDB 实战:FILL 空值填充与 LIMIT/SLIMIT 分页查询详解
  • 国内 AIGC 工具赋能 2D 游戏美术全流程实战
  • 基于 DeepFace 与 OpenCV 的实时人脸情绪分析
  • 2025 年 12 月 GESP CCF 编程能力等级认证 Python 一级真题
  • FPGA 商用级 ISP:动态坏点校正(DPCC)的滑窗架构与并行判决实现
  • 华为 OD 机试:采样过滤算法题解
  • SkyWalking 接入 Spring Cloud Alibaba 微服务:从链路追踪到告警
  • YOLO26n-pose 在 LSP 数据集的姿势估计训练与推理流程(Python/C++)
  • PyCharm 中 Copilot 插件 Claude 模型不可用问题修复
  • AI 数据标注平台的选型与实践:效率提升背后的技术逻辑
  • 数据结构:队列的各种实现与算法推荐
  • 数据结构——图:遍历、最小生成树与最短路径
  • 基于 Spring Boot 的学生成绩综合统计分析系统设计与实现
  • Kali Linux 虚拟机安装教程
  • DigitalOcean 云主机注册与创建指南
  • 基于 MATLAB 的动态环境下多无人机系统协同路径规划与防撞
  • Windows 本地部署 OpenClaw 并接入飞书机器人

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online