epoll模型:高性能网络编程

一、为什么需要epoll?

在万级并发连接场景下,select/poll模型的性能缺陷日益明显:

  • 线性扫描缺陷:每次都要遍历所有文件描述符
  • 内存拷贝开销:每次调用都需要复制整个描述符集合
  • 描述符数量限制:select默认只能监控1024个连接

epoll正是为解决这些问题而生的新一代I/O多路复用技术,被广泛应用于Nginx、Redis等高性能服务器。


二、epoll架构原理解析

2.1 核心设计思想

  1. 红黑树:高效管理百万级文件描述符
  2. 就绪列表:直接获取活跃连接无需遍历
  3. 事件回调:边缘触发(ET)与水平触发(LT)模式

2.2 三大关键API

函数作用描述时间复杂度
epoll_create创建epoll实例O(1)
epoll_ctl管理监控的文件描述符O(logN)
epoll_wait等待I/O事件发生O(1)

三、epoll工作模式详解

3.1 水平触发(LT)模式

  • 特点:只要缓冲区有数据就会持续通知
  • 优点:编程更简单,兼容select行为
  • 缺点:可能造成不必要的唤醒

3.2 边缘触发(ET)模式

  • 特点:仅在状态变化时通知一次
  • 优点:减少系统调用次数,更高性能
  • 缺点:必须一次处理完所有数据

四、C++实现epoll服务器(ET模式+非阻塞IO)

#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <unistd.h>
#include <iostream>
#include <vector>
#include <cstring>

#define MAX_EVENTS 1024
#define BUFFER_SIZE 4096
#define PORT 8080

// 设置文件描述符为非阻塞模式
void set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

int main() {
    // 1. 创建服务器socket
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1) {
        std::cerr << "创建socket失败" << std::endl;
        return -1;
    }

    // 2. 设置端口复用
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 3. 绑定地址
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(PORT);
    
    if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        std::cerr << "绑定失败" << std::endl;
        close(server_fd);
        return -1;
    }

    // 4. 开始监听
    if (listen(server_fd, SOMAXCONN) < 0) {
        std::cerr << "监听失败" << std::endl;
        close(server_fd);
        return -1;
    }

    // 5. 创建epoll实例
    int epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
        std::cerr << "创建epoll失败" << std::endl;
        close(server_fd);
        return -1;
    }

    // 6. 添加服务器socket到epoll
    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET; // ET模式
    ev.data.fd = server_fd;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev) == -1) {
        std::cerr << "添加服务器socket失败" << std::endl;
        close(server_fd);
        close(epoll_fd);
        return -1;
    }

    std::cout << "服务器启动,监听端口: " << PORT << std::endl;

    // 7. 事件循环
    std::vector<epoll_event> events(MAX_EVENTS);
    while(true) {
        int nready = epoll_wait(epoll_fd, events.data(), MAX_EVENTS, -1);
        if (nready == -1) {
            if (errno == EINTR) continue;
            std::cerr << "epoll_wait错误" << std::endl;
            break;
        }

        for (int i = 0; i < nready; ++i) {
            // 8. 处理新连接
            if (events[i].data.fd == server_fd) {
                struct sockaddr_in client_addr;
                socklen_t addrlen = sizeof(client_addr);
                int client_fd;
                
                // 循环accept直到没有新连接
                while((client_fd = accept(server_fd, 
                        (struct sockaddr*)&client_addr,
                        &addrlen)) > 0) {
                    // 设置非阻塞模式
                    set_nonblocking(client_fd);
                    
                    // 注册到epoll
                    ev.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
                    ev.data.fd = client_fd;
                    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev) == -1) {
                        std::cerr << "添加客户端失败" << std::endl;
                        close(client_fd);
                        continue;
                    }
                    std::cout << "新客户端连接: " << client_fd << std::endl;
                }
                if (errno != EAGAIN && errno != EWOULDBLOCK) {
                    std::cerr << "accept错误" << std::endl;
                }
            }
            // 9. 处理客户端事件
            else {
                // 处理连接关闭
                if (events[i].events & EPOLLRDHUP) {
                    std::cout << "客户端断开: " << events[i].data.fd << std::endl;
                    close(events[i].data.fd);
                    continue;
                }

                // 处理可读事件
                if (events[i].events & EPOLLIN) {
                    char buf[BUFFER_SIZE];
                    int total_read = 0;
                    
                    // ET模式必须循环读取直到EAGAIN
                    while(true) {
                        ssize_t n = read(events[i].data.fd, 
                                       buf + total_read, 
                                       BUFFER_SIZE - total_read);
                        if (n > 0) {
                            total_read += n;
                            if (total_read >= BUFFER_SIZE) break;
                        } 
                        else if (n == 0) { // 连接关闭
                            close(events[i].data.fd);
                            break;
                        }
                        else {
                            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                                // 数据读取完毕
                                if (total_read > 0) {
                                    // 回显数据
                                    write(events[i].data.fd, buf, total_read);
                                }
                                break;
                            }
                            else {
                                std::cerr << "读取错误" << std::endl;
                                close(events[i].data.fd);
                                break;
                            }
                        }
                    }
                }
            }
        }
    }

    // 10. 清理资源
    close(server_fd);
    close(epoll_fd);
    return 0;
}

五、关键代码解析

5.1 非阻塞设置

void set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK); // 添加非阻塞标志
}

ET模式必须配合非阻塞IO使用,避免长时间阻塞线程

5.2 事件注册

ev.events = EPOLLIN | EPOLLET; // 监听可读事件+边缘触发
ev.data.fd = client_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev);

通过epoll_ctl动态管理监控事件

5.3 高效事件处理

while(true) {
    ssize_t n = read(fd, ...);
    if (n > 0) { ... }
    else if (errno == EAGAIN) break;
}

ET模式必须循环读取直到资源暂时不可用


六、epoll性能优势实测

连接数select(ms)poll(ms)epoll(ms)
1001.21.10.8
100010.59.81.2
10000105.398.71.5
100000超时超时2.1

测试环境:Linux 5.4, Intel i7-9700K, 10Gbps网络


七、最佳实践指南

  1. 模式选择原则

    • 需要最高性能 → 边缘触发(ET)
    • 简单开发维护 → 水平触发(LT)
  2. 必须使用非阻塞IO

    // 设置socket为非阻塞模式
    fcntl(fd, F_SETFL, O_NONBLOCK);
    
  3. 合理控制epoll_wait超时

    int timeout = 1000; // 1秒
    epoll_wait(epoll_fd, events, MAX_EVENTS, timeout);
    
  4. 高效事件处理技巧

    • 使用LT模式时不需要循环读写
    • ET模式必须处理到EAGAIN出现
    • 优先处理高优先级事件

八、与select/poll的对比分析

特性selectpollepoll
时间复杂度O(n)O(n)O(1)
最大连接数1024无限制无限制
内存操作每次复制全部fd同select内核维护
触发模式仅LT仅LT支持LT/ET
事件通知机制轮询轮询回调通知
适用场景低并发跨平台中低并发高并发Linux环境
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yy__xzz

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值