逻辑杂谈(1.0.1)


本文中进行了对于另外一篇文章的简单总结和简化,可以在阅读完这篇文章之后去进行另外一篇文章的阅读,即逻辑杂谈(1)

Boost.Asio 的异步机制初探:启动时机与传输时机

启动时机

在最近的异步读写实例中,我注意到一些值得深入理解的点。其中最重要的一点,就是“异步驱动的执行者是谁”。

框架的本质

学过 Qt 的人应该清楚,框架和库的区别非常关键:

  • 库(Library):由开发者主动调用函数完成任务。
  • 框架(Framework):由框架主动调用你写的逻辑,开发者只需补充局部的业务处理。

ASIO 也是一种典型的框架思维。在同步逻辑中,我们仍然主动调用 API,而异步逻辑中,我们仅仅注册想要的行为,执行权则交由 ASIO 来驱动。

框架的执行逻辑

ASIO 内部维护了一个事件循环(io_context),它类似线程池的轮询机制,框架通过它来调度各类 I/O 事件。而对于使用者来说,最重要的,是明确事件循环的启动与执行时机

来看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include "boost/asio.hpp"
#include "Session.h"
using namespace std;

int main() {
try {
boost::asio::io_context _ioc;
Server s(_ioc, 10086);
_ioc.run(); // 真正启动服务器的时机
}
catch (std::exception& e) {
std::cerr << "Exception: " << e.what() << "\n";
}
return 0;
}

注意,_ioc.run() 才是服务器的真正启动点,而不是 Server 构造函数。我们不建议将 run 封装进构造函数中——那样会违反单一职责原则,造成耦合。

异步状态机的思想

理解 ASIO 的异步机制,本质上就是理解状态转移的过程。

状态机的抽象:函数执行流的每一个位置,视为程序的一个状态;当执行跳转到下一个函数,就发生了一次“状态转移”。

我们来看实际代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Server::Server(boost::asio::io_context& ioc, short port)
: _ioc(ioc), _acceptor(_ioc, tcp::endpoint(tcp::v4(), port)) {
std::cout << "Server start success on port: " << port << std::endl;
start_accept();
}

void Server::start_accept() {
Session* new_session = new Session(_ioc);
_acceptor.async_accept(new_session->getSocket(),
std::bind(&Server::handle_accept, this, new_session, std::placeholders::_1));
}

void Server::handle_accept(Session* new_session, const boost::system::error_code& error) {
if (!error) {
new_session->Start();
} else {
delete new_session;
}
start_accept(); // 注册下一次接收逻辑,实现循环
}

状态转移过程推演

  1. 构造函数中调用 start_accept(),首次注册异步接收。
  2. 异步函数 async_accept 不会阻塞,而是向 io_context 注册一个状态转移:
    • 触发点:客户端发起连接。
    • 转移点:执行 handle_accept
  3. 一旦客户端连接建立handle_accept 被框架调用,状态转移发生。
  4. handle_accept 结束时再次调用 start_accept(),重新注册下一次接收逻辑。

这就构成了一个回环式状态转移的循环。


传输时机

当客户端发起连接请求之后,服务器将进入下一阶段的处理——数据的接收与处理。

套接字的创建与替换

你需要知道:服务器并不会直接使用监听套接字来接收数据,而是由操作系统生成新的连接套接字来处理数据。监听套接字的职责仅是监听并接收连接请求。

1
2
_acceptor.async_accept(new_session->getSocket(), 
std::bind(&Server::handle_accept, this, new_session, std::placeholders::_1));

在一次状态转移中:

  • 操作系统在监听套接字接收请求后,会创建新的连接套接字。
  • async_accept 中的 new_session->getSocket() 会成为数据传输的新载体。
  • 此时,程序注册的 handle_accept() 就绑定到了这条连接上。

数据如何传输?

操作系统接收到数据后:

  1. 数据首先到达 TCP 的内核缓冲区。
  2. 然后通过 OS 的内部机制,映射到与该连接对应的套接字缓冲区。
  3. 如果程序注册了对应的读取逻辑(比如 async_read_some),那么就会触发下一步的状态转移。

但如果没有注册任何读取逻辑,则这些数据将被视为无用并被丢弃,造成资源浪费。

状态转移链的维护

在一次状态处理后,我们常常会再次注册下一次状态转移,例如:

1
start_accept();

这一点非常重要,因为 每一个异步操作只会注册一次状态转移。触发一次后就失效,如果不再注册,则程序不会再响应新的事件。


错误处理与双重析构风险

在复杂的网络逻辑中,存在如下风险:

  1. 客户端断开连接。
  2. 程序逻辑触发某个会话的销毁,同时又注册了新的状态处理。
  3. 客户端发送 FIN 包,ASIO 响应后再次触发状态处理逻辑。
  4. 如果新状态中尝试访问已销毁的会话对象,会造成 双重析构悬空指针,进而可能导致服务器崩溃。

因此,生命周期管理 是异步编程中最难也最重要的部分。需要确保每一个异步回调的对象都在有效生命周期内。


总结

  • 异步机制的核心是“状态转移”。
  • 每一个异步函数注册的其实是一次触发式的状态变换。
  • io_context.run() 是整个框架执行的起点。
  • 状态转移必须在每次处理后主动注册下一次,以实现循环。
  • 错误处理需要特别注意对象生命周期,避免析构冲突。

-------------本文结束 感谢阅读-------------