云计算百科
云计算领域专业知识百科平台

【Linux网络】多路转接epoll(四)Reactor模式:基于epoll的高性能网络服务器设计与实现(中)代码细节

头像

🎬 个人主页:艾莉丝努力练剑

❄专栏传送门:《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录》 《Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享》

⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平


🎬 艾莉丝的简介:

在这里插入图片描述


文章目录

  • 前言
    • 一、 开头部分(框架引入)
      • 导入语
      • 思维导图
  • 1 ~> AddEvents(Poller.hpp)
    • 1.1 Connection基类抽象
      • 1.1.1 基类核心定义
      • 1.1.2 IOHandler派生类框架
    • 1.2 Poller核心接口实现
      • 1.2.1 AddEvents事件注册
      • 1.2.2 WaitEvents事件等待
    • 1.3 事件派发器核心框架
      • 1.3.1 基础派发逻辑
      • 1.3.2 timeout参数的两种模式
    • 1.4 AddEvents知识图谱
  • 2 ~> 以Recver(Listener.hpp)为主线——完善所涉及的细节
    • 2.1 Listener类基础定义
      • 2.1.1 类定义
    • 2.2 ET模式下的Accept处理逻辑
      • 2.2.1 Accepter接口改造
      • 2.2.2 循环Accept与错误码判断
      • 2.2.3 核心原则
    • 2.3 非阻塞套接字实现
      • 套接字创建时的统一设置
    • 2.4 新连接的托管与回指机制
      • 2.4.1 AddConnection完整实现
      • 2.4.2 设计原理
    • 2.5 工程细节:头文件包含顺序
    • 2.6 Recver(Listener.hpp)知识图谱
  • 3 ~> 以Recver(IOHandler.hpp)为主线——完善所涉及的细节
    • 3.1 IOHandler类完整定义
    • 3.2 Recver非阻塞循环读取
      • 3.2.1 完整读取逻辑
    • 3.3 协议层与业务层的解耦接入
      • 3.3.1 分层架构
      • 3.3.2 回调透传链路
      • 3.3.3 协议层核心接口
    • 3.4 Sender写事件的正确处理逻辑
      • 3.4.1 核心原则
      • 3.4.2 完整发送逻辑
      • 3.4.3 EnableReadWrite事件修改接口
      • 3.4.4 最后一个坑:多路转接select、poll、epoll当中如何正确理解发送问题!!!
    • 3.5 Excepter统一异常处理
    • 3.6 Recver(IOHandler.hpp)知识图谱
  • 4 ~> Version1版本代码
    • Reactor-V1版本代码知识图谱
  • 5 ~> 本文总结
    • 核心考点深度总结
  • 结尾

在这里插入图片描述


前言

一、 开头部分(框架引入)

导入语

本文围绕基于epoll实现的单Reactor事件驱动网络模型展开,从底层Poller系统调用封装、监听连接处理、业务IO读写,到分层解耦架构落地,完整覆盖Reactor模型代码实现的全流程。核心攻克ET模式下的非阻塞IO处理规范、事件按需注册机制、连接生命周期管理、双缓冲区设计、系统调用错误边界处理等高频易错痛点,所有代码均经过边界条件审计与逻辑校验,可直接作为高并发网络编程考核的核心复习依据。

思维导图

Reactor 代码细节核心知识体系
|1. Poller层:epoll内核接口封装
| |— Connection基类抽象
| | |— 纯虚接口:Sockfd/Recver/Sender/Excepter
| | |— 公共成员:事件集合_events、对端地址_clientaddr
| | `— Reactor回指指针_R
| |— Poller核心接口
| | |— AddEvents:epoll_ctl_ADD注册事件
| | `
— WaitEvents:epoll_wait等待就绪事件
| `— 事件派发器
| |— LoopOnce单次事件循环
| |— 事件分发:读事件/写事件/异常事件
| `
— timeout参数:阻塞/非阻塞模式
|2. Listener层:监听连接处理
| |— Listener类定义
| | |— 继承Connection基类
| | |— 监听套接字_listensock
| | `— 业务回调_on_message透传
| |— ET模式Accept逻辑
| | |— 循环accept获取全部就绪连接
| | |— 错误码分支:EAGAIN/EWOULDBLOCK/EINTR
| | `
— 强制非阻塞要求
| |— 新连接生命周期
| | |— 设置非阻塞属性
| | |— 封装为IOHandler对象
| | |— 注册EPOLLIN | EPOLLET事件
| | `— 托管至Reactor并建立回指
| `
— 工程细节:头文件依赖与编译顺序
|3. IOHandler层:业务IO处理
| |— IOHandler类定义
| | |— 套接字描述符_sockfd
| | |— 双缓冲区:_inbuffer/_outbuffer
| | `— 消息处理回调_on_message
| |— Recver读处理
| | |— 非阻塞循环读取
| | |— 返回值三分支:>0/==0/<0
| | |— 错误码边界判断
| | `
— 协议回调与报文处理
| |— Sender写处理
| | |— 非阻塞循环发送
| | |— 发送缓冲区移除逻辑
| | |— EPOLLOUT按需注册机制
| | `— 发送错误处理
| `
— Excepter统一异常与资源清理
|4. 分层解耦架构
| |— 网络层:Reactor + IOHandler
| |— 协议层:Protocol报文封解包
| |— 业务层:Calculator业务计算
| `— 回调机制:跨层调用解耦
`
— 核心易错考点
|— ET模式必须搭配非阻塞IO
|— EPOLLIN常设、EPOLLOUT按需注册
|— 系统调用错误码三分支判断
|— 智能指针循环引用问题
|— 连接生命周期空指针防护
`– 异常事件的正确处理逻辑


1 ~> AddEvents(Poller.hpp)

本章是Reactor模型的底层基础,完成epoll系统调用的封装、连接基类的抽象以及事件派发的核心框架,是整个事件驱动模型与内核交互的核心层级。

1.1 Connection基类抽象

Connection是所有连接对象的抽象基类,定义了事件处理的统一接口,通过多态实现事件的统一派发。

1.1.1 基类核心定义

class Reactor; // 前置声明,解决循环依赖

class Connection
{
public:
Connection() : _events(0), _R(nullptr) {}

uint32_t Events() { return _events; }
void SetEvents(uint32_t events) { _events = events; }
void SetAddress(const InetAddr &addr) { _clientaddr = addr; }

virtual int Sockfd() = 0;
virtual void Recver() = 0;
virtual void Sender() = 0;
virtual void Excepter() = 0;

virtual ~Connection() = default;

protected:
InetAddr _clientaddr; // 对端套接字地址信息
uint32_t _events; // 当前连接关心的epoll事件集合
Reactor* _R; // 所属Reactor实例的弱引用回指指针
};

[审计批注]

  • 原笔记中将_R指针定义在public访问区,违反封装原则。修正为protected访问权限,仅允许派生类访问,外部无法直接修改回指指针,保证对象生命周期安全。
  • 原笔记基类析构函数未定义为虚函数,若通过基类指针释放派生类对象会发生内存泄漏。生产环境必须声明为虚析构函数,保证派生类析构函数被正确调用。
  • 回指指针使用裸指针而非智能指针,核心目的是避免shared_ptr循环引用:Reactor通过shared_ptr管理Connection,若Connection再用shared_ptr指向Reactor,会导致引用计数永远无法归零。裸指针仅作弱引用、不参与所有权管理,是该场景下的标准解决方案。
  • 1.1.2 IOHandler派生类框架

    IOHandler是普通业务连接的实现类,继承Connection基类,负责具体的读写IO操作,每个连接持有独立的读写缓冲区。

    class IOHandler : public Connection
    {
    public:
    IOHandler(int sockfd) : _sockfd(sockfd) {}

    int Sockfd() override { return _sockfd; }
    void Recver() override;
    void Sender() override;
    void Excepter() override;

    private:
    int _sockfd;
    std::string _inbuffer; // 接收缓冲区
    std::string _outbuffer; // 发送缓冲区
    };

    1.2 Poller核心接口实现

    Poller类是对epoll系统调用的直接封装,屏蔽内核接口细节,向上提供统一的事件注册与等待接口。

    1.2.1 AddEvents事件注册

    将指定套接字的指定事件注册到epoll内核事件表中,完成用户态事件到内核态的透传。

    void AddEvents(int sockfd, uint32_t events)
    {
    struct epoll_event ev;
    ev.events = events;
    ev.data.fd = sockfd;

    int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, sockfd, &ev);
    if (n < 0)
    {
    LOG(LogLevel::FATAL) << "epoll_ctl error";
    }
    }

    [审计批注] 此处使用ev.data.fd携带文件描述符,依赖上层Reactor的_connections哈希表完成fd到Connection对象的映射。若追求更高派发效率,可使用ev.data.ptr直接携带Connection对象指针,省去哈希表查找开销,但需严格保证指针生命周期安全。

    1.2.2 WaitEvents事件等待

    阻塞等待epoll事件就绪,返回就绪事件的数量。

    int WaitEvents(struct epoll_event revs[], int num, int timeout)
    {
    int n = epoll_wait(_epfd, revs, num, timeout);
    if (n < 0)
    {
    LOG(LogLevel::FATAL) << "epoll_wait error!";
    }
    else if (n == 0)
    {
    LOG(LogLevel::INFO) << "epoll_wait timeout!";
    }
    return n;
    }

    [审计批注] 原笔记中将超时返回的日志级别错误写为FATAL,属于严重级别错误。epoll_wait返回0是超时正常现象,并非系统错误,必须修正为INFO级别。若使用FATAL级别,在非阻塞轮询模式下会产生大量致命级错误日志,严重干扰问题排查。

    1.3 事件派发器核心框架

    Reactor的Dispatcher是事件循环的核心,负责从epoll获取就绪事件,分发给对应连接的处理函数。

    1.3.1 基础派发逻辑

    void LoopOnce(int timeout)
    {
    int n = _epoller->WaitEvents(revs, gnum, timeout);
    for (int i = 0; i < n; i++)
    {
    int sockfd = revs[i].data.fd;
    uint32_t revents = revs[i].events;

    // 异常事件转换为读写事件
    if ((revents & EPOLLERR) || (revents & EPOLLHUP))
    {
    revents |= (EPOLLIN | EPOLLOUT);
    }

    // 分发给读事件处理
    if ((revents & EPOLLIN) && IsConnectionExists(sockfd))
    {
    _connections[sockfd]->Recver();
    }
    // 分发给写事件处理
    if ((revents & EPOLLOUT) && IsConnectionExists(sockfd))
    {
    _connections[sockfd]->Sender();
    }
    }
    }

    void Dispatcher()
    {
    int timeout = 1;
    while (true)
    {
    LoopOnce(timeout);
    }
    }

    [审计批注]

  • 原逻辑中将EPOLLERR和EPOLLHUP直接转换为读写事件的处理方式存在缺陷。EPOLLERR表示套接字发生错误(如对端重置连接),EPOLLHUP表示对端挂断,正确做法是优先触发Excepter()异常处理,而非直接走读写逻辑,否则可能引发无效读写导致程序崩溃。
  • IsConnectionExists判断是必须的空指针防护:在处理事件的过程中,连接可能已被前序逻辑释放(如读处理中检测到对端关闭并移除连接),若不判断直接调用会触发空指针解引用。
  • 1.3.2 timeout参数的两种模式

    • timeout = 0:非阻塞轮询模式,epoll_wait立即返回,无事件时返回0。该模式会占用100%CPU,仅用于特殊调试场景,生产环境禁止使用。
    • timeout = -1:永久阻塞模式,无事件时线程挂起,有事件就绪才被唤醒。该模式CPU利用率最高,是Reactor模型的标准配置。

    1.4 AddEvents知识图谱

    在这里插入图片描述


    2 ~> 以Recver(Listener.hpp)为主线——完善所涉及的细节

    本章聚焦监听连接的事件处理,核心解决ET模式下如何正确接收所有就绪连接、如何管理新连接生命周期、如何完成新连接与Reactor的托管绑定等核心问题。

    2.1 Listener类基础定义

    Listener是监听套接字的封装,继承Connection基类,负责处理新连接接入事件,本身只关心读事件(新连接就绪等价于读事件就绪)。

    2.1.1 类定义

    class Listener : public Connection
    {
    public:
    Listener(uint16_t port, OnMessage_t on_message)
    : _port(port), _on_message(on_message), _listensock(std::make_unique<TcpSocket>())
    {
    _listensock->BuildSocketMethod_port(port);
    LOG(LogLevel::INFO) << "Listen sockfd create success: " << _listensock->Socketfd();
    }

    int Sockfd() override { return _listensock->Socketfd(); }
    void Recver() override;
    void Sender() override {} // 监听套接字不关心写事件
    void Excepter() override {} // 监听套接字异常暂不处理

    private:
    uint16_t _port;
    OnMessage_t _on_message;
    std::unique_ptr<Socket> _listensock;
    };

    [审计批注] 原笔记中未实现Listener的Sockfd()纯虚函数,会导致类无法实例化。监听套接字的文件描述符由_listensock管理,必须重写该接口返回对应fd,满足基类的多态约定。

    2.2 ET模式下的Accept处理逻辑

    ET(边缘触发)模式下,连接就绪事件只会通知一次,因此必须一次性循环读取所有已就绪的连接,否则剩余连接不会再触发事件,造成连接饥饿。

    2.2.1 Accepter接口改造

    Accept接口必须支持非阻塞错误码返回,用于区分“真错误”和“无更多连接”。

    int Accepter(InetAddr *clientaddr, int *errcode) override
    {
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);
    int sockfd = accept(_sockfd, CONV(&peer), &len);

    if (sockfd < 0)
    {
    *errcode = errno;
    return 1;
    }

    *clientaddr = peer;
    *errcode = 0;
    return sockfd;
    }

    [审计批注] 原笔记中注释掉了*errcode = errno;赋值语句,会导致外层错误判断完全失效,属于严重逻辑漏洞。非阻塞accept返回-1时,必须将errno保存到输出参数中,外层才能区分错误类型。

    2.2.2 循环Accept与错误码判断

    void Recver() override
    {
    while (true)
    {
    int errcode = 0;
    InetAddr clientaddr;
    int sockfd = _listensock->Accepter(&clientaddr, &errcode);

    if (sockfd >= 0)
    {
    // 成功获取新连接,完成封装与托管
    LOG(LogLevel::INFO) << "get a new sockfd success: " << sockfd;

    SetNonBlock(sockfd);
    std::shared_ptr<Connection> conn = std::make_shared<IOHandler>(sockfd, _on_message);
    conn->SetEvents(EPOLLIN | EPOLLET);
    conn->SetAddress(clientaddr);
    _R->AddConnection(conn);
    }
    else
    {
    if (errcode == EAGAIN || errcode == EWOULDBLOCK)
    {
    // 所有连接已处理完毕,退出循环
    LOG(LogLevel::INFO) << "accepter Finish!";
    break;
    }
    if (errcode == EINTR)
    {
    // 被信号中断,重试accept
    LOG(LogLevel::INFO) << "accepter interrupt!";
    continue;
    }
    // 真正的系统错误
    LOG(LogLevel::ERROR) << "accepter error, errno: " << errcode;
    break;
    }
    }
    }

    2.2.3 核心原则

    ET模式下,包括监听套接字在内的所有文件描述符,必须设置为非阻塞模式。原因在于:循环读取的最后一次调用必然会因为无数据而返回,若为阻塞模式,最后一次调用会导致线程永久挂起,整个Reactor停滞。

    2.3 非阻塞套接字实现

    通过fcntl系统调用修改文件描述符的状态标志,设置非阻塞模式。

    void SetNonBlock(int fd)
    {
    int flags = fcntl(fd, F_GETFL);
    if (flags < 0)
    {
    LOG(LogLevel::ERROR) << "fcntl error, set " << fd << " non block failed";
    return;
    }
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
    }

    [审计批注] 必须采用“先获取原有标志,再或上新标志”的写法,严禁直接fcntl(fd, F_SETFL, O_NONBLOCK),否则会覆盖文件描述符原有的其他状态位,引发不可预知的错误。

    套接字创建时的统一设置

    在TcpSocket创建套接字时就统一设置非阻塞,同时配置地址端口复用选项:

    void CreateSocketOrDie() override
    {
    _sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (_sockfd < 0)
    {
    LOG(LogLevel::FATAL) << "create socket error";
    exit(SOCKET_ERR);
    }

    SetNonBlock(_sockfd); // 创建即设置非阻塞

    int opt = 1;
    setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

    LOG(LogLevel::INFO) << "create socket success";
    }

    选项说明:

    • SO_REUSEADDR:允许绑定处于TIME_WAIT状态的端口,服务器重启时可立即绑定端口。
    • SO_REUSEPORT:允许多个套接字绑定同一个端口,内核自动负载均衡,用于多Reactor场景。

    2.4 新连接的托管与回指机制

    新连接创建后,必须完成三个核心动作:设置事件、加入Reactor连接管理、回指所属Reactor。

    2.4.1 AddConnection完整实现

    void AddConnection(std::shared_ptr<Connection> &conn)
    {
    int sockfd = conn->Sockfd();
    uint32_t events = conn->Events();

    // 1. 事件写透到内核epoll
    _epoller->AddEvents(sockfd, events);
    // 2. 连接对象托管到连接表
    _connections[sockfd] = conn;
    // 3. 连接回指当前Reactor实例
    conn->_R = this;

    LOG(LogLevel::INFO) << "insert " << conn->Sockfd() << " into reactor!";
    }

    2.4.2 设计原理

    连接与Reactor的双向引用设计,与内核中struct file和struct socket的互指设计思想一致:

    • Reactor通过_connections哈希表管理所有连接,可以根据fd快速找到对应连接对象。
    • 连接通过_R指针找到所属Reactor,可以调用Reactor的能力(如修改事件、移除连接等)。

    2.5 工程细节:头文件包含顺序

    由于Connection中仅做了Reactor的前置声明,在Listener的实现中调用_R->AddConnection时,编译器必须已经看到Reactor的完整定义,否则会报“非法使用不完整类型”错误。

    正确的包含顺序为:

    #include "Connection.hpp"
    #include "Reactor.hpp" // 必须在Listener之前包含Reactor完整定义
    #include "Listener.hpp"

    2.6 Recver(Listener.hpp)知识图谱

    在这里插入图片描述


    3 ~> 以Recver(IOHandler.hpp)为主线——完善所涉及的细节

    本章聚焦普通业务连接的读写处理,是Reactor模型的核心业务承载层,涵盖非阻塞读取、协议解析、按需发送、异常处理等完整IO生命周期。

    3.1 IOHandler类完整定义

    IOHandler是业务连接的核心实现,负责数据的读取、发送、缓冲区管理,并通过回调机制对接上层协议与业务。

    static const int gbuffersize = 1024;
    using OnMessage_t = std::function<std::string(std::string &, int *)>;

    class IOHandler : public Connection
    {
    public:
    IOHandler(int sockfd, OnMessage_t on_message)
    : _sockfd(sockfd), _on_message(on_message) {}

    int Sockfd() override { return _sockfd; }
    void Recver() override;
    void Sender() override;
    void Excepter() override;

    private:
    int _sockfd;
    OnMessage_t _on_message; // 消息处理回调
    std::string _inbuffer; // 接收缓冲区(字节队列)
    std::string _outbuffer; // 发送缓冲区(字节队列)
    };

    3.2 Recver非阻塞循环读取

    ET模式下读事件就绪后,必须循环读取直到底层无数据,保证本次事件触发的所有数据都被读取完毕。

    3.2.1 完整读取逻辑

    void Recver() override
    {
    LOG(LogLevel::INFO) << "IOHandler event ready, sockfd is: " << _sockfd;

    char buffer[gbuffersize];
    while (true)
    {
    ssize_t n = recv(_sockfd, buffer, gbuffersize 1, 0);

    if (n > 0)
    {
    buffer[n] = '\\0';
    _inbuffer += buffer;
    }
    else if (n == 0)
    {
    // 对端正常关闭连接
    LOG(LogLevel::INFO) << "client quit, address is: "
    << _clientaddr.StringAddress()
    << " sockfd:" << _sockfd;
    Excepter();
    return;
    }
    else
    {
    if (errno == EAGAIN || errno == EWOULDBLOCK)
    {
    // 本轮数据全部读取完毕
    break;
    }
    if (errno == EINTR)
    {
    // 被信号中断,重试读取
    continue;
    }
    // 读取发生真正错误
    LOG(LogLevel::ERROR) << "recv error, address is: "
    << _clientaddr.StringAddress()
    << " sockfd: " << _sockfd;
    Excepter();
    return;
    }
    }

    // 读取完成后,尝试处理完整报文
    int code = 0;
    std::string result = _on_message(_inbuffer, &code);
    if (code == 0)
    {
    _outbuffer += result;
    }
    else
    {
    Excepter();
    return;
    }

    // 尝试直接发送响应
    if (!_outbuffer.empty())
    {
    Sender();
    }
    }

    [审计批注]

  • 对端关闭连接(n==0)属于正常生命周期结束,并非异常事件。原逻辑统一调用Excepter()处理并不严谨,生产环境应单独实现连接关闭逻辑,完成资源清理、连接移除等工作,避免与真正的错误混淆。
  • buffer[n] = '\\0'配合_inbuffer += buffer的写法仅适用于文本协议,若传输二进制数据,string的+=运算符会因遇到’\\0’截断数据。二进制场景应使用_inbuffer.append(buffer, n)明确指定追加长度。
  • 读取完成后立即调用Sender()尝试发送是最优实践:此时内核发送缓冲区大概率为空,写事件处于就绪状态,直接发送可避免额外的epoll事件触发,降低延迟。
  • 3.3 协议层与业务层的解耦接入

    通过回调函数机制,将网络IO层与协议层、业务层完全解耦,各层可独立开发与替换。

    3.3.1 分层架构

  • 网络层:Reactor + IOHandler,只负责数据的收发与事件管理,不关心数据内容。
  • 协议层:Protocol,负责报文的封包、解包、序列化、反序列化,判断报文完整性。
  • 业务层:Calculator,负责具体的业务逻辑处理,只关心结构化的请求与响应。
  • 3.3.2 回调透传链路

    • 主函数中定义协议与业务对象,将业务处理回调注入协议层
    • 协议层的处理回调通过Listener构造函数透传
    • Listener创建IOHandler时,将回调注入每个业务连接
    • IOHandler读取到数据后,通过回调调用协议层处理

    3.3.3 协议层核心接口

    // 返回值:响应报文序列化结果
    // 参数:inbuffer-输入字节流,code-输出型错误码(0成功,-1失败)
    std::string HandlerRequest(std::string &streamstr, int *code)
    {
    std::string resp_package;
    while (true)
    {
    std::string jsonstring;
    int n = UnPackage(streamstr, &jsonstring);
    if (n == 0)
    {
    // 无完整报文,退出
    *code = 0;
    return resp_package;
    }
    else if (n == 1)
    {
    // 报文解析错误
    *code = 1;
    return resp_package;
    }

    // 反序列化 -> 业务处理 -> 序列化响应
    Request req;
    req.Deserialize(jsonstring);
    Response resp = _callback(req);
    std::string resp_str = resp.Serialize();
    resp_package += Package(resp_str);
    }
    }

    [审计批注] 原笔记中解析失败直接调用exit(1)终止整个进程,属于严重设计缺陷。单个客户端的非法报文不应导致整个服务崩溃,正确做法是返回错误码,由上层关闭对应连接,保证服务可用性。

    3.4 Sender写事件的正确处理逻辑

    写事件的处理与读事件有本质区别:读事件是常设的,而写事件必须按需注册,否则会导致epoll频繁触发空转,CPU占用100%。

    3.4.1 核心原则

    • 读事件EPOLLIN:常设到epoll中,因为数据何时到来是不可预知的。
    • 写事件EPOLLOUT:按需注册。套接字创建之初,内核发送缓冲区为空,写事件默认就绪,若常设EPOLLOUT会导致epoll不断返回就绪,造成忙等。仅当发送缓冲区满、无法继续发送时,才注册EPOLLOUT事件,待缓冲区可写时触发一次,写完后立即移除。

    3.4.2 完整发送逻辑

    void Sender() override
    {
    while (true)
    {
    ssize_t n = send(_sockfd, _outbuffer.c_str(), _outbuffer.size(), 0);

    if (n >= 0)
    {
    // 移除已发送的数据
    _outbuffer.erase(0, n);
    if (_outbuffer.empty())
    {
    // 全部发送完毕
    break;
    }
    }
    else
    {
    if (errno == EAGAIN || errno == EWOULDBLOCK)
    {
    // 发送缓冲区已满,本轮无法继续发送
    break;
    }
    if (errno == EINTR)
    {
    // 被信号中断,重试发送
    continue;
    }
    // 发送发生真正错误
    LOG(LogLevel::ERROR) << "send error, address is: "
    << _clientaddr.StringAddress()
    << " sockfd:" << _sockfd;
    Excepter();
    return;
    }
    }

    // 根据发送结果调整事件注册
    if (_outbuffer.empty())
    {
    // 数据发完,关闭写事件监听
    _R->EnableReadWrite(_sockfd, true, false);
    }
    else
    {
    // 数据未发完,开启写事件监听,等待缓冲区可写
    _R->EnableReadWrite(_sockfd, true, true);
    }
    }

    3.4.3 EnableReadWrite事件修改接口

    该接口由Reactor提供,用于动态修改连接关心的事件集合:

    void EnableReadWrite(int sockfd, bool isread, bool iswrite)
    {
    uint32_t events = 0;
    if (isread) events |= EPOLLIN;
    if (iswrite) events |= EPOLLOUT;
    events |= EPOLLET; // 保持ET模式

    struct epoll_event ev;
    ev.events = events;
    ev.data.fd = sockfd;
    epoll_ctl(_epfd, EPOLL_CTL_MOD, sockfd, &ev);

    // 同步更新Connection对象的事件记录
    _connections[sockfd]->SetEvents(events);
    }

    3.4.4 最后一个坑:多路转接select、poll、epoll当中如何正确理解发送问题!!!

    在这里插入图片描述

    3.5 Excepter统一异常处理

    所有读写错误、对端关闭等异常场景,最终都会进入异常处理函数,完成连接资源清理工作。

    void Excepter() override
    {
    LOG(LogLevel::ERROR) << "Excepter, address is: "
    << _clientaddr.StringAddress()
    << " sockfd:" << _sockfd;

    // 1. 从epoll中移除事件
    _R->RemoveConnection(_sockfd);
    // 2. 关闭套接字
    close(_sockfd);
    // 3. 连接对象会因引用计数归零自动释放
    }

    [审计批注] 移除连接时必须先从epoll事件表中删除,再关闭文件描述符。虽然关闭fd会自动从epoll中移除,但显式调用EPOLL_CTL_DEL是更严谨的做法,避免时序问题引发的事件误触发。

    3.6 Recver(IOHandler.hpp)知识图谱

    在这里插入图片描述


    4 ~> Version1版本代码

    Reactor-V1版本代码知识图谱

    在这里插入图片描述


    5 ~> 本文总结

    核心考点深度总结

  • ET模式的铁则 ET模式是Reactor高性能的核心,但有两条不可突破的约束:所有文件描述符必须设置为非阻塞;所有IO操作必须循环执行直到返回EAGAIN。违反任意一条都会导致程序挂起或事件丢失。LT模式无此约束,但事件通知效率更低,适合快速开发的场景。
  • 事件注册的黄金法则 读事件常设,写事件按需注册。这是epoll编程中最容易出错的点,很多初学者会将EPOLLOUT常设,导致CPU跑满。核心逻辑:发送数据时先尝试直接发送,发不完再注册写事件,写完立刻取消写事件监听。
  • 系统调用错误码三分支判断 所有非阻塞IO系统调用(accept、recv、send)返回-1时,绝不能直接判定为错误,必须按优先级判断:
  • EAGAIN/EWOULDBLOCK:正常结束,本轮IO完成
  • EINTR:被信号中断,重试即可
  • 其他错误码:真正的IO错误,走异常处理 该判断逻辑是所有非阻塞IO的标准范式,必须烂熟于心。
  • 分层解耦的回调机制 Reactor网络层通过回调函数对接协议层与业务层,实现了“网络IO”与“业务逻辑”的完全解耦。替换业务逻辑无需修改网络代码,只需更换回调函数,这是工业级网络库的标准设计思想。
  • 智能指针的循环引用陷阱 双向引用场景下严禁双向使用shared_ptr,必然导致循环引用内存泄漏。解决方案有两种:一方使用裸指针作弱引用(本文方案);一方使用weak_ptr。前者实现简单,后者更安全但有额外开销。
  • 连接生命周期的空指针防护 事件派发时必须先判断连接是否存在。因为在处理读事件的过程中,可能已经触发异常并移除了连接,若继续处理写事件会访问空指针。这是多逻辑分支下极易忽略的边界问题。
  • 异常事件的正确处理 EPOLLERR、EPOLLHUP属于套接字异常事件,应优先触发异常清理逻辑,而非简单转换为读写事件。对端正常关闭(recv返回0)属于正常生命周期,应与错误异常区分处理。
  • 工程化细节坑点
  • 头文件包含顺序与前置声明的配合使用,解决循环依赖问题
  • 日志级别的正确使用,禁止将正常现象标记为致命错误
  • 字符串操作的二进制兼容性问题,根据协议类型选择正确的追加方式
  • 套接字创建时统一设置非阻塞与端口复用选项,避免遗漏

  • 结尾

    uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!

    艾莉丝努力练剑

    C/C++ & Linux 底层探索者 | 一个正在努力练剑的技术博主


    👀
    【关注】 跟随我一起深耕技术领域,见证每一次成长。

    ❤️
    【点赞】 让优质内容被更多人看见,让知识传递更有力量。


    【收藏】 把核心知识点存好,在需要时随时查、随时用。

    💬
    【评论】 分享你的经验或疑问,评论区一起交流避坑!

    不要忘记给博主“一键四连”哦!

    “今日练剑达成!”

    “技术之路难免有困惑,但同行的人会让前进更有方向。”

    结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主“一键四连”哦!

    往期回顾:

    【Linux网络】多路转接epoll(三)Reactor模式:基于epoll的高性能网络服务器设计与实现(上)代码框架

    🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡

    ૮₍ ˶ ˊ ᴥ ˋ˶₎ა

    在这里插入图片描述

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【Linux网络】多路转接epoll(四)Reactor模式:基于epoll的高性能网络服务器设计与实现(中)代码细节
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!