
🎬 个人主页 :艾莉丝努力练剑
❄专栏传送门 :《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 ~> 事件派发机制与异常统一处理)
-
- [4.1 事件派发的核心流程](#4.1 事件派发的核心流程)
- [4.2 异常事件的统一处理方案](#4.2 异常事件的统一处理方案)
- [4.3 事件派发机制和异常处理的知识图谱](#4.3 事件派发机制和异常处理的知识图谱)
- [5 ~> LT与ET触发模式的底层原理与工程差异](#5 ~> LT与ET触发模式的底层原理与工程差异)
- [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功能,但存在两个不可忽视的工程级缺陷:
- 第一,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作为所有连接的抽象基类,定义了统一的接口与公共成员,其核心实现如下:
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协同工作:
- 内核通过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
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(×tamp, &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:边缘触发模式(可选,性能更优但编程复杂度高)- 实际生产环境中,监听套接字通常只设置
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
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处理、业务逻辑"四层彻底解耦,通过面向对象的抽象与多态特性,构建出高内聚、低耦合的服务器架构。
核心考点与易错点总结如下:
- 原生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的两种工作模式
🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა
