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

🎬 个人主页艾莉丝努力练剑
专栏传送门 :《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录
Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享

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


🎬 艾莉丝的简介:


文章目录

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


前言

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

整体学习框架导入语

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

思维导图

bash 复制代码
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后立即处理并回发,代码结构如下:

cpp 复制代码
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功能,但存在两个不可忽视的工程级缺陷:

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

1.2 事件处理逻辑的耦合问题

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

1.3 设计演进的核心方向

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

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

1.4 epoll服务器的多台架构知识图谱


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

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

2.1 一切皆连接的抽象设计

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

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

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

2.2 连接的两大分类

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

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

2.3 多路复用的分层封装

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


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

3.1 Connection基类的接口定义

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

cpp 复制代码
#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监听套接字。

cpp 复制代码
#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连接,负责具体的数据收发与业务处理。

cpp 复制代码
#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等,实现全局错误码规范统一。

cpp 复制代码
#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与连接的交互、执行事件派发。

cpp 复制代码
#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服务器的启动遵循"创建监听连接 -> 创建反应堆 -> 注册监听连接 -> 启动事件循环"的流程,入口代码如下:

cpp 复制代码
#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协同工作:

  1. 内核通过epoll红黑树管理所有被监听的fd,当fd上的事件就绪时,将其加入就绪队列。
  2. 用户态调用epoll_wait获取就绪事件列表,得到一组就绪的fd与对应事件。
  3. 遍历就绪事件列表,根据fd从连接表中找到对应的Connection对象。
  4. 根据事件类型,调用Connection对象对应的Recver、Sender方法。
  5. 处理完成后回到步骤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

c 复制代码
#pragma once

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

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

cpp 复制代码
#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

cpp 复制代码
#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的派生类。

cpp 复制代码
#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是否就绪!

cpp 复制代码
#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

cpp 复制代码
#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

cpp 复制代码
#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服务器的启动入口,负责初始化核心组件、配置服务器参数并启动事件循环。下面详细拆解每个步骤的实现细节与设计考量:

cpp 复制代码
#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:边缘触发模式(可选,性能更优但编程复杂度高)
    • 实际生产环境中,监听套接字通常只设置 EPOLLINEPOLLET 可根据需求选择。
3. Reactor容器的创建
  • 资源管理 :使用 std::unique_ptr<Reactor> 确保Reactor对象在作用域结束时自动释放资源。
  • 单一职责:Reactor专注于连接管理和事件派发,不涉及具体的业务逻辑。
4. 连接注册流程

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

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

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

  • 内部调用 epoll_wait() 阻塞等待事件就绪。
  • 遍历就绪事件列表,根据fd找到对应的Connection对象。
  • 根据事件类型调用对应的虚函数(Recver()Sender()Excepter())。
  • 循环持续运行,直到程序被信号中断或发生致命错误。
6. 错误处理与资源清理
  • 构造函数异常 :如果 ListenerReactor 构造函数失败,程序会通过异常退出。
  • 智能指针 :使用 shared_ptrunique_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 扩展建议

  1. 配置文件支持:可从配置文件读取端口、线程数、缓冲区大小等参数。
  2. 守护进程化 :添加 daemon() 调用,使服务器在后台运行。
  3. 日志系统集成:将启动信息、错误日志输出到文件或syslog。
  4. 性能监控:添加统计模块,记录连接数、吞吐量等指标。
  5. 信号处理:捕获SIGINT/SIGTERM实现优雅关闭,释放所有资源。

通过这个完整的主函数实现,Reactor服务器具备了生产环境所需的基本功能:灵活的配置、健壮的错误处理、清晰的启动流程和完整的事件循环机制。

6.4 编译链接模块:Makefile

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

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

cpp 复制代码
#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处理、业务逻辑"四层彻底解耦,通过面向对象的抽象与多态特性,构建出高内聚、低耦合的服务器架构。

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

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

掌握以上内容,即可建立起Reactor模式的完整知识体系,不仅能应对考核与面试,更能在实际工程开发中设计出健壮、高性能的网络服务器。

7.2 Reactor知识图谱


结尾

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

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ### 艾莉丝努力练剑 C/C++ & Linux 底层探索者 | 一个正在努力练剑的技术博主 *** ** * ** *** 👀 【关注】 跟随我一起深耕技术领域,见证每一次成长。 ❤️ 【点赞】 让优质内容被更多人看见,让知识传递更有力量。 ⭐ 【收藏】 把核心知识点存好,在需要时随时查、随时用。 💬 【评论】 分享你的经验或疑问,评论区一起交流避坑! 不要忘记给博主"一键四连"哦! "今日练剑达成!" "技术之路难免有困惑,但同行的人会让前进更有方向。" |

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

往期回顾

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

🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა