C++ asio网络编程(6)利用C11模拟伪闭包实现连接的安全回收
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、智能指针管理Session
- 二、用智能指针来实现Server的函数
- 1.start_accept()
- 1.引用计数注意点
- 2.std::bind 与异步回调函数的执行顺序分析
- 2.handle_accept
- 1.异步调用中函数执行顺序与智能指针插入顺序的问题
- 3.ClearSession
- 三、Session的uuid
- 1.Boost UUID(唯一标识符)简介与使用
- 四、Start
- shared_from_this() 使用详解
- 一、shared_from_this() 是什么?
- 二、为什么需要它?
- 三、怎么使用?
- 四、使用注意事项
- 五、使用场景:Boost.Asio 的 Session 生命周期
- 总结
- 五、读和写的回调
- 总结
前言
提示:这里可以添加本文要记录的大概内容:
之前的异步服务器为echo模式,但其存在安全隐患,就是在极端情况下客户端关闭导致触发写和读回调函数,二者都进入错误处理逻辑,进而造成二次析构的问题。
下面我们介绍通过C11智能指针构造成一个伪闭包的状态延长session的生命周期
提示:以下是本篇文章正文内容,下面案例可供参考
一、智能指针管理Session
我们可以通过智能指针的方式管理Session类,将acceptor接收的链接保存在Session类型的智能指针里。由于智能指针会在引用计数为0时自动析构,所以为了防止其被自动回收,也方便Server管理Session
我们在Server类中添加成员变量,该变量为一个map类型,key为Session的uid,value为该Session的智能指针。
一个智能指针管理一个session
uuid是用来标识每一个session的,方便后期查找和删除这个会话
🧠 再通俗点说:
想象你是开聊天室服务器的老板,来了很多用户(Session),你得:
给每个用户一个“编号”或“身份证号” → 这就是 UUID。
把用户都存在一个表里(map)。
要踢人(断开连接)的时候,只要知道身份证号就能精准找出来删掉
class Server
{
public:Server(boost::asio::io_context& ioc, short port);void ClearSession(std::string uuid);
private:void start_accept();//监听连接void handle_accept(std::shared_ptr<Session> , const boost::system::error_code& ec);//当有连接的时候触发这个回调函数boost::asio::io_context& _ioc;//因为上下文不允许被复制 所以用引用tcp::acceptor _acceptor;std::map<std::string, std::shared_ptr<Session>>_sessions;
};
当后面在回调函数的时候,出现错误我们就将这个session从map中移除
此时管理这个session的智能指针的引用计数就会-1
不过此时不一定引用计数就会为0,说不定其他地方比如回调函数中还持有这个智能指针,因为我们在进行回调函数bind绑定的时候,通过值来传递这个智能指针,这个智能指针的引用计数也会+1
二、用智能指针来实现Server的函数
Server::Server(boost::asio::io_context& ioc, short port) :_ioc(ioc), _acceptor(ioc, tcp::endpoint(tcp::v4(), port))
{start_accept();
}void Server::start_accept()
{shared_ptr<Session>new_session=make_shared<Session>(_ioc,this);_acceptor.async_accept(new_session->Socket(),std::bind(&Server::handle_accept,this,new_session,placeholders::_1));}void Server::handle_accept(shared_ptr<Session> new_session,
const boost::system::error_code& ec)
{if (!ec)//成功{new_session->Start();_sessions.insert(make_pair(new_session->GetUuid(), new_session));}else{//delete new_session;}start_accept();//再次准备
}void Server::ClearSession(std::string uuid)
{_sessions.erase(uuid);
}
1.start_accept()
1.引用计数注意点
我们在**start_accept()**中不再用new来创建一个session,而是用一个智能指针来管理
此时重点! 我们创建了一个智能指针,此时的引用计数为1
然后我们bind中也按值传递了这个指针,此时引用计数+1
然后 start_accept() 结束 引用计数-1
然后进入 handle_accept()
2.std::bind 与异步回调函数的执行顺序分析
✅ 问题描述:
在 C++ 中,当我们使用 std::bind 将某个函数(如 B)绑定为另一个函数(如 A)的一部分时,程序的实际执行流程是怎样的?
具体来说:
如果 A 函数中使用 std::bind 绑定了 B 函数作为回调,执行流程到底是:
- 进入 A → 进入 B → 退出 B → 退出 A
- 进入 A → 退出 A → 等待某个事件 → 再执行 B → 退出 B
🧠 梳理核心概念:
这取决于 你是否立即调用了绑定后的函数对象,以及是否在 异步环境中使用它
🔍 两种情况详细分析:
🚀 情况 1:同步调用(立即执行绑定函数)
void A() {auto boundB = std::bind(B, 123);boundB(); // 立即执行
}执行流程:
进入 A↓
进入 B(因为 boundB() 被立即调用)↓
退出 B↓
退出 A
🔄 情况 2:异步绑定(比如注册回调)
void A() {socket.async_read_some(buffer, std::bind(B, placeholders::_1, placeholders::_2));
}
这里:
● std::bind(B, …) 是将 B 打包成一个回调;
● async_read_some 是注册事件,不会立刻执行;
● 所以 A 很快返回,B 要等事件触发才执行
进入 A
↓
注册回调(并不会执行 B)
↓
退出 A
↓
[某个时间点事件触发]
↓
进入 B
↓
退出 B
所以这里的 handle_read 是回调函数,它会在数据到达时异步调用,而不是在start_accept()t 中立即调用
2.handle_accept
进入 handle_accept() 如果成功就进入这个session的读写通信就行,然后将这个session插入map中
1.异步调用中函数执行顺序与智能指针插入顺序的问题
❓问题描述
在 Server::handle_accept 函数中:
if (!ec) {new_session->Start();_sessions.insert(make_pair(new_session->GetUuid(), new_session));
}
Start() 函数内部调用了 async_read_some,并使用 shared_from_this() 将当前 session 指针绑定到回调函数中。
问题在于:
● new_session->Start() 里面启动了异步通信(如 async_read_some),
● _sessions.insert(…) 是在 Start() 完成后执行的。
那么问题是:
是否必须等 Start() 里面的异步通信完成并回调走完,insert() 才会执行?
还是说,只要 Start() 同步执行完,insert() 就会马上执行?
✅答案总结
insert(…) 会在 Start() 函数同步执行完毕后立即执行,不会等待异步读写操作或其回调完成。
原因:
● async_read_some(…) 本质上只是在内部注册了一个回调函数,不阻塞当前线程,不执行回调。
● 一旦当前的 Start() 函数体执行完,就会马上回到 handle_accept 继续执行 insert(…)。
● 而回调(handle_read)只有等有客户端数据传来,触发了 IO 事件,才会异步被执行
3.ClearSession
StartAccept函数中虽然new_session是一个局部变量,但是我们通过bind操作,将new_session作为数值传递给bind函数,而bind函数返回的函数对象内部引用了该new_session所以引用计数加1,这样保证了new_session不会被释放。
在HandleAccept函数里调用session的start函数监听对端收发数据,并将session放入map中,保证session不被自动释放。
此外,需要封装一个释放函数,将session从map中移除,当其引用计数为0则自动释放
void Server::ClearSession(std::string uuid)
{_sessions.erase(uuid);
}
三、Session的uuid
1.Boost UUID(唯一标识符)简介与使用
💡 什么是 UUID?
UUID(Universally Unique Identifier)是一个标准格式的 128 位数字,它的目标是在分布式系统中生成唯一 ID,可以避免因为数据库自增或时间戳导致的冲突
格式示例:
550e8400-e29b-41d4-a716-446655440000
UUID 通常用于:
● 唯一标识某个会话(Session)
● 唯一标识某个用户、资源
● 防止 ID 冲突
● 分布式系统或网络系统中的唯一对象标识
🧰 Boost UUID 的使用
Boost 提供了 boost::uuids 命名空间中的 UUID 功能。使用非常简单,以下是步骤和示例代码:
🔌 引入头文件
#include <boost/uuid/uuid.hpp> // uuid 类型
#include <boost/uuid/uuid_generators.hpp> // 生成器
#include <boost/uuid/uuid_io.hpp> // 支持 uuid 转字符串
🛠️ 生成一个随机 UUID
boost::uuids::random_generator generator;//一个函数对象
boost::uuids::uuid id = generator();
std::string uuid_str = boost::uuids::to_string(id);std::cout << "UUID: " << uuid_str << std::endl;
✅ random_generator() 使用系统随机数生成 UUID,符合 UUID v4(随机生成版本)
✅ 常见用法:类中作为 Session 唯一标识符
class Session {
public:Session() {boost::uuids::uuid uid = boost::uuids::random_generator()();_uuid = boost::uuids::to_string(uid);}std::string GetUuid() const { return _uuid; }private:std::string _uuid;
};
⚠️ 注意事项
● uuid 类型本身不是字符串,它是一个 16 字节数组。需要用 boost::uuids::to_string() 转成字符串。
● 生成器对象可以复用,但你也可以每次临时构造
📝 小结
● Boost.UUID 是生成唯一 ID 的利器,适合在网络通信、分布式系统、资源标识中使用。
● 在你当前写的 Session 类中用它生成 UUID,再放入 map<string, shared_ptr> 管理,非常合适。
● 常用接口是 random_generator() + to_string()
四、Start
我们进入Start函数中,我们用准备bind读函数
void Session::Start()
{memset(_data, 0, sizeof(_data));_socket.async_read_some(boost::asio::buffer(_data, max_length),std::bind(&Session::handle_read, this, placeholders::_1, placeholders::_2,shared_from_this()));}
我们知道我们要传入智能指针来管理
这里我们用shared_from_this()
因为这是这个Session本身的智能指针,就是在前面创造出来的,这里相当于自己的副本,这样会使得引用计数同步,就不会导致前面的已经析构了,但这里还在
shared_from_this() 使用详解
一、shared_from_this() 是什么?
shared_from_this() 是 std::enable_shared_from_this 提供的成员函数,用于在类的成员函数中获取当前对象的 shared_ptr 智能指针副本。
它的作用是:
✅ 在类对象已经被 shared_ptr 管理时,从内部安全地获取它自己的智能指针副本
二、为什么需要它?
如果你在类成员函数中通过 this 传递当前对象的裸指针给外部(如异步回调),可能在异步调用发生前对象就已被销毁,导致程序崩溃(悬空指针)
boost::asio::async_read(_socket, buffer,std::bind(&Session::handle_read, this, ...)); // ⚠️ 非安全
使用 shared_from_this() 可以延长对象生命周期,保证在异步回调中对象仍然有效
三、怎么使用?
1.继承 std::enable_shared_from_this
class Session : public std::enable_shared_from_this<Session>
{...
};
2.在类成员函数中使用 shared_from_this()
void Session::Start() {auto self = shared_from_this();boost::asio::async_read(_socket, buffer,std::bind(&Session::handle_read, self, ...)); // ✅ 绑定智能指针,防止提前析构
}
四、使用注意事项
五、使用场景:Boost.Asio 的 Session 生命周期
当前 Boost.Asio 的服务端代码中,Session 表示一个客户端连接,异步读取时这样使用:
_socket.async_read_some(boost::asio::buffer(_data, max_length),std::bind(&Session::handle_read, this, ..., shared_from_this()));
这里的 shared_from_this() 是为了把当前对象打包成 shared_ptr,传递给回调,确保异步过程中不会被提前销毁
总结
● shared_from_this() 适合需要延长对象生命周期的场景,尤其是异步/回调中。
● 使用前务必确保对象由 shared_ptr 创建。
● 适用于资源敏感类、异步类、会话类等典型需求
五、读和写的回调
在读和写的回调中我们分别传入管理这个session的智能指针就行,然后如果出错,就从map中移除就行,后续bind结束后就会逐渐的使得引用计数-1 当为0的时候就自动析了
//读的回调函数
void Session::handle_read(const boost::system::error_code& ec, size_t bytes_transferred, shared_ptr<Session>_self_shared)
{if (ec)//0正确 1错误{cout << "read error" << endl;//delete this;_server->ClearSession(_uuid);}else{cout << "server receivr data is "<<_data << endl;//将收到的数据发送回去boost::asio::async_write(_socket, boost::asio::buffer(_data, bytes_transferred),std::bind(&Session::handle_write,this, placeholders::_1, placeholders::_2, _self_shared));}
}//写的回调函数
void Session::handle_write(const boost::system::error_code& ec, size_t
bytes_transferred,shared_ptr<Session>_self_shared)
{ if (ec)//0正确 1错误{cout << "write error" << endl;_server->ClearSession(_uuid);}else{//发完了 就清除掉原先的memset(_data, 0, sizeof(_data));//继续读_socket.async_read_some(boost::asio::buffer(_data, bytes_transferred),std::bind(&Session::handle_read, this, placeholders::_1, placeholders::_2, _self_shared));}
}
总结
我们通过C11的bind和智能指针实现了类似于go,js等语言的闭包功能,保证在回调函数触发之前Session都是存活的