Linux下的三种 IO 复用

news2024/12/27 9:18:12

目录

一、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_ZEROFD_SETFD_CLRFD_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 并发特别多,可以考虑 pollepoll(强烈推荐,更适合高并发场景)。

  • 函数返回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;
    }

    // 调用listen
    listen(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++ )
				{
                    // 记录新的客户端 fd
					if ( 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_ctladd 操作时,不仅将文件描述符放到红黑树上,而且也注册了回调函数,内核在检测到某文件描述符可读/可写时会调用回调函数,将该文件描述符放在就绪链表中。用户调用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 类型

EPOLLIN

表示对应的文件描述符上有数据可读

EPOLLOUT

表示对应的文件描述符上可以写入数据

EPOLLRDHUP

表示对端已经关闭连接,或者关闭了写操作端的写入

EPOLLPRI

表示有紧急数据可读

EPOLLERR

表示发生错误

EPOLLHUP

表示文件描述符被挂起

EPOLLET

表示将 epoll 设置为边缘触发模式。在边缘触发模式下,事件只有在状态发生变化时才会报告一次,而不是像水平触发模式那样只要条件满足就持续报告。

EPOLLONESHOT

表示将事件设置为一次性事件。设置了这个标志后,当事件处理完后,epoll 会自动删除该事件,无需再次手动调用 epoll_ctl 删除。

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 10

int 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博客icon-default.png?t=O83Ahttps://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 1024

void 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博客icon-default.png?t=O83Ahttps://blog.csdn.net/JMW1407/article/details/107963618 还在用多线程?试试 Linux select 这个‘神操作’吧!icon-default.png?t=O83Ahttps://mp.weixin.qq.com/s/sRXjRUZS1BVx1ZtBsZifug

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2251633.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Windows常用DOS指令(附案例)

文章目录 1.dir 查看当前目录2.cd 进入指定目录3.md 创建指定目录4.cd> 创建指定文件5.rd 删除指定空目录6.del 删除指定文件7.copy 复制文件8.xcopy 批量复制9.ren 改名10.type 在命令行空窗口打开文件11.cls 清空DOS命令窗口12.chkdsk 检查磁盘使用情况13.time 显示和设置…

【Linux】匿名管道通信场景——进程池

&#x1f525; 个人主页&#xff1a;大耳朵土土垚 &#x1f525; 所属专栏&#xff1a;Linux系统编程 这里将会不定期更新有关Linux的内容&#xff0c;欢迎大家点赞&#xff0c;收藏&#xff0c;评论&#x1f973;&#x1f973;&#x1f389;&#x1f389;&#x1f389; 文章目…

C#基础之集合讲解

文章目录 1 集合1.1 数组1.1.1 简介1.1.2 声明使用1.1.2.1 声明 & 初始化1.1.2.2 赋值给数组1.1.2.3 访问数组元素 1.1.3 多维数组1.1.3.1 声明1.1.3.2 初始化二维数组1.1.3.3 访问二维数组元素 1.1.4 交错数组1.1.5 传递数组给函数1.1.6 Array1.1.6.1 简介1.1.6.2 属性1.1…

Azure DevOps Server:使用甘特图Gantt展示需求进度

自从Azure DevOps Server取消与Project Server的集成后&#xff0c;许多用户都在关注如何使用甘特图来展示项目进度。 在Azure DevOps Server开放扩展Extension功能后&#xff0c;许多开发者或专业开发团队做了很多甘特图Gantt相关的开发工作&#xff0c;使用比较多的是(GANTT …

数据湖的概念(包含数据中台、数据湖、数据仓库、数据集市的区别)--了解数据湖,这一篇就够了

文章目录 一、数据湖概念1、企业对数据的困扰2、什么是数据湖3、数据中台、数据湖、数据仓库、数据集市的区别 网上看了好多有关数据湖的帖子&#xff0c;还有数据中台、数据湖、数据仓库、数据集市的区别的帖子&#xff0c;发现帖子写的都很多&#xff0c;而且专业名词很多&am…

Kali Linux怎么开python虚拟环境

相信很多朋友再学习的过程中都会遇到一些pip失效&#xff0c;或者报错的时候&#xff0c;他们要求我们要使用虚拟环境&#xff0c;但是不知道怎么搭建&#xff0c;下面这篇文章就来告诉你如何搭建虚拟环境&#xff0c;这个方法在所有Linux的服务器都通用&#xff0c;就两行命令…

Flink四大基石之State(状态) 的使用详解

目录 一、有状态计算与无状态计算 &#xff08;一&#xff09;概念差异 &#xff08;二&#xff09;应用场景 二、有状态计算中的状态分类 &#xff08;一&#xff09;托管状态&#xff08;Managed State&#xff09;与原生状态&#xff08;Raw State&#xff09; 两者的…

【数据结构计数排序】计数排序

非比较排序概念 非比较排序是一种排序算法&#xff0c;它不是通过比较元素大小进行排序的&#xff0c;而是基于元素的特征和属性排序。这种排序方法在特定情况下&#xff0c;可以做到比元素比较排序&#xff08;快排&#xff0c;归并&#xff09;更有效率。尤其是在处理大量数…

Java GET请求 请求参数在Body中使用Json格式传参

业务需要调个三方接口 使用GET请求方式 但是&#xff01;请求参数不在Query中&#xff0c;竟然在Body中&#xff0c;使用Json格式传参 在API调试工具里面可以调通 在java代码里&#xff0c;死活调不通 网上搜了搜&#xff0c;找到一个靠谱的&#xff0c;记录一下 import o…

Linux的文件系统

这里写目录标题 一.文件系统的基本组成索引节点目录项文件数据的存储扇区三个存储区域 二.虚拟文件系统文件系统分类进程文件表读写过程 三.文件的存储连续空间存放方式缺点 非连续空间存放方式链表方式隐式链表缺点显示链接 索引数据库缺陷索引的方式优点&#xff1a;多级索引…

[golang][MAC]Go环境搭建+VsCode配置

一、go环境搭建 1.1 安装SDK 1、下载go官方SDK 官方&#xff1a;go 官方地址 中文&#xff1a;go 中文社区 根据你的设备下载对应的安装包&#xff1a; 2、打开压缩包&#xff0c;根据引导一路下一步安装。 3、检测安装是否完成打开终端&#xff0c;输入&#xff1a; go ve…

从繁琐到高效:智能生成PPT的神秘力量

在这个技术爆炸的时代&#xff0c;一场精彩的演讲离不开一份出色的PPT。但制作PPT&#xff0c;就像是一场与时间的博弈&#xff0c;费尽心思构思版式、精炼文案、选择配图&#xff0c;稍不留神&#xff0c;就会被拖入无底深渊。可是你知道吗&#xff1f;现在只需动动手指&#…

二分法篇——于上下边界的扭转压缩间,窥见正解辉映之光(1)

前言 二分法&#xff0c;这一看似简单却又充满哲理的算法&#xff0c;犹如一道精巧的数学之门&#xff0c;带领我们在问题的迷雾中找到清晰的道路。它的名字虽简单&#xff0c;却深藏着智慧的光辉。在科学的浩瀚星空中&#xff0c;二分法如一颗璀璨的星辰&#xff0c;指引着我们…

【软件各类应用解决方案】ERP企业资源管理系统整体解决方案,采购管理方案,仓库管理方案,财务管理方案,人力管理方案,资产管理方案,对账管理(word完整版)

目录 第一部分 概述 第二部分 方案介绍 第三部分 系统业务流程 3.1 关键需求概括分析 3.1.1 销售管理方面 3.1.2 采购管理方面 3.1.3 仓库管理方面 3.1.4 财务管理方面 3.1.5 人力资源方面 3.2 关键需求具体分析 3.2.1 财务管理 3.2.1.1会计凭证解决 3.2.1.2钞…

实验二 选择结构程序设计

实验名称 实验二 选择结构程序设计 实验目的 &#xff08;1&#xff09;掌握关系运算符和逻辑运算符的使用方法&#xff0c;能够表达复杂的逻辑条件。 &#xff08;2&#xff09;掌握if语句的使用方法&#xff0c;掌握多重条件下的if语句嵌套使用。 &#xff08;3&#xff09;…

第33周:运动鞋识别(Tensorflow实战第五周)

目录 前言 一、前期工作 1.1 设置GPU 1.2 导入数据 1.3 查看数据 二、数据预处理 2.1 加载数据 2.2 可视化数据 2.3 再次检查数据 2.4 配置数据集 2.4.1 基本概念介绍 2.4.2 代码完成 三、构建CNN网络 四、训练模型 4.1 设置动态学习率 4.2 早停与保存最佳模型…

Robot Screw Theory (Product of Exponentials)机器人螺旋理论(指数积)

Screw Theory 螺旋理论 Screw theory uses the fact that every rigid body transformation can be expressed by a rotational and translational movement.螺旋理论利用了每个刚体变换都可以通过旋转和平移运动来表示这一事实。 Robert Ball developed the theory in 19th ce…

【maven-5】Maven 项目构建的生命周期:深入理解与应用

1. 生命周期是什么 ​在Maven出现之前&#xff0c;项目构建的生命周期就已经存在&#xff0c;软件开发人员每天都在对项目进行清理&#xff0c;编译&#xff0c;测试及部署。虽然大家都在不停地做构建工作&#xff0c;但公司和公司间&#xff0c;项目和项目间&#xff0c;往往…

skywalking 配置elasticsearch持久化

下载和启动elasticsearch elasticsearch-7.17.25-linux-x86_64.tar.gz&#xff0c;解密文件tar -xvf elasticsearch-7.17.25-linux-x86_64.tar.gz 进入到bin目录&#xff0c;启动 elasticsearch -d 后台运行 下载skywalking服务包 apache-skywalking-apm-9.3.0.tar.gz&#x…

MySQL 复合查询

实际开发中往往数据来自不同的表&#xff0c;所以需要多表查询。本节我们用一个简单的公司管理系统&#xff0c;有三张表EMP,DEPT,SALGRADE 来演示如何进行多表查询。表结构的代码以及插入的数据如下&#xff1a; DROP database IF EXISTS scott; CREATE database IF NOT EXIST…