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…...
#渗透测试#红蓝攻防#HW#漏洞挖掘#漏洞复现01-笑脸漏洞(vsftpd)
免责声明 本教程仅为合法的教学目的而准备,严禁用于任何形式的违法犯罪活动及其他商业行为,在使用本教程前,您应确保该行为符合当地的法律法规,继续阅读即表示您需自行承担所有操作的后果,如有异议,请立即停…...
vue3项目中使用星火API
在node环境epxress中使用讯飞ai接口进行二次封装,通过ai对话回复提取,获得ai提取的文章摘要 本文章只是简单使用,更复杂功能比如调用星火API制作对话机器人可以查看文档,对于初次使用星火AI接口或许有帮助 讯飞星火大模型API-大模…...
digit_eye开发记录(3): C语言读取MNIST数据集
在前两篇,我们解读了 MNIST 数据集的 IDX 文件格式,并分别用 C 和 Python 做了 读取 MNIST 数据集的实现。 基于 C 的代码稍长,基于 Python 的代码则明显更短,然而它们的共同特点是:依赖了外部库: 基于 C …...
【linux】(23)对象存储服务-MinIo
MinIO 是一个高性能的对象存储服务,兼容 Amazon S3 API。 Docker安装MinIo 前提条件 确保您的系统已经安装了 Docker。如果还没有安装 Docker,可以参考 Docker 官方文档进行安装。 1. 拉取 MinIO Docker 镜像 首先,从 Docker Hub 拉取 Mi…...
如何使用Python解析从淘宝API接口获取到的JSON数据?
基本的 JSON 解析 当从淘宝 API 接口获取到数据后(假设数据存储在变量response_data中),首先要判断数据类型是否为 JSON。如果是,就可以使用 Python 内置的json模块进行解析。示例代码如下: import json # 假设respon…...
C# 2024年Visual Studio实用插件集合
在2024年,Visual Studio作为.NET开发者的首选IDE,其插件生态不断壮大,为开发者提供了更高效、便捷的开发体验。本文将介绍一些实用的Visual Studio插件,特别是针对C#开发者,帮助提升开发效率和代码质量。 1. GitHub C…...
Matlab Simulink HDL Coder开发流程(一)— 创建HDL兼容的Simulink模型
创建HDL兼容的Simulink模型 一、使用Balnk DUT模板二、从HDL Coder库中选择模块三、为DUT开发算法/功能四、为设计创建Testbench五、仿真验证设计功能六、Simulink模型生成HDL代码 这个例子说明了如何创建一个用于生成HDL代码的Simulink模型。要创建兼容HDL代码生成的MATLAB算法…...
详解Qt pdf 之QPdfSelection 选择文本类
文章目录 QPdfSelection 类详解前言 详细说明公共函数说明1. 构造函数2. text3. boundingRect4. isEmpty5. startPage6. endPage 使用场景示例代码代码说明总结 QPdfSelection 类详解 前言 QPdfSelection 是 Qt PDF 模块中的一个类,用于表示在 PDF 文档中被选中的…...
docker中redis查看key、删除key
查看docker启动的进程 docker ps这个命令会列出所有正在运行的容器,包括容器的 ID、镜像名称、创建时间、状态、端口映射和名称 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 1a2b3c4d5e6…...
【MySQL — 数据库基础】MySQL的安装与配置 & 数据库简单介绍
数据库基础 本节目标 掌握关系型数据库,数据库的作用掌握在Windows和Linux系统下安装MySQL数据库了解客户端工具的基本使用和SQL分类了解MySQL架构和存储引擎 1. 数据库的安装与配置 1.1 确认MYSQL版本 处理无法在 cmd 中使用 mysql 命令的情况&a…...
网站设计的销售/app下载推广平台
spring cloud-构建微服务架构的网关(API GateWay) 前言 在我们前面的博客中讲到,当服务A需要调用服务B的时候,只需要从Eureka中获取B服务的注册实例,然后使用Feign来调用B的服务,使用Ribbon来实现负载均衡,但是&#x…...
全球做空现货黄金的网站/网站建设流程是什么
转载于:https://www.cnblogs.com/cangshuchirou/p/9753461.html...
没有网站可以做搜索引擎营销吗/网站运营与维护
文|曾响铃 来源|科技向令说(xiangling0815) 2018年8月,北京,“外研社杯”中小学生英语大赛总决赛上,北京某所私立幼儿园的老师麦子,正在台下为自己的孩子加油。 作为中小学英语权威竞赛,“外…...
汶上县住房和建设局网站/网上哪里可以免费打广告
很多时候,我们往往希望在网站上添加一个网站地图,一方面是方便客户了解网站的布局架构,一方面是方便搜索引擎蜘蛛的抓取,那么用Phpcms V9这个程序作为企业的企业管理后台,怎么制作网站地图Sitemap静态地图,…...
自适应型网站建设报价/新闻头条新闻
1、使用git status 命令查看当前状态 上图表示当前位于一个叫做master的分支中 工作目录无需要提交的文件即工作目录中文件没有改动过 2、新建LICENSE文件 输入 git status命令 提示Untracked files (文件未被跟踪)指文件未被添加到暂存区或者git仓库 使用 git add <file>…...
男女明星直接做的视频网站/网络营销的企业有哪些
作者 | 曾响铃 文 | 响铃说 数字经济时代磅礴而来,数字化相关的应用井喷式出现,作为“后方”的数据存储,重要性也在不断提升。有数据显示,当前平均一个企业的数据应用超过100种,应用数量每年翻番。 在这种情况下&am…...