用IO多路复用之select与poll/epoll来浅谈网络聊天室

IO多路复用:

IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。
在这里插入图片描述

select

该函数用于监视文件描述符的变化情况–读写或是异常

在这里插入图片描述
参数:

nfds: 通常被设置为select所监听的所有文件描述符中的最大值+1

		指定被监听的文件描述符的总数

		文件描述符从0开始

fd_set: 文件描述符集合

		fd_set结构体仅包含一个整数数组,该数组的每个元素的每一位(bit)

		标记一个文件描述符

		fd_set能容纳 的文件描述符数量由FD_SETSIZE指定

		限制了select所有处理的文件描述符数量 

		FD_ZERO(fd_set *fdset); //清除所有位

		FD_SET(int fd,fd_set *fdset);   //设置fd

		FD_CLR(int fd,fd_set *fdset);   //清除fd

		int FD_ISSET(int fd,fd_set *fdset); //测试

	readfds,writefds,exceptfds 分别用于记录要监听是否

		可读、可写、异常事件的文件描述符符合

	select函数调用返回时,内核将修改readfds,writefds,excepts

	文件描述符集合,保留有数据可读、可写、异常的文件描述符

timeout:

	设置select函数的超时时间

	返回select调用返回后剩余的时间,如果调用失败时timeout不确定

	如果timeout变量成员都为0,则select立即返回

	如果timeout取值为NULL,select将一直阻塞,直到某个文件描述符就绪

	struct timeval{

		long tv_sec;   //秒数

		long tv_usec;  //微秒数

	}

返回值:

	select成功时返回就绪(可读、可写和异常)文件描述符的总和

	如果在超时时间内滑任何文件描述符就绪,返回0

	select失败返回-1并设置errno

	在select等待期间,程序接收到信号,select立即返回-1,

	errno为EINTR

原理:

	把需要监听的文件描述符集合交给内核去测试,保留就绪的文件描述



	每一次都需要重新把所有需要监听的文件描述符加入到文件描述符集合中

	内核中需要遍历所有的文件描述符

	当select返回之后,还需要循环去所有的文件描述符去判断是否就绪

	

	当文件描述符数量增大时,效率其实是急剧下降的

	select还受到FD_SETSIZE的限制 

	当select返回之后,没有并行,而是串行 一个一个接收客户端的数据,转发

select()函数与Linux驱动程序的关系

当用户调用select系统调用时,select系统调用会先调用poll_initwait(&table),然后调用驱动程序中 struct file_operations下的fop->poll函数,在这个函数里应该调用poll_wait(),将current加到某个等待队列(这里调用poll_wait()),并检查是否有效,如果无效就调用schedule_timeout();去睡眠。事件发生后,schedule_timeout()回来,调用fop->poll(),检查到可以运行,就调用poll_freewait(&table);从而完成select系统调用。重要的是fop->poll()里面要检查是否就绪,如果是,要返回相应标志。

聊天室

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <errno.h>
#include <assert.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>

#define NAME_LEN 48
#define MAX_CLIENTS 100
#define MSG_LEN 1024
typedef struct Client{
	int fd;
	struct sockaddr_in addr;
	char name[NAME_LEN];
}Client;

//全局变量 每个线程都会访问 
Client gcls[MAX_CLIENTS] = {};
int size = 0;

int init_server(const char *ip,unsigned short int port){
	int fd = socket(AF_INET,SOCK_STREAM,0);
	assert(fd != -1);
	struct sockaddr_in addr = {};
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port);
	addr.sin_addr.s_addr = inet_addr(ip);
	socklen_t len = sizeof(addr);
	int ret = bind(fd,(const struct sockaddr*)&addr,len);
	assert(ret != -1);
	ret = listen(fd,MAX_CLIENTS);
	assert(ret != -1);
	return fd;
}

void broadcast(int fd,const char *msg){
	int i;
	for(i=0;i<size;i++){
		if(fd != gcls[i].fd){
			send(gcls[i].fd,msg,strlen(msg)+1,0);	
		}	
	}
}

void remove_client(int fd){
	int i;
	for(i=0;i<size;i++){
		if(fd == gcls[i].fd){
			close(fd);
			gcls[i] = gcls[--size];
			break;
		}	
	}
}

void select_fd(int fd){
	Client cls = {};
	socklen_t len = sizeof(cls.addr);
	int ret = 0,i,cnt;
	fd_set readfds; //可读文件描述符集
	int maxfd = 0;  //记录最大的文件描述符
	while(true){
		FD_ZERO(&readfds);//文件描述符集合清空
		maxfd = fd;
		FD_SET(fd,&readfds);//服务器的fd  用于判断是否有客户端连接
		for(i=0;i<size;i++){
			FD_SET(gcls[i].fd,&readfds);//和客户端交互的文件描述符
			if(gcls[i].fd > maxfd)
				maxfd = gcls[i].fd;
		}
		cnt = select(maxfd+1,&readfds,NULL,NULL,NULL);//阻塞
		if(FD_ISSET(fd,&readfds)){//服务器的fd  有客户端连接上来了
			cls.fd = accept(fd,(struct sockaddr*)&cls.addr,&len);
			if(cls.fd != -1){
				gcls[size++] = cls;	//进入聊天室 没有发网名过来
			}
			--cnt;
		}
		for(i=0;i<size&&cnt>0;i++){
			if(FD_ISSET(gcls[i].fd,&readfds)){//确保有数据可读  线程池
				--cnt;
				char msg[MSG_LEN] = {};
				if(strcmp(gcls[i].name,"")==0){//发送网名
					ret = recv(gcls[i].fd,msg,MSG_LEN,0);
					if(ret <= 0){
						remove_client(gcls[i].fd);
						continue;
					}
					strcpy(gcls[i].name,msg);
					strcat(msg," 进入聊天室,大家欢迎!");
				}else{//发送数据
					strcpy(msg,gcls[i].name);
					strcat(msg,":");
					int len = strlen(msg);
					ret = recv(gcls[i].fd,msg+len,MSG_LEN-len,0);
					if(ret <= 0){
						msg[len-1] = '\0';
						strcat(msg," 退出群聊,大家欢送!");
					}
				}
				broadcast(gcls[i].fd,msg);
				if(ret <= 0){
					remove_client(gcls[i].fd);	
				}
			}	
		}
	}
}

int main(int argc,char *argv[]){
	if(argc < 3){
		printf("%s ip port\n",argv[0]);
		return -1;
	}
	int fd = init_server(argv[1],atoi(argv[2]));
	select_fd(fd);
	return 0;	
}


poll/epoll

poll和select实现功能差不多,但poll效率高。poll不需要每次都把所有的文件描述符加入到集合中(数组)
poll通知用户的方式不一样,select把没有就绪的文件描述符从集合中删除,poll内核直接设置集合中每一个数据的revents属性,相同的地方在于,调用完poll/select函数,需要遍历所有的文件描述符,判断是否就绪poll()接受一个指向结构’struct pollfd’列表的指针,其中包括了你想测试的文件描述符和事件。事件由一个在结构中事件域的比特掩码确定。当前的结构在调用后将被填写并在事件发生后返回。在SVR4(可能更早的一些版本)中的 "poll.h"文件中包含了用于确定事件的一些宏定义。事件的等待时间精确到毫秒 (但令人困惑的是等待时间的类型却是int),当等待时间为0时,poll()函数立即返回,-1则使poll()一直挂起直到一个指定事件发生。下面是pollfd的结构。
在这里插入图片描述
参数:

	fds:是一个struct pollfd结构体类型的数组

		struct pollfd{

			int fd;            //文件描述符

			short events;      //注册的事件  可读 可写

			short revents;     //实际发生的事件 由内核填充

		};

		POLLIN         数据可读

		POLLOUT        数据可写

	nfds:

		数组长度

	timeout:

		超时等待的毫秒数

		-1   阻塞

		0    立即返回

返回值:

	和select意义相同  就绪状态的文件描述符的个数

epoll是Linux特有的I/O复用函数

epoll把用户关心的文件描述符上的事件注册到内核的事件表中

不需要像select/poll每次都调用都要重传文件描述符集和事件集

epoll也不需要调用完成之后遍历所有的文件描述符集

epoll需要使用一个额外的文件描述符,用来唯一标识内核中事件表

pool来实现网络聊天室

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <errno.h>
#include <assert.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>

#define NAME_LEN 48
#define MAX_CLIENTS 100
#define MSG_LEN 1024
typedef struct Client{
	int fd;
	struct sockaddr_in addr;
	char name[NAME_LEN];
}Client;

Client gcls[MAX_CLIENTS+1] = {};//gcls[0] 不存储值
struct pollfd fds[MAX_CLIENTS+1] = {};
int size = 0;

int init_server(const char *ip,unsigned short int port){
	int fd = socket(AF_INET,SOCK_STREAM,0);
	assert(fd != -1);
	struct sockaddr_in addr = {};
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port);
	addr.sin_addr.s_addr = inet_addr(ip);
	socklen_t len = sizeof(addr);
	int ret = bind(fd,(const struct sockaddr*)&addr,len);
	assert(ret != -1);
	ret = listen(fd,MAX_CLIENTS);
	assert(ret != -1);
	return fd;
}

void broadcast(int fd,const char *msg){
	int i;
	for(i=1;i<size;i++){
		if(fd != gcls[i].fd){
			send(gcls[i].fd,msg,strlen(msg)+1,0);	
		}	
	}
}

void accept_client(int fd){
	struct sockaddr_in addr = {};
	socklen_t len = sizeof(addr);
	int cfd = accept(fd,(struct sockaddr*)&addr,&len);
	if(cfd != -1){
		Client cls = {};
		cls.fd = cfd;
		cls.addr = addr;
		gcls[size] = cls;
		fds[size].fd = cfd;
		fds[size].events = POLLIN;
		++size;
	}
}

int recv_data(int index){
	int fd = fds[index].fd;	
	char msg[MSG_LEN] = {};
	int ret = 0;
	if(strcmp(gcls[index].name,"")==0){
		ret = recv(fd,msg,MSG_LEN,0);
		if(ret <= 0){
			return 0;
		}
		strcpy(gcls[index].name,msg);
		strcat(msg," 进入聊天室,大家欢迎!");
	}else{
		strcpy(msg,gcls[index].name);
		strcat(msg,":");
		int len = strlen(msg);
		ret = recv(fd,msg+len,MSG_LEN-len,0);
		if(ret <= 0){
			msg[--len] = '\0';
			strcat(msg," 退出聊天室,大家欢送!");	
		}
	}
	broadcast(fd,msg);
	if(ret <= 0)
		return 0;
	return 1;
}
void select_fd(int fd){
	int ret = 0,i,cnt;
	//fds  struct pollfd fds[]  
	fds[0].fd = fd;        //文件描述符
	fds[0].events = POLLIN;//监听的事件
	size = 1;
	while(true){
		cnt = poll(fds,size,-1);//
		for(i=0;i<size&&cnt>0;i++){
			if(fds[i].revents & POLLIN){//fds[i].fd就绪
				--cnt;
				if(fds[i].fd == fd){//客户端连接上来
					accept_client(fd);//接收客户端连接请求				
				}else{//数据
					if(recv_data(i)==0){
						gcls[i] = gcls[size-1];
						fds[i] = fds[size-1];
						--size;
						--i;
					}
				}
			}	
		}
	}
}

int main(int argc,char *argv[]){
	if(argc < 3){
		printf("%s ip port\n",argv[0]);
		return -1;
	}
	int fd = init_server(argv[1],atoi(argv[2]));
	select_fd(fd);
	return 0;	
}


epoll来实现网络聊天室(QQ群)

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <errno.h>
#include <assert.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>

#define NAME_LEN 48
#define MAX_CLIENTS 100
#define MSG_LEN 1024
typedef struct Client{
	int fd;
	struct sockaddr_in addr;
	char name[NAME_LEN];
}Client;

Client gcls[MAX_CLIENTS+1] = {};
int size = 0;

int init_server(const char *ip,unsigned short int port){
	int fd = socket(AF_INET,SOCK_STREAM,0);
	assert(fd != -1);
	struct sockaddr_in addr = {};
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port);
	addr.sin_addr.s_addr = inet_addr(ip);
	socklen_t len = sizeof(addr);
	int ret = bind(fd,(const struct sockaddr*)&addr,len);
	assert(ret != -1);
	ret = listen(fd,MAX_CLIENTS);
	assert(ret != -1);
	return fd;
}

void broadcast(int fd,const char *msg){
	int i;
	for(i=0;i<size;i++){
		if(fd != gcls[i].fd){
			send(gcls[i].fd,msg,strlen(msg)+1,0);	
		}	
	}
}

void accept_client(int fd,int epfd){
	struct sockaddr_in addr = {};
	socklen_t len = sizeof(addr);
	int cfd = accept(fd,(struct sockaddr*)&addr,&len);
	if(cfd != -1){
		Client cls = {};
		cls.fd = cfd;
		cls.addr = addr;
		gcls[size] = cls;
		++size;
		struct epoll_event event = {};
		event.events = EPOLLIN;
		event.data.fd = cfd;
		int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&event);
		if(ret == -1){
			perror("epoll_ctl");	
		}
	}
}

int recv_data(int fd){
	int index = 0;
	for(;index<size;index++){
		if(gcls[index].fd == fd){
			break;	
		}	
	}
	char msg[MSG_LEN] = {};
	int ret = 0;
	if(strcmp(gcls[index].name,"")==0){
		ret = recv(fd,msg,MSG_LEN,0);
		if(ret <= 0){
			return 0;
		}
		strcpy(gcls[index].name,msg);
		strcat(msg," 进入聊天室,大家欢迎!");
	}else{
		strcpy(msg,gcls[index].name);
		strcat(msg,":");
		int len = strlen(msg);
		ret = recv(fd,msg+len,MSG_LEN-len,0);
		if(ret <= 0){
			msg[--len] = '\0';
			strcat(msg," 退出聊天室,大家欢送!");	
		}
	}
	broadcast(fd,msg);
	if(ret <= 0)
		return 0;
	return 1;
}
void select_fd(int fd){
	int epfd = epoll_create(MAX_CLIENTS);
	if(epfd == -1){
		perror("epoll_create");
		return;	
	}
	struct epoll_event event = {};
	event.events = EPOLLIN;  //读事件
	event.data.fd = fd;     //用户数据
	int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&event);
	if(ret == -1){
		perror("epoll_ctl");
		return ;
	}
	struct epoll_event events[MAX_CLIENTS+1] = {};
	int i;
	while(true){
		ret = epoll_wait(epfd,events,MAX_CLIENTS+1,-1);
		if(ret == -1){
			perror("epoll_wait");
			break;
		}
		for(i=0;i<ret;i++){
			if(events[i].data.fd == fd){//有客户端连接
				accept_client(fd,epfd);
			}else{//有数据需要接收
				if(events[i].events & EPOLLIN){
					ret = recv_data(events[i].data.fd);
					if(ret == 0){
						struct epoll_event ev = {};
						ev.events = EPOLLIN;
						ev.data.fd = events[i].data.fd;
						ret = epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,&ev);
						if(ret == -1){
							perror("epoll_ctl");	
						}
					}
				}
			}
		}
	}
}

int main(int argc,char *argv[]){
	if(argc < 3){
		printf("%s ip port\n",argv[0]);
		return -1;
	}
	int fd = init_server(argv[1],atoi(argv[2]));
	select_fd(fd);
	return 0;	
}


总结

select:

	用户通过3个参数分别传入可读、可写、异常的事件文件描述符集合

	内核通过对这些参数的在线修改来反馈其中的就绪事件

	使得用户每次调用select都需要重置这3个参数

poll:

	统一处理所有事件类型,因此只需要一个事件集参数。

	用户可以通过结构体events传入事件 

	内核通过修改结构体的revents反馈其中就绪的事件

	用户不需要每次都重置events参数

epoll:

	内核通过一个事件表直接管理用户注册的所有事件

	因此每次调用epoll_wait时,无须反复传入用户注册的事件

	epoll_wait参数events仅用来反馈就绪的事件

	不需要遍历所有的文件描述符集合

应用程序索引文件描述符的时间复杂度

select:  O(n)

poll:    O(n)

epoll:   O(1)

最大支持的文件描述符

select: 受多方制约

poll:   65535

epoll:  65535

工作模式

select: LT

poll:   LT

epoll:  默认是LT  支持ET高效模式

内核实现和工作效率

select:

	采用轮询方式来检测就绪事件,算法时间复杂度为O(n)

poll:

	采用轮询方式来检测就绪事件,算法时间复杂度为O(n)

epoll:

	采用回调方式来检测就绪事件,算法时间复杂度为O(1)
  • 0
    点赞
  • 0
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:1024 设计师:我叫白小胖 返回首页
评论

打赏作者

HOVL_C++

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值