网络编程(5)——模拟伪闭包实现连接的安全回收

六、day6

今天学习如何利用C11模拟伪闭包实现连接的安全回收 ,之前的异步服务器为echo模式,但存在安全隐患,在极端情况下客户端关闭可能会导致触发写和读回调函数,二者都进入错误处理逻辑,进而造成二次析构。今天学习如何通过C11智能指针构造伪闭包状态 (将智能指针传给函数对象,如果函数对象不消失,该指针也不会消失),延长session的生命周期,不会发生二次析构的风险

1)智能指针管理Server

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

复制代码
class Server
{
private:
	void start_accept();  // 启动一个acceptor
	// 当acceptor接收到连接后启动该函数
	void handle_accept(std::shared_ptr<Session> new_session, const boost::system::error_code& error);
	boost::asio::io_context& _ioc;
	tcp::acceptor _acceptor;
	std::map<std::string, std::shared_ptr<Session>> _sessions;
public:
	Server(boost::asio::io_context& ioc, short port);
	void ClearSession(std::string uuid);
};

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

但这里产生了一个问题,为什么new_session在**'}'**结束后仍不结束,而是bind后计数加一?这个问题在文章后面回答。

复制代码
void Server::start_accept() {
	// make_shared分配并构造一个 std::shared_ptr,_ioc, this是传给Session的参数
	std::shared_ptr<Session> new_session = std::make_shared<Session>(_ioc, this);
	// 开始一个异步接受操作,当new_session的socket与客户端连接成功时,调用https://zhida.zhihu.com/search?content_id=248268386&content_type=Article&match_order=2&q=%E5%9B%9E%E8%B0%83%E5%87%BD%E6%95%B0&zhida_source=entityhandle_accept
	// 为什么new_session在右括号结束后仍不结束,而是bind后计数加一?
	_acceptor.async_accept(new_session->Socket(), std::bind(&Server::handle_accept, this, new_session,
		std::placeholders::_1));
}

// 当handle_accept触发时,也就是start_accept的回调函数被触发,当该回调函数结束后从队列中移除后,new_session的引用计数减一
void Server::handle_accept(std::shared_ptr<Session> new_session, const boost::system::error_code& error) {
	// 如果没有错误(error 为 false),调用 new_session->Start() 来启动与旧客户端的会话
	if (!error) {
		new_session->Start();
		_sessions.insert(std::make_pair(new_session->GetUuid(), new_session));

	}
	else 
	// 无论当前连接是否成功,都重新调用 start_accept(),以便服务器能够继续接受下一个新客户端的连接请求。
	// 服务器始终保持在监听状态,随时准备接受新连接
	start_accept();
}

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

此外,需要封装一个释放函数,将session从map中移除,当其引用计数为0则自动释放

cpp 复制代码
void Server::ClearSession(std::string uuid) {
	_sessions.erase(uuid);
}

2)Session的uuid

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

cpp 复制代码
void Session::HandleWrite(const boost::system::error_code& error) {
    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));
        }
    }
    else {
        std::cout << "handle write failed, error is " << error.what() << endl;
        _server->ClearSession(_uuid);
    }
}
void Session::HandleRead(const boost::system::error_code& error, size_t  bytes_transferred){
    if (!error) {
        cout << "read data is " << _data << endl;
        //发送数据
        Send(_data, 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));
    }
    else {
        std::cout << "handle read failed, error is " << error.what() << endl;
        _server->ClearSession(_uuid);
    }
}

但以上操作仍有造成二次析构的隐患:

正常情况下上述服务器运行不会出现问题,但是当我们像day5一样模拟,在服务器要发送数据前打个断点,此时关闭客户端,在服务器就会先触发写回调函数的错误处理,再触发读回调函数的错误处理,这样session就会两次从map中移除,因为map中key唯一,所以第二次map判断没有session的key就不做移除操作了。

但是这么做还是会有崩溃问题,因为第一次在session写回调函数中移除session,session的引用计数就为0了,调用了session的析构函数,这样在触发session读回调函数时此时session的内存已经被回收了自然会出现崩溃的问题。解决这个问题可以利用智能指针引用计数和bind的特性,实现一个伪闭包的机制延长session的生命周期。

3)构造伪闭包

思路:

  1. 利用智能指针被复制或使用引用计数加一的原理保证内存不被回收
  2. bind操作可以将值绑定在一个函数对象上生成新的函数对象,如果将智能指针作为参数绑定给函数对象,那么智能指针就以值的方式被新函数对象使用,那么智能指针的生命周期将和新生成的函数对象一致,从而达到延长生命的效果。
cpp 复制代码
	void headle_read(const boost::system::error_code& error, size_t bytes_transferred,
		std::shared_ptr<Session> _self_shared);
	void haddle_write(const boost::system::error_code& error, std::shared_ptr<Session> _self_shared);

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

cpp 复制代码
void Session::haddle_write(const boost::system::error_code& error, std::shared_ptr<Session> _self_shared) {
	if (!error) {
		memset(_data, 0, max_length);
		_socket.async_read_some(boost::asio::buffer(_data, max_length),
			std::bind(&Session::headle_read, this, std::placeholders::_1, std::placeholders::_2, _self_shared));
	}
	else {
		cout << "write error" << error.value() << endl;
		_server->ClearSession(_uuid);
	}
}

同理,headle_read内部也实现了类似的绑定

cpp 复制代码
void Session::headle_read(const boost::system::error_code& error, size_t bytes_transferred,
	std::shared_ptr<Session> _self_shared) {
	if (!error) {
		cout << "server receive data is " << _data << endl;
		boost::asio::async_write(_socket, boost::asio::buffer(_data, bytes_transferred),
			std::bind(&Session::haddle_write, this, std::placeholders::_1, _self_shared));
	}
	else {
		cout << "read error" << endl;
		_server->ClearSession(_uuid);
	}
}

除此之外,在第一次绑定读写回调函数的时候要传入智能指针的值,但是要注意传入的方式,不能用两个智能指针管理同一块内存,如下用法是错误的

复制代码
void Session::Start() {
	memset(_data, 0, max_length); // 缓冲区清零
	// 从套接字中读取数据,并绑定回调函数headle_read
	_socket.async_read_some(boost::asio::buffer(_data, max_length),
		// 这里可以将shared_ptr<Session>(this)给bind绑定吗?
		// 不可以,会造成多个智能指针绑定同一块内存的问题
		std::bind(&Session::headle_read, this, std::placeholders::_1, std::placeholders::_2,
			shared_ptr<CSession>(this)));
}

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

复制代码
void Session::Start() {
	memset(_data, 0, max_length); // 缓冲区清零
	// 从套接字中读取数据,并绑定回调函数headle_read
	_socket.async_read_some(boost::asio::buffer(_data, max_length),
		// 这里可以将shared_ptr<Session>(this)给bind绑定吗?
		// 不可以,会造成多个智能指针绑定同一块内存的问题
		std::bind(&Session::headle_read, this, std::placeholders::_1, std::placeholders::_2,
			shared_from_this()));
}

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

复制代码
class Session:public std::enable_shared_from_this<Session>
{
private:
	tcp::socket _socket; // 处理客户端读写的套接字
	enum { max_length = 1024 };
	char _data[max_length]; 

	// headle回调函数
	void headle_read(const boost::system::error_code& error, size_t bytes_transferred,
		std::shared_ptr<Session> _self_shared);
	void haddle_write(const boost::system::error_code& error, std::shared_ptr<Session> _self_shared);
	std::string _uuid;
	Server* _server;
public:
	Session(boost::asio::io_context& ioc, Server* server) : _socket(ioc), _server(server){
		// random_generator是函数对象,加()就是函数,再加一个()就是调用该函数
		boost::uuids::uuid a_uuid = boost::uuids::random_generator()();
		_uuid = boost::uuids::to_string(a_uuid);
	}
	tcp::socket& Socket() { return _socket; }
	const std::string& GetUuid() const { return _uuid; }
	void Start();
	
};

再次测试,链接可以安全释放,并不存在二次释放的问题。可以在析构函数内打印析构的信息,发现只析构一次

1. 为什么new_session在'}'结束后仍不结束,而是bind后计数加一?

new_session通过bind绑定时,new_session的计数就会加一,所以在bind后,new_session的生命周期和新构造函数的生命周期相同,因为新生成的函数对象引用了new_session(new_session通过值传递的方式被复制构造函数使用)。所以只要新构造的bind回调函数没有被调用、移除,new_session的生命周期就始终存在,所以new_session不会随着'}'的结束而释放。

bind中的shared_from_this()通过自己生成一个与外界智能指针共享相同引用计数的指针,保证Cession不被释放或者异常释放,因为有可能回调函数还没有调用时,引用Session的应用计数便已经为0,此时如果回调函数被调用,就会发现Session已经被释放。所以为了防止Session被释放,需要生成一个智能指针使得Session的引用计数加一,并传给回调函数,这样能保证回调函数在没有调用之前,一直占用一个Session的引用计数,Session就不会被释放,就达成了一个延时的闭包效果。

总结:使用 std::bind 来绑定 new_session 作为回调参数时,引用计数也会加一,因为 std::bind 内部会将对象拷贝到新生成的函数对象中。这意味着,直到回调函数执行完毕且不再被引用时,new_session 的生命周期才会结束,所以在 } 结束后 new_session 不会被立即销毁。

2. 为什么session执行start函数启动异步操作后会立即将session插入至map种,而不是等异步操作执行完返回后才将session插入map?

复制代码
new_session->Start();
_sessions.insert(std::make_pair(new_session->GetUuid(), new_session));

代码执行顺序:

1)调用 new_session->Start():

当 Start() 被调用时,它会启动一些异步操作(如异步读取),但是这些异步操作并不会阻塞当前的执行流。它们只是注册了一个回调函数,并告诉操作系统在相关事件发生(如数据到达、连接完成等)时触发回调。所以,new_session->Start() 启动异步读取操作async_read_some后,控制权立即返回到 handle_accept() 中,而不会等待异步操作完成。

2)插入 _sessions:

new_session->Start() 启动异步操作后,下一条语句 _sessions.insert(...) 会立即执行。由于 insert 操作是同步的,服务器将 new_session 立即插入到 _sessions 容器中,这确保 new_session 在异步操作执行期间不会被销毁。

综上,异步操作是非堵塞的,调用 new_session->Start() 后,程序并不会等待异步操作(如 async_read_some)完成。这些异步操作会在某个事件发生后(如客户端发送数据)触发注册的回调函数,但它们本身不会阻塞代码执行。

3.智能指针new_session的计数增减过程?

1)在 Server::start_accept() 中创建 new_session ,引用计数:1

复制代码
std::shared_ptr<Session> new_session = std::make_shared<Session>(_ioc, this);

2)绑定到异步接受操作(同理,异步读、异步写都会绑定new_session,只有当异步读写结束之后,new_session的引用计数才会减一)引用计数:2

复制代码
_acceptor.async_accept(new_session->Socket(), std::bind(&Server::handle_accept, this, new_session, std::placeholders::_1));

因为 std::bind 创建了一个闭包,它持有对 new_session 的副本,并将其存储起来,直到异步操作完成并调用回调函数 handle_accept

3)在 Server::handle_accept() 中

复制代码
void Server::handle_accept(std::shared_ptr<Session> new_session, const boost::system::error_code& error) {
    if (!error) {
        new_session->Start();  // 启动会话
        _sessions.insert(std::make_pair(new_session->GetUuid(), new_session));  // 插入到_sessions中
    }
    start_accept();  // 继续等待新的客户端连接
}
  • 当 handle_accept() 被调用时,new_session 被传递给该函数,作为一个局部的 std::shared_ptr。此时引用计数增加 1,变为 3
  • 调用 new_session->Start() 后,服务器将 new_session 插入到 _sessions 容器中,引用计数增加 1,变为 4
  • 此时,new_session 的引用计数如下:
    • 异步操作 async_accept 中持有一个副本。
    • 传递给 handle_accept 时有一个副本。
    • 存储在 _sessions 容器中有一个副本。

4)调用 Session::Start() 并启动异步读取操作

复制代码
void Session::Start() {
    _socket.async_read_some(boost::asio::buffer(_data, max_length),
        std::bind(&Session::headle_read, this, std::placeholders::_1, std::placeholders::_2, shared_from_this()));
}
  • 在 Start() 函数中,调用 async_read_some() 时,将 shared_from_this() 传递给回调函数 std::bind,创建了另一个 std::shared_ptr<Session>,引用计数增加 1,变为 5。
  • **shared_from_this()**确保了回调函数中使用的 Session 对象不会在异步操作完成前被销毁。因为在headle_read()和haddle_write()函数中的_self_shared参数是通过 shared_from_this() 获得的 std::shared_ptr<Session>,并通过 std::bind 传递到 headle_read 回调函数中,这里的new_session引用指针并不会增加,因为_self_shared始终都只是同一个,所以在异步操作完成前session都不会被销毁,因为引用指针是通过shared_from_this增加的,在异步完成前,引用不会减一。

5)异步读取完成后调用 headle_read()

复制代码
void Session::headle_read(const boost::system::error_code& error, size_t bytes_transferred,
    std::shared_ptr<Session> _self_shared) {
    if (!error) {
        boost::asio::async_write(_socket, boost::asio::buffer(_data, bytes_transferred),
            std::bind(&Session::haddle_write, this, std::placeholders::_1, _self_shared));
    } else {
        _server->ClearSession(_uuid);
    }
}

引用计数:仍然是 5

在 headle_read 函数中,_self_shared 是 shared_from_this() 传递过来的 std::shared_ptr<Session>,此时 new_session 的引用计数保持不变。如果读取操作成功,继续绑定 async_write 操作,并传递 std::shared_ptr<Session> 给回调函数 haddle_write。如果发生错误,调用 _server->ClearSession(_uuid),从 _sessions 中删除 new_session,引用计数减少 1,变为 4。

6)异步写入完成后调用 haddle_write()

复制代码
void Session::haddle_write(const boost::system::error_code& error, std::shared_ptr<Session> _self_shared) {
    if (!error) {
        _socket.async_read_some(boost::asio::buffer(_data, max_length),
            std::bind(&Session::headle_read, this, std::placeholders::_1, std::placeholders::_2, _self_shared));
    } else {
        _server->ClearSession(_uuid);
    }
}

引用计数:保持不变

  • 写入操作完成后,再次启动新的异步读取操作,依然使用 _self_shared 传递给 async_read_some,保持 new_session 的引用计数不变。
  • 如果写入操作失败,调用 _server->ClearSession(_uuid),从 _sessions 中移除 new_session,引用计数减少 1。

7)客户端断开连接或发生错误时

复制代码
void Server::ClearSession(std::string uuid) {
    _sessions.erase(uuid);
}

引用计数减少:

  • 当客户端断开连接或发生错误时,ClearSession() 被调用,从 _sessions 容器中删除 new_session。这导致引用计数减少 1。
  • 此时,如果没有其他异步操作绑定 new_session,并且所有与 new_session 相关的回调函数都已经执行完毕,那么引用计数最终会减少到 0。
  • 当所有的异步操作完成,new_session 不再被任何地方持有,引用计数归零,此时 std::shared_ptr 自动销毁 Session 对象,释放其占用的内存。

4.为什么"当 handle_accept() 被调用时,new_session 被传递给该函数,作为一个局部的 std::shared_ptr。此时引用计数增加 1,变为 3"但是"在 headle_read 函数中,_self_shared 是 shared_from_this() 传递过来的 std::shared_ptr<Session>,此时 new_session 的引用计数保持不变。"new_session 同样被传递给该函数,但为什么这里的计数不加一?

**第一种情况:**handle_accept 中传递 new_session

复制代码
void Server::handle_accept(std::shared_ptr<Session> new_session, const boost::system::error_code& error) {
    if (!error) {
        new_session->Start();  // 启动会话
        _sessions.insert(std::make_pair(new_session->GetUuid(), new_session));  // 插入到_sessions中
    }
    start_accept();  // 继续等待新的客户端连接
}
  • 传递过程: handle_accept 函数接收的是 std::shared_ptr<Session>,也就是说,在调用 handle_accept 时,会将 new_session 作为参数按值传递
  • **为什么计数增加?:**在按值传递时,new_session 被拷贝了一份,这份拷贝的 std::shared_ptr 在函数内部存在,它持有同一个 Session 对象。这种拷贝操作会增加引用计数,因此当 handle_accept 被调用时,new_session 的引用计数增加 1。
  • **总结:**因为 new_session 是按值传递的,std::shared_ptr 的拷贝操作导致引用计数增加。

**第二种情况:**headle_read 中传递 _self_shared

复制代码
void Session::headle_read(const boost::system::error_code& error, size_t bytes_transferred,
    std::shared_ptr<Session> _self_shared) {
    if (!error) {
    }
}
  • 传递过程:_self_shared 是通过 shared_from_this() 获得的 std::shared_ptr<Session>,并通过 std::bind 传递到 headle_read 回调函数中。
  • **为什么计数不增加?:**这里的关键在于 _self_shared 并不是按值传递一个新的 std::shared_ptr,而是通过 std::bind 传递的已经存在的 std::shared_ptr(即 shared_from_this() 返回的那个 shared_ptr)。
  • 传递的是同一个 std::shared_ptr 对象: 由于std::bind 是按引用或按值传递 该 shared_ptr 对象(取决于如何绑定),回调函数接收到的是一个已经存在的 std::shared_ptr,而不是新拷贝的 std::shared_ptr。因此,这个传递操作并不会创建新的 shared_ptr 对象,因此不会增加引用计数。_self_shared 是通过参数传递 进入 headle_read 函数的,而不是像 new_session 那样通过 std::bind 进行传递(按值传递 )。因此,在这个函数的参数传递过程中,不再进一步增加引用计数。换句话说,_self_shared 的引用计数已经通过之前的 shared_from_this() 加一,所以这里不再加一。
  • **总结:**在 headle_read 中,传递的是 shared_from_this() 已经创建的 std::shared_ptr 的引用,传递过程中没有创建新的 shared_ptr 对象,因此引用计数保持不变
相关推荐
用户962377954481 天前
DVWA 靶场实验报告 (High Level)
安全
数据智能老司机1 天前
用于进攻性网络安全的智能体 AI——在 n8n 中构建你的第一个 AI 工作流
人工智能·安全·agent
数据智能老司机1 天前
用于进攻性网络安全的智能体 AI——智能体 AI 入门
人工智能·安全·agent
用户962377954481 天前
DVWA 靶场实验报告 (Medium Level)
安全
red1giant_star1 天前
S2-067 漏洞复现:Struts2 S2-067 文件上传路径穿越漏洞
安全
用户962377954481 天前
DVWA Weak Session IDs High 的 Cookie dvwaSession 为什么刷新不出来?
安全
cipher3 天前
ERC-4626 通胀攻击:DeFi 金库的"捐款陷阱"
前端·后端·安全
一次旅行6 天前
网络安全总结
安全·web安全
DianSan_ERP6 天前
电商API接口全链路监控:构建坚不可摧的线上运维防线
大数据·运维·网络·人工智能·git·servlet
red1giant_star6 天前
手把手教你用Vulhub复现ecshop collection_list-sqli漏洞(附完整POC)
安全