Acceptor模块的设计与实现

目录

  • 前言
  • [Acceptor 类的设计思想](#Acceptor 类的设计思想)
  • [Acceptor 类的实现](#Acceptor 类的实现)
  • 结语

前言

在上篇文章中,我们详细探讨了 Connection 模块的设计与实现。从套接字管理到缓冲区设计,从事件回调到协议切换,我们构建了一个功能完备的连接管理单元。它承载着数据收发的重任,管理着连接的生命周期,是整个网络库中承上启下的关键模块。

然而,一个 Connection 不会凭空产生。每一个新的客户端连接,都必须经过一个"入口"才能被系统接纳,继而创建出对应的 Connection 对象并注册到 EventLoop 的事件监控中。这个"入口",就是本节的主角------Acceptor

Acceptor 的职责非常纯粹:监听指定的端口,接收新连接,将新连接转交给上层处理。它不关心数据内容,不参与业务逻辑,只负责一件事------打开大门,迎接访客。

在实现思路上,Acceptor 本身也是一个 Channel 的使用者。它将自己持有的监听 socket 封装成一个 Channel,注册到 EventLoop 中,并设置读事件回调。当新连接到达时,EventLoop 检测到监听 socket 可读,便会调用 Acceptor 的回调函数,后者通过 accept() 获取新连接的文件描述符,再通过用户设置的回调函数将新连接交给上层处理。

为了更清晰地阐述这一模块的设计,我们可以提出这样一个问题:为什么 Acceptor 不直接在构造函数中启动读事件监控?

答案是------如果构造函数中启动了读事件监控,一旦有新连接在构造函数执行完毕后、回调函数设置之前到达,Channel 就会触发读事件并调用尚未设置的回调函数,导致新连接无法被正确处理,甚至造成资源泄漏。因此,我们将 Listen()SetAcceptCallback() 分离,强制用户在设置好回调函数后,再显式地启动监听。这是 API 设计中一个微小但至关重要的细节。

本文将从 Acceptor 的设计目标出发,逐步拆解其接口设计、核心实现以及与 EventLoopLoopThread 的协作关系。同时,我们还会讨论 Acceptor 如何与上一篇文章中提到的 Connection 模块衔接,共同构成服务器连接管理的完整链路。

让我们从最基础的接口设计开始,一步步构建这个网络库的"大门"。

Acceptor类的设计思想

在深入代码之前,我们先明确 Acceptor 的设计目标。它的职责可以概括为以下三点:

  1. 创建监听套接字:绑定指定端口,开始监听客户端的连接请求
  2. 事件监控与管理 :将监听套接字注册到 EventLoop 的事件监控体系中
  3. 新连接的接收与分发:当有新连接到达时,接受连接并将新连接的文件描述符交给上层处理

简单来说,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 系统调用,提供了 CreateServerAccept 等接口。它是 Acceptor 与操作系统内核交互的桥梁。

EventLoop *_loop :指向当前 Acceptor 所属的事件循环。Acceptor 需要将自己的 Channel 注册到 _loop 中,才能开始接收新连接的事件通知。

Channel _channel :将监听 socket 的文件描述符与事件回调绑定在一起。当 epoll_wait 返回监听 socket 可读时,EventLoop 会调用 _channelHandleEvent 方法,进而触发我们设置的读事件回调。

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));
     }

你可能注意到,AcceptorChannel 只启用了读事件监控(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被关闭,后续就不能正常使用。

这个函数的逻辑非常清晰:

  1. 调用 _socket.Accept() 从内核的全连接队列中取出一个已完成的套接字
  2. 如果 accept() 失败(例如没有新连接),直接返回
  3. 如果 _accept_callback 已被设置,将新连接的套接字对象传递给上层

这里需要注意的是,accept() 返回的是一个新的 socket ,这个 socket 与监听 socket 无关,它代表的是与客户端的已建立连接 。上层拿到这个 socket 后,通常会创建 Connection 对象,并将该 socket 封装进 Connection 中进行管理。


结语

至此,我们完成了 Acceptor 模块的完整设计与实现。让我们回顾一下这个模块在整个网络库中的定位与价值。

Acceptor 虽然代码量不大,职责也非常单一,但它在整个网络框架中扮演着不可或缺的"守门人"角色。它静静地监听在指定端口上,等待着每一个新连接的到来,一旦有客户端发起连接请求,它便迅速响应,将新连接的 socket 交付给上层处理,然后继续回到监听状态,周而复始。

回顾 Acceptor 的设计,有几个关键点值得我们再次品味:

第一,关于构造函数中 fd 的初始化顺序问题。 我们通过先给 _channel 传入无效 fd -1,然后在构造函数体中创建 socket 并调用 SetFd 的方式,优雅地绕过了成员初始化顺序的陷阱。这提醒我们:在 C++ 中,成员变量的初始化顺序遵循声明顺序,而非初始化列表顺序,理解这一点对于写出健壮的代码至关重要。

第二,关于"先配置,后启动"的设计原则。 我们将 SetAcceptCallbackListen 分离,强制用户先设置好回调函数,再显式启动监听。这看似是一个微小的 API 设计细节,实则避免了事件触发时回调函数尚未设置的竞态条件。一个好的接口设计,不仅能让用户用起来顺手,更能引导用户正确地使用。

第三,关于移动语义的使用。HandleRead 中,我们使用 std::move(newfd) 将 socket 对象的所有权转移给上层,避免了不必要的拷贝,也明确了"所有权转移"这一语义。在现代 C++ 中,合理使用移动语义可以让代码更加高效和清晰。

第四,关于 AcceptorLoopThread 的协作关系。 Acceptor 本身并不创建线程,也不管理线程,它只依赖于一个 EventLoop 指针来注册和移除事件。这种设计使得 Acceptor 可以灵活地工作在单线程或多线程环境中------在单线程场景下,它直接使用主线程的 EventLoop;在多线程场景下,它可以通过 LoopThread 获取一个专属的 EventLoop,实现连接的负载均衡。

第五,关于职责边界的划分。 Acceptor 只负责"接受连接"这一件事,至于连接建立之后如何处理数据、如何管理生命周期,统统不是它关心的范畴。这种"做一件事,并做好"的设计哲学,让 Acceptor 成为了一个可复用的通用组件,无论上层是 HTTP 服务器、WebSocket 服务器还是自定义协议服务器,Acceptor 都能完美适配。

至此,我们已经完成了从 EventLoop 事件驱动核心,到 Connection 连接管理,再到 Acceptor 连接接收的完整链路。在下一篇文章中,我们将把这些模块整合起来,设计并实现 TcpServer 类------它将作为整个网络库对外的统一入口,负责协调 AcceptorLoopThreadConnection 的协作,最终呈现出一个完整、可用的高性能网络服务器框架。

相关推荐
Scott9999HH41 分钟前
2026 年大模型 RAG 架构与多 Agent 协同下的 AI 搜索流量重构:企业级 GEO 技术底层与深度工程解析
人工智能·重构·架构
2601_9609067242 分钟前
Anthropic把Claude Code 2.1.236及以上版本的Fable 5会话
人工智能·kafka·时序数据库·etcd·tdengine
yijianxiangde10043 分钟前
AI Agent 开发
人工智能
新闻码图43 分钟前
京东物流国际指南:独立站物流重构的四步与选型五标准
人工智能·重构
东坡肘子44 分钟前
AI 想放弃了,人没有 -- 肘子的 Swift 周报 #150
人工智能·swiftui·swift
leagsoft_10031 小时前
联软科技推出UniNDR,打通终端、服务器与网络侧的威胁检测链路
服务器·网络·科技
云雾J视界1 小时前
英伟达AI服务器涨价超15%:HBM存储成本飙升,AI算力成本重构开始
服务器·人工智能·芯片·英伟达·hbm
hzxpaipai1 小时前
杭州网站运维|企业官网上线前技术检查清单:SEO、GEO、安全、服务器与备份
运维·服务器·nginx·安全
程序员AlbertTu1 小时前
M01 | 浮点数与误差:数值计算的世界观
c++·数值运算