Asio异步读写——连接的安全回收问题
📢之前的异步服务器为echo模式,但其存在安全隐患,就是在极端情况下客户端关闭导致触发写和读回调函数,二者都进入错误处理逻辑,进而造成二次析构的问题。通过C++11智能指针构造成一个伪闭包的状态延长session的生命周期可以实现连接的安全回收。
📢之前的异步服务器为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
思路:
- 利用智能指针被复制或使用引用计数加一的原理保证内存不被回收
- 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
🚀 闭环运作原理解析
通过上述架构,程序逻辑避开了极端断开情况下的二次析构死锁:
- 防止多次 Erase 带来的隐患:当客户端异常崩溃时,由于 Asio 队列中同时挂着
async_read与async_write。这两个网络事件都会被内核同时唤醒并报错。 - 第一路唤醒(假设为写):进入
HandleWrite错误分支,执行ClearSession。此时_sessions映射表将该 UUID 移除。由于_sessions中的智能指针销毁,CSession的核心引用计数下降。但因为 HandleRead 的 Asio 闭包和当前 HandleWrite 函数形参中的 _self_shared 都还各自强持有一份引用,对象并不会立刻析构。 - 第二路唤醒(假设为读):进入
HandleRead错误分支,同样执行ClearSession。此时 Map 里已经没有该元素,安全跳过。 - 平稳消亡:两路回调函数相继执行完大括号退出,它们各自持有的临时
_self_shared形参全部有序出栈。当最后一个_self_shared被销毁、引用计数最终归零时,~CSession()析构函数在整个调用栈的最后方被触发且仅触发一次,彻底解决了二次析构导致的闪退。
:::
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)