前言
上一篇完成了 Connection,解决了一个连接建立之后如何进行非阻塞收发、管理缓冲区以及处理关闭的问题。
不过服务器想要管理连接,首先要能够接收连接。今天实现的 Acceptor 就负责监听服务端端口,并在新客户端到来时调用 accept() 获取通信 fd,再把它交给上层的 TcpServer。
Acceptor 本身不负责收发数据,也不负责保存连接。它只做一件事:接收新连接并向上层转交。
一、Acceptor 的职责
Acceptor 内部组合了三个已经实现的模块:
Socket:创建监听套接字,并调用accept()获取新连接Channel:管理监听 fd 关心的读事件及其回调EventLoop:通过事件循环监控监听 fd
它们之间的关系可以概括为:
text
客户端完成连接
↓
监听 fd 触发可读事件
↓
EventLoop 通知 Channel
↓
Channel 调用 Acceptor::HandleRead()
↓
Socket::Accept() 返回通信 fd
↓
_accept_callback(newfd) 交给 TcpServer
这里要区分两个 fd:监听 fd 只负责等待新连接,accept() 返回的新 fd 才用于和某个客户端通信。后续每个新 fd 都会被封装为一个 Connection 对象。
二、完整实现
cpp
class Acceptor {
private:
Socket _socket;//用于创建监听套接字
EventLoop *_loop; //用于对监听套接字进行事件监控
Channel _channel; //用于对监听套接字进行事件管理
using AcceptCallback = std::function<void(int)>;
AcceptCallback _accept_callback;
private:
/*监听套接字的读事件回调处理函数---获取新连接,调用_accept_callback函数进行新连接处理*/
void HandleRead() {
int newfd = _socket.Accept();
if (newfd < 0) {
return ;
}
if (_accept_callback) _accept_callback(newfd);
}
int CreateServer(int port) {
bool ret = _socket.CreateServer(port);
assert(ret == true);
return _socket.Fd();
}
public:
/*不能将启动读事件监控,放到构造函数中,必须在设置回调函数后,再去启动*/
/*否则有可能造成启动监控后,立即有事件,处理的时候,回调函数还没设置:新连接得不到处理,且资源泄漏*/
Acceptor(EventLoop *loop, int port): _socket(CreateServer(port)), _loop(loop),
_channel(loop, _socket.Fd()) {
_channel.SetReadCallback(std::bind(&Acceptor::HandleRead, this));
}
void SetAcceptCallback(const AcceptCallback &cb) { _accept_callback = cb; }
void Listen() { _channel.EnableRead(); }
};
这段代码主要分为创建监听套接字、绑定内部事件回调、开启事件监控和转交新连接四步。
三、创建监听套接字
cpp
int CreateServer(int port) {
bool ret = _socket.CreateServer(port);
assert(ret == true);
return _socket.Fd();
}
Socket::CreateServer() 内部完成 socket()、bind() 和系统调用 listen(),最终得到监听 fd。这里的系统调用 listen() 是把 socket 变成监听套接字,其 backlog 参数表示内核连接队列长度,并不是服务器最大并发连接数。
而 Acceptor::Listen() 的含义不同:
cpp
void Listen() { _channel.EnableRead(); }
它没有再次调用系统的 listen(),只是让 Channel 开始监控监听 fd 的读事件。这两个名字虽然相同,但所处层次完全不同,很容易混淆。
四、监听 fd 为什么会触发读事件
普通通信 fd 可读,通常表示接收缓冲区中有数据;监听 fd 可读,则表示内核的已完成连接队列中存在可以被应用层取出的连接。
因此 Channel 收到监听 fd 的读事件后,会调用:
cpp
void HandleRead() {
int newfd = _socket.Accept();
if (newfd < 0) {
return ;
}
if (_accept_callback) _accept_callback(newfd);
}
accept() 不会把监听 fd 变成通信 fd,而是创建并返回一个新的 fd。监听 fd 会继续留在 EventLoop 中,等待后续客户端连接。
Acceptor 不知道应该怎样管理这个新 fd,所以通过 _accept_callback 将其交给上层。这种回调设计避免了 Acceptor 依赖 TcpServer 或 Connection 的具体实现。
五、最容易出错的地方:启动顺序
构造函数中只设置了内部读事件回调:
cpp
_channel.SetReadCallback(std::bind(&Acceptor::HandleRead, this));
但没有立即调用 _channel.EnableRead()。正确的外部使用顺序应该是:
text
构造 Acceptor
↓
设置 SetAcceptCallback()
↓
调用 Listen() 开启读事件监控
如果在构造函数中直接开启监控,监听 fd 可能马上触发事件。此时上层的 _accept_callback 还没有设置,HandleRead() 虽然成功取出了 newfd,却没有对象接管它,也没有关闭它,最终会造成连接丢失和 fd 泄漏。
这也是网络库中常见的"两阶段初始化":先完成对象和回调关系的配置,再真正启动事件处理。
六、另外两个容易忽略的细节
1. 成员初始化顺序
C++ 成员的初始化顺序由它们在类中的声明顺序决定,而不是由初始化列表的书写顺序决定。
当前 _socket 声明在 _channel 前面,因此会先创建监听 fd,之后 _channel(loop, _socket.Fd()) 才能安全使用它。如果把 _channel 声明到 _socket 前面,即使初始化列表仍然先写 _socket,实际初始化顺序也不会改变。
2. 回调中的 this 生命周期
cpp
std::bind(&Acceptor::HandleRead, this)
这个回调保存的是裸 this 指针。如果 Acceptor 已经销毁,但对应 Channel 仍然可能被事件循环调用,就会访问悬空指针。
因此必须保证停止事件监控和移除 Channel 之后,才能销毁 Acceptor。在后续设计中,通常由生命周期更长的 TcpServer 持有它。
七、LT 和 ET 模式下 accept 的区别
当前 HandleRead() 每次只调用一次 accept(),适合当前学习版本的 LT(水平触发)思路。如果已完成连接队列里还有连接,监听 fd 仍然处于可读状态,epoll 后续还会继续通知。
如果改成 ET(边缘触发)模式,只接收一个连接可能导致其余连接长时间留在队列中。此时监听 fd 应设置为非阻塞,并循环调用 accept(),直到返回 EAGAIN,表示本轮连接已经全部取完。
另外,工业实现还需要更细致地区分 EINTR、EAGAIN 和真正错误,并保证回调为空或抛出异常时,新 fd 仍能得到正确释放。这些可以作为后续优化,不影响当前版本对整体流程的理解。
八、面试常见问题
1. Acceptor 在 Reactor 模型中负责什么?
它负责处理监听 fd 的可读事件,调用 accept() 获取新连接,再把新连接分发给上层。它是连接建立阶段的事件处理器,不负责连接建立后的数据收发。
2. 为什么 accept 返回的新 fd 要交给上层?
因为 Acceptor 只负责接收连接。连接的读写、缓冲区、超时、关闭和业务回调应由 Connection 与 TcpServer 管理,这样各模块职责更清晰。
3. 为什么不能在构造函数中直接开启读事件?
因为上层接收回调可能尚未设置。若事件立即到来,新 fd 无人接管,会造成连接丢失和资源泄漏。
4. LT 与 ET 模式下 accept 有什么区别?
LT 模式下未取完连接时还会继续通知;ET 模式通常只在状态变化时通知一次,因此必须使用非阻塞监听 fd,并循环 accept() 直到 EAGAIN。
总结
今天实现的 Acceptor 将 Socket、Channel 和 EventLoop 组合起来,把监听套接字正式接入 Reactor 事件循环。
它的核心流程并不复杂:监听 fd 可读时调用 accept(),然后通过回调把新 fd 交给上层。真正需要注意的是模块边界和启动时序:监听 fd 与通信 fd 不能混淆,接收回调必须在开启事件监控之前设置,Acceptor 的生命周期也必须覆盖事件回调的执行过程。
下一步可以实现 TcpServer,由它持有 Acceptor、设置 _accept_callback,并把每个新 fd 封装成 Connection 放入连接表管理。这样服务器从监听端口、建立连接到非阻塞收发的完整链路就能串联起来。