epoll是Linux内核为处理大批量文件描述符而设计的I/O多路复用机制,它通过事件驱动的方式工作,不需要像select那样每次调用都遍历所有监听的文件描述符,因此在高并发场景下性能优势十分明显。在C++网络编程中,合理使用epoll可以大幅提升服务端程序的连接处理能力。

epoll核心接口说明
使用epoll需要掌握三个核心系统调用,分别对应epoll实例的创建、事件注册和事件等待:
- epoll_create:创建一个epoll实例,返回一个文件描述符用于后续操作
- epoll_ctl:向epoll实例中注册、修改或删除需要监听的文件描述符和事件
- epoll_wait:等待注册的事件发生,返回就绪的文件描述符和对应事件
epoll事件结构体
epoll的事件通过epoll_event结构体描述,该结构体的定义如下:
#include <sys/epoll.h>
struct epoll_event {
uint32_t events; // 监听的事件类型,比如EPOLLIN、EPOLLOUT等
epoll_data_t data; // 用户自定义数据,通常用来存储文件描述符
};
typedef union epoll_data {
void *ptr;
int fd; // 常用的文件描述符字段
uint32_t u32;
uint64_t u64;
} epoll_data_t;
完整C++使用示例
下面是一个简单的TCP服务端示例,展示如何使用epoll处理多个客户端连接:
#include <iostream>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <vector>
#define MAX_EVENTS 1024
#define BUFFER_SIZE 1024
#define PORT 8888
int main() {
// 创建TCP监听套接字
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) {
std::cerr << "创建socket失败" << std::endl;
return -1;
}
// 设置端口复用
int opt = 1;
if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
std::cerr << "设置端口复用失败" << std::endl;
close(listen_fd);
return -1;
}
// 绑定地址和端口
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
std::cerr << "绑定端口失败" << std::endl;
close(listen_fd);
return -1;
}
// 开始监听
if (listen(listen_fd, 10) == -1) {
std::cerr << "监听失败" << std::endl;
close(listen_fd);
return -1;
}
// 创建epoll实例
int epoll_fd = epoll_create(1);
if (epoll_fd == -1) {
std::cerr << "创建epoll实例失败" << std::endl;
close(listen_fd);
return -1;
}
// 将监听套接字加入epoll监听
struct epoll_event ev;
ev.events = EPOLLIN; // 监听可读事件
ev.data.fd = listen_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) {
std::cerr << "注册监听套接字失败" << std::endl;
close(listen_fd);
close(epoll_fd);
return -1;
}
// 事件数组,用于存储就绪的事件
struct epoll_event events[MAX_EVENTS];
std::cout << "服务端启动,监听端口:" << PORT << std::endl;
while (true) {
// 等待事件发生,超时时间设为-1表示永久等待
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (nfds == -1) {
std::cerr << "epoll_wait出错" << std::endl;
break;
}
// 遍历所有就绪的事件
for (int i = 0; i < nfds; ++i) {
if (events[i].data.fd == listen_fd) {
// 有新客户端连接
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
if (conn_fd == -1) {
std::cerr << "接受连接失败" << std::endl;
continue;
}
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
std::cout << "新客户端连接:" << client_ip << ":" << ntohs(client_addr.sin_port) << std::endl;
// 将新连接的套接字加入epoll监听
ev.events = EPOLLIN | EPOLLET; // 边缘触发模式
ev.data.fd = conn_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev) == -1) {
std::cerr << "注册客户端套接字失败" << std::endl;
close(conn_fd);
}
} else {
// 已连接套接字有事件发生
int conn_fd = events[i].data.fd;
if (events[i].events & EPOLLIN) {
// 可读事件,接收数据
char buffer[BUFFER_SIZE];
ssize_t n = read(conn_fd, buffer, sizeof(buffer) - 1);
if (n <= 0) {
// 客户端关闭连接或出错
std::cout << "客户端断开连接,fd:" << conn_fd << std::endl;
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, conn_fd, nullptr);
close(conn_fd);
} else {
buffer[n] = ' ';
std::cout << "收到数据,fd:" << conn_fd << ",内容:" << buffer << std::endl;
// 回显数据给客户端
write(conn_fd, buffer, n);
}
}
}
}
}
// 关闭文件描述符
close(listen_fd);
close(epoll_fd);
return 0;
}
开发注意事项
触发模式选择
epoll支持两种触发模式,需要根据场景选择:
- 水平触发(LT):默认模式,只要文件描述符处于就绪状态,每次调用
epoll_wait都会返回该事件,编程简单但可能多次触发 - 边缘触发(ET):只有文件描述符状态发生变化时才会触发事件,需要配合非阻塞套接字使用,避免一次事件只读取部分数据导致后续无法触发
非阻塞套接字
如果使用边缘触发模式,必须将套接字设置为非阻塞,否则在读取数据时可能会阻塞在read或write调用上,影响其他连接的处理。设置非阻塞的代码如下:
#include <fcntl.h>
int set_non_blocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) {
return -1;
}
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
事件处理完整性
在处理可读事件时,如果是边缘触发模式,需要循环读取直到返回EAGAIN错误,确保所有数据都被处理,避免遗漏数据。
性能对比
epoll与select、poll的性能对比如下:
| 机制 | 最大监听数 | 事件触发方式 | 高并发性能 |
|---|---|---|---|
| select | 受限于FD_SETSIZE(通常1024) | 轮询遍历所有描述符 | 低 |
| poll | 无硬性限制 | 轮询遍历所有描述符 | 中 |
| epoll | 无硬性限制 | 事件驱动,只返回就绪描述符 | 高 |