Linux IO 多路转接--Epoll

Linux IO 多路转接--Epoll

八月 02, 2025 次阅读

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);
  1. fd 封装为 epitem 节点。
  2. epitem 插入红黑树(rbr)。
  3. 内核为 fd 注册回调函数 ep_poll_callback,当 fd 有事件发生时,该回调会被触发。

步骤 3:事件触发与回调

  • fd 发生事件(如数据到达),内核调用 ep_poll_callback
    1. 将对应的 epitem 从红黑树移到就绪链表 (rdlist)。
    2. 唤醒阻塞在 epoll_wait 的进程。

步骤 4:获取就绪事件

epoll_wait(epfd, events, maxevents, timeout);
  1. 检查就绪链表 (rdlist):
    • 若不为空,将事件拷贝到用户空间。
    • 若为空,进程阻塞等待。
  2. 返回就绪事件的数量。

关键设计优势

组件 作用 时间复杂度
红黑树 (rbr) 存储所有监控的 fd,支持快速查找、插入、删除 O(log n)
就绪链表 (rdlist) 存放已触发的事件,epoll_wait 直接读取此链表 O(1)
回调机制 事件发生时通过回调函数 ep_poll_callbackepitem 加入就绪链表,避免轮询 O(1)

为什么高效呢?

  1. 红黑树管理监控的 fd

    • 插入/删除/查找的时间复杂度为 O(log n),适合管理大量 fd。
    • 自动去重(重复添加的 fd 会被识别)。
  2. 就绪链表直接返回事件

    • epoll_wait 只需检查链表是否为空,无需遍历所有 fd。
  3. 回调驱动

    • 事件发生时立即通知,无需轮询所有 fd(对比 select/poll 的 O(n) 复杂度)。

对比 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 模式
    只要条件满足(如缓冲区有数据),会持续触发事件(默认模式)。


总结

  1. epoll 通过红黑树 + 就绪链表实现高效事件管理
  2. 回调机制避免轮询,适合高并发场景。
  3. ET 模式性能更高,但需正确处理数据。
  4. 核心操作
    • epoll_create:创建实例。
    • epoll_ctl:管理监控的 fd。
    • epoll_wait:获取就绪事件。

Epoll 接口介绍

epoll_create 函数详解

epoll_create 是 Linux 系统提供的 I/O 多路复用 机制的核心函数之一,用于创建一个 epoll 实例(即 epoll 文件描述符),后续可以通过 epoll_ctlepoll_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 表示进程打开的文件描述符已达上限)。

核心作用

  1. 创建 epoll 实例
    内核会分配一个事件表(红黑树 + 就绪链表),用于存储和管理待监控的文件描述符。

  2. 返回文件描述符
    该 fd 用于后续的 epoll_ctl(增删改监控事件)和 epoll_wait(等待事件触发)。


使用示例

int epfd = epoll_create1(0);  // 创建 epoll 实例
if (epfd == -1) {
    perror("epoll_create1");
    exit(1);
}

// 后续通过 epoll_ctl 添加监控的 fd
// 通过 epoll_wait 等待事件

注意事项

  1. size 参数已过时
    在较新内核中,size 仅需传递一个正整数(如 1),实际监控数量由内核动态管理。

  2. 务必关闭 epoll 实例
    使用完毕后应调用 close(epfd) 释放资源,否则会导致文件描述符泄漏。

  3. 优先使用 epoll_create1

    • epoll_create1(0) 等价于 epoll_create(1)
    • epoll_create1(EPOLL_CLOEXEC) 可避免 exec 后的 fd 泄漏。
  4. 性能优势
    epollselect/poll 更高效,尤其适合高并发场景(如 Web 服务器)。


常见问题

Q1: 为什么 epoll_createsize 参数不再有用?

  • 早期内核用 size 预分配空间,但现代内核改为动态调整,只需传递一个合法值(如 1)。

Q2: epoll_createepoll_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,常见错误:
    • EBADFepfdfd 是无效文件描述符。
    • EEXISTEPOLL_CTL_ADDfd 已存在。
    • ENOENTEPOLL_CTL_MODEPOLL_CTL_DELfd 未注册。

使用示例

(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 参数可忽略

关键机制

  1. 高效的事件管理
    epoll_ctl 通过内核维护的红黑树管理监控的 fd,插入/删除/查找的时间复杂度为 O(log n)。

  2. 边缘触发(ET) vs 水平触发(LT)

    • ET 模式EPOLLET):事件仅在状态变化时触发一次,需非阻塞 I/O + 循环读/写。
    • LT 模式(默认):只要条件满足(如缓冲区有数据),会持续触发事件。
  3. epoll_data 的用途
    可通过 data.fddata.ptr 在事件触发时快速定位关联对象(如 socket 或回调函数)。


完整工作流程

1. epoll_create()   → 创建 epoll 实例
2. epoll_ctl()      → 添加/修改/删除监控的 fd
3. epoll_wait()     → 阻塞等待事件就绪
4. 处理事件          → 根据 event.data 找到对应 fd 处理 I/O
5. 循环 2-4         → 持续监控

常见问题

Q1: 为什么 EPOLLERREPOLLHUP 无需显式设置?

  • 内核会自动监控这些事件,即使未设置,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

    1. 将对应的 epitem 从红黑树移到 rdlist
    2. 唤醒阻塞在 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_waitselect/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

设计说明

  1. 禁止拷贝(继承nocopy)

    • 通过继承禁止拷贝构造和赋值,避免多个对象管理同一个epoll实例导致资源冲突。
  2. RAII管理资源

    • 构造函数创建epoll实例,析构函数自动关闭,避免资源泄漏。
  3. 错误处理

    • 关键操作(如epoll_create1)失败时直接退出,非关键操作(如epoll_ctl)仅记录错误。
  4. 接口封装

    • 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

设计说明

  1. Epoll核心机制

    • 使用Epoll类封装epoll_create/epoll_ctl/epoll_wait操作。
    • 监听socket和客户端socket均通过EPOLL_CTL_ADD加入监控。
  2. 事件处理流程

    • EpollWait获取就绪事件 → Dispatcher分发事件 → AcceptRecv处理。
  3. 客户端管理

    • 使用unordered_map存储客户端信息(fd为键,client_inf为值)。
    • 客户端断开时自动清理资源(EPOLL_CTL_DEL + erase)。
  4. 广播功能

    • 收到消息后,遍历clients表向所有客户端发送数据。
  5. 错误处理

    • 关键操作(如acceptrecv)均记录错误日志。

最终实现效果和 poll 版本的完全相同