目录
前言
在上篇文章中,我们详细探讨了 Connection 模块的设计与实现。从套接字管理到缓冲区设计,从事件回调到协议切换,我们构建了一个功能完备的连接管理单元。它承载着数据收发的重任,管理着连接的生命周期,是整个网络库中承上启下的关键模块。
然而,一个 Connection 不会凭空产生。每一个新的客户端连接,都必须经过一个"入口"才能被系统接纳,继而创建出对应的 Connection 对象并注册到 EventLoop 的事件监控中。这个"入口",就是本节的主角------Acceptor。
Acceptor 的职责非常纯粹:监听指定的端口,接收新连接,将新连接转交给上层处理。它不关心数据内容,不参与业务逻辑,只负责一件事------打开大门,迎接访客。
在实现思路上,Acceptor 本身也是一个 Channel 的使用者。它将自己持有的监听 socket 封装成一个 Channel,注册到 EventLoop 中,并设置读事件回调。当新连接到达时,EventLoop 检测到监听 socket 可读,便会调用 Acceptor 的回调函数,后者通过 accept() 获取新连接的文件描述符,再通过用户设置的回调函数将新连接交给上层处理。
为了更清晰地阐述这一模块的设计,我们可以提出这样一个问题:为什么 Acceptor 不直接在构造函数中启动读事件监控?
答案是------如果构造函数中启动了读事件监控,一旦有新连接在构造函数执行完毕后、回调函数设置之前到达,Channel 就会触发读事件并调用尚未设置的回调函数,导致新连接无法被正确处理,甚至造成资源泄漏。因此,我们将 Listen() 与 SetAcceptCallback() 分离,强制用户在设置好回调函数后,再显式地启动监听。这是 API 设计中一个微小但至关重要的细节。
本文将从 Acceptor 的设计目标出发,逐步拆解其接口设计、核心实现以及与 EventLoop 和 LoopThread 的协作关系。同时,我们还会讨论 Acceptor 如何与上一篇文章中提到的 Connection 模块衔接,共同构成服务器连接管理的完整链路。
让我们从最基础的接口设计开始,一步步构建这个网络库的"大门"。
Acceptor类的设计思想
在深入代码之前,我们先明确 Acceptor 的设计目标。它的职责可以概括为以下三点:
- 创建监听套接字:绑定指定端口,开始监听客户端的连接请求
- 事件监控与管理 :将监听套接字注册到
EventLoop的事件监控体系中 - 新连接的接收与分发:当有新连接到达时,接受连接并将新连接的文件描述符交给上层处理
简单来说,Acceptor 就是一个"连接工厂",它不关心连接建立之后的事情,只负责把连接"生产"出来,它的功能就是对监听套接字进行管理。
当我们获取了一个新建连接的描述符后,需要为这个连接封装一个 Connection 对象,并设置不同回调。但是因为 Acceptor 模块本身并不知道一个连接产生了事件该如何处理,因此获取一个通信连接后,Connection 的封装以及事件回调函数的设置都应该由服务器模块来设置。
当新连接到达时,它通过 accept() 获取新连接的文件描述符 ,然后需要将这个 fd 交给上层处理。这也代表着我们需要的回调函数(因为回调就是我们传给上层的途径)的函数类型,参数必须传入一个 int 类型,代表一个 newfd。
c++
class Acceptor
{
private:
Socket _socket; // 监听套接字对象,负责底层的 socket 操作
EventLoop *_loop; // 事件循环指针,用于注册和移除事件监控,对监听套接字进行事件监控
Channel _channel; // 监听套接字的事件管理,将 socket 和事件回调绑定
using AcceptCallback = std::function<void(Socket)>;//我们的socket的Accept是返回一个Socket类型对象
AcceptCallback _accept_callback; // 新连接到达时的回调函数
public:
};
逐个分析这些成员的作用:
Socket _socket :封装了底层的 socket 系统调用,提供了 CreateServer、Accept 等接口。它是 Acceptor 与操作系统内核交互的桥梁。
EventLoop *_loop :指向当前 Acceptor 所属的事件循环。Acceptor 需要将自己的 Channel 注册到 _loop 中,才能开始接收新连接的事件通知。
Channel _channel :将监听 socket 的文件描述符与事件回调绑定在一起。当 epoll_wait 返回监听 socket 可读时,EventLoop 会调用 _channel 的 HandleEvent 方法,进而触发我们设置的读事件回调。
AcceptCallback _accept_callback :这是一个由上层(通常是 TcpServer)设置的回调函数。当有新连接到达时,Acceptor 会调用此回调,将新连接的 socket 传递给上层,由上层创建对应的 Connection 对象。
这里有一个重要的设计决策:为什么 Acceptor 不直接创建 Connection 对象?
答案是------职责分离。Acceptor 只负责"接受连接"这一件事,而 Connection 的创建涉及协议选择、回调设置、上下文初始化等复杂逻辑,这些应当由更上层的 TcpServer 或业务代码来决定。通过回调的方式,我们将连接的"接受"与"创建"解耦,使 Acceptor 成为一个可复用的通用组件。
另外,_accept_callback 需要被外层设置,所以我们要有一个接口来设置它。而它如何被调用呢?那就是通过 Acceptor 的读事件回调 HandleRead 了。
当 Acceptor 的读回调被触发,就代表有了新连接,我们应该接收并调用 _accept_callback。
而公开接口的设计也比较简洁,除了构造函数外,就只有一个接口来设置 _accept_callback,以及启动监听事件。
对于构造函数来说,需要外界至少传入一个正确的EventLoop指针,和一个端口号,才能完成socket的服务端连接创建的工作。
c++
class Acceptor
{
private:
Socket _socket; // 监听套接字对象,负责底层的 socket 操作
EventLoop *_loop; // 事件循环指针,用于注册和移除事件监控,对监听套接字进行事件监控
Channel _channel; // 监听套接字的事件管理,将 socket 和事件回调绑定
using AcceptCallback = std::function<void(int)>;
AcceptCallback _accept_callback; // 新连接到达时的回调函数
private:
void HandleRead();
public:
Acceptor(EventLoop *loop,int port);
// 设置新连接到达时的回调函数
void SetAcceptCallBack();
// 启动监听:将 Channel 注册到 EventLoop,开启读事件监控
void Listen();
};
Acceptor类的实现
接下来就是我们 Acceptor 类的构造函数的实现,这里就有一个细节:构造函数中,初始化列表的初始化顺序问题。
在 C++ 中,成员变量的初始化顺序按照声明顺序,而非初始化列表的顺序。但是这就有个问题:

我们的 socket 该如何初始化呢?
没有默认构造函数的类型变量,必须在构造函数的初始化列表中完成初始化,我们的_channel正好就是一个没有默认构造的变量。
它的构造条件是有 EventLoop 和一个文件描述符,但是这个描述符按理来说需要由 socket 来调用 CreateServer 来创建一个服务端的连接才能获得一个有效的文件描述符。
这里有一个办法,那就是你可以调用一个辅助接口,在这个接口函数中,会先调用 _socket 的 CreateServer,让 socket 创建有效的服务端连接,这个时候再返回 socket 的 fd 充当构造函数的 socket 的传入参数,这样 socket 就初始化完毕了,可以为后面的 channel 提供参数,即:
c++
class Acceptor
{
private:
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));
}
};
但是,这样写是不规范的。的确可以达成咱们想要的这个效果,但这里其实是未定义行为,因为这个socket对象还没构造完成就调用,但为什么没有崩溃呢,是因为 _socket是Acceptor的成员,这个对象空间已经开辟好了,只不过还没有通过构造函数去初始化它的成员,所以通过 _socket去调用成员函数,或访问成员变量没有报错。
这里要重温一下,如果一个a类中包含了一个b类对象,那么一开始创建一个a类对象只会为其分配足够的空间,这个时候是内存已分配,但未初始化(原始垃圾数据)。
随后进行构造函数逻辑,先是构造函数的初始化列表,如果b没有默认构造,就要求b必须写在a的初始化列表里进行有参构造。如果有默认构造,哪怕没显式把b写在初始化列表,也会默认调用b的默认构造。在b的构造完成前,都是处于一种未初始化的状态。
只有在其调用了自己构造完成之后,才是初始化完毕。
我们这里就有点卡bug了,在调用socket的有参构造的时候,顺便通过我们的CreateServer调用了socket的CreateServer,要知道这个时候socket可是没有被初始化的。
在对语法掌握不牢固的时候,就不要这样写。
所以有另外一种写法,那就是先给channel一个无效的fd,比如-1。
随后在构造函数正文中先调用socket的CreateServer,再让channel的fd修改成socket的fd。
我们原本的channel对象里是没有这个接口的,所以我们可以给channel新增一个SetFd接口:
c++
class Channel
{
private:
int _fd; // 当前Channel对应的连接的文件描述符
public:
//新增一个改变Fd的接口
void SetFd(int fd)
{
// 如果当前 fd 已经在监控中,需要先移除
if (_fd != -1)
{
Remove(); // 从 EventLoop 的 epoll 中移除
}
_fd = fd;
// 如果 fd 有效,重置事件状态
if (_fd != -1)
{
_events = 0; // 重置关注的事件
// 注意:这里不应该自动注册到 epoll
// 应该由调用者通过 EnableRead/EnableWrite 显式启动
}
}
};
随后构造函数就能改成:
c++
Acceptor(EventLoop *loop, int port) :_loop(loop),_channel(loop,-1)
{
// 在构造函数体中完成 socket 创建
bool ret = _socket.CreateServer(port);
assert(ret == true);
// 设置 channel 的 fd 和回调
_channel.SetFd(_socket.Fd());
_channel.SetReadCallback(std::bind(&Acceptor::HandleRead, this));
}
你可能注意到,Acceptor 的 Channel 只启用了读事件监控(EnableRead),而没有启用写事件监控(EnableWrite)。
这是因为监听 socket 永远只关心"有新连接到达"这一件事 。新连接的到达在内核中表现为监听 socket 的"可读"状态,因此只需要监控 EPOLLIN 事件。写事件对于监听 socket 来说没有意义------它不需要发送数据。
Acceptor 本身并不主动做任何事情,它是被动的------它的所有工作都由 EventLoop 驱动。完整的协作流程如下:
text
1. 用户创建 Acceptor 对象
↓
2. 用户设置 _accept_callback
↓
3. 用户调用 Listen(),_channel.EnableRead()
↓
4. EventLoop::Start() 启动事件循环
↓
5. Poller::Poll() 通过 epoll_wait 监控所有 Channel
↓
6. 新连接到达,epoll_wait 返回监听 socket 的 EPOLLIN 事件
↓
7. EventLoop 遍历活跃 Channel,调用 channel->HandleEvent()
↓
8. Channel::HandleEvent() 检测到 _revents & EPOLLIN
↓
9. 调用 _read_callback(),即 Acceptor::HandleRead()
↓
10. Acceptor::HandleRead() 调用 _socket.Accept() 获取新连接
↓
11. 调用 _accept_callback(newfd),将新连接交给上层
↓
12. 上层创建 Connection 对象,注册到 EventLoop
说了这么多,所以handleRead与Listen的功能其实也说的很明白了,handleread会获取新连接,并把它交给上层------通过上层设置的事件回调:
c++
class Acceptor
{
private:
Socket _socket; // 监听套接字对象,负责底层的 socket 操作
EventLoop *_loop; // 事件循环指针,用于注册和移除事件监控,对监听套接字进行事件监控
Channel _channel; // 监听套接字的事件管理,将 socket 和事件回调绑定
using AcceptCallback = std::function<void(Socket)>;
AcceptCallback _accept_callback; // 新连接到达时的回调函数
private:
void HandleRead()
{
Socket newfd = _socket.Accept();
if (newfd.Fd() < 0)
{
return;
}
if (_accept_callback)
_accept_callback(std::move(newfd));
}
public:
Acceptor(EventLoop *loop, int port) : _loop(loop), _channel(loop, -1)
{
// 在构造函数体中完成 socket 创建
bool ret = _socket.CreateServer(port);
assert(ret == true);
// 设置 channel 的 fd 和回调
_channel.SetFd(_socket.Fd());
_channel.SetReadCallback(std::bind(&Acceptor::HandleRead, this));
}
// 设置新连接到达时的回调函数
void SetAcceptCallBack(const AcceptCallback &cb)
{
_accept_callback = cb;
}
// 启动监听:将 Channel 注册到 EventLoop,开启读事件监控
void Listen()
{
_channel.EnableRead();
}
};
注意,这里HandleRead中,给_accept_callback的传参是移动传参,我们这里构造的新socket对象会在函数结束时被销毁,我们一定要转移其的所有权,防止fd被关闭,后续就不能正常使用。
这个函数的逻辑非常清晰:
- 调用
_socket.Accept()从内核的全连接队列中取出一个已完成的套接字 - 如果
accept()失败(例如没有新连接),直接返回 - 如果
_accept_callback已被设置,将新连接的套接字对象传递给上层
这里需要注意的是,accept() 返回的是一个新的 socket ,这个 socket 与监听 socket 无关,它代表的是与客户端的已建立连接 。上层拿到这个 socket 后,通常会创建 Connection 对象,并将该 socket 封装进 Connection 中进行管理。
结语
至此,我们完成了 Acceptor 模块的完整设计与实现。让我们回顾一下这个模块在整个网络库中的定位与价值。
Acceptor 虽然代码量不大,职责也非常单一,但它在整个网络框架中扮演着不可或缺的"守门人"角色。它静静地监听在指定端口上,等待着每一个新连接的到来,一旦有客户端发起连接请求,它便迅速响应,将新连接的 socket 交付给上层处理,然后继续回到监听状态,周而复始。
回顾 Acceptor 的设计,有几个关键点值得我们再次品味:
第一,关于构造函数中 fd 的初始化顺序问题。 我们通过先给 _channel 传入无效 fd -1,然后在构造函数体中创建 socket 并调用 SetFd 的方式,优雅地绕过了成员初始化顺序的陷阱。这提醒我们:在 C++ 中,成员变量的初始化顺序遵循声明顺序,而非初始化列表顺序,理解这一点对于写出健壮的代码至关重要。
第二,关于"先配置,后启动"的设计原则。 我们将 SetAcceptCallback 与 Listen 分离,强制用户先设置好回调函数,再显式启动监听。这看似是一个微小的 API 设计细节,实则避免了事件触发时回调函数尚未设置的竞态条件。一个好的接口设计,不仅能让用户用起来顺手,更能引导用户正确地使用。
第三,关于移动语义的使用。 在 HandleRead 中,我们使用 std::move(newfd) 将 socket 对象的所有权转移给上层,避免了不必要的拷贝,也明确了"所有权转移"这一语义。在现代 C++ 中,合理使用移动语义可以让代码更加高效和清晰。
第四,关于 Acceptor 与 LoopThread 的协作关系。 Acceptor 本身并不创建线程,也不管理线程,它只依赖于一个 EventLoop 指针来注册和移除事件。这种设计使得 Acceptor 可以灵活地工作在单线程或多线程环境中------在单线程场景下,它直接使用主线程的 EventLoop;在多线程场景下,它可以通过 LoopThread 获取一个专属的 EventLoop,实现连接的负载均衡。
第五,关于职责边界的划分。 Acceptor 只负责"接受连接"这一件事,至于连接建立之后如何处理数据、如何管理生命周期,统统不是它关心的范畴。这种"做一件事,并做好"的设计哲学,让 Acceptor 成为了一个可复用的通用组件,无论上层是 HTTP 服务器、WebSocket 服务器还是自定义协议服务器,Acceptor 都能完美适配。
至此,我们已经完成了从 EventLoop 事件驱动核心,到 Connection 连接管理,再到 Acceptor 连接接收的完整链路。在下一篇文章中,我们将把这些模块整合起来,设计并实现 TcpServer 类------它将作为整个网络库对外的统一入口,负责协调 Acceptor、LoopThread 和 Connection 的协作,最终呈现出一个完整、可用的高性能网络服务器框架。