Bootstrap

网络编程(2)io多路复用(select、poll、epoll)

一、io多路复用

        在上一篇文章中,实现了一请求一线程的模型。其优点是代码简单,容易实现。但是在高并发的情况下,该模型就很难达到所需的并发量。此时我们就需要采用io多路复用的模型,来实现高并发。

        I/O多路复用的核心思想是通过一种机制,使得程序可以同时监视多个I/O事件,并在其中的一个或多个I/O事件就绪时,通知应用程序进行相应的处理。它允许一个进程或线程同时监听多个文件描述符的I/O事件,并在fd准备就绪时(可读可写)进行处理。这样,程序就无需为每个I/O操作都创建一个线程或进程,而是可以通过一个线程或进程来处理多个I/O操作,从而提高资源利用率和系统的高并发。在Linux系统中,I/O多路复用主要有三种实现方式:select、poll和epoll。

二、select

        io多路复用中的select是一种常用的同步io模型,它允许单个线程监视多个文件描述符(socketfd),并在一个或多个fd就绪时(可读可写)进行相应的读写操作。

2.1 select函数

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds被监听的sockfd数量(最大的sockfd加1)
readfds指向可读文件描述符集合的指针
writeds指向可写文件描述符集合的指针
指向异常指向异常条件文件描述符集和的指针
timeout等待时长,如果设置NULL,则一直阻塞

       发生错误时 select函数返回-1,超时返回0。正常时返回发生事件的sockfd总数。需要说明的是,sockfd从3开始。其中fd 0、1、2、分别为标准输入、标准输出、和标准错误输出。

2.2 fd_set集合与宏操作

        fd_set结构体用于表示文件描述符集合,它通常以一个位数组的形式实现,每一位代表一个文件描述符。对fd_set的操作通常由以下宏来完成:

FD_ZERO(fd_set *set);//清空指定的文件描述符集合set,将其所有位都置为0。
FD_SET(int fd, fd_set *set);//将指定的文件描述符fd添加到文件描述符集合set中,相应的位将被置为1。
FD_CLR(int fd, fd_set *set)
//将指定的文件描述符fd从文件描述符集合set中移除,相应的位将被清零(置为0)。
FD_ISSET(int fd, fd_set *set);
//检查指定的文件描述符fd是否在文件描述符集合set中,如果存在,则返回true;否则,返回false。

2.3 实现步骤

1、初始化:创建一个文件描述符集合,用于存放需要监视的文件描述符(socket)。

	fd_set rfds, rset;//rfds为sockfd集和,rset为可读集和
	FD_ZERO(&rfds);//对集合清零
	FD_SET(sockfd, &rfds);//将sockfd与集和绑定(相应位置1)

	int maxfd = sockfd;(fd的个数)

2、监听:调用select函数,将文件描述符集合传递给内核,让内核监视这些文件描述符的状态变化(如可读、可写、异常)。

int nready = select(maxfd+1,&rset, NULL, NULL, NULL);//返回可读可写fd的数量

3、阻塞等待:select函数会阻塞当前线程,直到一个或多个文件描述符就绪,或者超时。

if(FD_ISSET(sockfd, &rset)){
//如果rset集合中有fd被设置1(即变得可读),这意味着有新的客户端连接请求
	int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
//连接新的客户端
	printf("accept finshed: %d\n", clientfd);
//打印新客户端的fd编号
	FD_SET(clientfd, &rfds);
//将新的客户端fd加进rfds集合中
	if(clientfd > maxfd)maxfd = clientfd;
//更新当前最大的fd数
}

4、处理客户端数据:进行相关读写操作

int i = 0;
for(i = sockfd+1; i <= maxfd; i++){
//遍历从sockfd+1到maxfd的所有文件描述符(跳过监听套接字本身)。
	if(FD_ISSET(i, &rset)){//如果某个文件描述符在rset集合中被设置,则尝试使用recv()接收数据。
	
    char buffer[1024] = {0};

	int count = recv(i, buffer, 1024, 0);
	if(count == 0){//如果recv()返回0,表示客户端已关闭连接,此时关闭套接字并从rfds集合中移除。
		printf("client disconnect: %d\n", i);
		close(i);
		FD_CLR(i, &rfds);

		continue;
	}
	printf("RECV:%s\n", buffer);
//将收到的数据发送出去
	count = send(i, buffer, 1024, 0);
	printf("SEND:%d\n", count);
	}
}

完整代码:

#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <unistd.h>

int main(){

	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	struct sockaddr_in servaddr;
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(2000);
	
	if(-1 == bind(sockfd,(struct sockaddr*)&servaddr,sizeof(struct sockaddr))){
		printf("bind failed:%s\n",strerror(errno));
	}
	listen(sockfd,10);	
	printf("listen finshed: %d\n", sockfd);
	struct sockaddr_in clientaddr;
	socklen_t len = sizeof(clientaddr);
	
//select
	fd_set rfds, rset;
	FD_ZERO(&rfds);//对集合清零
	FD_SET(sockfd, &rfds);//将sockfd与集和绑定

	int maxfd = sockfd;
	
	while(1){

		rset = rfds;

		int nready = select(maxfd+1,&rset, NULL, NULL, NULL);//返回可读可写fd的数量

		if(FD_ISSET(sockfd, &rset)){//是否置1,置1则进行accept
			int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
			printf("accept finshed: %d\n", clientfd);

			FD_SET(clientfd, &rfds);

			if(clientfd > maxfd)maxfd = clientfd;

		}
		//recv
		int i = 0;
		for(i = sockfd+1; i <= maxfd; i++){

			if(FD_ISSET(i, &rset)){
				char buffer[1024] = {0};

				int count = recv(i, buffer, 1024, 0);
				if(count == 0){
					printf("client disconnect: %d\n", i);
					close(i);
					FD_CLR(i, &rfds);

					continue;
				}
				printf("RECV:%s\n", buffer);

				count = send(i, buffer, 1024, 0);
				printf("SEND:%d\n", count);
			}
		}
	}
	getchar();
	return 0;

}

三、poll

        select虽然实现了io多路复用,但参数较多。poll是对select的一种改进。

3.1 结构体pollfd

struct pollfd {
    int fd; // 委托内核监听的文件描述符 //
    short events; // 委托内核检测文件描述符的什么事件,如输入、输出、错误等//
    short revents; // 文件描述符实际发生的事件 -> 传出 //
};

 3.2 函数poll

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fdspollfd类型的数组,存储了待检测的fd的信息
nfdsfd的个数(maxfd+1)
timeout阻塞时间,-1时一直阻塞

poll函数返回-1时表示失败。返回一个大于0的整数时,表示检测的集合中已就绪的文件描述符的总个数。

3.2 实现步骤

        

struct pollfd fds[1024] = {0};
fds[sockfd].fd = sockfd;
fds[sockfd].events = POLLIN;//可读

int maxfd = sockfd;
//初始化pollfd数组//
while(1){

	int nready = poll(fds, maxfd+1, -1);

	if(fds[sockfd].revents & POLLIN){
//如果当 sockfd 的读缓冲区有数据时(即有新连接到来),进行连接//
		int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
		printf("accept finshed: %d\n", clientfd);
//将客服端fd添加进pollfd数组//
		fds[clientfd].fd = clientfd;
		fds[clientfd].events = POLLIN;
//更新maxfd值//
		if(clientfd > maxfd)maxfd = clientfd;
	}
//遍历每个客户端fd//
	int i = 0;
	for(i = sockfd+1; i <= maxfd; i++){
//可读时进行数据的收发//
		if(fds[i].revents & POLLIN){
			char buffer[1024] = {0};
			int count = recv(i, buffer, 1024, 0);
//没有数据时说明客户端断开连接//
                if(count == 0){
				printf("client disconnect: %d\n", i);
				close(i);//释放客户端fd
//在pollfd数组中将客户端fd移除//
				fds[i].fd = -1;
				fds[i].events = 0;

				continue;
			}
			printf("RECV:%s\n", buffer);

			count = send(i, buffer, 1024, 0);
			printf("SEND:%d\n", count);
		}
	}
}

四、epoll

        如今云主机大多都是选择Linux,是因为Linux引入了epoll这个机制。

4.1、 结构体epoll_event

        epoll_event是Linux内核提供的一个数据结构,它在epoll机制中用于表示事件。

struct epoll_event {
    uint32_t events;   // Epoll 事件 //
    epoll_data_t data; // 联合体,用于存储用户自定义的数据 //
};
typedef union epoll_data {
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

        用户可以使用data成员来存储与事件相关的自定义数据,当事件发生时,该数据会返回给用户,方便用户识别是哪一个文件描述符发生了事件。

4.2、函数

4.2.1、epoll_create

int epoll_create(int size);

        用于创建一个epoll实例,返回一个文件描述符。size是内核的建议大小,实际中大于0即可。

4.2.2、epoll_ctl

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

        用于控制epoll实例,添加、删除或修改监听的fd及其事件。

epfdepoll实例的文件描述符
op操作类型如添加(EPOLL_CTL_ADD)删除(EPOLL_CTL_DEL)修改(EPOLL_CTL_MOD)
fd要监听的fd
event指向epoll_event结构体的指针,描述了监听fd的事件与数据

4.2.3、epoll_wait

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

等待 epoll实例上的事件发生,并返回就绪的文件描述符列表。

events用于存储就绪事件的数组
maxevents数组大小
timeout等待时间,-1为阻塞

 

4.3、代码流程

int epfd = epoll_create(1);//创建一个新的epoll实例
//设置epoll事件
struct epoll_event ev;
ev.events = EPOLLIN;//只关心读事件
ev.data.fd = sockfd;//将服务器fd与事件关联
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);//在epoll实例epfd中添加监听fd

while(1){
//创建事件集合
	struct epoll_event events[1024] = {0};
	int nready = epoll_wait(epfd, events, 1024, -1);//等待事件发生,并返回就绪fd

	int i = 0;
	for(i = 0;i < nready; i++){//遍历事件集合

		int connfd = events[i].data.fd;
		if(connfd == sockfd){//如果事件对应服务器fd,则接收新连接
			int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr,&len);
			printf("accept finshed: %d\n", clientfd);
//设置新连接fd的事件
			ev.events = EPOLLIN;
			ev.data.fd = clientfd;
//将客户端fd添加到实例epfd中
			epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
		}else if(events[i].events & EPOLLIN){//如果事件是读事件,则进行读取数据
			char buffer[1024] = {0};

			int count = recv(connfd, buffer, 1024, 0);
			if(count == 0){//如果读取到的数据量为0则断开disconnect
				printf("client disconnect: %d\n", connfd);
				close(connfd);
//将客户端从实例中删除
				epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
				continue;
			}
			printf("RECV: %s\n", buffer);

			count = send(connfd, buffer, count, 0);
			printf("SEND: %d\n", count);
		}
			
	}
	
}

https://github.com/0voice

;