epoll两种事件触发模式有什么区别

水平触发和边缘触发是 epoll 提供的两种事件通知模式,它们在处理文件描述符的 I/O 事件时有不同的行为:

水平触发

  • 默认模式:这是 epoll 的默认工作模式,与传统的 selectpoll 类似。
  • 行为:只要文件描述符上有数据可读、可写或发生错误,epoll_wait 就会返回该文件描述符。即使你没有处理这些事件,它们也会在后续的 epoll_wait 调用中继续返回。
  • 优点:编程相对简单,因为你可以逐步处理事件,不必担心错过任何通知。
  • 缺点:如果处理不当,可能导致低效,因为每次调用 epoll_wait 都可能返回大量已经通知过的文件描述符。

边缘触发

  • 非默认模式:需要在调用 epoll_ctl 时显式指定。
  • 行为:只有在文件描述符的状态发生变化时(例如,从不可读变为可读)才会通知。换句话说,事件只会在状态变化的瞬间被触发。
  • 优点:减少了不必要的通知次数,适合高性能应用,因为它减少了系统调用的次数。
  • 缺点:编程复杂度较高,因为你需要确保在每次事件触发时尽可能多地处理数据(例如,循环读取直到没有数据可读),否则可能会错过后续的数据到达。

使用场景

  • 水平触发适合于简单的应用程序或者不需要极致性能优化的场景,因为它的编程模型相对简单。
  • 边缘触发适合于需要高性能和低延迟的应用程序,比如高并发的网络服务器,但要求开发人员更加小心地处理 I/O 操作,以避免错过事件。

水平触发示例

#include 
#include 
#include 
#include 
#include 
#include 

#define MAX_EVENTS 10  // epoll 实例中最大事件数
#define PORT 8080      // 服务器监听端口

int main() {
    int server_fd, new_socket, epoll_fd;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    struct epoll_event ev, events[MAX_EVENTS];

    // 创建服务器 socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置地址和端口
    address.sin_family = AF_INET;           // 使用 IPv4
    address.sin_addr.s_addr = INADDR_ANY;   // 监听所有接口
    address.sin_port = htons(PORT);         // 设置端口

    // 绑定 socket
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 监听连接
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 创建 epoll 实例
    epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
        perror("epoll_create1");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 添加服务器 socket 到 epoll 实例中
    ev.events = EPOLLIN;  // 水平触发为默认模式
    ev.data.fd = server_fd;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev) == -1) {
        perror("epoll_ctl: server_fd");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 事件循环
    while (true) {
        // 等待事件发生
        int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (nfds == -1) {
            perror("epoll_wait");
            break;
        }

        // 处理每个事件
        for (int n = 0; n < nfds; ++n) {
            if (events[n].data.fd == server_fd) {
                // 接受新的连接
                new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen);
                if (new_socket == -1) {
                    perror("accept");
                    continue;
                }
                // 将新连接添加到 epoll 实例中
                ev.events = EPOLLIN;
                ev.data.fd = new_socket;
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &ev) == -1) {
                    perror("epoll_ctl: new_socket");
                    close(new_socket);
                    continue;
                }
            } else {
                // 处理客户端数据
                char buffer[1024];
                int bytes_read = read(events[n].data.fd, buffer, sizeof(buffer));
                if (bytes_read <= 0) {
                    // 如果读取失败或连接关闭,关闭文件描述符
                    close(events[n].data.fd);
                } else {
                    buffer[bytes_read] = '\0';  // 确保字符串以 null 结尾
                    std::cout << "Received: " << buffer << std::endl;
                }
            }
        }
    }

    close(server_fd);
    return 0;
}

边缘触发示例

要使用边缘触发模式,你需要在设置事件时添加 EPOLLET 标志,并确保在处理事件时读取所有数据。以下是如何修改上述代码以使用边缘触发模式的示例:

// 在添加 socket 到 epoll 实例时,使用 EPOLLET
ev.events = EPOLLIN | EPOLLET;  // 使用边缘触发模式

在处理客户端数据时,需要确保读取所有数据:

while (true) {
    char buffer[1024];
    int bytes_read = read(events[n].data.fd, buffer, sizeof(buffer));
    if (bytes_read <= 0) {
        if (bytes_read == -1 && errno == EAGAIN) {
            // 所有数据已被读取
            break;
        }
        // 关闭连接
        close(events[n].data.fd);
        break;
    }
    buffer[bytes_read] = '\0';
    std::cout << "Received: " << buffer << std::endl;
}

注意

  • 非阻塞模式:在边缘触发模式下,确保 socket 是非阻塞的,以避免在读取或写入时阻塞。你可以使用 fcntl 函数将 socket 设置为非阻塞。
  • 数据读取:在边缘触发模式下,必须在每次事件触发时完全处理所有 I/O 操作,以确保不会遗漏任何数据。

你可能感兴趣的:(服务器,服务器,c++,linux)