📢之前的异步服务器为echo模式,但其存在安全隐患,就是在极端情况下客户端关闭导致触发写和读回调函数,二者都进入错误处理逻辑,进而造成二次析构的问题。
通过C++11智能指针构造成一个伪闭包的状态延长session的生命周期可以实现连接的安全回收。

智能指针管理Session

我们可以通过智能指针的方式管理Session类,将acceptor接收的链接保存在Session类型的智能指针里。由于智能指针会在引用计数为0时自动析构,所以为了防止其被自动回收,也方便Server管理Session,我们在Server类中添加成员变量,该变量为一个map类型,key为Session的uid,value为该Session的智能指针。

class CServer
{
public:
    /**
     * @brief 构造函数:初始化服务器
     * @param io_context Asio 的核心上下文对象,负责驱动所有异步 I/O 事件的轮询和分发
     * @param port 服务器监听的 TCP 端口号
     */
    CServer(boost::asio::io_context& io_context, short port);

    /**
     * @brief 清理并移除指定会话
     * @param session_id 要清除的会话的唯一标识符(通常是 UUID 或连接生成的字符串)
     * @note 当客户端断开连接或发生错误时,调用此函数从本地映射表中移除该会话,释放内存
     */
    void ClearSession(std::string);

private:
    /**
     * @brief 异步接受连接的回调函数(事件处理器)
     * @param new_session 刚刚成功创建的会话对象指针(新客户端)
     * @param error 错误码,用于检查接受连接的过程中是否发生了网络或系统错误
     * @note 当有新客户端连入时,Asio 会自动触发并调用这个函数
     */
    void HandleAccept(shared_ptr<CSession>, const boost::system::error_code & error);

    /**
     * @brief 启动异步接受连接的监听状态
     * @note 该函数会向 io_context 注册一个投递“等待连接”的异步操作,是非阻塞的
     */
    void StartAccept();

    // === 成员变量 ===

    /**
     * @brief 引用 Asio 核心上下文对象
     * @note 整个服务器生命周期内,所有异步读写、定时器、接收连接都必须依赖这个上下文
     */
    boost::asio::io_context &_io_context;

    /**
     * @brief 服务器监听的端口号
     */
    short _port;

    /**
     * @brief TCP 连接接收器(Acceptor)
     * @note 专门用来绑定(Bind)IP和端口,并监听(Listen)来自客户端的 TCP 三次握手连接
     */
    tcp::acceptor _acceptor;

    /**
     * @brief 会话管理映射表(Session Map)
     * @note Key: 字符串类型的会话唯一标识符 (Session ID)
     * @note Value: 智能指针包裹的会话对象 (CSession)
     * @note 作用:统一管理所有当前在线的客户端连接,防止会话对象中途被异常销毁
     */
    std::map<std::string, shared_ptr<CSession>> _sessions;
};

通过Server中的_sessions这个map管理链接,可以增加Session智能指针的引用计数,只有当Session从这个map中移除后,Session才会被释放。所以在接收连接的逻辑里将Session放入map。

void CServer::StartAccept() {
    // 1. 创建一个新的会话对象(CSession),使用智能指针 shared_ptr 进行内存托管。
    //    将 io_context 传给它用于后续的读写,将 this(当前服务器指针)传给它方便后续回调(如出错时调用 ClearSession)。
    shared_ptr<CSession> new_session = make_shared<CSession>(_io_context, this);
    
    // 2. 投递一个异步接收连接的请求(非阻塞)。
    //    - 参数 1:传入新会话底层管理的底层 Socket,当有客户端连接成功时,这个 Socket 会被初始化。
    //    - 参数 2:通过 std::bind 绑定回调函数 HandleAccept。
    //      这里把 this(当前服务器对象指针)和刚刚创建的 new_session 一并绑定进去,以便在连接成功时能拿到它们。
    //      placeholders::_1 是占位符,代表届时 Asio 传递过来的错误码(error_code)。
    _acceptor.async_accept(new_session->GetSocket(), std::bind(&CServer::HandleAccept, this, new_session, placeholders::_1));
}

void CServer::HandleAccept(shared_ptr<CSession> new_session, const boost::system::error_code& error){
    // 1. 判断网络事件触发时是否发生了错误(!error 表示没有错误,即连接成功)
    if (!error) {
        // 1.1 连接成功,启动该会话。通常会在 Start() 内部投递第一次异步读(async_read)操作。
        new_session->Start();
        
        // 1.2 将这个新会话存入服务器的映射表(Map)中进行统一管理。
        //     Key 是该会话的唯一标识符(Uuid),Value 是会话的智能指针。
        _sessions.insert(make_pair(new_session->GetUuid(), new_session));
    }
    else {
        // 1.3 连接失败(例如:客户端在三次握手期间强行断开、系统文件描述符耗尽等)
        cout << "session accept failed, error is " << error.what() << endl;
    }

    // 2. 核心步骤:重新调用 StartAccept()。
    //    由于 async_accept 是一次性的,处理完当前这次连接(无论成功还是失败)后,
    //    必须再次投递新的异步接收请求,这样服务器才能继续监听并处理下一个客户端的连接。
    StartAccept();
}

StartAccept函数中虽然new_session是一个局部变量,但是我们通过bind操作,将new_session作为数值传递给bind函数,而bind函数返回的函数对象内部引用了该new_session所以引用计数增加1,这样保证了new_session不会被释放
在HandleAccept函数里调用session的start函数监听对端收发数据,并将session放入map中,保证session不被自动释放。
此外,需要封装一个释放函数,将session从map中移除,当其引用计数为0则自动释放

void CServer::ClearSession(std::string uuid) {
    _sessions.erase(uuid);
}

Session的uuid

关于session的uuid可以通过boost提供的生成唯一id的函数获得,当然也可以自己实现雪花算法。

CSession::CSession(boost::asio::io_context& io_context, CServer* server):
    // 1. 初始化列表:使用传入的 io_context 初始化会话自带的 TCP Socket
    //    这个 Socket 随后会被传给服务器的 acceptor,用来承载具体的客户端连接
    _socket(io_context), 
    
    // 2. 初始化列表:保存指向管理该会话的 CServer 服务器对象的指针
    //    方便会话在发生网络错误或断开连接时,能够回调 server->ClearSession(...) 把自己清理掉
    _server(server) {
    
    // 3. 调用 Boost 库的随机生成器,生成一个 128 位的安全随机 UUID(通用唯一识别码)
    //    第一个括号 boost::uuids::random_generator() 构造了一个生成器对象
    //    第二个括号 () 是调用该对象的重载运算符 operator(),从而真正产生一个 uuid 对象
    boost::uuids::uuid  a_uuid = boost::uuids::random_generator()();
    
    // 4. 将生成的 uuid 对象转换为标准字符串(std::string),并赋值给成员变量 _uuid
    //    这个字符串将作为该客户端连接在服务器 CServer::_sessions 映射表中的唯一 Key 键
    _uuid = boost::uuids::to_string(a_uuid);
}

另外我们修改Session中读写回调函数关于错误的处理,当读写出错的时候清除连接

void CSession::HandleWrite(const boost::system::error_code& error) {
    // 1. 判断异步写操作完成时是否发生错误(!error 表示没有错误,即数据成功发送到了网络缓冲区)
    if (!error) {
        // 1.1 加锁保護:因为 _send_que 队列可能会在外部线程被调用 Send 时修改,必须保证线程安全
        std::lock_guard<std::mutex> lock(_send_lock);
        
        // 1.2 弹出队列首部节点:说明刚刚投递的那个数据包已经圆满发送完毕,将其移出队列
        _send_que.pop();
        
        // 1.3 核心检查:如果发现队列里还有后续积压的数据包(说明在发送期间又有新数据想发)
        if (!_send_que.empty()) {
            // 1.3.1 取出下一个等待发送的数据包引用
            auto &msgnode = _send_que.front();
            
            // 1.3.2 链式投递:再次发起新一轮的异步写操作,将下一个包发送出去。
            //       通过 std::bind 将自身 HandleWrite 再次绑定为回调,形成“发完一个,自动发下一个”的闭环。
            boost::asio::async_write(_socket, boost::asio::buffer(msgnode->_data, msgnode->_max_len),
                std::bind(&CSession::HandleWrite, this, std::placeholders::_1));
        }
    }
    else {
        // 1.4 发送失败(例如客户端突然断开连接、网络超时等)
        std::cout << "handle write failed, error is " << error.what() << endl;
        
        // 1.5 发生致命错误,通知服务器将当前会话从在线映射表中抹去,触发析构释放资源
        _server->ClearSession(_uuid);
    }
}

void CSession::HandleRead(const boost::system::error_code& error, size_t  bytes_transferred){
    // 1. 判断异步读操作触发时是否发生错误(!error 表示成功接收到了客户端发来的网络数据包)
    if (!error) {

        // 1.1 打印出当前接收到的业务数据内容(注意:这里要求 _data 缓冲区是以 \0 结尾的标准字符串)
        cout << "read data is " << _data << endl;
        
        // 1.2 经典的 Echo 回显机制:收到什么就立马通过 Send 函数发送什么回去
        //     参数 1:数据缓冲区的指针; 参数 2:本次网络实际接收到的字节数
        Send(_data, bytes_transferred);
        
        // 1.3 擦除清空缓冲区:为下一次接收新数据做准备,防止旧数据残留污染
        memset(_data, 0, MAX_LENGTH);
        
        // 1.4 核心步骤:重新投递异步读请求(async_read_some 表示只要底层网卡收到一点数据就立马触发)。
        //     由于异步读是一次性的,必须在回调末尾再次调用,从而维持一个持续监听客户端输入的“无尽读循环”。
        _socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2));
    }
    else {
        // 1.5 读取失败(最常见的情况是客户端主动关闭了连接,或者网线断开)
        std::cout << "handle read failed, error is " << error.what() << endl;
        
        // 1.6 发生网络异常,同样通知服务器调用 ClearSession 清理当前连接,防止内存泄漏
        _server->ClearSession(_uuid);
    }
}

隐患

正常情况下上述服务器运行不会出现问题,但是当我们像上次一样模拟,在服务器要发送数据前打个断点,此时关闭客户端,在服务器就会先触发写回调函数的错误处理,再触发读回调函数的错误处理,这样session就会两次从map中移除,因为map中key唯一,所以第二次map判断没有session的key就不做移除操作了。
⚠️**但是这么做还是会有崩溃问题,因为第一次在session写回调函数中移除session,session的引用计数就为0了,调用了session的析构函数,这样在触发session读回调函数时此时session的内存已经被回收了自然会出现崩溃的问题。解决这个问题可以利用智能指针引用计数和bind的特性,实现一个伪闭包的机制延长session的生命周期。**

构造伪闭包

:::info
思路:

  1. 利用智能指针被复制或使用引用计数加一的原理保证内存不被回收
  2. bind操作可以将值绑定在一个函数对象上生成新的函数对象,如果将智能指针作为参数绑定给函数对象,那么智能指针就以值的方式被新函数对象使用,那么智能指针的生命周期将和新生成的函数对象一致,从而达到延长生命的效果。

:::

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

以HandleWrite举例,在bind时传递_self_shared指针增加其引用计数,这样_self_shared的生命周期就和async_write的第二个参数(也就是asio要求的回调函数对象)生命周期一致了。

// 注意:相比于之前,函数参数中多了一个 _self_shared(当前会话对象自身的智能指针)
void CSession::HandleWrite(const boost::system::error_code& error, shared_ptr<CSession> _self_shared) {
    // 1. 判断异步写操作完成时是否发生错误(!error 表示数据发送成功)
    if (!error) {
        // 1.1 加锁保護:保证跨线程操作发送队列 _send_que 时的线程安全性
        std::lock_guard<std::mutex> lock(_send_lock);
        
        // 1.2 弹出队列首部节点:移出已经成功发送完毕的数据包
        _send_que.pop();
        
        // 1.3 核心检查:如果发送队列中还有积压的下一个数据包
        if (!_send_que.empty()) {
            // 1.3.1 获取下一个待发送数据包的引用
            auto &msgnode = _send_que.front();
            
            // 1.3.2 链式投递异步写:继续发送下一个包。
            //       【关键点】:在 std::bind 中,不仅绑定了 this 和错误码占位符,
            //       还将接收到的 _self_shared 再次传给了下一个 HandleWrite 回调。
            //       这样可以把智能指针像接力棒一样不断向下传递,确保生命周期安全。
            boost::asio::async_write(_socket, boost::asio::buffer(msgnode->_data, msgnode->_max_len),
                std::bind(&CSession::HandleWrite, this, std::placeholders::_1, _self_shared));
        }
    }
    else {
        // 1.4 发送失败(例如:网络异常、对端关闭)
        std::cout << "handle write failed, error is " << error.what() << endl;
        
        // 1.5 发生错误,通知服务器从映射表中移除该会话
        _server->ClearSession(_uuid);
    }
    // 1.6 函数执行完毕。如果上面没有再次投递 async_write,那么形参 _self_shared 离开作用域时引用计数减 1。
    //     如果网络已经出错,且外部没有其他地方引用该 CSession,它会在这里彻底安全析构。
}
// 注意:相比于初级版本,函数参数中增加了一个 _self_shared(当前会话对象自身的智能指针)
void CSession::HandleRead(const boost::system::error_code& error, size_t  bytes_transferred, shared_ptr<CSession> _self_shared){
    // 1. 判断异步读操作完成时是否发生错误(!error 表示成功接收到了客户端发来的网络数据)
    if (!error) {

        // 1.1 打印出当前接收到的业务数据内容
        cout << "read data is " << _data << endl;
        
        // 1.2 经典的 Echo 回显机制:调用发送接口,把刚才收到的数据原封不动发回给客户端
        //     参数 1:数据缓冲区的指针; 参数 2:本次网络实际接收到的有效字节数
        Send(_data, bytes_transferred);
        
        // 1.3 擦除清空缓冲区:为下一次读取新数据做准备,防止旧数据残留污染下一次接收的内容
        memset(_data, 0, MAX_LENGTH);
        
        // 1.4 核心步骤:重新投递异步读请求(async_read_some)。
        //     【关键点】:在 std::bind 中,除了绑定 this、错误码和传输字节数占位符外,
        //     还将接收到的 _self_shared 智能指针再次绑定并传递给了下一次的 HandleRead 回调。
        //     只要这个异步读操作还在等待(客户端没有发数据),Asio 的底层队列就会一直持有这个智能指针,
        //     从而强行保证当前会话对象在内存中不会被销毁。
        _socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&CSession::HandleRead, this, 
            std::placeholders::_1, std::placeholders::_2, _self_shared));
    }
    else {
        // 1.5 读取失败(最常见的情况是客户端主动关闭了连接、网络异常断开、或者读取超时)
        std::cout << "handle read failed, error is " << error.what() << endl;
        
        // 1.6 发生网络异常,通知服务器调用 ClearSession 清理当前连接。
        //     当服务器将该会话从 map 中 erase 掉,且当前函数执行完毕退出、_self_shared 局部变量销毁后,
        //     引用计数将彻底归零,整个会话对象就会在此时安全地触发析构函数,完美释放资源。
        _server->ClearSession(_uuid);
    }
}

⚠️****除此之外,我们也要在第一次绑定读写回调函数的时候传入智能指针的值,但是要注意传入的方式,不能用两个智能指针管理同一块内存。

void CSession::Start(){
    // 1. 初始化接收缓冲区:将大小为 MAX_LENGTH 的本地字符数组 _data 彻底清零
    //    确保开始接收数据前,缓冲区是一张干净的“白纸”,防止内存中残留的旧数据造成干扰
    memset(_data, 0, MAX_LENGTH);
    
    // 2. 投递服务器对该客户端的【第一次】异步读数据请求(非阻塞操作)
    //    - 参数 1:把刚才清空的 _data 包装成 Asio 标准的缓冲区对象 buffer,限制最大接收长度为 MAX_LENGTH
    //    - 参数 2:通过 std::bind 绑定数据收完后的回调函数 HandleRead。
    //      - placeholders::_1 和 _2 分别是错误码(error_code)和实际接收到的字节数(bytes_transferred)的占位符。
    //      - 【高能注意】:第三个参数传入了 `shared_ptr<CSession>(this)`,试图通过强行构造一个自身的智能指针,
    //        将其作为“接力棒”丢给 Asio 队列,用来在异步期间强行锁住当前对象的生命周期。
    _socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&CSession::HandleRead, this, 
        std::placeholders::_1, std::placeholders::_2, shared_ptr<CSession>(this)));
}

📌**shared_ptr(this)生成的新智能指针和this之前绑定的智能指针并不共享引用计数,所以要通过shared_from_this()函数返回智能指针,该智能指针和其他管理这块内存的智能指针共享引用计数。**

void CSession::Start(){
    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_from_this()));
}

shared_from_this()函数并不是session的成员函数,要使用这个函数需要继承std::enable_shared_from_this

/**
 * @brief 客户端会话类(CSession)
 * @note 继承 std::enable_shared_from_this 使得类内部可以安全地通过 shared_from_this() 
 *       获取指向自身(this)的 std::shared_ptr,从而在异步网络 I/O 期间安全地延长生命周期。
 */
class CSession:public std::enable_shared_from_this<CSession>
{
public:
    /**
     * @brief 构造函数:初始化一个新的客户端会话
     * @param io_context 异步 I/O 上下文,用来初始化底层的 socket
     * @param server 指向管理该会话的服务器(CServer)的指针,用于网络出错时回调通知
     */
    CSession(boost::asio::io_context& io_context, CServer* server);

    /**
     * @brief 获取当前会话底层的 TCP Socket 引用
     * @return tcp::socket& 供服务器类的 acceptor 接收连接(async_accept)时使用
     */
    tcp::socket& GetSocket();

    /**
     * @brief 获取当前会话的全网唯一标识符(UUID)
     * @return std::string& 映射表中查找、管理、删除该连接的 Key 键
     */
    std::string& GetUuid();

    /**
     * @brief 启动会话
     * @note 激活连接的入口,通常负责清空缓冲区并投递第一次异步读(async_read_some)请求
     */
    void Start();

    /**
     * @brief 发送数据的统一业务接口
     * @param msg 指向要发送的数据缓冲区的指针
     * @param max_length 拟发送的数据长度
     * @note 内部会将数据打包进队列,并安全触发异步写循环
     */
    void Send(char* msg,  int max_length);

private:
    /**
     * @brief 异步读取数据的回调处理器
     * @param error 错误码,用于检测读操作是否正常(如客户端是否断开)
     * @param bytes_transferred 本次实际接收到的数据字节数
     * @param _self_shared 核心安全机制:通过形参强行捕获自身的智能指针,保证回调执行时对象在内存中存活
     */
    void HandleRead(const boost::system::error_code& error, size_t  bytes_transferred, shared_ptr<CSession> _self_shared);

    /**
     * @brief 异步写入数据的回调处理器
     * @param error 错误码,用于检测发送是否成功
     * @param _self_shared 核心安全机制:防止网络发送积压或中途断开时,当前对象由于外部 erase 导致野指针闪退
     */
    void HandleWrite(const boost::system::error_code& error, shared_ptr<CSession> _self_shared);

    // === 成员变量 ===

    /**
     * @brief 当前连接专属的 TCP Socket 实例,负责底层的读写数据流
     */
    tcp::socket _socket;

    /**
     * @brief 当前会话的唯一 ID 字符串(由构造函数中的 Boost.UUID 随机生成)
     */
    std::string _uuid;

    /**
     * @brief 接收数据的本地缓冲区(定长字符数组,大小为 MAX_LENGTH)
     */
    char _data[MAX_LENGTH];

    /**
     * @brief 指向隶属服务器对象的指针
     */
    CServer* _server;

    /**
     * @brief 发送数据包的缓冲队列
     * @note 因为 Asio 禁止并发投递 async_write,所有待发数据必须依次进入该队列,由队首单个包驱动写循环
     */
    std::queue<shared_ptr<MsgNode> > _send_que;

    /**
     * @brief 互斥锁,专门用于保护发送队列 _send_que 的线程安全
     * @note 防止业务多线程并发调用 Send 投递数据时,导致队列内部结构损坏
     */
    std::mutex _send_lock;
};

同样的道理,我们在发送的时候也要绑定智能指针作为参数, 这里不做赘述。
再次测试,链接可以安全释放,并不存在二次释放的问题。可以在析构函数内打印析构的信息,发现只析构一次

实现示例

#pragma once
#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>

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

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

const int MAX_LENGTH = 1024;

/**
 * @brief 内存数据包节点 (应用层发送缓冲区缓存)
 */
struct MsgNode {
    char* _data;
    int _max_len;

    MsgNode(const char* msg, int max_length) : _max_len(max_length) {
        _data = new char[_max_len];
        memcpy(_data, msg, _max_len);
    }

    ~MsgNode() {
        delete[] _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;
};

/**
 * @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) {
    boost::uuids::uuid a_uuid = boost::uuids::random_generator()();
    _uuid = boost::uuids::to_string(a_uuid);
}

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->_max_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> _self_shared) {
    if (!error) {
        std::cout << "Read data from [" << _uuid << "]: " << _data << std::endl;
        
        // Echo 回显机制
        Send(_data, bytes_transferred);
        
        std::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, _self_shared));
    }
    else {
        std::cout << "Handle read failed, error: " << error.message() << std::endl;
        // 出错时通知服务器移除映射关系。由于形参 _self_shared 锁住了生命周期,此处的 erase 绝对不会引发当前上下文崩溃!
        _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->_max_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);
}

:::info
🚀 闭环运作原理解析

通过上述架构,程序逻辑避开了极端断开情况下的二次析构死锁:

  1. 防止多次 Erase 带来的隐患:当客户端异常崩溃时,由于 Asio 队列中同时挂着 async_readasync_write。这两个网络事件都会被内核同时唤醒并报错。
  2. 第一路唤醒(假设为写):进入 HandleWrite 错误分支,执行 ClearSession。此时 _sessions 映射表将该 UUID 移除。由于 _sessions 中的智能指针销毁,CSession 的核心引用计数下降。但因为 HandleRead 的 Asio 闭包和当前 HandleWrite 函数形参中的 _self_shared 都还各自强持有一份引用,对象并不会立刻析构。
  3. 第二路唤醒(假设为读):进入 HandleRead 错误分支,同样执行 ClearSession。此时 Map 里已经没有该元素,安全跳过。
  4. 平稳消亡:两路回调函数相继执行完大括号退出,它们各自持有的临时 _self_shared 形参全部有序出栈。当最后一个 _self_shared 被销毁、引用计数最终归零时,~CSession() 析构函数在整个调用栈的最后方被触发且仅触发一次,彻底解决了二次析构导致的闪退。

:::

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐