C/C++ 结合 epoll 实现对多个文件描述符(FD) 的监听

一、实现思路(多 FD 监听核心步骤)

  1. 创建 epoll 实例:作为管理所有监听 FD 的「总控制器」;
  2. 准备待监听的 FD:比如 TCP 监听 FD(监听网络连接)、标准输入 FD(STDIN_FILENO,监听控制台输入);
  3. 将所有 FD 加入 epoll 实例:为每个 FD 设置要监听的事件(如 EPOLLIN 读事件);
  4. 循环调用 epoll_wait:阻塞等待任意 FD 就绪,返回就绪的事件列表;
  5. 遍历就绪事件:根据 FD 类型(标准输入 / 监听 FD / 客户端 FD)分逻辑处理;
  6. 资源清理:FD 失效时从 epoll 移除并关闭,避免泄漏。

二、完整可运行代码(C/C++ 兼容)

cpp

运行

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>

// 常量定义
#define PORT 8888
#define MAX_EVENTS 1024  // 单次最多处理的就绪事件数
#define BUF_SIZE 1024    // 数据缓冲区大小

// 错误处理封装(简化代码)
void err_exit(const char* msg) {
    perror(msg);
    exit(EXIT_FAILURE);
}

// 向 epoll 添加要监听的 FD
void add_epoll_fd(int epoll_fd, int fd, uint32_t events) {
    struct epoll_event ev;
    ev.events = events;
    ev.data.fd = fd;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) {
        err_exit("epoll_ctl add fd failed");
    }
}

int main() {
    // ===================== 步骤1:创建 epoll 实例 =====================
    int epoll_fd = epoll_create1(0);  // 创建 epoll 句柄,参数 0 即可
    if (epoll_fd == -1) {
        err_exit("epoll_create1 failed");
    }

    // ===================== 步骤2:准备要监听的多个 FD =====================
    // 1. 监听标准输入 FD(STDIN_FILENO = 0):监听控制台输入
    add_epoll_fd(epoll_fd, STDIN_FILENO, EPOLLIN);
    std::cout << "已监听标准输入(控制台输入任意内容按回车触发)\n";

    // 2. 创建 TCP 监听 FD:监听网络连接
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        err_exit("socket create failed");
    }

    // 端口复用(避免程序重启时端口被占用)
    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 绑定端口
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;  // 监听所有网卡
    serv_addr.sin_port = htons(PORT);
    if (bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
        err_exit("bind failed");
    }

    // 开始监听(backlog=5)
    if (listen(listen_fd, 5) == -1) {
        err_exit("listen failed");
    }
    // 将 TCP 监听 FD 加入 epoll
    add_epoll_fd(epoll_fd, listen_fd, EPOLLIN);
    std::cout << "已监听 TCP 端口 " << PORT << "(可通过 telnet 127.0.0.1 8888 测试)\n";

    // ===================== 步骤3:循环监听并处理就绪事件 =====================
    struct epoll_event events[MAX_EVENTS];  // 存放就绪事件的数组
    while (true) {
        // 调用 epoll_wait,阻塞等待 FD 就绪(timeout=-1 永久阻塞)
        int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (nfds == -1) {
            // 处理信号中断(如 Ctrl+C),非真正错误,重试
            if (errno == EINTR) {
                std::cout << "epoll_wait 被信号中断,重试...\n";
                continue;
            }
            err_exit("epoll_wait failed");
        }

        // 遍历所有就绪事件(仅遍历前 nfds 个,epoll 高效的核心)
        for (int i = 0; i < nfds; ++i) {
            int fd = events[i].data.fd;
            uint32_t revents = events[i].events;

            // ------------- 处理1:标准输入 FD 就绪(控制台输入)-------------
            if (fd == STDIN_FILENO) {
                char buf[BUF_SIZE] = {0};
                ssize_t n = read(STDIN_FILENO, buf, sizeof(buf)-1);
                if (n > 0) {
                    std::cout << "[标准输入] 收到内容:" << buf;  // buf 包含换行符,无需额外换行
                    // 若输入 "exit" 则退出程序
                    if (strncmp(buf, "exit", 4) == 0) {
                        std::cout << "收到 exit 指令,程序退出...\n";
                        close(epoll_fd);
                        close(listen_fd);
                        exit(EXIT_SUCCESS);
                    }
                }
            }
            // ------------- 处理2:TCP 监听 FD 就绪(新客户端连接)-------------
            else if (fd == listen_fd) {
                struct sockaddr_in cli_addr;
                socklen_t cli_len = sizeof(cli_addr);
                // 接受新连接
                int conn_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, &cli_len);
                if (conn_fd == -1) {
                    perror("accept failed");
                    continue;
                }
                // 将新客户端 FD 加入 epoll,监听读事件(ET 模式)
                add_epoll_fd(epoll_fd, conn_fd, EPOLLIN | EPOLLET);
                std::cout << "[网络连接] 新客户端接入:" 
                          << inet_ntoa(cli_addr.sin_addr) << ":" 
                          << ntohs(cli_addr.sin_port) << " (fd=" << conn_fd << ")\n";
            }
            // ------------- 处理3:客户端 FD 就绪(有数据可读/连接断开)-------------
            else if (revents & (EPOLLIN | EPOLLERR | EPOLLHUP)) {
                char buf[BUF_SIZE] = {0};
                ssize_t n = read(fd, buf, sizeof(buf)-1);
                if (n > 0) {
                    // 读取到客户端数据,回显
                    std::cout << "[客户端 fd=" << fd << "] 收到数据:" << buf << std::endl;
                    write(fd, buf, n);  // 回显给客户端
                } else {
                    // n<=0:客户端断开或出错,清理资源
                    if (n == 0) {
                        std::cout << "[客户端 fd=" << fd << "] 正常断开连接\n";
                    } else {
                        perror("[客户端 fd=%d] 读取错误");
                    }
                    // 从 epoll 移除 FD 并关闭
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, nullptr);
                    close(fd);
                }
            }
        }
    }

    // 资源清理(实际循环不会走到这里,exit 会提前退出)
    close(epoll_fd);
    close(listen_fd);
    return 0;
}

三、代码关键说明(多 FD 监听核心)

  1. 多 FD 监听的核心:只需通过 epoll_ctl(EPOLL_CTL_ADD) 将所有需要监听的 FD(如 STDIN_FILENOlisten_fdconn_fd)添加到同一个 epoll 实例,epoll 会统一管理这些 FD 的事件。

  2. 不同 FD 的事件区分:通过 events[i].data.fd 区分就绪的 FD 类型(标准输入 / 监听 FD / 客户端 FD),然后分逻辑处理 —— 这是多 FD 监听的关键,确保不同 FD 的事件不会混淆。

  3. ET 模式的使用:客户端 FD 使用 EPOLLIN | EPOLLET(边缘触发),提升高并发下的效率;标准输入和监听 FD 用默认的 LT 模式(水平触发),更易处理。

  4. 错误处理与资源清理

    • 每个 FD 失效时(如客户端断开),必须调用 epoll_ctl(EPOLL_CTL_DEL) 从 epoll 移除,再 close,避免 FD 泄漏;
    • 处理 epoll_waitEINTR 中断(如 Ctrl+C),避免程序误退出。

四、编译与运行步骤

1. 编译(C/C++ 均可)

bash

运行

# C++ 编译(推荐)
g++ epoll_multi_fd.cpp -o epoll_multi_fd
# C 编译(将代码后缀改为 .c,去掉 std:: 前缀,用 printf 替代 cout)
gcc epoll_multi_fd.c -o epoll_multi_fd
2. 运行程序

bash

运行

./epoll_multi_fd
3. 测试多 FD 监听
  • 测试标准输入:在控制台输入任意内容(如 hello),按回车,程序会打印「[标准输入] 收到内容:hello」;输入 exit 可退出程序。
  • 测试网络连接:打开新终端,执行 telnet 127.0.0.1 8888,程序会打印「新客户端接入」;在 telnet 中输入内容,程序会回显,关闭 telnet 则程序提示「客户端断开」。

五、扩展:监听更多类型的 FD

你可以基于这个示例,轻松扩展监听其他类型的 FD:

  • 本地文件 FD:打开文件(open())后,将 FD 加入 epoll(需注意普通文件默认是「一直就绪」,需结合业务逻辑处理);
  • 管道 FD:创建管道(pipe())后,监听读 / 写端 FD;
  • UDP socket FD:创建 UDP socket 后,加入 epoll 监听读事件。

总结

  1. epoll 监听多 FD 的核心是:将所有待监听 FD 通过 epoll_ctl(ADD) 加入同一个 epoll 实例;
  2. 通过 epoll_wait 获取就绪事件后,根据 data.fd 区分 FD 类型,分逻辑处理;
  3. 每个 FD 失效时必须「从 epoll 移除 + 关闭 FD」,避免资源泄漏;
  4. 示例同时监听了「标准输入」和「TCP 监听 FD」,体现了 epoll 对任意 FD 类型的通用监听能力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值