Linux下的三种 IO 复用
目录
一、Select
1、函数 API
2、使用限制
3、使用 Demo
二、Poll
三、epoll
0、 实现原理
1、函数 API
2、简单代码模板
3、LT/ET 使用过程
(1)LT 水平触发
(2)ET边沿触发
4、使用 Demo
四、参考链接
一、Select
在 Linux 中,select 就是一种经典的 I/O 复用机制。它允许服务器在一个线程内监控多个 I/O 事件(比如多个客户端的连接状态)。当服务器调用 select(),它会依次“询问”每个连接是否有事件发生,如果有事件发生了就立即处理。这样,服务器不需要为每个连接创建线程,使用单线程就可以服务于多个客户端,从而节省了资源,提升了效率。
1、函数 API

在实际使用 select 时,我们会用到几个重要的函数和宏,分别是 select() 本身,以及操作 fd_set 结构的 FD_ZERO、FD_SET、FD_CLR、FD_ISSET 等宏函数。
#include <sys/select.h>
/* select() 是 I/O 复用的核心函数,用来等待多个文件描述符的状态变化。参数说明 :nfds :要监控的文件描述符的数量,通常是 fd_set 中最大的文件描述符值加 1。readfds :监控是否有数据可读的文件描述符集合。writefds :监控是否有数据可写的文件描述符集合。exceptfds:监控异常事件的文件描述符集合。timeout :超时时间,NULL 表示无限等待,超时后 select 返回 0。返回值:成功时,返回就绪的文件描述符的总数。出错时,返回 -1,并设置 errno 以指示错误类型。
*/
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);/*在 select 中,我们使用 fd_set 结构来标记哪些文件描述符需要被监控。fd_set 是一个位数组(bitmap),每个位代表一个文件描述符的位置。如果某个位被设置为 1,表示我们希望 select 监控这个文件描述符。这里有几个重要的宏函数,用于操作 fd_set。
*/
// 将 fd_set 清空,所有位清零。
FD_ZERO(&fd_set)
// 将指定的文件描述符 fd 加入 fd_set,即把 fd_set 中 fd 的位设置为 1。
FD_SET(fd, &fd_set)
// 将指定的文件描述符 fd 从 fd_set 中移除,即把 fd_set 中 fd 的位清零。
FD_CLR(fd, &fd_set)
// 检查 fd_set 中指定的文件描述符 fd 是否被设置为 1,若为 1 表示该文件描述符有事件发生。
FD_ISSET(fd, &fd_set)
2、使用限制
- 连接数限制:
select在大部分系统中最多支持 1024 个连接,如果 fd 并发特别多,可以考虑poll或epoll(强烈推荐,更适合高并发场景)。

- 函数返回:
select()返回 IO 就绪的 fd 个数,而且参数 fd_set 将被刷新,只记录准备就绪的 IO 的 fd,未就绪的 fd 将被移除。这块很容易混淆,若 fd_set 不是一次性的,建议在执行 select 之前进行备份,每次执行 select 时使用临时变量传参。
# 1、监听 fd 集合:3、4、5内核空间 fd_set 结构
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 1 | 1 | 1 | 0 |...|
+---+---+---+---+---+---+---+---+0 1 2 3 4 5 6 ...
(内核监控文件描述符 3、4、5 的状态)# 2、执行 select 后,只有 fd 4 准备就绪,
# 则其余 fd 在 fd_set 中全部被剔除(置为 0)内核空间 fd_set 更新
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 | 1 | 0 | 0 |...|
+---+---+---+---+---+---+---+---+0 1 2 3 4 5 6 ...
(仅文件描述符 4 发生事件,保留 1)
- 灵活阻塞:
select本身是阻塞的,也可传参 timeval 变量,设置阻塞事件,该时间可以根据业务场景合理安排。若时间太小,则浪费 CPU 资源,CPU 会无故的频繁切换内核态和用户态;若时间太长,又可能无法及时处理 IO 时间。
3、使用 Demo
下面代码实现了基于 select 的多并发服务器。
int tcp_Server_Select(int argc, char **args)
{char server_ip[MAX_IP_LENGTH] = "127.0.0.1";uint16_t server_port = 8088;// 可自定义服务器绑定的 IP 与 端口if ( argc >= 1 ){strcpy(server_ip, args[1]);}if ( argc >= 2 ){server_port = atoi(args[2]);}// 记录客户端 fd int clients_fd[FD_SETSIZE - 2];int max_fd = -1, clients_count = -1;// select 监听的 fd 列表,其中 set_tmp 是负责传参,poll_set 负责全局fd_set set_tmp, poll_set;for ( int i = 0; i < FD_SETSIZE - 2; i++ ){clients_fd[i] = -1;}//创建 TCP 监听套接字int listen_fd = socket(AF_INET, SOCK_STREAM, 0);if ( listen_fd < 0 ){log_error("Create Socket fd Failed");printf("Create Server FD Failed\n");return FAILURE;}//服务器端口复用int yes = 1;setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));//给服务器 socket 绑定 ip 和端口信息struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip);int result = bind(listen_fd, (struct sockaddr *)&server, sizeof(server));if (result == -1){log_error("Failed to bind Server Net Address");printf("Failed to bind Server Net Address");return FAILURE;}// 调用listenlisten(listen_fd, 10);// select fd_set 置空FD_ZERO(&poll_set);// 将服务器的监听 fd 也添加到 fd_set 中,负责监听是否有新的客户端接入FD_SET(listen_fd, &poll_set);max_fd = listen_fd;printf("TCP Server Listen On %s:%hu with fd %d\n", server_ip, server_port, listen_fd);while(1){// tmp 变量只在本次循环有效,所以需要使用 poll_set 保存变量,每次循环开始重新赋值。set_tmp = poll_set;int ready_count = select(max_fd + 1, &set_tmp, NULL, NULL, NULL);if ( ready_count < 0 ){printf("Failed to execute select\n");log_error("Failed to execute select");break;}else if ( ready_count > 0 ){// 先检查是否有新的 TCP 客户端接入printf("ready count %d\n", ready_count);if ( FD_ISSET(listen_fd, &set_tmp) ){struct sockaddr_in client;socklen_t len = sizeof(client);// accept 调用一次接入一个 tcp 客户端int client_fd = accept(listen_fd, (struct sockaddr *)&client, &len);for ( int i = 0; i < FD_SETSIZE - 2; i++ ){// 记录新的客户端 fdif ( clients_fd[i] == -1 ){clients_fd[i] = client_fd;if ( clients_count < i + 1 ){clients_count = i + 1;}printf("client fd %d --- i %d --- clients_count %d\n", client_fd, i, clients_count);break;}}log_info("FD SetSize %d", FD_SETSIZE);if ( clients_count < FD_SETSIZE - 1 ){// 若未达到连接边界,则将新的客户端 fd 添加到监听集合中FD_SET(client_fd, &poll_set);max_fd = client_fd > max_fd? client_fd: max_fd;//输出客户端信息char ip[MAX_IP_LENGTH] = "";unsigned short port = ntohs(client.sin_port);inet_ntop(AF_INET, &client.sin_addr.s_addr, ip, MAX_IP_LENGTH);printf("client %s is connected %hu port\n", ip, port);log_info("client %s is connected %hu port", ip, port);}else{printf("Number of Clients reaches max limit\n");}ready_count--;}// 处理客户端 IO 事件for ( int i = 0; i < clients_count && ready_count > 0; i++ ){int client_fd = clients_fd[i];if ( client_fd < 0 ){continue;}// 若该客户端准备就绪,则执行 recv 接受消息if ( FD_ISSET(client_fd, &set_tmp) ){printf("client fd %d with i %d\n", client_fd, i);char msg[MAX_MSG_LENGTH] = "";char msg_res[MAX_MSG_LENGTH] = "Recevied Successfully";int len = recv(client_fd, msg, sizeof(msg), 0);// 异常情况,将剔除客户端if ( len <= 0 ){printf("Release Fd %d\n", client_fd);close(client_fd);clients_fd[i] = -1;FD_CLR(client_fd, &poll_set);}else{printf("TCP Client Send: %s\n", msg);if ( send(client_fd, msg_res, strlen(msg_res), 0) > 0 ){printf("---- Response Successfully With %s\n\n", msg_res);}to_lower_case(msg);//printf("--%s--\n", msg);// 客户端主动退出if ( !strcmp("exit", msg) ) {close(client_fd);clients_fd[i] = -1;FD_CLR(client_fd, &poll_set);printf("Close Socket FD %d\n", client_fd);}}--ready_count;}}}}close(listen_fd);return 0;
}
二、Poll
poll 是 select 的一种改进版本,它消除了 select 的文件描述符数量限制,API 函数使用起来稍有不同。poll 函数与 select 原理相似,也是一种基于轮询的 I/O 多路复用机制,它通过一个 struct pollfd 结构体数组来管理多个文件描述符。
#include <poll.h>
/* Type used for the number of file descriptors. */
typedef unsigned long int nfds_t;struct pollfd {int fd; // 文件描述符short events; // 监听事件short revents; // 就绪事件
};/*fds: 监听的 fd 列表nfds:监听的 fd 数量timeout:监听阻塞超时时间,< 0 永远等待;0 立即返回;> 0 等待的毫秒数
*/
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// return:表示此时有多少个监控的描述符就绪,若超时则为0,出错为-1。
Event 类型如下所示,感兴趣的可以看下:

poll 和 select 函数一样,两者都需要遍历整个文件描述符集合来检查状态,因此性能都会随着文件描述符数量的增加而线性下降。
poll 与 select 的不同之处:
(1)poll 函数采用链表的方式替代原来 select 中 fd_set 结构,因此可监听文件描述符数量不受限,在处理大量文件描述符时可能更具优势。
(2)poll 函数返回后,可以通过 pollfd 结构中的内容进行处理就绪文件描述符,相比 select 效率要高。避免了 select 需要重置文件描述符集合的问题。
(3)需要维护一个 struct pollfd 结构体数组,这可能会增加一些编程复杂性。
poll 的使用示例如下:
#include <stropts.h>
#include <poll.h>
...
struct pollfd fds[2];
int timeout_msecs = 500;
int ret;
int i;/* Open STREAMS device. */
fds[0].fd = open("/dev/dev0", ...);
fds[1].fd = open("/dev/dev1", ...);
fds[0].events = POLLOUT | POLLWRBAND;
fds[1].events = POLLOUT | POLLWRBAND;ret = poll(fds, 2, timeout_msecs);if (ret > 0) {/* An event on one of the fds has occurred. */for ( i=0; i < 2; i++ ) {if (fds[i].revents & POLLWRBAND) {/* Priority data may be written on device number i. */
...}if (fds[i].revents & POLLOUT) {/* Data may be written on device number i. */
...}if (fds[i].revents & POLLHUP) {/* A hangup has occurred on device number i. */
...}}
}
三、epoll
重头戏来了,下面介绍 linux 中的高并发 IO 复用 epoll,很多服务器(例如 nginx)部署在 linux 中时都会使用 epoll 机制实现该并发 IO 操作,避免阻塞。
0、 实现原理
epoll 将“维护等待队列”和“阻塞进程”两个步骤分开。先用epoll_ctl函数维护监听队列,再调用epoll_wait函数阻塞进程。这种设计提高了效率,特别是在需要监视的 socket 相对固定的场景下。

在内核中,epoll 使用红黑树来跟踪所有待检测的文件描述符。红黑树是一种高效的数据结构(时间复杂度O(logN)),支持快速查找、插入和删除操作。这使得 epoll 能够高效地管理大量文件描述符。
epoll 采用事件驱动的方式,仅在文件描述符状态发生变化时才会通知应用程序。这避免了每次遍历整个文件描述符集合的问题,从而提高了性能。epoll 使用一个双向链表来记录就绪事件,在执行 epoll_ctl 的 add 操作时,不仅将文件描述符放到红黑树上,而且也注册了回调函数,内核在检测到某文件描述符可读/可写时会调用回调函数,将该文件描述符放在就绪链表中。用户调用epoll_wait时,只需检查这个列表是否有存在注册的事件(红黑树)即可,避免了遍历所有文件描述符。
1、函数 API
#include <sys/epoll.h>/**/
struct epoll_event
{uint32_t events; /* 指定要监听的事件类型 */epoll_data_t data; /* 用户数据变量 */
} __EPOLL_PACKED;/*
epoll_data_t是一个共用体,其 4 个成员中使用最多的是 fd,它指定事件所从属的目标文件描述符。ptr成员可以用来指定与fd相关的用户数据。但由于epoll_data_t是一个共用体,我们不能同时使用其ptr成员和fd成员,因此,如果要将文件描述符和用户数据关联起来,以实现快速的数据访问,只能放弃使用epoll_data_t的fd成员,而在ptr指向的用户数据中包含fd。
*/
typedef union epoll_data
{void *ptr; // 指定与fd相关的用户数据int fd; // 指定事件所从属的目标文件描述符uint32_t u32;uint64_t u64;
} epoll_data_t;/*创建 epoll 实例, 并返回该实例的 fd。该函数会在内核中新建红黑树用于存储 epoll_ctl 管理的 fd,还会新建双向链表用于记录已就绪的 fd。需要注意,在使用完 epoll 后,必须调用 close() 关闭该 fd,否则会浪费描述符资源。返回值: 成功时返回一个文件描述符(非负整数),失败时返回 -1 并设置 errno。
*/
int epoll_create1(int flags);/*添加、修改或删除监听的文件描述符参数:epfd: epoll 实例的文件描述符。op: 操作类型,可以是以下之一:EPOLL_CTL_ADD: 注册新的文件描述符到 epoll 实例中。EPOLL_CTL_MOD: 修改已注册的文件描述符的事件。EPOLL_CTL_DEL: 从 epoll 实例中删除文件描述符。fd: 需要监听的文件描述符。event: 指向 epoll_event 结构的指针,用于指定事件和用户数据。返回值: 成功时返回 0,失败时返回 -1 并设置 errno。
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);/*等待事件发生,当事件发生时,会将对应的 fd 添加到 epoll 就绪队列中。参数:epfd: epoll 实例的文件描述符。events: 用于存储发生事件的数组。maxevents: 数组的最大长度。timeout: 超时时间(毫秒)。如果为 -1,则无限等待;如果为 0,则立即返回。返回值: 成功时返回就绪的文件描述符数量,失败时返回 -1 并设置 errno。
*/
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
| Event 类型 | |
|
| 表示对应的文件描述符上有数据可读 |
|
| 表示对应的文件描述符上可以写入数据 |
|
| 表示对端已经关闭连接,或者关闭了写操作端的写入 |
|
| 表示有紧急数据可读 |
|
| 表示发生错误 |
|
| 表示文件描述符被挂起 |
|
| 表示将 epoll 设置为边缘触发模式。在边缘触发模式下,事件只有在状态发生变化时才会报告一次,而不是像水平触发模式那样只要条件满足就持续报告。 |
|
| 表示将事件设置为一次性事件。设置了这个标志后,当事件处理完后,epoll 会自动删除该事件,无需再次手动调用 |
2、简单代码模板
- 创建epoll实例:通过
epoll_create函数创建一个epoll对象。 - 维护监听列表:使用
epoll_ctl函数添加、删除或修改需要监视的文件描述符。 - 接收数据:当文件描述符收到数据后,中断程序会操作epoll对象,而不是直接操作进程。
- 阻塞和唤醒进程:当进程运行到
epoll_wait时,内核会将进程放入epoll对象的等待队列中,阻塞进程。当文件描述符接收到数据,中断程序一方面修改就绪列表,另一方面唤醒epoll等待队列中的进程
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>#define MAX_EVENTS 10int main() {int epoll_fd = epoll_create1(0);if (epoll_fd == -1) {perror("epoll_create1");exit(EXIT_FAILURE);}struct epoll_event event;struct epoll_event events[MAX_EVENTS];int listen_sock = /* ... */; // 初始化监听套接字event.data.fd = listen_sock;event.events = EPOLLIN;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_sock, &event) == -1) {perror("epoll_ctl");exit(EXIT_FAILURE);}while (1) {int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);if (nfds == -1) {perror("epoll_wait");exit(EXIT_FAILURE);}for (int i = 0; i < nfds; i++) {if (events[i].data.fd == listen_sock) {// 处理新连接} else {// 处理现有连接的数据}}}close(epoll_fd);return 0;
}
3、LT/ET 使用过程
摘自Linux下的I/O复用技术 — epoll如何使用(epoll_create、epoll_ctl、epoll_wait) 以及 LT/ET 使用过程解析_主动去触发epoll事件-CSDN博客
https://blog.csdn.net/JMW1407/article/details/107963618
(1)LT 水平触发
Level Triggered
socket接收缓冲区不为空,说明有数据可读, 读事件一直触发socket发送缓冲区不满,说明可以继续写入数据 ,写事件一直触发- 符合思维习惯,epoll_wait返回的事件就是socket的状态
LT 处理过程:
- accept 一个连接,添加到 epoll 中监听 EPOLLIN 事件.
- 当 EPOLLIN 事件到达时,读取 fd 中的数据并处理 .
- 当需要写出数据时,把数据 write 到 fd 中;如果数据较大,无法一次性写出,那么在 epoll 中监听EPOLLOUT 事件 。
- 当 EPOLLOUT 事件到达时,继续把数据 write 到 fd 中;如果数据写出完毕,那么在 epoll 中关闭EPOLLOUT 事件。
//LT模式的工作流程
void lt( epoll_event* events, int number, int epollfd, int listenfd )
{char buf[ BUFFER_SIZE ];for ( int i = 0; i < number; i++ ){int sockfd = events[i].data.fd;if ( sockfd == listenfd ){struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );addfd( epollfd, connfd, false );}else if ( events[i].events & EPOLLIN ){//只要socket读缓存中还有未读出的数据,这段代码就被触发printf( "event trigger once\n" );memset( buf, '\0', BUFFER_SIZE );int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );if( ret <= 0 ){close( sockfd );continue;}printf( "get %d bytes of content: %s\n", ret, buf );}else{printf( "something else happened \n" );}}
}
(2)ET边沿触发
Edge Triggered
socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件(从无到有)socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件(从有到无)- 仅在状态变化时触发事件
ET 处理流程
- accept 一个一个连接,添加到 epoll 中监听 EPOLLIN|EPOLLOUT 事件;
- 当 EPOLLIN 事件到达时,读取 fd 中的数据并处理,read 需要一直读,直到返回 EAGAIN 为止
- 当需要写出数据时,把数据 write 到fd中,直到数据全部写完,或者 write 返回 EAGAIN
- 当 EPOLLOUT 事件到达时,继续把数据 write 到fd中,直到数据全部写完,或者 write 返回 EAGAIN
从 ET 的处理过程中可以看到,ET 的要求是需要一直读写,直到返回 EAGAIN,否则就会遗漏事件。而 LT 的处理过程中,直到返回 EAGAIN 不是硬性要求,但通常的处理过程都会读写直到返回 EAGAIN,但 LT 比 ET 多了一个开关 EPOLLOUT 事件的步骤
当我们使用 ET 模式的 epoll 时,我们应该按照以下规则设计:
- 在接收到一个 I/O 事件通知后,立即处理该事件。程序在某个时刻应该在相应的文件描述符上尽可能多地执行I/O。
- 在ET模式下,在使用epoll_ctl注册文件描述符的事件时,应该把描述符设置为非阻塞的(非常重要)。
因为程序采用循环(ET里面采用while循环,看清楚呦,LE是if判断)来对文件描述符执行尽可能多的I/O,而文件描述符又被设置为可阻塞的,那么最终当没有更多的I/O可执行时,I/O系统调用就会阻塞。基于这个原因,每个被检查的文件描述符通常应该置为非阻塞模式,在得到I/O事件通知后重复执行I/O操作,直到相应的系统调用(比如read(),write())以错误码EAGAIN或EWOULDBLOCK的形式失败。
//ET模式的工作流程
void et( epoll_event* events, int number, int epollfd, int listenfd )
{char buf[ BUFFER_SIZE ];for ( int i = 0; i < number; i++ ){int sockfd = events[i].data.fd;if ( sockfd == listenfd ){struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );addfd( epollfd, connfd, true );}else if ( events[i].events & EPOLLIN ){//这段代码不会被重复触发,所以我们循环读取数据,以确保把socket读缓存中的所有数据读出printf( "event trigger once\n" );while( 1 ){memset( buf, '\0', BUFFER_SIZE );int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );if( ret < 0 ){//对于非阻塞IO,下面条件成立表示数据已经全部读取完毕。//此后,epoll就能再次触发sockfd上的EPOLLIN事件,已驱动下一次读操作if( ( errno == EAGAIN ) || ( errno == EWOULDBLOCK ) ){printf( "read later\n" );break;}close( sockfd );break;}else if( ret == 0 ){close( sockfd );}else{printf( "get %d bytes of content: %s\n", ret, buf );}}}else{printf( "something else happened \n" );}}
}
4、使用 Demo
基于 epoll 实现的高并发 TCP 服务器。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <errno.h>
#include <fcntl.h>#define PORT 8080
#define MAX_EVENTS 1024
#define BUFFER_SIZE 1024void set_nonblocking(int sockfd) {int opts;opts = fcntl(sockfd, F_GETFL);if (opts < 0) {perror("fcntl(F_GETFL)");exit(EXIT_FAILURE);}opts = (opts | O_NONBLOCK);if (fcntl(sockfd, F_SETFL, opts) < 0) {perror("fcntl(F_SETFL)");exit(EXIT_FAILURE);}
}int main() {int listen_fd, conn_fd, nfds, epoll_fd;struct sockaddr_in server_addr, client_addr;socklen_t client_len = sizeof(client_addr);struct epoll_event ev, events[MAX_EVENTS];char buffer[BUFFER_SIZE];int done = 0;// 创建监听套接字if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {perror("socket");exit(EXIT_FAILURE);}// 设置地址复用int opt = 1;if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {perror("setsockopt");close(listen_fd);exit(EXIT_FAILURE);}// 绑定端口和地址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) {perror("bind");close(listen_fd);exit(EXIT_FAILURE);}// 监听端口if (listen(listen_fd, SOMAXCONN) == -1) {perror("listen");close(listen_fd);exit(EXIT_FAILURE);}// 创建 epoll 实例epoll_fd = epoll_create1(0);if (epoll_fd == -1) {perror("epoll_create1");close(listen_fd);exit(EXIT_FAILURE);}// 将监听套接字添加到 epoll 实例中ev.events = EPOLLIN;ev.data.fd = listen_fd;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) {perror("epoll_ctl: listen_fd");close(listen_fd);close(epoll_fd);exit(EXIT_FAILURE);}printf("Server is listening on port %d\n", PORT);// 主循环:等待事件并处理while (!done) {nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);if (nfds == -1) {perror("epoll_wait");close(listen_fd);close(epoll_fd);exit(EXIT_FAILURE);}for (int i = 0; i < nfds; ++i) {if (events[i].data.fd == listen_fd) {// 处理新的连接请求while ((conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_len)) != -1) {set_nonblocking(conn_fd);ev.events = EPOLLIN | EPOLLET; // 边缘触发模式ev.data.fd = conn_fd;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev) == -1) {perror("epoll_ctl: conn_fd");close(conn_fd);continue;}printf("New connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));}if (errno != EAGAIN && errno != EWOULDBLOCK) {perror("accept");done = 1;}} else {// 处理客户端数据或断开连接int client_fd = events[i].data.fd;ssize_t bytes_read = read(client_fd, buffer, sizeof(buffer) - 1);if (bytes_read == -1) {if (errno != EAGAIN && errno != EWOULDBLOCK) {perror("read");close(client_fd);epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL); // 从 epoll 监听队列中删除文件描述符}} else if (bytes_read == 0) {// 客户端关闭连接printf("Closed connection on descriptor %d\n", client_fd);close(client_fd);epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL); // 从 epoll 监听队列中删除文件描述符} else {// 回显数据给客户端buffer[bytes_read] = '\0';write(client_fd, buffer, bytes_read);}}}}close(listen_fd);close(epoll_fd);return 0;
}
四、参考链接
Linux下的I/O复用技术 — epoll如何使用(epoll_create、epoll_ctl、epoll_wait) 以及 LT/ET 使用过程解析_主动去触发epoll事件-CSDN博客
https://blog.csdn.net/JMW1407/article/details/107963618 还在用多线程?试试 Linux select 这个‘神操作’吧!
https://mp.weixin.qq.com/s/sRXjRUZS1BVx1ZtBsZifug
相关文章:
Linux下的三种 IO 复用
目录 一、Select 1、函数 API 2、使用限制 3、使用 Demo 二、Poll 三、epoll 0、 实现原理 1、函数 API 2、简单代码模板 3、LT/ET 使用过程 (1)LT 水平触发 (2)ET边沿触发 4、使用 Demo 四、参考链接 一、Select 在…...
通过 SSH 进行WordPress网站的高级服务器管理
我在管理hostease的服务器时,时常需要通过SSH登录服务器进行修改。而在网站管理中,SSH不仅是一个基础工具,更是高级用户用来精细化管理和优化服务器的重要工具。通过SSH,你可以深入监控服务器的性能、精细管理系统资源,…...
速盾高防cdn支持移动端独立缓存
随着移动互联网的快速发展,移动端网页访问量也越来越大。然而,移动端的网络环境相对不稳定,用户体验可能会受到影响。因此,使用高防CDN来加速移动端网页访问,成为越来越多网站运营者的首选。 速盾高防CDN是一种分布式…...
PMP–一、二、三模、冲刺–分类–8.质量管理
文章目录 技巧五、质量管理 一模8.质量管理--质量管理计划--质量管理计划包括项目采用的质量标准,到底有没有满足质量需求,看质量标准即可。6、 [单选] 自项目开始以来,作为项目经理同事的职能经理一直公开反对该项目,在讨论项目里…...
如何快速使用Unity 的UPR---1资源检测保姆级
关于我们的性能检测工具已经有很多了,比如UWA的或者是我们的Unity 的UPR 都是很好的,今天说一下UPR吧 官方网址 :UPR - Unity专业性能优化工具 这个是官方给的Demo 选择你的平台就可以 这个可以作为一个参考但是不是很建议用官方的因为我们…...
pytorch中的.clone() 和 .detach()
在PyTorch中,.clone() 和 .detach() 是两个用于处理张量(Tensor)的方法,它们各自有不同的用途: .clone(): .clone() 方法用于创建一个张量的副本(深拷贝)。这意味着原始张量和新张量…...
三十二:网络爬虫的工作原理与应对方式
随着互联网的快速发展,网络爬虫(Web Crawlers)作为一种自动化工具,被广泛应用于搜索引擎、数据采集、网站监控等领域。网络爬虫的作用是通过自动化程序,模拟人类浏览网页的行为,自动下载和解析网页内容&…...
nodejs相关知识介绍
1、nodejs官方文档: https://nodejs.org/zh-cn nodejs可以用nvm进入安装; 2、npm说明: npm官方教程:https://npm.p2hp.com/ npm是 Node.js 的标准包管理器,也就是说nodejs安装好,npm也就安装好了&#…...
MySQL排它锁
MySQL排它锁原理 MySQL中的排它锁(Exclusive Lock),也称为独占锁,是一种确保在事务期间,其他事务无法对锁定数据进行读取或修改的锁机制。当一个事务对某一行数据加上排它锁后,其他事务无法对该行数据进行…...
HarmonyOS4+NEXT星河版入门与项目实战(22)------动画(属性动画与显示动画)
文章目录 1、属性动画图解2、案例实现-小鱼移动游戏1、代码实现2、代码解释3、资源图片4、实现效果3、显示动画4、案例修改-显示动画5、总结1、属性动画图解 这里我们用一张完整的图来汇整属性动画的用法格式和使用的主要属性范围,如下所示: 2、案例实现-小鱼移动游戏 1、代…...
Vue3 Ts 如何获取组件的类型
vue3 Ts ref 子组件 1、默认写法 typeof:获取ts类型 InstanceType:获取模版的实例 <tempolate><myComponent ref"myCompRef"> </tempolate><script setup lang"ts"> import { ref } from "vue&quo…...
RAG数据拆分之PDF
引言RAG数据简介PDF解析方法及工具代码实现总结 二、正文内容 引言 本文将介绍如何将RAG数据拆分至PDF格式,并探讨PDF解析的方法和工具,最后提供代码示例。 RAG数据简介 RAG(关系型属性图)是一种用于表示实体及其关系的图数据…...
【算法day1】数组:双指针算法
题目引用 这里以 1、LeetCode704.二分查找 2、LeetCode27.移除元素 3、LeetCode977.有序数组的平方 这三道题举例来说明数组中双指针的妙用。 1、二分查找 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜…...
Ubuntu 22.04 离线安装软件包
在使用最小化安装时,默认是不带有vim 或者nano编辑器的,如果你的环境不能上外网就需要离线安装。 首先你需要先找一台可以上网的ubuntu系统(虚拟机搭建也行),下载所有的依赖包,然后上传到需要安装的服务器…...
网络安全——浅谈HTTP协议
HTTP请求 HTTP请求是客户端往服务端发送请求动作,告知服务器自己的要求。 HTTP请求由状态行、请求头、请求正文三部分组成: 状态行:包括请求方式Method、资源路径URL、协议版本Version;请求头:包括一些访问的域名、…...
鸿蒙开发-在ArkTS中制作音乐播放器
音频播放功能实现 导入音频播放相关模块 首先需要从ohos.multimedia.audio模块中导入必要的类和接口用于音频播放。例如: import audio from ohos.multimedia.audio;创建音频播放器实例并设置播放源 可以通过audio.createAudioPlayer()方法创建一个音频播放器实…...
Rust学习笔记_03——元组
Rust学习笔记_01——基础 Rust学习笔记_02——数组 Rust学习笔记_03——元组 文章目录 Rust学习笔记_03——元组元组1. 定义元祖2. 访问元组中的元素3. 元组的解构4. 元组不可遍历和切片5. 元组作为函数返回值6. 单元元组7. 代码演示 元组 在Rust编程语言中,元组&a…...
LabVIEW内燃机气道试验台测控系统
基于LabVIEW软件开发的内燃机气道试验台测控系统主要应用于内燃机气道的性能测试和数据分析,通过高精度的测控技术,有效提升内燃机的测试精度和数据处理能力。 项目背景 随着内燃机技术的发展,对其气道性能的精准测量需求日益增加。该系统通…...
git 本地同步远端分支
一、关联远程仓库 本地仓库关联远端仓库 git remote add origin https://github.com/user/repository.git 二、获取远程分支信息 获取远程仓库的最新分支信息 git fetch origin 三、创建或切换到本地分支以跟踪远程分支 1. 创建分支 创建分支并关联到远端分支 git bra…...
用Pycharm安装manim
由于版本和工具的差异,manim的安装方式不尽相同。本文用Pycharm来安装manim. 一、准备工作:安装相应版本的python、pycharm和ffmpeg. 此处提供一种安装ffmpeg的方式 下载地址:FFmpeg 下载后,解压到指定目录。 配置环境变量&am…...
UE5 学习系列(二)用户操作界面及介绍
这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…...
微信小程序之bind和catch
这两个呢,都是绑定事件用的,具体使用有些小区别。 官方文档: 事件冒泡处理不同 bind:绑定的事件会向上冒泡,即触发当前组件的事件后,还会继续触发父组件的相同事件。例如,有一个子视图绑定了b…...
云计算——弹性云计算器(ECS)
弹性云服务器:ECS 概述 云计算重构了ICT系统,云计算平台厂商推出使得厂家能够主要关注应用管理而非平台管理的云平台,包含如下主要概念。 ECS(Elastic Cloud Server):即弹性云服务器,是云计算…...
(十)学生端搭建
本次旨在将之前的已完成的部分功能进行拼装到学生端,同时完善学生端的构建。本次工作主要包括: 1.学生端整体界面布局 2.模拟考场与部分个人画像流程的串联 3.整体学生端逻辑 一、学生端 在主界面可以选择自己的用户角色 选择学生则进入学生登录界面…...
PL0语法,分析器实现!
简介 PL/0 是一种简单的编程语言,通常用于教学编译原理。它的语法结构清晰,功能包括常量定义、变量声明、过程(子程序)定义以及基本的控制结构(如条件语句和循环语句)。 PL/0 语法规范 PL/0 是一种教学用的小型编程语言,由 Niklaus Wirth 设计,用于展示编译原理的核…...
Typeerror: cannot read properties of undefined (reading ‘XXX‘)
最近需要在离线机器上运行软件,所以得把软件用docker打包起来,大部分功能都没问题,出了一个奇怪的事情。同样的代码,在本机上用vscode可以运行起来,但是打包之后在docker里出现了问题。使用的是dialog组件,…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
【深度学习新浪潮】什么是credit assignment problem?
Credit Assignment Problem(信用分配问题) 是机器学习,尤其是强化学习(RL)中的核心挑战之一,指的是如何将最终的奖励或惩罚准确地分配给导致该结果的各个中间动作或决策。在序列决策任务中,智能体执行一系列动作后获得一个最终奖励,但每个动作对最终结果的贡献程度往往…...
如何在Windows本机安装Python并确保与Python.NET兼容
✅作者简介:2022年博客新星 第八。热爱国学的Java后端开发者,修心和技术同步精进。 🍎个人主页:Java Fans的博客 🍊个人信条:不迁怒,不贰过。小知识,大智慧。 💞当前专栏…...
云原生安全实战:API网关Envoy的鉴权与限流详解
🔥「炎码工坊」技术弹药已装填! 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 一、基础概念 1. API网关 作为微服务架构的统一入口,负责路由转发、安全控制、流量管理等核心功能。 2. Envoy 由Lyft开源的高性能云原生…...
