Asio异步通信——处理粘包问题


文章目录

粘包

粘包问题

粘包问题是服务器收发数据常遇到的一个现象,当客户端发送多个数据包给服务器时,服务器底层的tcp接收缓冲区收到的数据为粘连在一起的

粘包原因

:::color4

因为TCP底层通信是面向字节流的,TCP只保证发送数据的准确性和顺序性,字节流以字节为单位,客户端每次发送N个字节给服务端,N取决于当前客户端的发送缓冲区是否有数据,比如发送缓冲区总大小为10个字节,当前有5个字节数据(上次要发送的数据比如'loveu')未发送完,那么此时只有5个字节空闲空间,我们调用发送接口发送hello world!其实就是只能发送Hello给服务器,那么服务器一次性读取到的数据就很可能是loveuhello。而剩余的world!只能留给下一次发送,下一次服务器接收到的就是world!

:::
其他原因

  1. 客户端的发送频率远高于服务器的接收频率,就会导致数据在服务器的tcp接收缓冲区滞留形成粘连,比如客户端1s内连续发送了两个hello world!,服务器过了2s才接收数据,那一次性读出两个hello world!。
  2. tcp底层的安全和效率机制不允许字节数特别少的小包发送频率过高,tcp会在底层累计数据长度到一定大小才一起发送,比如连续发送1字节的数据要累计到多个字节才发送,可以了解下tcp底层的Nagle算法。
  3. 再就是我们提到的最简单的情况,发送端缓冲区有上次未发送完的数据或者接收端的缓冲区里有未取出的数据导致数据粘连。

处理粘包

处理粘包的方式主要采用应用层定义收发包格式的方式,这个过程俗称切包处理 ,常用的协议被称为tlv协议(消息id+消息长度+消息内容),

复制代码
我们先简化发送的格式,格式变为消息长度+消息内容的方式,之后再完善为tlv格式。  

完善服务器逻辑

完善消息节点

:::color1
完善之前设计过的消息节点的数据结构MsgNode

  1. 两个参数的构造函数做了完善,之前的构造函数通过消息首地址和长度构造节点数据,现在需要在构造节点的同时把长度信息也写入节点,该构造函数主要用来发送数据时构造发送信息的节点。
  2. 一个参数的构造函数为较上次新增的,主要根据消息的长度构造消息节点,该构造函数主要是接收对端数据时构造接收节点调用的。
  3. 新增一个Clear函数清除消息节点的数据,主要是避免多次构造节点造成开销。

:::

cpp 复制代码
class MsgNode
{
    friend class CSession;
public:
    MsgNode(char * msg, short 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:
    short _cur_len;
    short _total_len;
    char* _data;
};

完善 CSession 类

cpp 复制代码
//消息最大长度
#define MAX_LENGTH  1024*2
//表示数据包头部的大小
#define HEAD_LENGTH 2

//收到的消息结构
//存储接受的消息体信息
std::shared_ptr<MsgNode> _recv_msg_node;
//是否处理完头部信息
bool _b_head_parse;
//收到的头部结构
//存储接收的头部信息
std::shared_ptr<MsgNode> _recv_head_node;

完善接收逻辑

cpp 复制代码
void CSession::HandleRead(const boost::system::error_code& error, size_t bytes_transferred, std::shared_ptr<CSession> shared_self){
    // 【检查错误码】如果没有发生网络错误,则开始处理接收到的数据
    if (!error) {
        // 已经移动(处理)的字符数,作为 _data 缓冲区的读取偏移量指针
        int copy_len = 0;
        
        // 【核心循环】只要本次 async_read_some 接收到的数据还没被消耗完,就持续循环处理(应对粘包)
        while (bytes_transferred > 0) {
            
            // 【分支A】当前状态:正在解析头部(_b_head_parse 为 false)
            if (!_b_head_parse) {
                
                // 情况 1:收到的新数据 + 之前残留的头部数据,总和仍然【不足】一个完整的头部大小
                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);
                    // 继续投递异步读任务,等待更多数据以凑齐头部,注意通过 shared_self 保持 Session 生命周期
                    _socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), 
                        std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self));
                    // 头部不完整,无法继续处理,直接返回退出当前回调
                    return;
                }
                
                // 情况 2:收到的新数据足够凑齐一个完整的头部(可能刚好够,也可能比头部多)
                // 计算凑齐该头部还需要从 _data 中复制多少字节
                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;
                    // 从服务器中移除并销毁当前 Session,断开连接
                    _server->ClearSession(_uuid);
                    return;
                }
                
                // 头部成功解析,根据拿到的 data_len 创建接收消息体的节点
                _recv_msg_node = make_shared<MsgNode>(data_len);

                // 情况 2.1:凑齐头部后,剩下的数据【小于】头部规定的消息体长度(说明数据发生拆包,未收全)
                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(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self));
                    // 【状态流转】标记头部已解析完成,下次进入回调时将直接走【分支B】处理剩余消息体
                    _b_head_parse = true;
                    return;
                }

                // 情况 2.2:凑齐头部后,剩下的数据【大于或等于】头部规定的消息体长度(说明完整收到了一个或多个包)
                // 复制一个完整消息体大小的数据到接收节点中
                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;
                
                // 在消息末尾添加字符串结束符 '\0',防止打印或处理时内存越界
                _recv_msg_node->_data[_recv_msg_node->_total_len] = '\0';
                cout << "receive data is " << _recv_msg_node->_data << endl;
                
                // 【业务处理】此处可以将收到的完整消息原样回发给客户端进行测试(Echo服务)
                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(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self));
                    return;
                }
                // 如果 bytes_transferred > 0,说明缓冲区里还有数据(粘包),继续 while 循环解析下一个包的头部
                continue;
            }

            // 【分支B】当前状态:已经处理完头部,正在处理上次未接收完的消息体数据
            // 计算当前这个消息体还差多少字节才完整
            int remain_msg = _recv_msg_node->_total_len - _recv_msg_node->_cur_len;
            
            // 情况 1:本次接收到的全部新数据,仍【不足】以填满上次未完结的消息体
            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;
                // 清空缓冲区并继续异步读,状态保持 _b_head_parse = true 不变
                ::memset(_data, 0, MAX_LENGTH);
                _socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), 
                    std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self));
                return;
            }
            
            // 情况 2:本次接收的数据【足够】填满上次未完结的消息体(可能刚好填满,也可能多出下一个包的数据)
            // 复制所需的剩余字节数,使当前消息体完整
            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(_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(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self));
                return;
            }
            // 还有多余数据(发生粘包),继续 while 循环去解析下一个包的头部
            continue;
        }
    }
    // 【错误处理】如果 async_read_some 返回了网络错误(例如客户端断开连接 EOF 等)
    else {
        std::cout << "handle read failed, error is " << error.what() << endl;
        // 关闭套接字
        Close();
        // 从服务器 Session 管理器中清除当前 Session 节点的智能指针,触发析构释放资源
        _server->ClearSession(_uuid);
    }
}

处理粘包过程

copy_len 记录的是当前缓冲区中已经处理过的数据长度。由于 TCP 传输存在一次性接收到多个数据包(粘包)的情况,因此需要依靠 copy_len 作为偏移量,精准标记并在循环中依次处理各个数据包。

当程序收到数据后,核心的处理流程及状态流转如下:

  • 首先判断头部是否处理 :检查 _b_head_parse 是否为 false。如果为 false,说明当前正在处理包头阶段,此时进入以下判断:
    • 收到的数据不足头部大小 :如果当前接收到的数据(加上之前残留的头部数据)小于 HEAD_LENGTH,则说明头部尚未接收完整。程序将本次接收到的数据全部追加放入 _recv_head_node 节点中保存,随后继续调用异步读取函数监听对端发送的数据,并直接返回。
    • 收到的数据等于或大于头部大小 :如果收到的数据比头部多,说明可能存在多个逻辑包粘连,需要做切包处理。程序会根据之前保留在 _recv_head_node 中的长度,计算出剩余未取出的头部长度,并将这部分数据通过 memcpy 拷贝并补齐到 _recv_head_node 中。随后通过网络字节序转换或直接拷贝的方式,将节点中的数据写入 short 类型的 data_len 变量里,从而精准获取后续消息体的实际长度。
  • 接下来处理包体(消息体) :获取到 data_len 后,程序开始创建消息节点并比对长度:
    • 消息体未接收完(拆包) :判断当前缓冲区中未处理数据的长度是否小于该消息体总共需要接收的长度。如果小于,说明消息体数据尚未接收完全,程序会把当前未处理的所有数据先写入 _recv_msg_node 中,更新已接收长度,并继续投递异步读事件监听后续数据。此时,将 _b_head_parse 置为 true(标记头部已解析完毕),然后返回。
    • 消息体接收完全 :如果未处理数据的长度大于或等于消息体的总长度,说明当前包体已经完整接收。程序将属于该消息体的数据全部拷贝到 _recv_msg_node 中,在末尾添加结束符后,即可调用发送函数(如 Send)将数据原样返回给对端进行测试。
  • 多包粘连的处理与循环 :在完整接收并处理完一个逻辑包后,程序会将 _b_head_parse 重新置为 false 并清空头部节点,以准备解析下一个包。此时必须判断 bytes_transferred(剩余未处理字节数)是否小于等于 0:
    • 如果小于等于 0,说明本次缓冲区内只有一个逻辑包且已处理完毕,程序直接清空缓冲区,继续调用异步读函数监听新数据并返回。
    • 如果大于 0,说明缓冲区内还残留有下一个数据包的内容(发生粘包),程序不会退出,而是通过 continue 继续执行上述循环操作,去解析下一个包的头部。
  • 头部已处理、包体未完结的状态(分支处理) :如果进入函数时 _b_head_parse 为 true,说明在进入本次回调之前,包头已经接收并解析完成,但是包体触发了拆包未接收完。此时程序会直接跳过头部解析的分支,去继续接收和拼接上次未完结的消息体数据。其内部对于"数据依旧不够"或"数据足够填满包体并处理粘包"的切包判定逻辑,与上述包体处理完全一致。

完整服务器

cpp 复制代码
#include <iostream>
#include <memory>
#include <string>
#include <map>
#include <queue>
#include <mutex>
#include <cstring>
#include <boost/asio.hpp>
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>

//消息最大长度
#define MAX_LENGTH  1024*2
//表示数据包头部的大小
#define HEAD_LENGTH 2

using boost::asio::ip::tcp;
using namespace std;

// 声明前置,方便互相引用
class CServer;
class CSession;

/**
 * @brief 内存数据包节点 (应用层发送缓冲区缓存)
 */
class MsgNode
{
    friend class CSession;
public:
    MsgNode(const char * msg, short 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:
    short _cur_len;
    short _total_len;
    char* _data;
};

/**
 * @brief 客户端会话类(CSession)
 */
class CSession : public std::enable_shared_from_this<CSession>
{
public:
    CSession(boost::asio::io_context& io_context, CServer* server);
    ~CSession();
    
    tcp::socket& GetSocket();
    std::string& GetUuid();
    void Start();
    void Send(const char* msg, int max_length);

private:
    void HandleRead(const boost::system::error_code& error, size_t bytes_transferred, std::shared_ptr<CSession> _self_shared);
    void HandleWrite(const boost::system::error_code& error, std::shared_ptr<CSession> _self_shared);

    tcp::socket _socket;
    std::string _uuid;
    char _data[MAX_LENGTH];
    CServer* _server;
    std::queue<std::shared_ptr<MsgNode>> _send_que;
    std::mutex _send_lock;

    //收到的消息结构
    //存储接受的消息体信息
    std::shared_ptr<MsgNode> _recv_msg_node;
    //是否处理完头部信息
    bool _b_head_parse;
    //收到的头部结构
    //存储接收的头部信息
    std::shared_ptr<MsgNode> _recv_head_node;
};

/**
 * @brief 服务器主控类(CServer)
 */
class CServer
{
public:
    CServer(boost::asio::io_context& io_context, short port);
    void ClearSession(std::string uuid);

private:
    void StartAccept();
    void HandleAccept(std::shared_ptr<CSession> new_session, const boost::system::error_code& error);

    boost::asio::io_context& _io_context;
    short _port;
    tcp::acceptor _acceptor;
    std::map<std::string, std::shared_ptr<CSession>> _sessions;
};


// === CSession 构造与析构 ===
CSession::CSession(boost::asio::io_context& io_context, CServer* server)
    : _socket(io_context), 
      _server(server), 
      _b_head_parse(false) // 1. 明确初始化头部解析状态为 false
{
    boost::uuids::uuid a_uuid = boost::uuids::random_generator()();
    _uuid = boost::uuids::to_string(a_uuid);

    // 2. 必须为头部节点分配内存,大小为 HEAD_LENGTH (2字节)
    _recv_head_node = std::make_shared<MsgNode>(HEAD_LENGTH); 
    
    // 3. 消息体节点先置空,等头部解析出长度后再行分配
    _recv_msg_node = nullptr; 
}

CSession::~CSession() {
    std::cout << ">>> [Safe Release] CSession deconstructed! UUID: " << _uuid << std::endl;
}

tcp::socket& CSession::GetSocket() { return _socket; }
std::string& CSession::GetUuid() { return _uuid; }

// === 通信启动入口 ===
void CSession::Start() {
    std::memset(_data, 0, MAX_LENGTH);
    // 使用 shared_from_this() 安全地获取与外层共享的智能指针,通过 Bind 投递初次监听
    _socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH),
        std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_from_this()));
}

// === 发送统一接口 ===
void CSession::Send(const char* msg, int max_length) {
    bool pending_write = false;
    {
        std::lock_guard<std::mutex> lock(_send_lock);
        // 如果队列不为空,说明当前有异步写操作正在内核中执行
        if (!_send_que.empty()) {
            pending_write = true;
        }
        // 将需要发送的数据打包进本地队列
        _send_que.push(std::make_shared<MsgNode>(msg, max_length));
    }

    // 如果当前没有异步写操作在挂起,必须由当前线程主动"点火"触发第一次异步写
    if (!pending_write) {
        auto& msgnode = _send_que.front();
        // 关键点:发送时同样绑定 shared_from_this(),防止发送中途 Session 被提前擦除销毁
        boost::asio::async_write(_socket, boost::asio::buffer(msgnode->_data, msgnode->_total_len),
            std::bind(&CSession::HandleWrite, this, std::placeholders::_1, shared_from_this()));
    }
}

// === 异步读回调 ===
void CSession::HandleRead(const boost::system::error_code& error, size_t bytes_transferred, std::shared_ptr<CSession> shared_self){
    // 【检查错误码】如果没有发生网络错误,则开始处理接收到的数据
    if (!error) {
        // 已经移动(处理)的字符数,作为 _data 缓冲区的读取偏移量指针
        int copy_len = 0;
        
        // 【核心循环】只要本次 async_read_some 接收到的数据还没被消耗完,就持续循环处理(应对粘包)
        while (bytes_transferred > 0) {
            
            // 【分支A】当前状态:正在解析头部(_b_head_parse 为 false)
            if (!_b_head_parse) {
                
                // 情况 1:收到的新数据 + 之前残留的头部数据,总和仍然【不足】一个完整的头部大小
                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);
                    // 继续投递异步读任务,等待更多数据以凑齐头部,注意通过 shared_self 保持 Session 生命周期
                    _socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), 
                        std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self));
                    // 头部不完整,无法继续处理,直接返回退出当前回调
                    return;
                }
                
                // 情况 2:收到的新数据足够凑齐一个完整的头部(可能刚好够,也可能比头部多)
                // 计算凑齐该头部还需要从 _data 中复制多少字节
                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;
                    // 从服务器中移除并销毁当前 Session,断开连接
                    _server->ClearSession(_uuid);
                    return;
                }
                
                // 头部成功解析,根据拿到的 data_len 创建接收消息体的节点
                _recv_msg_node = make_shared<MsgNode>(data_len);

                // 情况 2.1:凑齐头部后,剩下的数据【小于】头部规定的消息体长度(说明数据发生拆包,未收全)
                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(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self));
                    // 【状态流转】标记头部已解析完成,下次进入回调时将直接走【分支B】处理剩余消息体
                    _b_head_parse = true;
                    return;
                }

                // 情况 2.2:凑齐头部后,剩下的数据【大于或等于】头部规定的消息体长度(说明完整收到了一个或多个包)
                // 复制一个完整消息体大小的数据到接收节点中
                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;
                
                // 在消息末尾添加字符串结束符 '\0',防止打印或处理时内存越界
                _recv_msg_node->_data[_recv_msg_node->_total_len] = '\0';
                cout << "receive data is " << _recv_msg_node->_data << endl;
                
                // 【业务处理】此处可以将收到的完整消息原样回发给客户端进行测试(Echo服务)
                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(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self));
                    return;
                }
                // 如果 bytes_transferred > 0,说明缓冲区里还有数据(粘包),继续 while 循环解析下一个包的头部
                continue;
            }

            // 【分支B】当前状态:已经处理完头部,正在处理上次未接收完的消息体数据
            // 计算当前这个消息体还差多少字节才完整
            int remain_msg = _recv_msg_node->_total_len - _recv_msg_node->_cur_len;
            
            // 情况 1:本次接收到的全部新数据,仍【不足】以填满上次未完结的消息体
            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;
                // 清空缓冲区并继续异步读,状态保持 _b_head_parse = true 不变
                ::memset(_data, 0, MAX_LENGTH);
                _socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), 
                    std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self));
                return;
            }
            
            // 情况 2:本次接收的数据【足够】填满上次未完结的消息体(可能刚好填满,也可能多出下一个包的数据)
            // 复制所需的剩余字节数,使当前消息体完整
            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(_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(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self));
                return;
            }
            // 还有多余数据(发生粘包),继续 while 循环去解析下一个包的头部
            continue;
        }
    }
    // 【错误处理】如果 async_read_some 返回了网络错误(例如客户端断开连接 EOF 等)
    else {
        std::cout << "handle read failed, error is " << error.what() << endl;
        // 关闭套接字
        _socket.close();
        // 从服务器 Session 管理器中清除当前 Session 节点的智能指针,触发析构释放资源
        _server->ClearSession(_uuid);
    }
}

// === 异步写回调 ===
void CSession::HandleWrite(const boost::system::error_code& error, std::shared_ptr<CSession> _self_shared) {
    if (!error) {
        std::lock_guard<std::mutex> lock(_send_lock);
        _send_que.pop(); // 弹出已经发送成功的包
        
        // 检查队列内是否还有业务层积压的包
        if (!_send_que.empty()) {
            auto& msgnode = _send_que.front();
            boost::asio::async_write(_socket, boost::asio::buffer(msgnode->_data, msgnode->_total_len),
                std::bind(&CSession::HandleWrite, this, std::placeholders::_1, _self_shared));
        }
    }
    else {
        std::cout << "Handle write failed, error: " << error.message() << std::endl;
        _server->ClearSession(_uuid);
    }
}

CServer::CServer(boost::asio::io_context& io_context, short port)
    : _io_context(io_context), _port(port), _acceptor(io_context, tcp::endpoint(tcp::v4(), port)) {
    std::cout << "CServer initialized on port: " << _port << " , start listening..." << std::endl;
    StartAccept();
}

void CServer::StartAccept() {
    // 采用 make_shared 规避原始 raw 指针构建
    std::shared_ptr<CSession> new_session = std::make_shared<CSession>(_io_context, this);
    
    _acceptor.async_accept(new_session->GetSocket(),
        std::bind(&CServer::HandleAccept, this, new_session, std::placeholders::_1));
}

void CServer::HandleAccept(std::shared_ptr<CSession> new_session, const boost::system::error_code& error) {
    if (!error) {
        std::cout << "New client connected! UUID: " << new_session->GetUuid() << std::endl;
        new_session->Start();
        // 将强指针移入映射表,此时生命周期引用计数加 1
        _sessions.insert(std::make_pair(new_session->GetUuid(), new_session));
    }
    else {
        std::cout << "Session accept failed, error: " << error.message() << std::endl;
    }

    // 重新开启下一次接受循环
    StartAccept();
}

void CServer::ClearSession(std::string uuid) {
    // 即使读、写错误同时回调进入此函数,map.erase() 本身也是幂等的
    // 第二次调用会因为找不到对应的 key 而安全地什么都不做
    _sessions.erase(uuid);
}

int main()
{
    try {
        // 创建 Asio 核心上下文服务对象
        boost::asio::io_context io_context;

        // 指定服务器监听的端口号,此处使用 10086 与客户端对应
        short port = 10086;

        // 实例化服务器主控类,内部会启动底层 Socket 的监听(listen)和异步接收(async_accept)
        CServer server(io_context, port);

        // 【启动事件循环】阻塞当前主线程,交由 io_context 接管并开始轮询调度所有的异步网络事件
        // 当有客户端连接、数据可读或写满内核缓冲区时,会自动触发相应的 Handle 回调
        io_context.run();
    }
    // 捕获可能抛出的系统或者 Asio 网络层异常
    catch (std::exception& e) {
        std::cerr << "Server exception encountered: " << e.what() << std::endl;
    }

    return 0;
}

完善客户端逻辑

客户端的发送也要遵循先发送数据2个字节的数据长度,再发送数据消息的结构。

接收时也是先接收两个字节数据获取数据长度,再根据长度接收消息。

cpp 复制代码
#include <iostream>
#include <memory>
#include <string>
#include <cstring>
#include <boost/asio.hpp>


using boost::asio::ip::tcp;
using namespace std;

//消息最大长度
#define MAX_LENGTH  1024*2
//表示数据包头部的大小
#define HEAD_LENGTH 2

int main()
{
    try {
        boost::asio::io_context ioc;
        tcp::endpoint remote_ep(boost::asio::ip::make_address("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;
        }

        // ====== 修改为循环,让客户端不退出 ======
        while (true) {
            std::cout << "Enter message: ";
            char request[MAX_LENGTH];
            std::cin.getline(request, MAX_LENGTH);
            
            // 如果输入 exit 则主动退出循环,关闭连接
            if (strcmp(request, "exit") == 0) {
                break;
            }

            size_t request_length = strlen(request);
            if (request_length == 0) continue;

            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));
            
            // 接收回显头部
            char reply_head[HEAD_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 };
            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 << "\n\n";
        }
    }
    catch (std::exception& e) {
        std::cerr << "Exception: " << e.what() << endl;
    }
    return 0;
}

粘包测试

为了测试粘包,需要制造粘包产生的现象,可以让客户端发送的频率高一些,服务器接收的频率低一些,这样造成前后端收发数据不一致导致多个数据包在服务器tcp缓冲区滞留产生粘包现象。

cpp 复制代码
void CSession::PrintRecvData(char* data, int length) {
    // 【创建字符串流】用于方便地进行格式化输入输出转换
    stringstream ss;
    
    // 初始化结果字符串,以 "0x" 开头,表示这是一个十六进制格式的字符串
    string result = "0x";
    
    // 【核心转换循环】遍历字节数组中的每一个字节
    for (int i = 0; i < length; i++) {
        // 定义一个临时字符串,用于接收当前字节转换后的十六进制文本
        string hexstr;
        
        // 【格式化写入流】
        // hex: 设置为十六进制输出格式
        // std::setw(2): 限制每个字节占用 2 个字符位 width
        // std::setfill('0'): 如果十六进制不足 2 位(例如 0x0A 变成 A),则前面补 '0'
        // int(data[i]): 将 char 强转为 int,否则流会把它当作普通字符而不是数值处理
        // endl: 换行符,作为流数据分隔符(也为后续 >> 读取做准备)
        ss << hex << std::setw(2) << std::setfill('0') << int(data[i]) << endl;
        
        // 【从流中读取】将刚刚格式化好的十六进制字符串提取到 hexstr 中
        ss >> hexstr;
        
        // 将当前字节的十六进制字符串追加到最终的 result 结果后面
        result += hexstr;
    }
    
    // 【控制台打印】输出最终拼接完成的十六进制裸数据
    std::cout << "receive raw data is : " << result << endl;;
}

然后将这个函数放到HandleRead里,每次收到数据就调用这个函数打印接收到的最原始的数据,然后睡眠2秒再进行收发操作,用来延迟接收对端数据制造粘包,之后的逻辑不变

cpp 复制代码
void CSession::HandleRead(const boost::system::error_code& error, size_t  bytes_transferred, std::shared_ptr<CSession> shared_self){
    if (!error) {
        PrintRecvData(_data, bytes_transferred);
        std::chrono::milliseconds dura(2000);
        std::this_thread::sleep_for(dura);
    }
}
cpp 复制代码
#include <iostream>
#include <memory>
#include <string>
#include <cstring>
#include <boost/asio.hpp>


using boost::asio::ip::tcp;
using namespace std;

//消息最大长度
#define MAX_LENGTH  1024*2
//表示数据包头部的大小
#define HEAD_LENGTH 2

int main()
{
    try {
        // 【创建上下文服务】核心的 I/O 上下文对象,所有 Asio 相关的 I/O 操作都需要基于它进行
        boost::asio::io_context   ioc;
        
        // 【构造服务器端点】指定目标服务器的 IP 地址和端口号(此处为本地回环地址 127.0.0.1,端口 10086)
        tcp::endpoint  remote_ep(address::from_string("127.0.0.1"), 10086);
        
        // 【创建套接字】利用 io_context 构造一个基于 TCP 协议的 socket 对象
        tcp::socket  sock(ioc);
        
        // 初始化错误码,先赋予一个默认的错误值(主机未找到)
        boost::system::error_code   error = boost::asio::error::host_not_found; ;
        
        // 【发起同步连接】调用 connect 函数向远程服务器发起连接请求,结果会写入 error 中
        sock.connect(remote_ep, error);
        
        // 【连接结果检查】如果 error 为真,说明连接建立失败
        if (error) {
            cout << "connect failed, code is " << error.value() << " error msg is " << error.message();
            // 打印错误信息后直接退出程序
            return 0;
        }

        // ==================== 【发送子线程】 ====================
        // 创建一个独立的线程专门负责循环发送数据,通过 lambda 表达式按引用 [&sock] 捕获 socket
        thread send_thread([&sock] {
            // 死循环结构,保证发送操作可以无限循环进行
            for (;;) {
                // 当前线程休眠 2 毫秒,防止由于死循环无间隔发送导致 CPU 占满以及服务器缓冲区被冲垮
                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 };
                
                // 【组包步骤1】将 2 字节的消息体长度拷贝到发送缓冲区的前 2 个字节(作为固定包头)
                memcpy(send_data, &request_length, 2);
                
                // 【组包步骤2】紧接着包头后面(偏移 2 字节的位置),拷贝实际的消息体内容
                memcpy(send_data + 2, request, request_length);
                
                // 【同步阻塞写】将"包头+包体"(总长度为 request_length + 2)一次性发送给服务器
                // 在多线程环境下,Asio 保证了对同一个 socket 同时进行一个读线程和一个写线程是安全的
                boost::asio::write(sock, boost::asio::buffer(send_data, request_length + 2));
            }
            });

        // ==================== 【接收子线程】 ====================
        // 创建另一个独立的线程专门负责循环接收服务器回显,通过 lambda 表达式按引用 [&sock] 捕获 socket
        thread recv_thread([&sock] {
            // 死循环结构,保证接收操作可以无限循环进行
            for (;;) {
                // 当前线程休眠 2 毫秒,调和读写节奏
                this_thread::sleep_for(std::chrono::milliseconds(2));
                
                cout << "begin to receive..." << endl;
                
                // 定义用于存储接收到的回显包头的缓冲区
                char reply_head[HEAD_LENGTH];
                
                // 【接收包头】同步阻塞读取,必须读满 HEAD_LENGTH(2字节)个字节才会返回
                size_t reply_length = boost::asio::read(sock, boost::asio::buffer(reply_head, HEAD_LENGTH));
                
                // 定义变量用于存储从包头解析出来的消息体实际长度
                short msglen = 0;
                // 【解析包头】将接收到的 2 字节包头数据拷贝到短整型变量 msglen 中
                memcpy(&msglen, reply_head, HEAD_LENGTH);
                
                // 定义并初始化用于存储接收到的回显消息体的缓冲区
                char msg[MAX_LENGTH] = { 0 };
                
                // 【接收包体】根据解析出的 msglen 长度,同步阻塞读取,必须读满 msglen 个字节才会返回,以此解决拆包问题
                size_t  msg_length = boost::asio::read(sock, boost::asio::buffer(msg, msglen));

                // 【打印回显结果】将完整接收的消息体内容精准输出到控制台
                std::cout << "Reply is: ";
                // 使用 write 打印,避免因缺少 '\0' 导致乱码
                std::cout.write(msg, msglen) << endl;
                // 打印当前接收到的数据长度
                std::cout << "Reply len is " << msglen;
                std::cout << "\n";
            }
            });

        // 【等待线程结束】主线程在此处阻塞,分别等待发送线程和接收线程执行完毕
        // 由于两个线程内部都是 for(;;) 死循环,因此主线程将永远阻塞在 send_thread.join() 这一行
        send_thread.join();
        recv_thread.join();
    }
    // 【异常捕获】如果在多线程运行或连接中途发生网络链路断开(如服务器关闭),write/read 会抛出异常在此捕获
    catch (std::exception& e) {
        std::cerr << "Exception: " << e.what() << endl;
    }
    return 0;
}

再次启动服务器和客户端,看到粘包现象了,我们的服务器也能稳定切割数据包并返回正确的消息给客户端。


简易方式

之前我们通过async_read_some函数监听读事件,并且绑定了读事件的回调函数HandleRead

cpp 复制代码
_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&CSession::HandleRead, this, 
std::placeholders::_1, std::placeholders::_2, SharedSelf()));

async_read_some 这个函数的特点是只要对端发数据,服务器接收到数据,即使没有收全对端发送的数据也会触发HandleRead函数,所以我们会在HandleRead回调函数里判断接收的字节数,接收的数据可能不满足头部长度,可能大于头部长度但小于消息体的长度,可能大于消息体的长度,还可能大于多个消息体的长度,所以要切包等,这些逻辑写起来很复杂,所以我们可以通过读取指定字节数,直到读完这些字节才触发回调函数,那么**可以采用async_read函数,这个函数指定读取指定字节数,只有完全读完才会触发回调函数**。

获取头部数据

cpp 复制代码
void CSession::Start(){
    _recv_head_node->Clear();
    //读取指定的头部长度,大小为HEAD_LENGTH字节数,只有读完HEAD_LENGTH字节才触发HandleReadHead函数
    boost::asio::async_read(_socket, boost::asio::buffer(_recv_head_node->_data, HEAD_LENGTH), std::bind(&CSession::HandleReadHead, this, 
        std::placeholders::_1, std::placeholders::_2, SharedSelf()));
}
cpp 复制代码
void CSession::HandleReadHead(const boost::system::error_code& error, size_t  bytes_transferred, std::shared_ptr<CSession> shared_self) {
    // 【检查错误码】如果没有发生网络错误,则说明成功读取到了头部数据,开始处理
    if (!error) {
        // 【边界检查】如果实际接收到的字节数小于法定的头部长度(HEAD_LENGTH),判定为异常协议包
        if (bytes_transferred < HEAD_LENGTH) {
            cout << "read head lenth error";
            // 关闭当前 Socket 连接
            Close();
            // 从服务器 Session 管理器中擦除当前节点,触发析构
            _server->ClearSession(_uuid);
            // 终止后续逻辑,直接返回
            return;
        }

        // 头部接收完,解析头部
        // 定义一个短整型变量,用来存储解析出来的实际消息体(包体)长度
        short data_len = 0;
        // 【解析包头】从接收头部数据的缓冲区 _recv_head_node->_data 中拷贝 2 字节到 data_len 中
        memcpy(&data_len, _recv_head_node->_data, HEAD_LENGTH);
        cout << "data_len is " << data_len << endl;
        // 此处省略字节序转换(如网络字节序 ntohs 转换为主机字节序)
        // ...
        
        // 头部长度非法
        // 【安全检查】如果解析出的消息体长度超过了服务器允许的最大单包长度(MAX_LENGTH),判定为恶意包或协议错误
        if (data_len > MAX_LENGTH) {
            std::cout << "invalid data length is " << data_len << endl;
            // 直接从服务器中清除该会话,断开连接,防止内存被撑爆
            _server->ClearSession(_uuid);
            return;
        }

        // 【分配包体空间】根据刚刚解析出来的合法 data_len,实例化(创建)专门用于接收包体的内存节点
        _recv_msg_node = make_shared<MsgNode>(data_len);
        
        // 【精准读取包体】投递一个异步全量读取任务(async_read)
        // async_read 保证必须读满缓冲区的目标长度(即 _recv_msg_node->_total_len 字节)才会触发回调
        // 从而在底层彻底帮我们解决了包体被拆包、分批到达的零散问题
        boost::asio::async_read(_socket, boost::asio::buffer(_recv_msg_node->_data, _recv_msg_node->_total_len), 
            std::bind(&CSession::HandleReadMsg, this,
            std::placeholders::_1, std::placeholders::_2, SharedSelf()));
    }
    // 【错误处理】如果底层在读取包头时就发生网络错误(如客户端强退、断网、超时等)
    else {
        std::cout << "handle read failed, error is " << error.what() << endl;
        // 关闭套接字
        Close();
        // 释放资源,销毁会话
        _server->ClearSession(_uuid);
    }
}

获取消息体

HandleReadMsg函数内解析消息体,解析完成后打印收到的消息,接下来继续监听读事件,监听读取指定头部大小字节,触发HandleReadHead函数, 然后再在HandleReadHead内继续监听读事件,获取消息体长度数据后触发HandleReadMsg函数,从而达到循环监听的目的。

cpp 复制代码
void CSession::HandleReadMsg(const boost::system::error_code& error, size_t  bytes_transferred,
    std::shared_ptr<CSession> shared_self) {
    // 【检查错误码】如果没有发生网络错误,说明 async_read 已经精准读满了包体所需的全部字节
    if (!error) {
        // 【调用抓包打印】将本次接收到的二进制裸数据以十六进制格式输出到控制台
        // 注意:这里传入的是 _data,若之前 HandleReadHead 读进的是 _recv_msg_node->_data,请确保此处数据源匹配
        PrintRecvData(_data, bytes_transferred);
        
        // 【定义延迟时间】创建一个 2000 毫秒(2秒)的时间段对象 dura
        std::chrono::milliseconds dura(2000);
        // 【强制线程休眠】让当前执行回调的线程挂起(阻塞)2 秒钟
        // 警告:在实际生产环境的 Asio 线程中 sleep 会卡死整个事件循环,此处通常仅用于本地模拟或测试延迟
        std::this_thread::sleep_for(dura);
        
        // 【防越界处理】在包体数据的末尾(总长度位置)强行写入字符串结束符 '\0'
        // 这样可以确保后续将数据当作 C 风格字符串或用 cout 打印时,内存不会发生越界读取
        _recv_msg_node->_data[_recv_msg_node->_total_len] = '\0';
        
        // 【控制台打印】输出最终完整组装好的消息体文本内容
        cout << "receive data is " << _recv_msg_node->_data << endl;
        
        // 【业务回发(Echo)】调用发送接口,将收到的完整消息原样异步发送回给客户端
        Send(_recv_msg_node->_data, _recv_msg_node->_total_len);
        
        // 再次接收头部数据
        // 【状态重置】清空头部节点缓冲区中的残留数据,为读取下一个数据包的包头做准备
        _recv_head_node->Clear();
        
        // 【投递下一轮读取】重新发起一个异步全量读取任务(async_read)去读取后续新包的 2 字节包头
        // 当读满 2 字节包头后,程序会自动回调进入 HandleReadHead,从而形成周而复始的闭环网络流
        boost::asio::async_read(_socket, boost::asio::buffer(_recv_head_node->_data, HEAD_LENGTH),
            std::bind(&CSession::HandleReadHead, this, std::placeholders::_1, std::placeholders::_2,
                SharedSelf()));
    }
    // 【错误处理】如果在读取包体中途发生网络链路异常(如客户端中途闪退、断网等)
    else {
        cout << "handle read msg failed,  error is " << error.what() << endl;
        // 关闭当前会话的套接字
        Close();
        // 从服务器 Session 映射表中移除当前 Session,彻底释放该连接的所有资源
        _server->ClearSession(_uuid);
    }
}
相关推荐
博客18001 天前
酷宝的使用方法,超好用的免费界面库,C++、MFC可用
c++·mfc·界面库·库来帮·酷宝
郝学胜_神的一滴1 天前
CMake 026:属性体系精讲、四大作用域全解 & 实战代码落地
c++·cmake
众少成多积小致巨2 天前
JNI (Java Native Interface) 技术手册中文参考指南
android·java·c++
clint4566 天前
C++进阶(1)——前景提要
c++
夜悊6 天前
C++代码示例:进制数简单生成工具
c++
郝学胜_神的一滴6 天前
CMake 021: IF 条件判据详诠
c++·cmake
_wyt0017 天前
洛谷 B3930 [GESP202312 五级] 烹饪问题 题解
c++·gesp
玖玥拾7 天前
C/C++ 数据结构(七)栈、容器适配器
c语言·数据结构·c++··容器适配器
网络研究院7 天前
2026年网络安全
网络·安全·法律·法规·趋势·发展
酣大智7 天前
ARP代理--工作原理
运维·网络·arp·arp代理