当前位置: 首页 > article >正文

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_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.kler.cn/a/419173.html

相关文章:

  • 虚拟机玩游戏,轻松实现多开不同IP
  • Vue 3 的双向绑定原理
  • 远程桌面协助控制软件 RustDesk v1.3.3 多语言中文版
  • 【云原生系列】迁移云上需要考虑哪些问题
  • Permute for Mac 媒体文件格式转换软件 安装教程【音视频图像文件转换,简单操作,轻松转换,提高效率】
  • win10-Docker打不开this can prevent Docker from starting. Use at your own risk.
  • 文件比较和文件流
  • 大数据治理的介绍与认识
  • LeetCode题解:30.串联所有单词的子串【Python题解超详细,KMP搜索、滑动窗口法】,知识拓展:Python中的排列组合
  • 贝叶斯统计:高斯分布均值μ的后验分布推导
  • 详解QtPDF之 QPdfLink
  • 基于PHP的物流配送管理信息系统的设计与实现
  • 【redis】如何跑
  • flink学习(12)——checkPoint
  • 【Maven】依赖冲突如何解决?
  • 【链表】力扣 2. 两数相加
  • 基于yolov8、yolov5的吸烟行为检测识别系统(含UI界面、训练好的模型、Python代码、数据集)
  • 如何在 VPS 上使用 Git 设置自动部署
  • linux cenos redis 单机部署
  • 【Linux】磁盘 | 文件系统 | inode
  • 图解人工智能:从规则到深度学习的全景解析
  • LabVIEW将TXT文本转换为CSV格式(多行多列)
  • digit_eye开发记录(3): C语言读取MNIST数据集
  • EtherCAT转DeviceNe台达MH2与欧姆龙CJ1W-DRM21通讯案例
  • grpc与rpcx的区别
  • Qt 面试题学习13_2024-12-1