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

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

头像

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

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

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


🎬 艾莉丝的简介:

在这里插入图片描述


文章目录

  • 前言
    • 一、 开头部分(框架引入)
      • 整体学习框架导入语
      • 思维导图
  • 1 ~> 原生Epoll服务器的核心痛点与设计缺陷
    • 1.1 原始IOHandler的实现与固有缺陷
    • 1.2 事件处理逻辑的耦合问题
    • 1.3 设计演进的核心方向
    • 1.4 epoll服务器的多台架构知识图谱
  • 2 ~> Reactor模式的核心设计思想:“先描述,再组织”
    • 2.1 一切皆连接的抽象设计
    • 2.2 连接的两大分类
    • 2.3 多路复用的分层封装
  • 3 ~> Reactor核心组件的实现细节
    • 3.1 Connection基类的接口定义
      • 设计要点说明
    • 3.2 Listener:监听连接的实现
      • 设计要点说明
    • 3.3 IOHandler:普通IO连接的框架
      • 设计要点说明
    • 3.4 Poller:Epoll多路复用的封装
      • 设计要点说明
    • 3.5 Reactor容器:连接的组织与管理
      • 设计要点说明
    • 3.6 服务器启动流程
      • 启动流程说明
    • 3.7 知识图谱
      • 3.7.1 关系类的先描述与多态设计:Connection类和Listener类
      • 3.7.2 事件驱动引擎Poller模块的封装
  • 4 ~> 事件派发机制与异常统一处理
    • 4.1 事件派发的核心流程
    • 4.2 异常事件的统一处理方案
    • 4.3 事件派发机制和异常处理的知识图谱
      • 核心事件派发机制
      • Dispatcher多态派发回路的核心设计
      • 异常流的内化转换与“统一异常处理”机制
  • 5 ~> LT与ET触发模式的底层原理与工程差异
    • 5.1 水平触发(LT)的工作机制
      • LT模式的核心特性
    • 5.2 边缘触发(ET)的工作机制
      • ET模式的核心特性
    • 5.3 两种模式的工程取舍
  • 6 ~> 代码框架和代码展示
    • 6.1 Reactor新增模块
      • 公共:Common.hpp
      • 针对套接字的二次封装:Connection.hpp
      • IO处理器:lOHandler.hpp
      • 连接管理器:Listener.hpp
      • epoll模型:Poller.hpp(用了Epoll)
    • 6.2 日志类配套封装类
      • 日志类:Logger.hpp
      • 互斥锁:Socket.hpp
    • 6.3 主函数:Main.cc
      • 6.3.1 代码
      • 6.3.2 Main.cc知识图谱
      • 6.3.3 代码详解与设计要点
        • 1. 命令行参数解析
        • 2. 监听连接的创建与配置
        • 3. Reactor容器的创建
        • 4. 连接注册流程
        • 5. 事件派发循环
        • 6. 错误处理与资源清理
      • 6.3.4 启动流程时序图
      • 6.3.5 扩展建议
    • 6.4 编译链接模块:Makefile
    • 6.5 网络和本地socket转换的类:InetAddr.hpp
  • 7 ~> 总结
    • 7.1 核心考点和易错点
    • 7.2 Reactor知识图谱
  • 结尾

在这里插入图片描述


前言

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

整体学习框架导入语

本文围绕基于epoll的Reactor反应堆模式展开,系统性拆解高性能网络服务器从原生epoll实现到模块化Reactor架构的完整演进路径。原生epoll代码虽能实现基础的多路IO转接,但存在连接管理混乱、缓冲区无归属、事件处理耦合度高、异常处理分散等工程化缺陷,无法支撑高并发场景下的稳定性与可维护性。Reactor模式通过“一切皆连接”的抽象思想,以多态封装不同类型的套接字行为,将事件监听、连接管理、IO处理分层解耦,是工业级网络服务器的标准设计范式。本文将从原始代码的痛点出发,逐层拆解Reactor的组件设计、事件派发逻辑、异常处理方案,并深入辨析LT与ET两种触发模式的底层差异,形成完整的知识闭环。

思维导图

Reactor模式核心知识体系
|— 原生Epoll服务器的痛点
| |— IOHandler缓冲区问题
| | |— 栈缓冲区无法处理半包
| | `— 多连接缓冲区无归属
| |— 事件处理耦合
| | |— 监听套接字与普通套接字逻辑混杂
| | `
— 异常处理分散在各处
| `— 代码可扩展性差
|
|— Reactor核心设计思想
| `
— 先描述,再组织
| |— 描述:Connection抽象所有连接
| `— 组织:容器统一管理所有连接
|
|— 核心组件实现
| |— Connection基类
| | |— 纯虚接口:Recver/Sender/Excepter
| | |— 公共成员:输入缓冲区/输出缓冲区/事件集合
| | `
— 公共方法:Sockfd/Events/SetEvents
| |
| |— Listener(监听连接)
| | |— 继承Connection基类
| | |— 内部封装Tcp监听套接字
| | `— Recver负责执行Accept获取新连接
| |
| |— IOHandler(普通IO连接)
| | |— 继承Connection基类
| | |— 对应客户端通信套接字
| | `
— 实现完整的读写异常逻辑
| |
| |— Poller(多路复用封装)
| | |— 封装epoll_create/epoll_ctl/epoll_wait
| | |— AddEvents:注册fd与事件到内核
| | `— WaitEvents:获取就绪事件集合
| |
| `
— Reactor容器
| |— 内部维护unordered_map连接表
| |— AddConnection:注册连接到反应堆
| `— Dispatcher:事件循环与派发
|
|— 事件派发机制
| |— 事件循环:epoll_wait阻塞等待
| |— 读事件分发:调用对应连接的Recver
| |— 写事件分发:调用对应连接的Sender
| `
— 异常统一处理:错误事件转读写事件
|
`— LT与ET触发模式
|— 水平触发(LT)
| |— 有数据就持续通知
| |— 编程简单,不易丢数据
| `
— 用户态内核态拷贝次数多
|
`— 边缘触发(ET)
|— 仅状态变化时通知一次
|— 必须一次性读完所有数据
|— 效率更高,减少通知次数
`
— 编程复杂度高,必须非阻塞


1 ~> 原生Epoll服务器的核心痛点与设计缺陷

在这里插入图片描述

1.1 原始IOHandler的实现与固有缺陷

原生epoll服务器的IO处理逻辑直接在栈上开辟缓冲区,单次recv后立即处理并回发,代码结构如下:

void IOHandler(int fd)
{
// 栈上临时缓冲区,存在本质缺陷
char buffer[1024];
ssize_t n = recv(fd, buffer, sizeof(buffer) 1, 0);
if(n > 0)
{
buffer[n] = 0;
LOG(LogLevel::INFO) << "client say#" << buffer;
std::string echo_string = "echo#";
echo_string += buffer;
send(fd, echo_string.c_str(), echo_string.size(), 0);
}
else if(n == 0)
{
// 客户端断开,移除epoll并关闭fd
int n = epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr);
LOG(LogLevel::INFO) << "client quit, epoll_ctl del event:" << fd;
close(fd);
}
else
{
// 读取错误,移除epoll并关闭fd
int n = epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr);
LOG(LogLevel::WARNING) << "recv error, epoll_ctl del event:" << fd;
close(fd);
}
}

这段代码仅能完成最基础的echo功能,但存在两个不可忽视的工程级缺陷:

  • 第一,TCP是面向字节流的协议,单次recv无法保证读取到一个完整的应用层报文。若报文长度超过1024字节,或因网络分片导致数据分多次到达,栈缓冲区会在函数返回后被释放,剩余半包数据无法与下一次读取的数据拼接,最终导致报文解析失败。
  • 第二,服务器会同时管理大量文件描述符,每个连接的读取进度、未处理数据完全独立。共用临时缓冲区会导致不同连接的数据互相覆盖,无法实现连接级别的数据隔离。
  • 1.2 事件处理逻辑的耦合问题

    原生代码中,监听套接字与普通客户端套接字的事件处理逻辑混杂在Dispatcher中,需要通过判断fd类型来执行不同的处理分支。随着连接类型增多,分支判断会持续膨胀,代码可扩展性极差。 同时,异常处理逻辑分散在读取、写入等多个函数中,错误处理路径不统一,容易出现资源泄漏或逻辑遗漏。

    1.3 设计演进的核心方向

    解决上述问题的核心思路是面向对象的封装与分层:

  • 为每个文件描述符绑定专属的输入输出缓冲区与客户端信息,形成“连接”的抽象,即Connection。
  • 将不同类型的连接(监听连接、普通IO连接)通过继承与多态统一接口,消除分支判断。
  • 将epoll的系统调用封装为独立的事件监听模块,与连接管理解耦。
  • 用统一的容器管理所有连接,形成完整的事件反应堆。
  • 1.4 epoll服务器的多台架构知识图谱

    在这里插入图片描述


    2 ~> Reactor模式的核心设计思想:“先描述,再组织”

    Reactor模式的核心哲学是“先描述,再组织”,这是操作系统管理资源的经典思想在网络服务器中的落地。 “先描述”指的是用一个统一的抽象类描述所有类型的连接,定义通用的读写异常接口,屏蔽不同连接的行为差异; “再组织”指的是用合适的容器将所有连接对象管理起来,通过文件描述符快速映射到对应的连接对象,实现高效的事件派发。

    在这里插入图片描述

    2.1 一切皆连接的抽象设计

    在Reactor的视角中,无论是用于监听新连接的listen套接字,还是用于数据通信的普通套接字,本质上都是“被epoll监听、会产生事件、需要处理事件”的连接。因此可以抽象出一个Connection基类,规定所有连接必须具备的能力:

    • 能够返回自身对应的文件描述符
    • 能够处理读就绪事件
    • 能够处理写就绪事件
    • 能够处理异常事件
    • 能够保存自身关心的事件类型

    这种设计的核心价值在于多态:事件派发层不需要知道当前处理的是监听套接字还是普通套接字,只需要拿到Connection基类指针,调用对应的虚函数即可,底层会自动执行对应类型的处理逻辑。

    在这里插入图片描述

    2.2 连接的两大分类

    基于Connection基类,派生出两类核心连接:

  • Listener(监听连接):对应服务器的listen套接字,只关心读事件。读事件就绪代表有新连接到来,其Recver方法内部执行accept操作,创建新的普通连接并注册到Reactor中。该类型的写事件与异常事件可做空实现。
  • IOHandler(IO连接):对应已建立的客户端通信套接字,同时关心读写异常事件。Recver负责读取数据并解析报文,Sender负责将输出缓冲区的数据发送出去,Excepter负责清理连接资源。
  • 2.3 多路复用的分层封装

    epoll的系统调用属于底层事件监听能力,与业务连接逻辑不属于同一层级。因此将epoll的创建、事件注册、事件等待等操作封装为Poller模块,向上层提供简洁的事件管理接口。 这种封装也为后续替换多路复用方案(如换用poll、select)保留了扩展空间,只需新增Poller的子类即可,上层Reactor代码无需修改。


    3 ~> Reactor核心组件的实现细节

    3.1 Connection基类的接口定义

    Connection作为所有连接的抽象基类,定义了统一的接口与公共成员,其核心实现如下:

    #pragma once
    #include <iostream>
    #include <string>
    #include "InetAddr.hpp"

    // 连接基类:描述所有类型的连接
    class Connection
    {
    public:
    Connection() : _events(0)
    {}

    // 获取当前连接的文件描述符,纯虚函数,由子类实现
    virtual int Sockfd() = 0;
    // 读事件处理
    virtual void Recver() = 0;
    // 写事件处理
    virtual void Sender() = 0;
    // 异常事件处理
    virtual void Excepter() = 0;

    ~Connection()
    {}

    // 获取/设置当前连接关心的事件
    uint32_t Events() { return _events; }
    void SetEvents(uint32_t events) { _events = events; }

    protected:
    std::string _inbuffer; // 接收缓冲区,累积未处理的读数据
    std::string _outbuffer; // 输出缓冲区,暂存待发送的数据
    InetAddr _clientaddr; // 对端地址信息
    uint32_t _events; // 当前连接关心的epoll事件集合
    };

    设计要点说明

    • 所有成员变量设置为protected权限,子类可以直接访问,同时对外屏蔽内部实现。
    • 输入输出缓冲区采用std::string实现,利用其自动扩容的特性,适配不同长度的报文,解决粘包半包问题。数据读取时先写入输入缓冲区,待报文完整后再进行业务处理;数据发送时先写入输出缓冲区,等待写事件就绪后再批量发出。
    • Sockfd设置为纯虚函数,是因为不同子类的文件描述符存储方式不同:Listener的fd封装在内部的Socket对象中,IOHandler的fd是自身的成员变量。基类无法统一存储fd,因此通过虚函数由子类提供获取fd的方式,这是多态的典型应用。
    • _events成员记录该连接需要被epoll监听的事件类型,由外部设置,注册事件时直接读取该字段即可。

    3.2 Listener:监听连接的实现

    Listener是Connection的子类,专门负责监听端口、接受新连接,内部封装了完整的TCP监听套接字。

    #pragma once
    #include <iostream>
    #include <string>
    #include <memory>
    #include "Socket.hpp"
    #include "Logger.hpp"
    #include "Connection.hpp"

    // 连接管理器:监听套接字的专属连接
    class Listener : public Connection
    {
    public:
    Listener(uint16_t port)
    : _port(port),
    _listensock(std::make_unique<TcpSocket>())
    {
    // 构造时直接完成套接字创建、绑定、监听
    _listensock->BuildSocketMethod(_port);
    }

    // 返回监听套接字的文件描述符
    int Sockfd() override
    {
    return _listensock->Socketfd();
    }

    // 读事件就绪:执行accept获取新连接
    void Recver() override
    {
    // 内部调用套接字的Accepter方法,获取新连接的fd与对端地址
    // 后续在此处创建IOHandler对象,并注册到Reactor中
    }

    // 监听套接字不处理写事件,空实现
    void Sender() override
    {}

    // 监听套接字异常处理,空实现
    void Excepter() override
    {}

    private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensock; // 封装的TCP监听套接字
    };

    设计要点说明

    • Listener在构造阶段就完成了监听套接字的全部初始化工作,包括创建socket、bind、listen,符合RAII设计原则。
    • 监听套接字仅关心EPOLLIN事件,读事件就绪即代表有新的连接请求到达,此时调用Recver执行accept逻辑。
    • 写事件与异常事件均为空实现,通过多态特性保证事件派发时无需判断连接类型,统一调用接口即可。

    3.3 IOHandler:普通IO连接的框架

    IOHandler同样继承自Connection,对应每一个已建立的客户端TCP连接,负责具体的数据收发与业务处理。

    #pragma once
    #include <iostream>
    #include <string>
    #include "Connection.hpp"
    #include "Logger.hpp"

    // IO处理器:普通客户端通信连接
    class IOHandler : public Connection
    {
    public:
    IOHandler(int sockfd, const InetAddr& client)
    {
    _sockfd = sockfd;
    _clientaddr = client;
    }

    int Sockfd() override
    {
    return _sockfd;
    }

    // 读事件就绪:从套接字读取数据到输入缓冲区
    void Recver() override
    {
    // 循环读取数据到_inbuffer,直到读完或暂时无数据
    // 读取完成后,判断输入缓冲区中是否有完整报文
    // 若有完整报文则执行业务处理,结果写入_outbuffer
    // 若需要发送数据,则注册EPOLLOUT事件
    }

    // 写事件就绪:将输出缓冲区的数据发送出去
    void Sender() override
    {
    // 将_outbuffer中的数据通过send发出
    // 若全部发送完成,取消EPOLLOUT事件注册
    // 若未发完,保留事件等待下一次写就绪
    }

    // 异常处理:关闭连接,从Reactor中移除
    void Excepter() override
    {
    // 执行资源清理,关闭文件描述符
    // 从Reactor的连接表中移除当前连接
    }

    private:
    int _sockfd; // 当前通信连接的文件描述符
    };

    设计要点说明

    • 每个IOHandler对象与一个客户端套接字一一绑定,拥有独立的输入输出缓冲区,彻底解决多连接数据隔离与半包拼接问题。
    • 读取数据时不直接执行业务逻辑,而是先累积到输入缓冲区,由应用层协议判断报文完整性,实现业务与IO的解耦。
    • 发送数据时不直接阻塞发送,而是先写入输出缓冲区,通过EPOLLOUT事件驱动发送,适配非阻塞IO的特性。

    3.4 Poller:Epoll多路复用的封装

    Poller模块封装了epoll的全部系统调用,向上层提供事件注册与事件等待的接口,屏蔽底层系统调用细节。所有系统级错误码统一在Common.hpp中枚举管理,包含SOCKET_ERR、BIND_ERR、LISTEN_ERR、EPOLL_ERR等,实现全局错误码规范统一。

    #pragma once
    #include <iostream>
    #include <string>
    #include <cstdlib>
    #include <sys/epoll.h>
    #include "Common.hpp"
    #include "Logger.hpp"

    static const int gsize = 128;
    using namespace LogModule;

    class Poller
    {
    public:
    Poller()
    {
    // 创建epoll模型
    _epfd = epoll_create(gsize);
    if(_epfd < 0)
    {
    LOG(LogLevel::FATAL) << "epoll_create error!";
    exit(EPOLL_ERR);
    }
    LOG(LogLevel::INFO) << "create epollfd success:" << _epfd;
    }

    ~Poller()
    {
    // 析构时关闭epoll文件描述符
    close(_epfd);
    }

    // 向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::DEBUG) << "epoll_ctl add:" << sockfd << " success";
    }
    }

    // 等待就绪事件,返回就绪事件的数量
    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!";
    }
    return n;
    }

    private:
    int _epfd; // epoll模型的文件描述符
    };

    设计要点说明

    • Poller在构造时完成epoll_create,析构时关闭epfd,符合RAII语义,避免资源泄漏。
    • AddEvents对应epoll_ctl的ADD操作,上层只需传入fd与事件集合,无需关心epoll_event结构体的细节。
    • WaitEvents对应epoll_wait,上层传入就绪事件数组、数组大小与超时时间,直接返回就绪事件数量。
    • 该设计具备良好的扩展性,若需替换为poll或select,只需新增Poller子类并实现相同接口,上层Reactor代码无需改动。

    3.5 Reactor容器:连接的组织与管理

    Reactor(原TcpServer)是整个反应堆的核心容器,负责管理所有连接对象、协调Poller与连接的交互、执行事件派发。

    #pragma once
    #include <iostream>
    #include <string>
    #include <memory>
    #include <unordered_map>
    #include "Logger.hpp"
    #include "Connection.hpp"
    #include "Poller.hpp"

    static const int gnum = 128;

    class Reactor
    {
    public:
    Reactor() : _epoller(std::make_unique<Poller>())
    {}

    ~Reactor()
    {}

    // 新增连接到反应堆
    void AddConnection(std::shared_ptr<Connection> &conn)
    {
    int sockfd = conn->Sockfd();
    uint32_t events = conn->Events();
    // 1. 将fd与事件注册到内核epoll中
    _epoller->AddEvents(sockfd, events);
    // 2. 将连接对象托管到连接表中
    _connections[sockfd] = conn;
    }

    // 事件派发主循环
    void Dispatcher()
    {
    int timeout = 1000;
    while(true)
    {
    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();
    }
    }
    }
    }

    private:
    // 判断连接是否存在于管理表中,保证代码健壮性
    bool IsConnectionExists(int sockfd)
    {
    return _connections.find(sockfd) != _connections.end();
    }

    private:
    // 1. epoll多路复用模型
    std::unique_ptr<Poller> _epoller;
    // 2. 连接管理表:fd -> 连接对象的映射
    std::unordered_map<int, std::shared_ptr<Connection>> _connections;
    // 3. 就绪事件输出缓冲区
    struct epoll_event revs[gnum];
    };

    设计要点说明

    • 连接管理采用unordered_map实现fd到Connection对象的哈希映射,查找时间复杂度为O(1),能够高效地根据就绪fd找到对应的连接对象。
    • AddConnection执行两步核心操作:先将事件注册到内核epoll,再将连接对象加入用户态的管理表,保证内核与用户态的连接信息一致。
    • Dispatcher是整个服务器的事件主循环,持续调用epoll_wait获取就绪事件,并分发给对应连接的处理函数。
    • IsConnectionExists接口用于校验fd的合法性,避免连接已被移除但事件仍在就绪队列中导致的野指针问题,提升代码健壮性。

    3.6 服务器启动流程

    整个Reactor服务器的启动遵循“创建监听连接 -> 创建反应堆 -> 注册监听连接 -> 启动事件循环”的流程,入口代码如下:

    #include "Connection.hpp"
    #include "Listener.hpp"
    #include "Reactor.hpp"
    #include <memory>

    static const int gport = 8080;

    int main()
    {
    // 1. 创建监听连接对象,绑定端口
    std::shared_ptr<Connection> conn = std::make_shared<Listener>(gport);
    // 设置监听连接关心的事件:读事件 + 边缘触发
    conn->SetEvents(EPOLLIN | EPOLLET);

    // 2. 创建Reactor反应堆实例
    std::unique_ptr<Reactor> reactor = std::make_unique<Reactor>();

    // 3. 将监听连接注册到反应堆中
    reactor->AddConnection(conn);

    // 4. 启动事件派发循环,服务器开始运行
    reactor->Dispatcher();

    return 0;
    }

    启动流程说明

    • 第一步创建Listener对象时,内部会自动完成TCP套接字的创建、地址绑定与端口监听,完成服务器的端口监听准备。
    • 通过SetEvents设置该连接需要被epoll监听的事件类型,监听套接字只需监听读事件,可根据性能需求选择LT或ET模式。
    • 将监听连接注册到Reactor后,调用Dispatcher进入事件循环,服务器开始持续监听事件并处理。当有新连接到来时,Listener的Recver会被触发,创建新的IOHandler并注册到Reactor中,实现连接的动态接入。

    3.7 知识图谱

    3.7.1 关系类的先描述与多态设计:Connection类和Listener类

    在这里插入图片描述

    3.7.2 事件驱动引擎Poller模块的封装

    在这里插入图片描述


    4 ~> 事件派发机制与异常统一处理

    4.1 事件派发的核心流程

    Reactor的事件派发遵循“内核监听、用户态分发”的分层逻辑,内核态与用户态通过epoll协同工作:

  • 内核通过epoll红黑树管理所有被监听的fd,当fd上的事件就绪时,将其加入就绪队列。
  • 用户态调用epoll_wait获取就绪事件列表,得到一组就绪的fd与对应事件。
  • 遍历就绪事件列表,根据fd从连接表中找到对应的Connection对象。
  • 根据事件类型,调用Connection对象对应的Recver、Sender方法。
  • 处理完成后回到步骤2,进入下一轮事件等待。
  • 整个过程中,派发层完全不需要感知连接的具体类型,所有差异都通过多态在子类中实现,这是Reactor模式解耦的核心体现。

    4.2 异常事件的统一处理方案

    epoll的异常事件包括EPOLLERR(套接字错误)与EPOLLHUP(对端挂断)。若单独为异常事件添加处理分支,会导致每个连接的异常逻辑分散在派发层与连接内部两处,维护成本高。 Reactor采用异常转读写的统一处理方案: 当检测到异常事件时,不直接调用Excepter,而是手动将事件标记为读+写就绪,强制触发读写逻辑。此时连接在执行读/写操作时必然会返回错误,进而在读写函数内部执行异常清理与连接释放。

    这种设计的优势在于:

    • 所有连接的退出逻辑都收敛在Recver/Sender内部,异常处理路径唯一,便于维护与排查问题。
    • 派发层逻辑更简洁,无需单独处理异常分支,减少了代码冗余。
    • 兼容各种异常场景,无论是读取时出错还是写入时出错,都能通过统一路径释放资源。

    4.3 事件派发机制和异常处理的知识图谱

    核心事件派发机制

    在这里插入图片描述

    Dispatcher多态派发回路的核心设计

    在这里插入图片描述

    异常流的内化转换与“统一异常处理”机制

    在这里插入图片描述


    5 ~> LT与ET触发模式的底层原理与工程差异

    5.1 水平触发(LT)的工作机制

    水平触发是epoll的默认工作模式,其核心特性是:只要文件描述符上还有未处理的数据,就会持续不断地向用户态发送就绪通知。 可以用“打电话”的类比理解:来电后只要未接通,电话就会持续响铃,直到接听并处理完毕。对应到编程中,只要接收缓冲区里还有数据,epoll_wait就会一直返回该fd的读就绪事件。

    LT模式的核心特性

    • 编程简单:用户不需要一次性读完所有数据,没读完的话下一次epoll_wait还会继续通知,不容易丢数据。
    • 通知次数多:如果数据分多次慢慢读取,会触发大量的就绪通知,增加用户态与内核态的切换开销。
    • 支持阻塞与非阻塞IO:既可以用阻塞方式读写,也可以用非阻塞方式,容错性高。

    5.2 边缘触发(ET)的工作机制

    边缘触发的核心特性是:只有当文件描述符的状态发生变化时,才会发送一次就绪通知。即便缓冲区里还有未处理的数据,只要没有新的事件发生,也不会再次通知。 对应“打电话”的类比:来电只响一声就停止,必须在这一次通知内处理完所有数据,否则就会错过事件,直到下一次新数据到来才会再次触发。

    ET模式的核心特性

    • 通知次数少:无论缓冲区有多少数据,只在状态变化时通知一次,大幅减少内核态与用户态的切换次数,效率更高。
    • 编程要求高:用户必须在一次通知中把缓冲区里的所有数据全部读完,否则剩余数据不会再触发通知,会导致数据残留、连接假死。
    • 必须配合非阻塞IO:如果使用阻塞IO,循环读取到最后会因为无数据而阻塞在recv上,导致整个事件循环卡死。因此ET模式下所有套接字必须设置为非阻塞。

    5.3 两种模式的工程取舍

    对比维度水平触发(LT)边缘触发(ET)
    通知机制 有数据就持续通知 仅状态变化时通知一次
    编程复杂度 低,不易出错 高,必须循环读取且非阻塞
    性能开销 通知次数多,切换开销大 通知次数少,切换开销小
    数据安全性 高,不会丢数据 低,处理不当易丢数据
    适用场景 并发量一般、追求稳定性的业务 高并发场景、追求极致性能

    在工业级实现中,监听套接字通常推荐使用ET模式,因为新连接事件属于状态变化,单次accept即可处理完毕;普通业务连接可根据团队技术储备与业务特性选择,LT模式更稳妥,ET模式性能更优。


    6 ~> 代码框架和代码展示

    在这里插入图片描述

    6.1 Reactor新增模块

    公共:Common.hpp

    #pragma once

    // 错误码放这里,统一进行管理
    enum
    {
    SOCKET_ERR = 1,
    BIND_ERR,
    LISTEN_ERR,
    EPOLL_ERR
    };

    针对套接字的二次封装:Connection.hpp

    #pragma once

    // 每一个fd,后续都对应一个connection连接 — 针对套接字的二次封装
    #include <iostream>
    #include <string>
    #include "InetAddr"

    // “先描述” – 基类
    class Connection
    {
    public:
    Connection() : _events(0)
    {}

    // // sockfd下沉了,这里也注释掉
    // Connection() : _sockfd(0)
    // {}

    // 有了Events和Sockfd,任何一个连接就可以获取对应的套接字和事件了
    uint32_t Events()
    {
    return _events;
    }
    // 设置标志位
    void SetEvents(uint32_t events)
    {
    _events = events;
    }
    virtual void Recver() = 0;
    virtual void Sender() = 0;
    virtual void Excepter() = 0;
    ~Connection(){}
    protected:
    // int _sockfd; // 套接字 — 可能是listen套接字也可能是普通套接字 — 挪地方,把sockfd沉下去,就不在这里写了
    std::string _inbuffer; // 接收缓冲区,一个套接字一个
    std::string _outbuffer; // 发送缓冲区

    InetAddr _clientaddr; // client socket套接字对应客户端地址

    uint32_t _events; // Connection关心什么事件

    // TODO — 除了这些,未来还需要什么再继续往下加
    };

    IO处理器:lOHandler.hpp

    #pragma once

    #include <iostream>
    #include <string>
    #include "Connection.hpp"
    #include "Logger.hpp"

    // =========> IO处理器 <=========
    class IOHandler : public Connection
    {
    public:
    IOHandler()
    {}

    int Sockfd() override
    {
    return _sockfd;
    }

    void Recver() override
    {

    }

    void Sender() override
    {

    }

    void Excepter() override
    {

    }

    ~IOHandler()
    {}
    private:
    int _sockfd;
    };

    连接管理器:Listener.hpp

    Listener模块是一个特殊的connection,也是connection的派生类。

    #pragma once

    #include <iostream>
    #include <string>
    #include <memory>
    #include "Logger.hpp"
    #include "Connection.hpp"
    #include "Socket.hpp" // 作为一个Listener,必须包含Socket.hpp模块

    // ===========> 连接管理器(虽然这么叫,但是特别像当年的TcpServer.hpp)- 先描述 <============
    // Listener模块是一个特殊的connection,也是connection的派生类

    class Listener : public Connection
    {
    public:
    Listener(uint16_t port)
    : _port(port),
    _listensock(std::make_unique<TcpSocket>())
    {
    _listensock->BuildSocketMethod(_port);
    }

    int Sockfd() override
    {
    return _listensock->Socketfd();
    }

    void Recver() override
    {
    // _listensock->Accepter(); // –> 未来,Listener的Recver就是底层调用一下Accepter,但是其它的套接字调用就是调用Recver
    }

    void Sender() override
    {

    }

    void Excepter() override
    {

    }

    // 析构这里不写了
    private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensock;
    };

    epoll模型:Poller.hpp(用了Epoll)

    Poller.hpp将来帮助我们监听所有的fd是否就绪!

    #pragma once

    // 专门进行事件管理的epoll模型
    // Poller.hpp将来帮助我们监听所有的fd是否就绪!

    #include <iostream>
    #include <string>
    #include <sys/epoll.h>
    // exit的头文件
    #include <cstdlib>
    // 退出码封装成Common.hpp(公共)类,包含一下
    #include "Common.hpp"
    #include "Logger.hpp"

    // // 封装一下IN(读)事件、OUT(写)事件,对事件做一下二次包装
    // // 这样添加事件的时候就不用管什么EPOLLIN、EPOLLOUT什么的了,今天不做这个了,我直接把EPOLLIN、EPOLLOUT暴露出去
    // #define IN EPOLLIN
    // #define IN EPOLLOUT

    static const int gsize = 128;

    using namespace LogModule;

    class Poller
    {
    Poller()
    {
    _epfd = epoll_create(gsize);
    // epoll创建失败,不用玩了
    if(_epfd < 0)
    {
    LOG(LogLevel::FATAL) << "epoll_create error!";
    exit(EPOLL_ERR);
    }
    LOG(LogLevel::INFO) << "epoll_create success: " << _epfd;
    }

    // Poller也需要给提供一个接口:添加事件的接口
    void AddEvents(int sockfd,uint32_t events)
    {
    // …
    }

    // 就绪事件
    int WaitEvents(struct epoll_event revs[],int num,int timeout) // num是数组缓冲区大小
    {
    int n = epoll_wait(_epfd,revs,num,timeout);
    if(n < 0)
    {
    LOG(LogLevel::FATAL) << "epoll_wait error!";
    }
    return n;
    }

    ~Poller()
    {

    }

    private:
    int _epfd;
    };

    // // 这个工作做了的话,代码量就更夸张了,感兴趣让AI写一下
    // class Poller
    // {
    // public:
    // // 创建模型、删除模型、获取就绪事件、设置对应的就绪事件
    // virtual bool Create() = 0;
    // virtual bool Destroy() = 0;
    // virtual void GetEvents() = 0;
    // virtual void SetEvents() = 0;
    // };

    // // 三种多路转接方法都实现,想用哪种就用哪种
    // class SelectPoller : Poller
    // {

    // };

    // class PollPoller : Poller
    // {

    // };

    // class EpollPoller : Poller
    // {

    // };

    6.2 日志类配套封装类

    日志类:Logger.hpp

    #ifndef __LOGGER_HPP__
    #define __LOGGER_HPP__

    #include <iostream>
    #include <cstdio>
    #include <string>
    // #include <filename>
    #include <fstream>
    #include <ctime>
    #include <filesystem> // C++17的封装:头文件是filesystem
    #include <memory> // 真正想要的日志类
    #include <sstream> // 设计一个内部类,stringstream的头文件
    #include <unistd.h> // getpid要包一下头文件
    #include "Mutex.hpp"

    namespace LogModule
    {
    // 基础工作1:获取时间戳
    std::string GetTimeStamp()
    {
    time_t timestamp = time(nullptr);
    struct tm data_time;
    localtime_r(&timestamp, &data_time); // _r:可重入

    char data_time_str[128]; // 时间戳字符串
    // 选择C封装的接口,格式化输出比较容易
    snprintf(data_time_str,sizeof(data_time_str),"%4d-%02d-%02d %02d:%02d:%02d",
    data_time.tm_year + 1900,
    data_time.tm_mon + 1,
    data_time.tm_mday,
    data_time.tm_hour,
    data_time.tm_min,
    data_time.tm_sec
    );
    return data_time_str;
    }

    // 基础工作2:日志等级

    // 日志等级v1:整型枚举:int类型
    // v1.1:枚举就用C++的方式,强制性地带上class(把将来的作用域带上)
    enum class LogLevel
    {
    // 日志等级由以下几种元素构成
    DEBUG,
    INFO,
    WARNING,
    ERROR,
    FATAL // 致命的,影响后面的代码跑了,必须debug了
    };

    // // v1.2:枚举类可以重载 << 运算符
    // std::ostream& operator<<(std::ostream& os, LogLevel level)
    // {
    // os << static_cast<int>(level);
    // return os;
    // }

    // // v1.3:枚举类可以重载 << 运算符
    // std::ostream& operator<<(std::ostream& os, LogLevel level)
    // {
    // os << static_cast<int>(level);
    // return os;
    // }

    // 日志等级v2:日志等级转换成字符串!
    // switch case语句:根据日志等级返回对应的字符串表示
    std::string LogLevelToString(LogLevel level)
    {
    switch(level)
    {
    case LogLevel::DEBUG:
    return "DEBUG";
    case LogLevel::INFO:
    return "INFO";
    case LogLevel::WARNING:
    return "WARNING";
    case LogLevel::ERROR:
    return "ERROR";
    case LogLevel::FATAL:
    return "FATAL";
    default:
    return "UNKNOWN";
    }
    }

    // 基础工作3:日志刷新–>策略模式

    // 基类:策略基类,设置刷新策略的
    // ===> 刷新日志 <===
    class LogStrategy
    {
    public:
    // 虚函数
    virtual ~LogStrategy() = default;
    virtual void SyncLog(const std::string &Logmessage) = 0;
    };

    // —–> 策略1:控制台打印 <—–
    class ConsoleLogStrategy : public LogStrategy // (多态)子类:继承纯虚接口类
    {
    public:
    // 构造函数、析构函数
    ConsoleLogStrategy() {}
    ~ConsoleLogStrategy() {}
    void SyncLog(const std::string &Logmessage) override
    {
    // 加锁,锁的保护
    // 消息会出现错乱、交叉(临界资源显示器没有被锁保护)
    LockGuard lockguard(&_mutex);
    std::cout << Logmessage << std::endl;
    }

    private:
    Mutex _mutex;
    };

    // —–> 策略2:文件内打印 <—–
    // 路径代表目录,知道在哪个目录底下
    // 所有的日志默认就在当前目录底下,有一个.log的文件
    // static const std::string glogdir = "./log/"; // 模仿glog
    // static const std::string glogfilename = "log.txt";

    // 在 C++ 中,普通的 static const std::string 不能在类体内直接初始化,除非使用 C++17 引入的 inline 关键字
    static inline const std::string glogdir = "./log/";
    static inline const std::string glogfilename = "log.txt";

    class FileLogStrategy : public LogStrategy // (多态)子类:继承纯虚接口类
    {
    public:
    FileLogStrategy(const std::string &dir = glogdir,const std::string &filename = glogfilename)
    : _logdir(dir),_logfilename(filename)
    {
    // log / log.txt
    // 为了线程安全,往文件里写入也要带上锁
    LockGuard lockguard(&_mutex); // 保证了原子性
    if(std::filesystem::exists(_logdir)) // exists:判断路径是否存在
    {
    return;
    }
    else
    {
    try
    {
    // 目录存在返回不存在创建:底层是调的mkdir的系统调用,搞个抛异常,因为可能会出错
    std::filesystem::create_directories(_logdir); // 对应一个或多个 mkdir 系统调用
    }
    catch(const std::filesystem::filesystem_error &e)
    {
    std::cerr << e.what() << "\\n";
    }
    }
    }

    ~FileLogStrategy()
    {}

    // 打开一个文件
    void SyncLog(const std::string &Logmessage) override
    {
    // 加锁
    LockGuard lockguard(&_mutex);
    std::string target = _logdir + _logfilename;
    // 日志必须追加写入
    std::ofstream out(target,std::ios::app); // 追加写入文件
    // 打开文件
    if(!out.is_open())
    {
    return;
    }
    // 在打开和关闭文件之间进行文件写入
    // out.write(Logmessage.c_str(), Logmessage.size()); // write写入
    out << Logmessage << "\\n"; // C++流写入
    // 关闭文件
    out.close();
    }
    private:
    // 告诉我指定的文件工作目录是什么?日志文件名是什么?
    // 设置参数来规定
    std::string _logdir;
    std::string _logfilename; // ./log/XXX.log
    Mutex _mutex;
    };

    // 我们真正想要的日志类:一个日志将来选择哪一种策略?
    class Logger
    {
    public:
    Logger()
    {
    UseConsoleLogStrategy();
    }
    ~Logger()
    {}
    // 智能指针,
    void UseConsoleLogStrategy() // 显示器策略
    {
    // <RAII(资源获取即初始化)思想>!
    _strategy = std::make_unique<ConsoleLogStrategy>(); // 日志输出到 屏幕/终端
    // 1. 创建 ConsoleLogStrategy 对象
    // 2. 赋值给 _strategy
    // 3. 当 Logger 对象销毁时,_strategy 自动销毁,释放内存

    // std::make_unique<ConsoleLogStrategy>() 做了三件事:
    // 1. 创建对象 :在堆上 new 一个 ConsoleLogStrategy 对象
    // 2. 包装成智能指针 :用 std::unique_ptr 包装这个对象
    // 3. 自动管理内存 :对象生命周期结束时自动 delete,防止内存泄漏
    }
    void UseFileLogStrategy() // 文件策略
    {
    _strategy = std::make_unique<FileLogStrategy>(); // 日志写入到(路径)./log/log.txt 文件
    }

    // 设计一个内部类,访问外部属性会快一点
    // ==========> 左半部分 <==========
    // 目标是把一个类对象,变成一个string字符串
    class LogMessage
    {
    public:
    // 当前时间已经有函数可以帮我获取了:基础工作的重要性
    LogMessage(LogLevel level,std::string &filename,int line,Logger&self)
    :_current_time(GetTimeStamp()),
    _level(level),
    _pid(getpid()),
    _filename(filename),
    _line(line),
    _logger(self), // 由内部类来引用
    _strategy(self._strategy.get()) // 保存当前策略指针
    {
    // stringstream:C++标准库中处理字符串流的类
    std::stringstream ss;
    // 字符串拼接
    ss << "[" << _current_time << "]"
    << "[" << LogLevelToString(_level) << "]"
    << "[" << _pid << "]"
    << "[" << _filename << "]"
    << "[" << _line << "]"
    << "- ";
    _loginfo = ss.str();
    }

    // 仿函数:这个设计非常巧妙,值得学习
    // 它实现了链式调用,让日志打印可以像 std::cout 一样连续使用 <<
    template <typename T> // 将来怎么调用日志?
    LogMessage &operator<<(const T &info)
    {
    std::stringstream ss;
    ss << info; // // 将任意类型转为字符串
    _loginfo += ss.str(); // 拼接参数到日志内容
    return *this; // 返回自身引用,支持链式调用
    }

    ~LogMessage() // RALL风格的日志刷新!
    {
    // v2版本
    if(_strategy)
    {
    _strategy->SyncLog(_loginfo); // 使用保存的策略,而不是实时获取
    }

    // // v1版本
    // if(_logger._strategy)
    // {
    // // 类内可以通过.来访问类内属性
    // _logger._strategy->SyncLog(_loginfo); // 实时获取当前策略
    // // 直接可以刷新到显示器或者文件里去了
    // }
    }
    private:
    std::string _current_time; // 当前时间
    LogLevel _level; // 日志等级
    pid_t _pid; // 进程pid
    std::string _filename; // 输出日志对应的文件名
    int _line; // 行号
    // std::strinh _logcontent; // 日志内容
    std::string _loginfo; // 一条完整的日志

    Logger &_logger; // 外部类的引用
    LogStrategy *_strategy; // 保存创建时的策略指针
    };

    // Logger对象打印日志的时候,故意返回一个临时的LogMessage对象
    // 为什么要返回一个临时的内部类对象?
    LogMessage operator()(LogLevel level,std::string filename,int line)
    {
    return LogMessage(level,filename,line,*this);
    }
    private:
    std::unique_ptr<LogStrategy> _strategy; // 刷新日志的策略
    };

    Logger logger;

    // 使用宏,包装我们的日志打印过程,宏有一个特点,#define A B,B替换成为A
    // 预定义宏,在编译的时候由预处理器自动替换成对应的信息
    #define LOG(level) logger(level,__FILE__,__LINE__)

    // —> 动态调整日志策略 <—
    // 全局变量使用控制台的策略
    #define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleLogStrategy()
    // 文件版本的策略
    #define ENABLE_FILE_LOG_STRATEGY() logger.UseFileLogStrategy()

    } // namespace LogModule

    #endif

    互斥锁:Socket.hpp

    #ifndef __SOCKRT_HPP
    #define __SOCKRT_HPP

    #include <iostream>
    #include <string>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <memory>
    #include "InetAddr.hpp"
    #include "Logger.hpp"
    // 退出码封装成Common.hpp(公共)类,包含一下
    #include "Common.hpp"

    static const int gbacklog = 16;

    using namespace LogModule;

    // 基类
    // 把socket创建过层,模版化,方法化 — 模版方法模式
    class Socket
    {
    public:
    virtual ~Socket() {}
    virtual void CreateSocketOrDie() = 0;
    virtual void BindSocketOrDie(uint16_t port) = 0;
    virtual void ListenSocketOrDie() = 0;
    // virtual std::shared_ptr<Socket> Accepter(InetAddr *clientaddr) = 0;

    // 基类方法那里改成int
    virtual int Accepter(InetAddr *clientaddr) = 0;
    virtual void ConnectOrDie(const std::string &serverip, uint16_t serverport) = 0;
    virtual int Socketfd() = 0;
    virtual void Close() = 0;

    virtual int Recv(std::string *outstr) = 0;
    virtual int Send(const std::string &outstr) = 0;

    public:
    void BuildSocketMethod(uint16_t port)
    {
    CreateSocketOrDie();
    BindSocketOrDie(port);
    ListenSocketOrDie();
    }
    void BuildClientSocketMethod(const std::string &serverip, uint16_t serverport)
    {
    CreateSocketOrDie();
    ConnectOrDie(serverip, serverport);
    }
    };

    class TcpSocket : public Socket
    {
    public:
    TcpSocket() : _sockfd(1)
    {
    }
    TcpSocket(int sockfd) : _sockfd(sockfd)
    {
    }

    void CreateSocketOrDie() override
    {
    _sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (_sockfd < 0)
    {
    LOG(LogLevel::FATAL) << "create socket error";
    exit(SOCKET_ERR);
    }
    int opt = 1;
    setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));
    LOG(LogLevel::INFO) << "create socket success";
    }
    void BindSocketOrDie(uint16_t port) override
    {
    InetAddr local(port);
    int n = bind(_sockfd, local.Addr(), local.AddrLen());
    if (n < 0)
    {
    LOG(LogLevel::FATAL) << "bind socket error";
    exit(BIND_ERR);
    }
    LOG(LogLevel::INFO) << "bind socket success";
    }
    void ListenSocketOrDie() override
    {
    int n = listen(_sockfd, gbacklog);
    if (n < 0)
    {
    LOG(LogLevel::FATAL) << "listen socket error";
    exit(LISTEN_ERR);
    }
    LOG(LogLevel::INFO) << "listen socket success";
    }
    // std::shared_ptr<Socket> Accepter(InetAddr *clientaddr) override
    // {
    // struct sockaddr_in peer;
    // socklen_t len = sizeof(peer);
    // int sockfd = accept(_sockfd, CONV(&peer), &len);
    // if (sockfd < 0)
    // {
    // return nullptr;
    // }
    // *clientaddr = peer;
    // return std::make_unique<TcpSocket>(sockfd);
    // }

    // 获取新连接改一下
    int Accepter(InetAddr *clientaddr) override
    {
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);
    int sockfd = accept(_sockfd, CONV(&peer), &len);
    if (sockfd < 0)
    {
    return 1;
    }
    *clientaddr = peer;
    return sockfd;
    }
    int Socketfd() override
    {
    return _sockfd;
    }
    void Close() override
    {
    if (_sockfd >= 0)
    {
    close(_sockfd);
    _sockfd = 1;
    }
    }
    int Recv(std::string *outstr) override // 读写的依旧是字符串
    {
    char buffer[1024];
    ssize_t n = recv(_sockfd, buffer, sizeof(buffer) 1, 0); // bug
    if (n > 0)
    {
    buffer[n] = 0;
    *outstr += buffer; // +=的本质是拼接,入队列,outstr当做一个字节流队列!
    return n;
    }
    else if (n == 0)
    {
    return 0;
    }
    else
    {
    return 1;
    }
    }
    int Send(const std::string &outstr) override
    {
    return send(_sockfd, outstr.c_str(), outstr.size(), 0);
    }
    void ConnectOrDie(const std::string &serverip, uint16_t serverport) override
    {
    InetAddr serveraddr(serverport, serverip);
    int n = connect(_sockfd, serveraddr.Addr(), serveraddr.AddrLen());
    if (n != 0)
    {
    LOG(LogLevel::FATAL) << "connect " << serveraddr.StringAddress() << " failed";
    return;
    }
    LOG(LogLevel::INFO) << "connect " << serveraddr.StringAddress() << " success";
    }

    private:
    int _sockfd;
    };

    6.3 主函数:Main.cc

    6.3.1 代码

    主函数是Reactor服务器的启动入口,负责初始化核心组件、配置服务器参数并启动事件循环。下面详细拆解每个步骤的实现细节与设计考量:

    #include "Connection.hpp"
    #include "Listener.hpp"
    #include "Reactor.hpp"
    #include <memory>
    #include <cstdlib>
    #include <iostream>

    // 默认监听端口,可通过命令行参数覆盖
    const uint16_t DEFAULT_PORT = 8080;

    // 打印使用说明
    void PrintUsage(const char* program_name)
    {
    std::cout << "Usage: " << program_name << " [port]" << std::endl;
    std::cout << " port: 监听端口号 (默认: " << DEFAULT_PORT << ")" << std::endl;
    std::cout << "示例: " << program_name << " 8888" << std::endl;
    }

    int main(int argc, char *argv[])
    {
    uint16_t port = DEFAULT_PORT;

    // 1. 解析命令行参数
    if (argc > 2) {
    PrintUsage(argv[0]);
    return 1;
    }
    if (argc == 2) {
    try {
    int parsed_port = std::stoi(argv[1]);
    if (parsed_port <= 0 || parsed_port > 65535) {
    std::cerr << "错误: 端口号必须在 1-65535 范围内" << std::endl;
    return 1;
    }
    port = static_cast<uint16_t>(parsed_port);
    } catch (const std::exception& e) {
    std::cerr << "错误: 无效的端口号 '" << argv[1] << "'" << std::endl;
    PrintUsage(argv[0]);
    return 1;
    }
    }

    std::cout << "启动 Reactor 服务器,监听端口: " << port << std::endl;

    // 2. 创建监听连接对象(Listener)
    // Listener 继承自 Connection,在构造时自动完成 socket()、bind()、listen()
    // 使用智能指针管理生命周期,确保异常安全
    std::shared_ptr<Connection> conn = std::make_shared<Listener>(port);

    // 3. 配置监听连接的事件类型
    // EPOLLIN: 监听读事件(新连接到达)
    // EPOLLET: 边缘触发模式,提高性能(可选,也可用默认的LT模式)
    // 注意:监听套接字通常不需要EPOLLOUT事件,这里保留是为了演示
    conn->SetEvents(EPOLLIN | EPOLLET);

    // 4. 创建Reactor反应堆实例
    // Reactor 是核心容器,管理所有连接和事件循环
    // 使用unique_ptr确保Reactor在main结束时正确析构
    std::unique_ptr<Reactor> reactor = std::make_unique<Reactor>();

    // 5. 将监听连接注册到反应堆
    // 这一步完成两个关键操作:
    // a) 将监听套接字注册到epoll内核事件表
    // b) 将连接对象添加到用户态连接管理表
    reactor->AddConnection(conn);

    std::cout << "监听连接已注册到Reactor,开始事件派发循环…" << std::endl;
    std::cout << "按 Ctrl+C 停止服务器" << std::endl;

    // 6. 启动事件派发循环
    // Dispatcher() 是阻塞调用,内部是无限循环
    // 只有在发生致命错误或程序被信号中断时才会返回
    reactor->Dispatcher();

    // 7. 清理资源(智能指针自动管理,这里主要是日志输出)
    std::cout << "服务器正常关闭" << std::endl;
    return 0;
    }

    6.3.2 Main.cc知识图谱

    在这里插入图片描述

    6.3.3 代码详解与设计要点

    1. 命令行参数解析
    • 灵活性:支持通过命令行参数指定监听端口,便于部署和测试。
    • 健壮性:对端口号进行范围校验(1-65535),防止无效输入。
    • 用户友好:提供清晰的用法说明,帮助用户正确启动服务器。
    2. 监听连接的创建与配置
    • 自动初始化:Listener 构造函数内部完成TCP套接字的创建、绑定和监听,符合RAII原则。
    • 事件模式选择:
      • EPOLLIN:监听新连接到达事件(必须)
      • EPOLLET:边缘触发模式(可选,性能更优但编程复杂度高)
      • 实际生产环境中,监听套接字通常只设置 EPOLLIN,EPOLLET 可根据需求选择。
    3. Reactor容器的创建
    • 资源管理:使用 std::unique_ptr<Reactor> 确保Reactor对象在作用域结束时自动释放资源。
    • 单一职责:Reactor专注于连接管理和事件派发,不涉及具体的业务逻辑。
    4. 连接注册流程

    AddConnection(conn) 方法执行两个关键操作:

  • 内核注册:调用 epoll_ctl(EPOLL_CTL_ADD) 将监听套接字添加到epoll实例。
  • 用户态管理:将连接对象指针存入 unordered_map<int, std::shared_ptr<Connection>> 哈希表。
  • 5. 事件派发循环

    Dispatcher() 方法是服务器的核心事件循环:

    • 内部调用 epoll_wait() 阻塞等待事件就绪。
    • 遍历就绪事件列表,根据fd找到对应的Connection对象。
    • 根据事件类型调用对应的虚函数(Recver()、Sender()、Excepter())。
    • 循环持续运行,直到程序被信号中断或发生致命错误。
    6. 错误处理与资源清理
    • 构造函数异常:如果 Listener 或 Reactor 构造函数失败,程序会通过异常退出。
    • 智能指针:使用 shared_ptr 和 unique_ptr 自动管理资源,避免内存泄漏。
    • 信号处理:实际生产代码应添加信号处理(如SIGINT、SIGTERM),实现优雅关闭。

    6.3.4 启动流程时序图

    IOHandler

    内核epoll

    Poller模块

    Reactor容器

    Listener对象

    main函数

    IOHandler

    内核epoll

    Poller模块

    Reactor容器

    Listener对象

    main函数

    #mermaid-svg-IDDtgXJPZAB1imz3{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IDDtgXJPZAB1imz3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IDDtgXJPZAB1imz3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IDDtgXJPZAB1imz3 .error-icon{fill:#552222;}#mermaid-svg-IDDtgXJPZAB1imz3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IDDtgXJPZAB1imz3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IDDtgXJPZAB1imz3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IDDtgXJPZAB1imz3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IDDtgXJPZAB1imz3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IDDtgXJPZAB1imz3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IDDtgXJPZAB1imz3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IDDtgXJPZAB1imz3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IDDtgXJPZAB1imz3 .marker.cross{stroke:#333333;}#mermaid-svg-IDDtgXJPZAB1imz3 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IDDtgXJPZAB1imz3 p{margin:0;}#mermaid-svg-IDDtgXJPZAB1imz3 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-IDDtgXJPZAB1imz3 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-IDDtgXJPZAB1imz3 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-IDDtgXJPZAB1imz3 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-IDDtgXJPZAB1imz3 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-IDDtgXJPZAB1imz3 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-IDDtgXJPZAB1imz3 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-IDDtgXJPZAB1imz3 .sequenceNumber{fill:white;}#mermaid-svg-IDDtgXJPZAB1imz3 #sequencenumber{fill:#333;}#mermaid-svg-IDDtgXJPZAB1imz3 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-IDDtgXJPZAB1imz3 .messageText{fill:#333;stroke:none;}#mermaid-svg-IDDtgXJPZAB1imz3 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-IDDtgXJPZAB1imz3 .labelText,#mermaid-svg-IDDtgXJPZAB1imz3 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-IDDtgXJPZAB1imz3 .loopText,#mermaid-svg-IDDtgXJPZAB1imz3 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-IDDtgXJPZAB1imz3 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-IDDtgXJPZAB1imz3 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-IDDtgXJPZAB1imz3 .noteText,#mermaid-svg-IDDtgXJPZAB1imz3 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-IDDtgXJPZAB1imz3 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-IDDtgXJPZAB1imz3 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-IDDtgXJPZAB1imz3 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-IDDtgXJPZAB1imz3 .actorPopupMenu{position:absolute;}#mermaid-svg-IDDtgXJPZAB1imz3 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-IDDtgXJPZAB1imz3 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-IDDtgXJPZAB1imz3 .actor-man circle,#mermaid-svg-IDDtgXJPZAB1imz3 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-IDDtgXJPZAB1imz3 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    socket()

    bind()

    listen()

    epoll_create()

    alt

    [新连接到达]

    [数据可读/写]

    loop

    [事件循环]

    1. 创建Listener(port)

    2. SetEvents(EPOLLIN|EPOLLET)

    3. 创建Reactor实例

    3.1 创建Poller()

    4. AddConnection(conn)

    4.1 AddEvents(fd, events)

    epoll_ctl(EPOLL_CTL_ADD)

    4.2 存入连接表

    5. Dispatcher()

    5.1 WaitEvents()

    epoll_wait()

    返回就绪事件

    就绪事件列表

    5.2 遍历事件并派发

    Recver() [accept]

    创建IOHandler并注册

    Recver()/Sender()

    6.3.5 扩展建议

  • 配置文件支持:可从配置文件读取端口、线程数、缓冲区大小等参数。
  • 守护进程化:添加 daemon() 调用,使服务器在后台运行。
  • 日志系统集成:将启动信息、错误日志输出到文件或syslog。
  • 性能监控:添加统计模块,记录连接数、吞吐量等指标。
  • 信号处理:捕获SIGINT/SIGTERM实现优雅关闭,释放所有资源。
  • 通过这个完整的主函数实现,Reactor服务器具备了生产环境所需的基本功能:灵活的配置、健壮的错误处理、清晰的启动流程和完整的事件循环机制。

    6.4 编译链接模块:Makefile

    Reactor_server:Main.cc
    g++ -o $@ $^ -std=c++17
    .PHONY:clean
    clean:
    rm -f Reactor_server

    6.5 网络和本地socket转换的类:InetAddr.hpp

    #pragma once

    // 网络和本地socket转换的类

    #include <iostream>
    #include <string>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>

    #define CONV(addr) ((struct sockaddr *)(addr))

    class InetAddr
    {
    public:
    InetAddr()
    {
    }
    // n to h
    InetAddr(struct sockaddr_in &addr) : _net_addr(addr)
    {
    _port = ntohs(_net_addr.sin_port);
    _ip = inet_ntoa(_net_addr.sin_addr);
    }
    InetAddr(uint16_t port, std::string ip = "0.0.0.0")
    : _port(port), _ip(ip)
    {
    _net_addr.sin_family = AF_INET;
    _net_addr.sin_port = htons(_port);
    _net_addr.sin_addr.s_addr = inet_addr(_ip.c_str()); // 等价 INADDR_ANY
    }
    uint16_t Port() { return _port; }
    std::string Ip() { return _ip; }
    struct sockaddr *Addr()
    {
    return CONV(&_net_addr);
    }
    bool operator==(const InetAddr &addr)
    {
    return (_ip == addr._ip) && (_port == addr._port); // ?
    }
    void operator=(const struct sockaddr_in &addr)
    {
    _net_addr = addr;
    _port = ntohs(_net_addr.sin_port);
    _ip = inet_ntoa(_net_addr.sin_addr);
    }
    socklen_t AddrLen()
    {
    return sizeof(_net_addr);
    }
    std::string StringAddress()
    {
    return "[" + _ip + ":" + std::to_string(_port) + "]";
    }
    ~InetAddr()
    {
    }

    private:
    // 本地地址
    uint16_t _port;
    std::string _ip;
    // 网络地址
    struct sockaddr_in _net_addr;
    };


    7 ~> 总结

    7.1 核心考点和易错点

    Reactor模式是高性能网络编程的基石,其本质是将“事件监听、连接管理、IO处理、业务逻辑”四层彻底解耦,通过面向对象的抽象与多态特性,构建出高内聚、低耦合的服务器架构。

    核心考点与易错点总结如下:

  • 原生epoll的核心缺陷:栈缓冲区无法解决粘包半包与多连接隔离问题,这是引入Connection抽象的根本原因。必须理解为什么每个连接需要独立的输入输出缓冲区,以及缓冲区如何配合应用层协议解决粘包问题,这是从demo到工业级代码的第一道门槛。
  • Reactor的设计思想:“先描述,再组织”是贯穿始终的核心。Connection负责描述连接的共性与差异,unordered_map负责组织所有连接,Poller负责封装底层多路复用,Reactor负责协调调度。必须能清晰说出每个组件的职责与协作关系,以及分层设计带来的扩展性优势。
  • 多态的应用:Listener与IOHandler都继承自Connection,事件派发时通过基类指针调用虚函数,无需判断连接类型。这是Reactor模式扩展性的核心来源,也是面试中的高频考点,尤其要理解Sockfd设计为纯虚函数的底层原因。
  • 异常统一处理:将EPOLLERR与EPOLLHUP转换为读写事件的设计思路,是工程化的重要技巧。必须理解这种设计的优势,以及异常最终是如何在连接内部被处理的,能够对比“派发层处理异常”与“连接内部处理异常”的优劣。
  • LT与ET的深度辨析:不仅要记住两种模式的表面差异,更要理解底层的通知机制、对编程的要求、性能差异的根源。尤其要注意ET模式必须使用非阻塞IO、必须循环读取的原因,以及ET模式下漏读数据的严重后果,能够清晰说明两种模式的适用场景与工程取舍。
  • 容器的选择:连接管理采用unordered_map而非普通数组或链表,核心原因是需要通过fd进行O(1)的快速查找,适配事件派发的高性能要求,要能解释哈希表在该场景下的优势与潜在问题。
  • 资源生命周期管理:连接的创建、注册、移除、销毁的完整生命周期是易错点,必须保证从epoll中移除fd与关闭fd的顺序,以及从连接表中移除对象的时机,避免出现野指针、重复关闭或资源泄漏。
  • 掌握以上内容,即可建立起Reactor模式的完整知识体系,不仅能应对考核与面试,更能在实际工程开发中设计出健壮、高性能的网络服务器。

    7.2 Reactor知识图谱

    在这里插入图片描述


    结尾

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

    艾莉丝努力练剑

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


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

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


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

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

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

    “今日练剑达成!”

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

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

    往期回顾:

    【Linux网络】多路转接epoll(二):epoll的两种工作模式

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

    ૮₍ ˶ ˊ ᴥ ˋ˶₎ა

    在这里插入图片描述

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

    评论 抢沙发

    评论前必须登录!