linux网络编程之IO多路复用模型
目录
五、IO多路复用
1. 读事件和写事件
2. select、poll 和 epoll区别
3. select
1)底层工作流程
2)相关函数
3)触发方式
4)代码实现
5)缺点
4. poll
1)底层工作流程
2)相关函数
3)触发方式
4)代码实现
5)优、缺点
5、阻塞与非阻塞IO
1) 阻塞 I/O(Blocking)
2) 非阻塞 I/O(Non-blocking)
3) 函数调用行为差异
4) 设置非阻塞
5) 使用非阻塞 I/O 的注意事项
6、epoll
1) 底层工作流程
2)相关函数
3)触发方式
4)阻塞IO代码实现
5)非阻塞IO代码实现
5)优、缺点
五、IO多路复用
I/O 多路复用使得程序能同时监听多个文件描述符(一个线程/进程同时处理多个 socket 的事件),能够提高程序的性能,Linux下实现I/O 多路复用的系统调用主要有 select、poll 和 epoll。
✅ 优势:
减少线程/进程数量,降低上下文切换开销
实现高并发、点对多连接管理
1. 读事件和写事件
读事件:
1)已连接队列中有已经准备好的socket(即有新的客户端请求连接)
2)接收缓存区中有数据可读(即对端数据已到达)
3)连接断开(即对端调用close()函数关闭连接)
写事件:
1)发送缓存区没有满,可以写入数据(即可以向对端发送数据)
2. select、poll 和 epoll区别
特性 | select | poll | epoll (Linux 特有) |
---|---|---|---|
最大连接数限制 | 有限制(默认1024,可修改) | 理论无限(由系统资源限制) | 理论无限(高效支持大量并发) |
数据结构 | 位图数组(fd_set) | 动态数组(结构体数组) | 内核维护的红黑树 + 就绪链表 |
触发方式 | 水平触发(LT) | 水平触发(LT) | 支持 水平(LT) 与 边沿(ET) |
机制 | 每次复制fd集合进内核,内核线性遍历 | 传入fd数组,内核线性扫描 | 内核维护就绪链表,回调机制 |
每次调用是否需传入所有 FD | 是 | 是 | 否,只需一次注册 |
内核与用户态交互 | 每次都需将全部 fd 拷贝进出内核 | 同上 | 注册时拷贝一次,后续不再拷贝 |
性能 | O(n) | O(n) | O(1)(就绪事件直接通知) |
3. select
select
是一种 I/O 多路复用机制,用于在一个线程中同时监听多个文件描述符(fd)的状态(是否可读、可写、是否异常),一旦有一个或多个 fd 就绪,程序就可以进行相应处理。
适用于:低并发 或 跨平台兼容性要求强的场景(如 Linux 和 Windows 均支持)。
1)底层工作流程
select
模型核心思路:
用户构建需要的
fd_set
集合:readfds
、writefds
、exceptfds
,填入关心的 fd;调用
select()
进入内核态;内核遍历每个 fd,检查其状态;
就绪的 fd 被标记出来;
返回到用户态,程序通过
FD_ISSET
查哪些 fd 就绪;再处理这些 fd。
📌 特点:
属于 水平触发(Level Trigger);
每次调用
select()
都要重新构建fd_set
;内核遍历所有 fd,时间复杂度为 O(n);
最大监听 fd 数有限(默认 1024,
FD_SETSIZE
);
用户态
┌────────────────────────────────────────┐
│ 1. 设置 fd_set │
│ 2. 调用 select() --> 进入内核 │
└────────────────────────────────────────┘
│
▼
内核态
┌────────────────────────────────────────────────────┐
│ 1. 拷贝 fd_set │
│ 2. 为每个 fd 建立 wait queue │
│ 3. 检查缓冲区是否就绪 │
│ 若都未就绪,当前进程进入休眠 wait │
│ 4. 某 fd 状态变化 → 事件到达 → 唤醒 wait queue 中的进程 │
│ 5. 设置结果 fd_set,仅保留就绪的 │
└────────────────────────────────────────────────────┘
│
▼
用户态
┌────────────────────────────────────────┐
│ 1. 返回就绪 fd 数量 │
│ 2. 使用 FD_ISSET(fd, &readfds) 判断具体 │└────────────────────────────────────────┘
2)相关函数
fd_set
操作宏(位图操作)
fd_set readfds; // 创建bitmap,大小为128字节(1024位);
void FD_CLR(int fd, fd_set *set); // 将socket对应bitmap中标志位置为0(删除);
int FD_ISSET(int fd, fd_set *set); // 判断socket是否在bitmap, 不在返回0, 在返回>0
void FD_SET(int fd, fd_set *set); // 将socket加入到bitmap中(标志位置1);
void FD_ZERO(fd_set *set); // 初始化bitmap,把bitmap的每一位都置为0
- select()
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);- 参数:- nfds : 委托内核检测的最大文件描述符的值 + 1- 内核需要线性遍历这些集合中的文件描述符,这个值是循环结束的条件- 在Window中这个参数是无效的,指定为-1即可- readfds : 要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读的属性- 一般检测读事件- 对应的是对方发送过来的数据,因为读是被动的接收数据,检测的就是读缓冲区- 是一个传入传出参数- writefds : 要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写的属性- 委托内核检测写缓冲区是不是还可以写数据(不满的就可以写)- exceptfds : 检测发生异常的文件描述符的集合(一般不会用到)- timeout : 设置的超时时间struct timeval {long tv_sec; /* seconds */long tv_usec; /* microseconds */};- NULL : 永久阻塞,直到检测到了文件描述符有变化- tv_sec = 0 tv_usec = 0, 不阻塞- tv_sec > 0 tv_usec > 0, 阻塞对应的时间- 返回值 :- -1 : 失败- -0 : 超时- >0 (n) : 检测的集合中有n个文件描述符发生了变化
3)触发方式
水平触发(Level Trigger,LT):
- select()监视的socket如果发生了事件,select()会返回(通知应用程序处理事件);
- 如果事件没有被处理或没有处理完成,再次调用select()的时候,会立即优先处理上次没有处理的事件或接着处理上次没有完成的事件。
4)代码实现
int main() {tcpServer server;// 初始化服务器,监听8080端口,返回监听socketint listen_fd = server.initServer(8080);if (listen_fd < 0) {cerr << "Server initialization failed." << endl;return -1;}fd_set readfds; // 用于监听读事件的socket集合,大小为16字节(1024位)的bitmap;FD_ZERO(&readfds); // 初始化readfds,把bitmap的每一位都置为0;FD_SET(listen_fd, &readfds); // 把服务端监听的socket加入readfds集合;int maxfd = listen_fd; // 当前所有监听的socket中最大的fd// 设置select的阻塞超时时间为10秒struct timeval timeout{};timeout.tv_sec = 10; timeout.tv_usec = 0;while (true) {cout << "Waiting for client connection..." << endl;// 因select会修改传入的fd_set,仅保留就绪的fd,因此需拷贝一份传给selectfd_set tmpfds = readfds; // 调用select周期性检测所有文件描述符int infds = select(maxfd + 1, &tmpfds, nullptr, nullptr, &timeout);if (infds < 0) {cerr << "select() failed" << endl;break;} else if (infds == 0) {cerr << "select() timeout" << endl;continue;}// 遍历所有fd,查看哪个发生了事件for (int eventfd = 0; eventfd <= maxfd; ++eventfd) {if (FD_ISSET(eventfd, &tmpfds) == 0) continue; // 如果eventfd在bitmap中为0,表示没有事件// 如果就绪的是监听新连接的fd,说明有新的客户端连接请求if (eventfd == listen_fd) { int client_fd = server.acceptClient(); // 接收客户端连接请求if (client_fd != -1) {FD_SET(client_fd, &readfds); // 把用于服务端与客户端通信的fd添加进fd_setmaxfd = max(maxfd, client_fd); // 更新maxfd的值}} else { // 否则说明接收缓冲区有数据可读,或连接已关闭,处理所有已连接客户端对应的任务char buffer[1024] = {0}; // 存放客户端发来的数据int len = server.recvData(eventfd, buffer, sizeof(buffer));if (len <= 0) {// 表示客户端断开连接或读取错误cout << "client eventfd " << eventfd << " disconnect" << endl;server.closeSocket(eventfd); // 关闭客户端socketFD_CLR(eventfd, &readfds); // 从监听集合中移除该socket// 如果断开的是最大fd,重新查找最大fdif (eventfd == maxfd) {for (int i = maxfd - 1; i >= 0; --i) {if (FD_ISSET(i, &readfds)) {maxfd = i;break;}}}} else { // 有客户端发送数据过来cout << "client eventfd " << eventfd << " data is received: " << buffer << endl;server.sendData(eventfd, buffer, strlen(buffer)); // 把数据回传给客户端}}}}// 最后关闭监听socket,清理资源server.closeSocket(listen_fd);return 0;
}
5)缺点
4. poll
1)底层工作流程
poll
模型核心逻辑:
用户构建一个
pollfd[]
数组,每个元素表示一个监听的 fd 和感兴趣的事件;调用
poll()
,进入内核态;内核扫描所有 fd,检测是否满足事件;
返回到用户态,程序读取结果并处理。
📌 特点:
不限制监听 fd 的最大数(受系统资源限制);
每次调用都需要重新传递整个数组;
仍然需要遍历所有 fd,时间复杂度 O(n);
同样为 水平触发(Level Trigger) 模型。
用户态
┌────────────────────────────────────────┐
│ 1. 构造 pollfd[] │
│ 2. 调用 poll() --> 进入内核 │
└────────────────────────────────────────┘
│
▼
内核态
┌────────────────────────────────────────────────────┐
│ 1. 遍历数组中的 socket fd │
│ 2. 判断是否满足 events 指定的可读/可写条件 │
│ 3. 设置对应 revents 字段 │
│ 4. 有事件 → 返回就绪数量,没有事件 → 等待或超时 │
└────────────────────────────────────────────────────┘
│
▼
用户态
┌────────────────────────────────────────┐
│ 1. 返回值为就绪 fd 个数 │
│ 2. 遍历数组 → 判断 revents 字段 │
└────────────────────────────────────────┘
2)相关函数
pollfd
结构体定义
struct pollfd {int fd; // 监听的文件描述符short events; // 关注的事件short revents; // 返回的就绪事件
};
- poll()
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
--参数--fds: 监听的 pollfd 数组;--nfds: 数组大小;--timeout: 超时时间,单位毫秒,特殊值:--返回值--0:立即返回;--1:无限阻塞直到事件发生。
-
事件标志位
事件名 | 含义 |
---|---|
POLLIN | 有数据可读 |
POLLOUT | 可写入数据 |
POLLERR | 错误发生 |
POLLHUP | 对端关闭连接 |
POLLNVAL | 描述符未打开或非法 |
3)触发方式
同select。
4)代码实现
int main() {tcpServer server;// 初始化服务器,监听8080端口,返回监听socketint listen_fd = server.initServer(8080);if (listen_fd < 0) {cerr << "Server initialization failed." << endl;return -1;}std::vector<pollfd> fds;// poll监听listen_fd的读事件// POLLIN表示读事件 ,POLLOUT表示写事件pollfd pfd = {listen_fd, POLLIN, 0}; // POLLIN | POLLOUT 为读写事件fds.push_back(pfd);while (true) {cout << "Waiting for client connection..." << endl;// 调用poll周期性检测所有文件描述符int infds = poll(fds.data(), fds.size(), 5000);if (infds < 0) {cerr << "poll() failed" << endl;break;} else if (infds == 0) {cerr << "poll() timeout" << endl;continue;}// 遍历所有fd,查看哪些发生了事件for (size_t i = 0; i < fds.size(); ++i) {if (fds[i].fd < 0) continue; // 如果fds[i].fd为-1,表示没有事件,忽略if ((fds[i].revents & POLLIN) == 0) continue; //如果没有读事件, continue// 如果就绪的是监听新连接的fd,说明有新的客户端连接请求if(fds[i].fd == listen_fd){int client_fd = server.acceptClient(); // 接收客户端连接请求if (client_fd != -1) {pollfd pfd = {client_fd, POLLIN, 0};fds.push_back(pfd); // 把用于服务端与客户端通信的fd添加进fds}} else { // 否则说明接收缓冲区有数据可读,或连接已关闭,处理所有已连接客户端对应的任务char buffer[1024] = {0}; // 存放客户端发来的数据int len = server.recvData(fds[i].fd, buffer, sizeof(buffer));if (len <= 0) { // 表示客户端断开连接或读取错误 cout << "client eventfd " << fds[i].fd<< " disconnect" << endl;server.closeSocket(fds[i].fd); // 关闭客户端socketfds[i].fd= -1; // 修改客户端socket对应数组位置元素为-1, poll将忽略它} else { // 有客户端发送数据过来cout << "client eventfd " << fds[i].fd<< " data is received: " << buffer << endl;// 返还回去server.sendData(fds[i].fd, buffer, strlen(buffer)); // 把数据回传给客户端}}// 清除无效 fd(fd == -1)fds.erase(std::remove_if(fds.begin(), fds.end(),[](const pollfd& p) { return p.fd == -1; }), fds.end());}}// 最后关闭监听socket,清理资源server.closeSocket(listen_fd);return 0;
}
5)优、缺点
- 优点
优点 | 说明 |
---|---|
无最大连接数限制 | 取决于内存和内核配置 |
数据结构更灵活 | 使用结构体数组代替位图 |
支持更多事件 | 可监听 POLLERR、POLLHUP 等 |
- 缺点
缺点 | 说明 |
---|---|
每次调用都复制整个数组 | 用户态和内核态之间拷贝开销大 |
仍需线性遍历所有 fd | 时间复杂度 O(n),低效 |
水平触发机制 | 与 select 相同,低效处理大并发 |
5、阻塞与非阻塞IO
1) 阻塞 I/O(Blocking)
所谓“阻塞”,是指调用某个 I/O 操作时,调用线程会被挂起,直到操作完成后才返回。
-
典型例子:
recv()
、accept()
等函数 在没有数据时会一直等待;send()在发送缓冲区满了后也会阻塞
。 -
过程:系统调用 → 内核等待数据就绪 → 数据到达 → 从内核拷贝到用户空间 → 返回 → 线程恢复执行。
recv
() 举例:
2) 非阻塞 I/O(Non-blocking)
“非阻塞”意味着调用 I/O 函数时,如果条件不满足(如没有数据),会立即返回错误(例如 errno = EAGAIN)而不是挂起线程。
-
用户程序可以自主决定何时再次调用,而不被卡住。
-
过程:系统调用 → 如果内核数据未就绪 → 立即返回 EAGAIN / EWOULDBLOCK → 用户可以继续做别的事,稍后再试。
3) 函数调用行为差异
常见错误码:
EAGAIN/EWOULDBLOCK
:非阻塞 IO,无可用数据;
EINTR
:调用被信号中断;
ECONNRESET
:连接被重置;
ENOTCONN
:未连接时调用 send/recv。
调用函数 | 阻塞IO行为 | 非阻塞IO行为 |
---|---|---|
accept() | 没有连接请求则阻塞 | 没有请求则返回 -1 并设置 errno = EAGAIN |
read() / recv() | 没有数据可读则阻塞 | 没有数据时立即返回 -1,errno = EAGAIN |
write() / send() | 缓冲区满则阻塞 | 缓冲区满时返回 -1,errno = EAGAIN |
4) 设置非阻塞
方式一:直接创建非阻塞IO
// 创建 IPv4 TCP 套接字,非阻塞模式 listen_fd_ = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);// 接受新连接,返回新的客户端 fd,设置非阻塞 conn_fd = accept4(listen_fd_, (sockaddr*)&client_addr, &len, SOCK_NONBLOCK);
方式二:将已经创建的IO设置为非阻塞
使用
fcntl()
函数设置fd为非阻塞// 获取当前属性 int flags = fcntl(fd, F_GETFL, 0); // 设置为非阻塞 fcntl(fd, F_SETFL, flags | O_NONBLOCK);
5) 使用非阻塞 I/O 的注意事项
⚠️必须检查返回值
所有
recv()/write()/accept()
都要判断返回值是否是-1
,并检查errno == EAGAIN
。⚠️*必须用循环读写
在
EPOLLET
模式中,如果数据没读完,epoll
不会再通知,你就永远收不到剩下的数据了!⚠️非阻塞本身不等于异步
非阻塞是“马上返回”,不是“后台完成”。异步 I/O(如
aio_read
)是由内核来完成整个任务,并通过回调/信号返回结果。⚠️避免 busy loop(忙轮询)
非阻塞配合
epoll_wait()
、select()
使用,否则你得写一个轮询循环,占用 CPU。
6、epoll
epoll
是 Linux 内核为处理大量文件描述符提供的一种 事件通知机制,采用 事件驱动模型,相比 select
/poll
实现了更高效的 I/O 事件监听。
1) 底层工作流程
基本流程:
用户调用
epoll_create
创建一个epoll
实例(内核中是一个红黑树+就绪链表结构)。使用
epoll_ctl
注册/修改/删除要监听的文件描述符及其关心的事件(如 EPOLLIN 可读、EPOLLOUT 可写)。内核内部将这些 fd 保存在一棵红黑树中,便于快速查找。
用户调用
epoll_wait
等待事件触发,内核将就绪的 fd 放入就绪链表,避免遍历所有 fd。高效关键点:
fd 就绪时会自动被内核加入就绪链表,
epoll_wait
直接从该列表返回,避免了poll
的 O(n) 线性遍历。
┌──────────────────────────────────────────────┐
│ 1. 创建 epoll 实例:epoll_create() │
│ 2. 注册监听 fd:epoll_ctl(ADD, fd, events) │
│ 3. 等待事件触发:epoll_wait() │
└──────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────┐
│ 1. 内核为每个 fd 建立事件回调机制(不是每次轮询) │
│ 2. 当 fd 上有事件发生时(可读/可写),驱动层触发回调 │
│ 3. 内核将就绪事件放入就绪链表(ready list) │
│ 4. epoll_wait() 检查 ready list→ 返回就绪事件 │
└────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 1. epoll_wait() 返回就绪的 fd 和事件 │
│ 2. 遍历 events[] → 根据 events[i].data.fd 处理 │
└──────────────────────────────────────────────┘
2)相关函数
epoll_event
结构体定义
typedef union epoll_data {void *ptr; // 存放指针(可指向连接对象、上下文结构体等)int fd; // 存放文件描述符(最常用)uint32_t u32; uint64_t u64;
} epoll_data_t;struct epoll_event {uint32_t events; // 事件类型(EPOLLIN、EPOLLOUT等)epoll_data_t data; // 用户自定义数据
};
- 函数定义
// 创建 epoll 实例
int epoll_create(int size); // size 已废弃,但需要>0
int epoll_create1(int flags); // 推荐// 注册/修改/删除 fd
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);--op 可以是:EPOLL_CTL_ADD:添加新的 fdEPOLL_CTL_MOD:修改已存在的 fdEPOLL_CTL_DEL:删除 fd--event.events 常用:EPOLLIN:可读EPOLLOUT:可写EPOLLET:边缘触发(Edge Triggered)EPOLLONESHOT:只监听一次事件// 等待事件
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); --epfd:创建的epollfd--events:返回就绪的事件列表,列表大小为epoll_wait的返回值--maxevents:最多返回的events个数--timeout:超时时间,-1:表示无限等待0:表示立即返回>0:表示正常超时返回时间--返回值:0:表示超时时间范围内无就绪事件列表返回-1:表示错误,通过errno来识别具体错误信息>0: 就绪的事件列表的大小
3)触发方式
✅ LT 模式 (水平触发)
支持阻塞和非阻塞的socket。select、poll只有水平触发,epoll默认是水平触发,可以设置为边沿触发。
读事件:如果 epoll_wait()触发了读事件,表示有数据可读,如果程序没有把数据读完,再次调用 epoll_wait() 的时候,将立即再次触发读事件。
写事件:如果发送缓冲区没有满,表示可以写入数据,只要发送缓冲区没有被写满,再次调用 epoll_wait() 的时候,将会立即触发写事件。✅ ET 模式(边沿触发)
是高速工作方式,只支持非阻塞的socket。
读事件: (边沿触发情况下,accept和recv需要在循环中调用)。如果 epoll_wait() 触发了读事件后,不管程序有没有处理读事件epoll_wait() 都不会再触发读事件,只有当新的数据到达时,才会再次触发读事件。
写事件: epoll_wait()触发了写事件后,如果发送缓冲区仍可以写(发送缓冲区没有满),epoll_wait()不会再次触发写事件,只有当发送缓冲区"由满变为不满"时,才会触发写事件。
4)阻塞IO代码实现
int main() {tcpServer server;// 初始化服务器,监听8080端口,返回监听socketint listen_fd = server.initServer(8080);if (listen_fd < 0) {cerr << "Server initialization failed." << endl;return -1;}// 创建epoll句柄int epollfd = epoll_create(1);epoll_event ev{}; // 声明事件的数据结构ev.data.fd = listen_fd; // 指定事件的自定义数据,会随着epoll_wait()返回的事件一并返回ev.events = EPOLLIN; // 让epoll监视listen_fd的读事件// 把需要监视的fd加入到epollfd中epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_fd, &ev);std::vector<epoll_event> evs(1024);; // 存放epoll返回的事件,自定义大小,这里定义10个while (true) {// 等待被监视的fd有读事件发生int infds = epoll_wait(epollfd, evs, 10, -1);if(infds < 0){cerr << "epoll() is failed" << endl;}else if(infds == 0){cerr << "epoll() is timeout" << endl;}// 遍历所有fd,查看哪些发生了事件for(int i = 0; i <infds; ++i){// 如果就绪的是监听新连接的fd,说明有新的客户端连接请求if(evs[i].data.fd == listen_fd){// 连接int client_fd = server.acceptClient();if (client_fd != -1) {epoll_event ev_client{};ev_client.events = EPOLLIN;ev_client.data.fd = client_fd;// 将用于服务端与客户端通信的fd添加进epoll监听epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev_client);}}else{ // 否则说明接收缓冲区有数据可读,或连接已关闭,处理所有已连接客户端对应的任务char buffer[1024] = {0}; // 存放客户端发来的数据int len = server.recvData(evs[i].data.fd, buffer, sizeof(buffer));if (len <= 0) { // 表示客户端断开连接或读取错误cout << "client eventfd " << evs[i].data.fd << " disconnect" << endl;server.closeSocket(evs[i].data.fd); // 关闭客户端socket// 从epollfd中删除客户端的socket,如果socket被关闭了,会自动从epollfd中删除,所以,以下代码不必启用// epoll_ctl(epollfd, EPOLL_CTL_DEL, evs[i].data.fd, 0);} else { // 有客户端发送数据过来cout << "client eventfd " << evs[i].data.fd << " data is received: " << buffer << endl;// 返还回去server.sendData(evs[i].data.fd, buffer, strlen(buffer)); // 把数据回传给客户端}}}}// 最后关闭监听socket,清理资源server.closeSocket(listen_fd);return 0;
}
5)非阻塞IO代码实现
修改项 | 内容 |
---|---|
✅ 设置 listen_fd 和 client_fd 为非阻塞 | 防止 accept 和 read 阻塞 |
✅ 所有 read() 操作用 while 循环读取直到 EAGAIN | 确保边缘触发不漏事件 |
✅ 监听事件添加 EPOLLET 边缘触发标志 | 提升性能(少一次 wakeup) |
✅ accept() 使用循环,直到返回 EAGAIN | 多连接同时到达不会遗漏 |
// 设置非阻塞
void setNonBlocking(int fd) {int flags = fcntl(fd, F_GETFL, 0);fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}int main() {// 初始化服务器,监听端口int listen_fd = server.initServer(8080);if (listen_fd < 0) {cerr << "Server initialization failed." << endl;return -1;}setNonBlocking(listen_fd); // 设置监听socket为非阻塞int epollfd = epoll_create(1);epoll_event ev{};ev.data.fd = listen_fd;ev.events = EPOLLIN | EPOLLET; // 设置为边缘触发epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_fd, &ev);std::vector<epoll_event> evs(1024); // 存放epoll返回的事件while (true) {int infds = epoll_wait(epollfd, evs.data(), 1024, -1);if (infds < 0) {cerr << "epoll() failed" << endl;break;}for (int i = 0; i < infds; ++i) {int fd = evs[i].data.fd;if (fd == listen_fd) {// 循环 accept,直到没有连接可处理(EAGAIN)while (true) {int client_fd = server.acceptClient();if (client_fd == -1) break;setNonBlocking(client_fd); // 设置客户端 socket 为非阻塞epoll_event ev_client{};ev_client.data.fd = client_fd;ev_client.events = EPOLLIN | EPOLLET;epoll_ctl(epollfd, EPOLL_CTL_ADD, client_fd, &ev_client);}} else {// 客户端通信,使用循环读取直到没有数据while (true) {char buffer[1024] = {0};int len = server.recvData(fd, buffer, sizeof(buffer));if (len < 0) {if (errno == EAGAIN || errno == EWOULDBLOCK)break; // 没有更多数据可读else {cerr << "recv error on fd " << fd << endl;server.closeSocket(fd);break;}}else {cout << "client " << fd << " data received: " << buffer << endl;server.sendData(fd, buffer, strlen(buffer)); // 回传}}}}}server.closeSocket(listen_fd);return 0;
}
5)优、缺点
- 优点
特点 | 描述 |
---|---|
高性能 | 基于就绪链表返回,不遍历所有 fd,适用于高并发 |
支持边缘触发 | 提升通知效率 |
支持大规模 fd | 不受 FD_SETSIZE 限制 |
内核支持优化 | 内核 2.6 后引入,专为性能设计 |
- 缺点
问题 | 描述 |
---|---|
编程复杂 | ET 模式需小心处理读写完整性 |
仅支持 Linux | 可移植性较差 |
用户需管理 fd 生命周期 | 如忘记 epoll_ctl(DEL) 会内存泄漏 |