Linux IO 多路转接--Epoll
epoll 工作原理详解
epoll 是 Linux 高效 I/O 多路复用的核心机制,其设计通过 红黑树 + 就绪链表 实现高性能事件管理。以下是其核心工作原理:
epoll 的核心数据结构
(1) eventpoll 结构体(epoll 实例)
每个 epoll 实例(由 epoll_create 创建)对应一个 eventpoll 结构体,包含:
struct eventpoll {
struct rb_root rbr; // 红黑树根节点(存储所有监控的 fd)
struct list_head rdlist; // 就绪链表(存放已触发的事件)
};
- 红黑树 (
rbr):存储所有通过epoll_ctl注册的文件描述符(fd),以epitem为节点。 - 就绪链表 (
rdlist):存放所有已触发事件的epitem。
(2) epitem 结构体(事件项)
每个被监控的 fd 对应一个 epitem:
struct epitem {
struct rb_node rbn; // 红黑树节点
struct list_head rdllink; // 就绪链表节点
struct epoll_filefd ffd; // 关联的 fd 信息
struct eventpoll *ep; // 所属的 epoll 实例
struct epoll_event event; // 监控的事件类型(如 EPOLLIN)
};
epoll 的工作流程
步骤 1:创建 epoll 实例
int epfd = epoll_create1(0);
- 内核创建
eventpoll结构体,初始化红黑树 (rbr) 和就绪链表 (rdlist)。
步骤 2:注册监控事件
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
- 将
fd封装为epitem节点。 - 将
epitem插入红黑树(rbr)。 - 内核为
fd注册回调函数ep_poll_callback,当fd有事件发生时,该回调会被触发。
步骤 3:事件触发与回调
- 当
fd发生事件(如数据到达),内核调用ep_poll_callback:- 将对应的
epitem从红黑树移到就绪链表 (rdlist)。 - 唤醒阻塞在
epoll_wait的进程。
- 将对应的
步骤 4:获取就绪事件
epoll_wait(epfd, events, maxevents, timeout);
- 检查就绪链表 (
rdlist):- 若不为空,将事件拷贝到用户空间。
- 若为空,进程阻塞等待。
- 返回就绪事件的数量。
关键设计优势
| 组件 | 作用 | 时间复杂度 |
|---|---|---|
红黑树 (rbr) |
存储所有监控的 fd,支持快速查找、插入、删除 |
O(log n) |
就绪链表 (rdlist) |
存放已触发的事件,epoll_wait 直接读取此链表 |
O(1) |
| 回调机制 | 事件发生时通过回调函数 ep_poll_callback 将 epitem 加入就绪链表,避免轮询 |
O(1) |
为什么高效呢?
红黑树管理监控的 fd
- 插入/删除/查找的时间复杂度为 O(log n),适合管理大量 fd。
- 自动去重(重复添加的 fd 会被识别)。
就绪链表直接返回事件
epoll_wait只需检查链表是否为空,无需遍历所有 fd。
回调驱动
- 事件发生时立即通知,无需轮询所有 fd(对比
select/poll的 O(n) 复杂度)。
- 事件发生时立即通知,无需轮询所有 fd(对比
对比 select/poll
| 特性 | select/poll | epoll |
|---|---|---|
| 监控 fd 数量 | 有限制(如 1024) | 无限制(仅受系统内存限制) |
| 时间复杂度 | O(n)(每次遍历所有 fd) | O(1)(仅处理就绪事件) |
| fd 管理方式 | 每次调用需重新传入 fd 列表 | 内核维护红黑树,只需增量操作 |
| 适用场景 | 低并发、fd 数量少 | 高并发(如 Nginx、Redis) |
边缘触发(ET) vs 水平触发(LT)
ET 模式:
事件仅触发一次,需非阻塞 I/O + 循环读/写,避免数据残留。ev.events = EPOLLIN | EPOLLET; // 边缘触发LT 模式:
只要条件满足(如缓冲区有数据),会持续触发事件(默认模式)。
总结
- epoll 通过红黑树 + 就绪链表实现高效事件管理。
- 回调机制避免轮询,适合高并发场景。
- ET 模式性能更高,但需正确处理数据。
- 核心操作:
epoll_create:创建实例。epoll_ctl:管理监控的 fd。epoll_wait:获取就绪事件。
Epoll 接口介绍
epoll_create 函数详解
epoll_create 是 Linux 系统提供的 I/O 多路复用 机制的核心函数之一,用于创建一个 epoll 实例(即 epoll 文件描述符),后续可以通过 epoll_ctl 和 epoll_wait 管理并监控多个文件描述符(如 socket)的 I/O 事件。
函数原型
#include <sys/epoll.h>
int epoll_create(int size); // Linux 2.6.8 之前需要指定 size
int epoll_create1(int flags); // 更现代的版本
参数说明
size(已废弃,但需 > 0)
早期内核用此参数预估监控的文件描述符数量,但自 Linux 2.6.8 起,内核会动态调整大小,此参数仅需传递一个大于 0 的值(通常填 1)。flags(仅epoll_create1)
可选标志位:0:默认行为(同epoll_create)。EPOLL_CLOEXEC:设置文件描述符的close-on-exec标志(进程执行exec时自动关闭epoll实例)。
返回值
- 成功:返回一个
epoll文件描述符(非负整数),后续通过此 fd 操作epoll实例。 - 失败:返回
-1,并设置errno(如EMFILE表示进程打开的文件描述符已达上限)。
核心作用
创建
epoll实例
内核会分配一个事件表(红黑树 + 就绪链表),用于存储和管理待监控的文件描述符。返回文件描述符
该 fd 用于后续的epoll_ctl(增删改监控事件)和epoll_wait(等待事件触发)。
使用示例
int epfd = epoll_create1(0); // 创建 epoll 实例
if (epfd == -1) {
perror("epoll_create1");
exit(1);
}
// 后续通过 epoll_ctl 添加监控的 fd
// 通过 epoll_wait 等待事件
注意事项
size参数已过时
在较新内核中,size仅需传递一个正整数(如 1),实际监控数量由内核动态管理。务必关闭
epoll实例
使用完毕后应调用close(epfd)释放资源,否则会导致文件描述符泄漏。优先使用
epoll_create1epoll_create1(0)等价于epoll_create(1)。epoll_create1(EPOLL_CLOEXEC)可避免exec后的 fd 泄漏。
性能优势
epoll比select/poll更高效,尤其适合高并发场景(如 Web 服务器)。
常见问题
Q1: 为什么 epoll_create 的 size 参数不再有用?
- 早期内核用
size预分配空间,但现代内核改为动态调整,只需传递一个合法值(如 1)。
Q2: epoll_create 和 epoll_create1 的区别?
epoll_create1支持EPOLL_CLOEXEC标志,更安全且符合现代编程习惯。
Q3: epoll 实例的数量限制?
- 受系统全局限制(
/proc/sys/fs/epoll/max_user_instances),默认通常足够大。
在 Debian 中文件名叫 max_user_watches
╭─ljx@VM-16-15-debian ~/linux_review/web_io/Epoll
╰─➤ cat /proc/sys/fs/epoll/max_user_watches
408820
总结
| 关键点 | 说明 |
|---|---|
| 函数作用 | 创建 epoll 实例,用于高效监控大量文件描述符的 I/O 事件。 |
| 推荐函数 | 优先使用 epoll_create1(0) 或 epoll_create1(EPOLL_CLOEXEC)。 |
| 返回值 | 成功返回 epoll 文件描述符,失败返回 -1。 |
| 后续操作 | 通过 epoll_ctl 管理监控列表,epoll_wait 等待事件。 |
| 资源释放 | 必须调用 close(epfd) 关闭 epoll 实例。 |
epoll_ctl 函数详解
epoll_ctl 是 Linux epoll 机制的核心函数之一,用于 管理 epoll 实例监控的文件描述符(fd)。它负责向 epoll 实例 添加、修改或删除 需要监控的 fd 及其关注的事件(如可读、可写、异常等)。
函数原型
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
epfd |
int |
epoll 实例的文件描述符(由 epoll_create 创建) |
op |
int |
操作类型: • EPOLL_CTL_ADD(添加 fd)• EPOLL_CTL_MOD(修改 fd)• EPOLL_CTL_DEL(删除 fd) |
fd |
int |
需要监控的目标文件描述符(如 socket fd) |
event |
struct epoll_event* |
监控的事件配置(EPOLL_CTL_DEL 时可设为 NULL) |
struct epoll_event 结构体
struct epoll_event {
uint32_t events; // 监控的事件类型(如 EPOLLIN、EPOLLOUT)
epoll_data_t data; // 用户数据(通常用于存储 fd 或回调信息)
};
typedef union epoll_data {
void *ptr;
int fd; // 常用:关联的文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;
事件类型(events 字段)
events 是一个位掩码,常用标志包括:
| 事件类型 | 说明 |
|---|---|
EPOLLIN |
监控 fd 是否可读(有数据到达) |
EPOLLOUT |
监控 fd 是否可写(发送缓冲区空闲) |
EPOLLERR |
监控 fd 是否发生错误(默认自动监控,无需显式设置) |
EPOLLHUP |
监控 fd 是否被挂断(如对端关闭连接) |
EPOLLET |
设置为边缘触发(Edge-Triggered)模式(默认是水平触发 LT) |
EPOLLONESHOT |
事件触发后自动从监控列表中移除(需重新 EPOLL_CTL_MOD 才能再次监控) |
返回值
- 成功:返回
0。 - 失败:返回
-1,并设置errno,常见错误:EBADF:epfd或fd是无效文件描述符。EEXIST:EPOLL_CTL_ADD时fd已存在。ENOENT:EPOLL_CTL_MOD或EPOLL_CTL_DEL时fd未注册。
使用示例
(1) 添加监控的 socket
int epfd = epoll_create1(0); // 创建 epoll 实例
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建 socket
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 监控可读事件 + 边缘触发
ev.data.fd = sockfd; // 保存 socket fd
if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev) == -1) {
perror("epoll_ctl");
exit(1);
}
(2) 修改监控事件
ev.events = EPOLLIN | EPOLLOUT; // 增加监控可写事件
epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);
(3) 删除监控的 fd
epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL); // event 参数可忽略
关键机制
高效的事件管理
epoll_ctl通过内核维护的红黑树管理监控的 fd,插入/删除/查找的时间复杂度为 O(log n)。边缘触发(ET) vs 水平触发(LT)
- ET 模式(
EPOLLET):事件仅在状态变化时触发一次,需非阻塞 I/O + 循环读/写。 - LT 模式(默认):只要条件满足(如缓冲区有数据),会持续触发事件。
- ET 模式(
epoll_data的用途
可通过data.fd或data.ptr在事件触发时快速定位关联对象(如 socket 或回调函数)。
完整工作流程
1. epoll_create() → 创建 epoll 实例
2. epoll_ctl() → 添加/修改/删除监控的 fd
3. epoll_wait() → 阻塞等待事件就绪
4. 处理事件 → 根据 event.data 找到对应 fd 处理 I/O
5. 循环 2-4 → 持续监控
常见问题
Q1: 为什么 EPOLLERR 和 EPOLLHUP 无需显式设置?
- 内核会自动监控这些事件,即使未设置,
epoll_wait也会返回它们。
Q2: EPOLLONESHOT 的作用?
- 确保事件只触发一次(如避免多线程同时处理同一个 fd),需重新
EPOLL_CTL_MOD后才能再次触发。
Q3: 如何监控多个事件?
- 用位或操作组合:
ev.events = EPOLLIN | EPOLLOUT | EPOLLET;。
总结
| 操作 | 函数 | 作用 |
|---|---|---|
| 创建 epoll 实例 | epoll_create1 |
初始化事件监控表 |
| 管理监控的 fd | epoll_ctl |
增删改 fd 及其关注的事件 |
| 等待事件 | epoll_wait |
获取就绪的 fd 列表 |
epoll_wait 函数详解
epoll_wait 是 Linux epoll 机制的核心函数,用于 阻塞等待并获取 I/O 就绪的事件。它是实现高并发事件驱动的关键,相比 select/poll 具有更高的效率。
函数原型
#include <sys/epoll.h>
int epoll_wait(
int epfd, // epoll 实例的文件描述符
struct epoll_event *events, // 存放就绪事件的数组
int maxevents, // 每次最多返回的事件数量
int timeout // 超时时间(毫秒)
);
参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
epfd |
int |
epoll 实例的文件描述符(由 epoll_create 创建) |
events |
struct epoll_event* |
输出参数,用于存储就绪事件的数组 |
maxevents |
int |
每次调用最多返回的事件数量(必须 ≤ events 数组的大小) |
timeout |
int |
超时时间(毫秒): • -1:无限阻塞• 0:立即返回(非阻塞)• >0:指定超时时间 |
返回值
- 成功:返回就绪事件的数量(≥0)。
- 失败:返回
-1,并设置errno(如EINTR表示被信号中断)。
核心工作原理
epoll_wait 的工作流程如下:
(1) 检查就绪链表
- 内核首先检查
eventpoll结构体中的 就绪链表 (rdlist)。- 如果链表 非空,说明有事件已触发,直接将这些事件拷贝到用户空间的
events数组。 - 如果链表 为空,则根据
timeout决定是否阻塞等待。
- 如果链表 非空,说明有事件已触发,直接将这些事件拷贝到用户空间的
(2) 阻塞与唤醒
阻塞:若就绪链表为空,且
timeout != 0,当前线程/进程会被阻塞,直到:- 有事件触发(通过回调函数
ep_poll_callback将事件加入rdlist)。 - 超时时间到达。
- 被信号中断(返回
EINTR)。
- 有事件触发(通过回调函数
唤醒:当监控的
fd发生事件时,内核调用ep_poll_callback:- 将对应的
epitem从红黑树移到rdlist。 - 唤醒阻塞在
epoll_wait的线程/进程。
- 将对应的
(3) 返回就绪事件
- 将
rdlist中的事件拷贝到events数组,并返回就绪事件的数量。 - 时间复杂度:O(1)(直接遍历就绪链表,无需扫描所有监控的
fd)。
使用示例
#include <sys/epoll.h>
#include <unistd.h>
int main() {
int epfd = epoll_create1(0); // 创建 epoll 实例
struct epoll_event ev, events[10];
// 添加监控的 fd(示例为标准输入)
ev.events = EPOLLIN; // 监控可读事件
ev.data.fd = STDIN_FILENO;
epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);
while (1) {
// 等待事件(阻塞直到有输入)
int n = epoll_wait(epfd, events, 10, -1);
if (n == -1) {
perror("epoll_wait");
break;
}
// 处理就绪事件
for (int i = 0; i < n; i++) {
if (events[i].data.fd == STDIN_FILENO) {
char buf[1024];
read(STDIN_FILENO, buf, sizeof(buf));
printf("Input: %s", buf);
}
}
}
close(epfd);
return 0;
}
关键特性
(1) 高效的事件获取
- 直接访问就绪链表:无需遍历所有监控的
fd(对比select/poll的 O(n) 复杂度)。 - 零拷贝:内核将就绪事件从
rdlist直接拷贝到用户空间。
(2) 支持高并发
- 仅处理实际触发的事件,适合管理 数十万并发连接(如 Nginx、Redis)。
(3) 触发模式
- 水平触发(LT,默认):
只要fd满足就绪条件(如缓冲区有数据),epoll_wait会持续返回该事件。 - 边缘触发(ET):
仅在fd状态变化时触发一次,需配合非阻塞 I/O 循环读/写。
常见问题
Q1: epoll_wait 和 select/poll 的区别?
| 对比项 | select/poll |
epoll_wait |
|---|---|---|
| 时间复杂度 | O(n)(遍历所有 fd) | O(1)(仅处理就绪事件) |
| fd 数量限制 | 有限制(如 1024) | 无限制(受系统内存约束) |
| 效率 | 低(每次需重新传入 fd 集合) | 高(内核维护监控列表) |
Q2: 为什么 epoll_wait 需要指定 maxevents?
- 防止用户空间
events数组溢出(内核最多返回maxevents个事件)。
Q3: 如何实现非阻塞调用?
- 设置
timeout = 0,此时epoll_wait会立即返回(无论是否有事件)。
总结
| 关键点 | 说明 |
|---|---|
| 作用 | 等待并返回就绪的 I/O 事件 |
| 高效原因 | 基于就绪链表(O(1) 时间复杂度),无需轮询 |
| 触发模式 | 水平触发(LT,默认)或边缘触发(ET) |
| 超时控制 | -1(阻塞)、0(非阻塞)、>0(毫秒超时) |
| 适用场景 | 高并发服务器(如 Nginx、Redis) |
基于 epoll 的多人聊天服务器实现
和基于 poll 的实现大多相同,但在服务端的是线上有本质区别,前者需要自己管理客户端对应的 fd,而后者则不需要。
首先我们封装一下 Epoll 接口
#ifndef _EPOLL_HPP_
#define _EPOLL_HPP_ 1
#include <iostream>
#include <sys/epoll.h> // epoll相关系统调用
#include "nocopy.hpp" // 禁止拷贝的基类
#include "log.hpp" // 日志模块
// 默认的epoll_wait超时时间(毫秒)
inline const int default_time_out = 3000;
// 错误码枚举
enum {
epoll_create_error = 1, // epoll创建失败的错误码
};
/**
* @brief Epoll事件管理类(继承nocopy禁止拷贝)
* @note 封装epoll_create/epoll_ctl/epoll_wait操作
*/
class Epoll: public nocopy // 禁止拷贝构造和赋值
{
public:
/**
* @brief 构造函数:创建epoll实例
* @note 失败时直接退出程序(严重错误)
*/
Epoll()
: timeout(default_time_out) // 初始化超时时间
{
// 创建epoll实例(参数0表示默认行为)
epfd = epoll_create1(0);
if(epfd == -1) {
lg(Error, "epoll_create false, errno: %d, errstr: %s", errno, strerror(errno));
exit(epoll_create_error); // 创建失败直接退出
}
lg(Info, "epoll create success, epoll fd: %d", epfd);
}
/**
* @brief 等待就绪事件
* @param events 输出参数,存放就绪事件的数组
* @param num 数组的最大容量
* @return 就绪事件的数量(失败返回-1)
*/
int EpollWait(struct epoll_event events[], int num) {
int n = epoll_wait(epfd, events, num, timeout); // 阻塞等待事件
if(n == -1) {
lg(Error, "epoll_wait false, errno: %d, errstr: %s", errno, strerror(errno));
}
return n; // 返回就绪事件数
}
/**
* @brief 控制epoll监控的文件描述符
* @param op 操作类型(EPOLL_CTL_ADD/MOD/DEL)
* @param fd 目标文件描述符
* @param event 监控的事件标志(EPOLLIN/EPOLLOUT等)
*/
void EpollCtl(int op, int fd, uint32_t event) {
if(op == EPOLL_CTL_DEL) {
// 删除操作不需要event参数
if(epoll_ctl(epfd, op, fd, nullptr) == -1) {
lg(Error, "epoll control false, errno: %d, errstr: %s", errno, strerror(errno));
}
} else {
// 添加/修改操作需要设置event
struct epoll_event ev;
ev.data.fd = fd; // 保存fd到用户数据字段
ev.events = event; // 设置监控的事件类型
if(epoll_ctl(epfd, op, fd, &ev)) {
lg(Error, "epoll control false, errno: %d, errstr: %s", errno, strerror(errno));
}
}
}
/**
* @brief 析构函数:关闭epoll实例
*/
~Epoll() {
close(epfd); // 释放epoll文件描述符
}
private:
int epfd; // epoll实例的文件描述符
int timeout; // epoll_wait的超时时间(毫秒)
};
#endif
设计说明
禁止拷贝(继承nocopy)
- 通过继承禁止拷贝构造和赋值,避免多个对象管理同一个epoll实例导致资源冲突。
RAII管理资源
- 构造函数创建epoll实例,析构函数自动关闭,避免资源泄漏。
错误处理
- 关键操作(如
epoll_create1)失败时直接退出,非关键操作(如epoll_ctl)仅记录错误。
- 关键操作(如
接口封装
EpollWait:封装epoll_wait,返回就绪事件数。EpollCtl:统一处理EPOLL_CTL_ADD/MOD/DEL操作,简化调用。
利用这份包装,我们就可以非常方便地实现基于 epoll 的服务器了。
#ifndef _POLL_SERVER_HPP_
#define _POLL_SERVER_HPP_ 1
#include <iostream>
#include <poll.h>
#include <sys/socket.h>
#include <unordered_map> // 用于存储客户端信息的哈希表
#include "tcp.hpp" // TCP基础操作封装
#include "epoll.hpp" // Epoll封装类
// 常量定义
constexpr const size_t max_fd_sz = 1 << 10; // 最大文件描述符数量(1024)
const u_int16_t default_port = 8888; // 默认服务端口
const int default_fd = -1; // 无效文件描述符标识
const int none_event = 0; // 无事件标志
// 客户端信息结构体
struct client_inf {
std::string client_addr; // 客户端IP地址
u_int16_t client_port; // 客户端端口号
};
/**
* @brief 基于Epoll的TCP服务器类
* @note 使用Epoll实现高并发事件处理
*/
class EpollServer {
public:
/**
* @brief 构造函数
* @param port 服务器监听端口(默认8888)
*/
EpollServer(u_int16_t port = default_port)
: port_(port) // 初始化端口
{
}
/**
* @brief 初始化服务器
* @note 创建监听socket并加入epoll监控
*/
void Init() {
// 1. 创建监听socket
listen_sockfd = tcp::Socket();
lg(Info, "listening sock create success, sockfd: %d", listen_sockfd);
// 2. 绑定端口
tcp::Bind(listen_sockfd, port_);
lg(Info, "listening sock bind success");
// 3. 开始监听
tcp::Listen(listen_sockfd);
// 4. 将监听socket加入epoll监控(关注可读事件)
ep.EpollCtl(EPOLL_CTL_ADD, listen_sockfd, EPOLLIN);
}
/**
* @brief 启动服务器主循环
*/
void Start() {
while (true) {
// 等待事件(recvs数组用于接收就绪事件)
int n = ep.EpollWait(recvs, max_fd_sz);
if (n == 0) {
lg(Info, "None client join and no client send message");
} else {
lg(Info, "Get a new link");
Dispatcher(n); // 处理就绪事件
}
}
}
private:
/**
* @brief 事件分发器
* @param size 就绪事件数量
*/
void Dispatcher(int size) {
for (int i = 0; i < size; ++i) {
uint32_t events = recvs[i].events;
int fd = recvs[i].data.fd;
if (events & EPOLLIN) { // 可读事件
if (fd == listen_sockfd) {
Accept(size); // 处理新连接
} else {
Recv(i); // 处理客户端数据
}
}
}
}
/**
* @brief 接受新客户端连接
* @param size 当前就绪事件数量(未使用)
*/
void Accept(int size) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
// 接受新连接
int client_sockfd = accept(listen_sockfd, (sockaddr *)&client, &len);
if (client_sockfd == -1) {
lg(Error, "listening sock accept false, [%d]: %s", errno, strerror(errno));
return;
}
// 获取客户端地址信息
std::string client_addr;
u_int16_t client_port;
tcp::GetAddrAndPort(client, client_addr, client_port);
// 存储客户端信息
clients[client_sockfd] = client_inf{
client_addr: client_addr,
client_port: client_port
};
lg(Info, "accept a new client [%s: %d]", client_addr.c_str(), client_port);
// 将新socket加入epoll监控
ep.EpollCtl(EPOLL_CTL_ADD, client_sockfd, EPOLLIN);
}
/**
* @brief 处理客户端数据
* @param client_pos 就绪事件数组中的位置
*/
void Recv(int client_pos) {
int sockfd = recvs[client_pos].data.fd;
// 接收数据
ssize_t n = recv(sockfd, client_buffer, sizeof(client_buffer) - 1, 0);
if (n == -1) { // 接收错误
lg(Error, "recv false, errno: %d, errstr: %s", errno, strerror(errno));
} else if (n == 0) { // 客户端断开连接
lg(Info, "client [%s: %d] quit, bye!",
clients[sockfd].client_addr.c_str(),
clients[sockfd].client_port);
// 从epoll监控中移除并清理客户端信息
ep.EpollCtl(EPOLL_CTL_DEL, sockfd, -1);
clients.erase(sockfd);
} else { // 正常收到数据
client_buffer[n] = 0; // 添加字符串结束符
// 构造广播消息(格式:[IP:Port]# 消息内容)
std::string out_buffer = "[" + clients[sockfd].client_addr + ": " +
std::to_string(clients[sockfd].client_port) + "]# " +
client_buffer;
// 广播给所有客户端
for (auto &[fd, inf] : clients) {
if (send(fd, out_buffer.c_str(), out_buffer.size(), 0) == -1) {
lg(Error, "send data to client [%s: %d] false",
inf.client_addr.c_str(), inf.client_port);
}
}
}
}
private:
int listen_sockfd; // 监听socket文件描述符
u_int16_t port_; // 服务端口号
std::unordered_map<int, client_inf> clients; // 客户端信息表(fd -> 信息)
struct epoll_event recvs[max_fd_sz]; // 就绪事件数组
char client_buffer[1024]; // 数据接收缓冲区
Epoll ep; // Epoll实例
};
#endif
设计说明
Epoll核心机制
- 使用
Epoll类封装epoll_create/epoll_ctl/epoll_wait操作。 - 监听socket和客户端socket均通过
EPOLL_CTL_ADD加入监控。
- 使用
事件处理流程
EpollWait获取就绪事件 →Dispatcher分发事件 →Accept或Recv处理。
客户端管理
- 使用
unordered_map存储客户端信息(fd为键,client_inf为值)。 - 客户端断开时自动清理资源(
EPOLL_CTL_DEL+erase)。
- 使用
广播功能
- 收到消息后,遍历
clients表向所有客户端发送数据。
- 收到消息后,遍历
错误处理
- 关键操作(如
accept、recv)均记录错误日志。
- 关键操作(如
最终实现效果和 poll 版本的完全相同