多路复用 select poll epoll

多路复用:

使用一个进程(且只有主线程)同监控若干个文件描述符的读写,这种读写模式叫多路复用。多用于TCP的服务端,用于监控客户端的连接和数据的收发

​ 优点:不需要频繁地创建进程、销毁进程,从而节约了内存资源、时间资源,也避免了进程之间的竞争、等 待。

​ 缺点:单个客户端的任务不能太耗时,否则其他客户端就会感知到

​ 适合并发量高,但任务短小的情景,如:Web服务器

select

int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);
功能:监控多个文件描述符的读、写、异常	操作

fd_set:文件描述符的集合

void FD_CLR(int fd, fd_set *set);
功能:从集合中删除文件描述符
int  FD_ISSET(int fd, fd_set *set);
功能:判断文件描述符是否在集合中
void FD_SET(int fd, fd_set *set);
功能:把文件描述符添加到集合中
void FD_ZERO(fd_set *set);
功能:清空集合

nfds:被监控的文件描述符中最大+1
readfds:监控读操作的文件描述符集合
writefds:监控写操作的文件描述符集合
exceptfds:监控异常操作的文件描述符集合
timeout:设置超时时间
返回值:监控到发生相关操作的描述符个数,超时返回0,错误返回-1
   
注意:
    readfds、writefds、exceptfds	这三个参数既是输入也是输出,也就是说调用select函数时里面存	放着需要监控的文件描述符,函数返回后发生操作的文件描述符存在里面
    
select设计不合理的地方:
    1、每次调用select时都需要向它传递被监控者的信息
    2、调用结束后若想知道具体哪些文件描述符发生了相关操作,需要把所有文件描述符都测试一遍
select的优点:
    它是最早的多路复用函数,几乎所有的操作系统都支持,程序的兼容性高
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>

int main(int argc,const char* argv[])
{
	//	创建socket对象
	int svr_fd = socket(AF_INET,SOCK_STREAM,0);
	if(0 > svr_fd)
	{
		perror("socket");
		return EXIT_FAILURE;
	}
	//	准备通信地址
	struct sockaddr_in addr = {};
	addr.sin_family = AF_INET;
	addr.sin_port = htons(6789);
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	socklen_t addrlen = sizeof(addr);

	//	绑定
	if(bind(svr_fd,(struct sockaddr*)&addr,addrlen))
	{
		perror("bind");
		return EXIT_FAILURE;
	}
	//	网络监听
	if(listen(svr_fd,10))
	{
		perror("listen");
		return EXIT_FAILURE;
	}
	//	定义一个文件描述符集合并清空
	fd_set reads;
	FD_ZERO(&reads);
	
	//	把需要网络等待的socket描述符添加到集合中
	FD_SET(svr_fd,&reads);

	//	定义一个超时时间
	struct timeval timeout = {5,500};

	//	记录最大的文件描述符
	int max_fd = svr_fd;
	
	char buf[4096] = {};
	size_t buf_size = sizeof(buf);

	for(;;)
	{
		//	备份集合
		fd_set reads_copy = reads;
		//	调用select 最大描述符 读操作 超时时间
		int ret = select(max_fd+1,&reads_copy,NULL,NULL,&timeout);
		if(ret > 0)
		{
			//	先测试网络等待的socket描述符
			if(FD_ISSET(svr_fd,&reads_copy))
			{
				//	调用accept连接客户端
				int cli_fd = accept(svr_fd,(struct sockaddr*)&addr,&addrlen);
				if(0 > cli_fd)
				{
					perror("accept");
				}
				else
				{
					//	把客户端的socket描述符添加到监控集合中
					FD_SET(cli_fd,&reads);
					if(cli_fd > max_fd)
					{
						max_fd = cli_fd;
					}
				}
			}
			else
			{
				//	测试其他socket描述符是发生读操作
				for(int fd=3; fd<=max_fd; fd++)
				{
					if(FD_ISSET(fd,&reads_copy) && fd != svr_fd)
					{
						int ret = recv(fd,buf,buf_size,0);
						if(0 >= ret)
						{
							FD_CLR(fd,&reads);
							printf("客户端%d退出\n",fd);
							continue;
						}
						printf("recv:%s bits:%d\n",buf,ret);
						strcat(buf,":return");
						ret = send(fd,buf,strlen(buf)+1,0);

						if(0 >= ret)
						{
							FD_CLR(fd,&reads);
							printf("客户端%d退出\n",fd);
						}

					}
				}
			}
		}
	}
	//	注意:需要一个数组用于储存所有正在被监听的描述符
}

pselect

int pselect(int nfds, fd_set *readfds, fd_set *writefds,
                   fd_set *exceptfds, const struct timespec *timeout,
                   const sigset_t *sigmask);
功能:与select类似
区别:
	1、超时时间类型不同
	struct timeval {
               long    tv_sec;         /* seconds */
               long    tv_usec;        /* microseconds */
    };

    struct timespec {
               long    tv_sec;         /* seconds */
               long    tv_nsec;        /* nanoseconds */
    };
	
	2、pselect函数timeout,既是输入也是输出,它可以返回剩余时间,而select不会改变timeout
	
	3、pselect监听时可以通过sigmask参数设置要屏蔽的信号,可以保障pselect的监听可以不被信号打扰

poll

 struct pollfd {
               int   fd;         //	被监听的文件描述符
               short events;     //	要监听的事件
               short revents;    //	实际监听到的时间
               		POLLIN		普通优先级的读事件
               		POLLPRI		高优先级的读事件
               		POLLOUT		普通优先级的写事件
               		POLLRDHUP	对方socket关闭
               		POLLERR		错误事件
               		POLLHUP		对方挂起
               		POLLNVAL	非法描述符
 };
 
 int poll(struct pollfd *fds, nfds_t nfds, int timeout);
 fds:pollfd结构体数组
 nfds:数组的长度
 timeout:超时时间

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

int add_fds(struct pollfd* fds,int nfds,int fd,short events)
{
	for(int i=0; i<nfds; i++)
	{
		if(0 == fds[i].fd)
		{
			fds[i].fd = fd;
			fds[i].events = events;
			return 0;
		}
	}
	return -1;
}

// TCP sever
int main(int argc,const char* argv[])
{
	// 创建socket对象 
	int svr_fd = socket(AF_INET,SOCK_STREAM,0);
	if(0 > svr_fd)
	{
		perror("socket");
		return EXIT_FAILURE;
	}

	// 准备通信地址
	struct sockaddr_in addr = {};
	addr.sin_family = AF_INET;
	addr.sin_port = htons(6789);
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	socklen_t addrlen = sizeof(addr);

	// 绑定
	if(bind(svr_fd,(struct sockaddr*)&addr,addrlen))
	{
		perror("bind");
		return EXIT_FAILURE;
	}

	// 网络监听
	if(listen(svr_fd,10))
	{
		perror("bind");
		return EXIT_FAILURE;
	}

	// 创建pollfd数组并初始化 calloc
	struct pollfd* fds = calloc(sizeof(struct pollfd),10);

	// 设置[0]位置要监听的描述符和事件
	fds[0].fd = svr_fd;
	fds[0].events = POLLIN;

	// 定义超时时间
	int timeout = 10000;
	char buf[4096] = {};
	size_t buf_size = sizeof(buf);

	for(;;)
	{
		// 调用poll函数
		int ret = poll(fds,10,timeout);
		if(0 ==ret)continue;
		if(0 > ret)
		{
			perror("poll");
			return EXIT_FAILURE;
		}

		// 判断[0]位置是否有读事件产生
		if(fds[0].revents & POLLIN)
		{
		
			// accept 连接客户端
			int cli_fd = accept(svr_fd,(struct sockaddr*)&addr,&addrlen);
			if(0 < cli_fd)
			{
				// 把客户端的socket描述符添加空pollfd数组的空位置,并设置事件
				if(add_fds(fds,10,cli_fd,POLLIN))
				{
					printf("客户端数量已满");
				}
			}
		}
		else
		{
			// 遍历pollfd数组判断其它位置是否有读事件产生
			for(int i=1; i<10; i++)
			{
				if(!(fds[i].revents&POLLIN))
				{
					continue;
				}
				int ret = recv(fds[i].fd,buf,buf_size,0);
				if(0 >= ret)
				{
					printf("客户端%d退出\n",fds[i].fd);
					fds[i].fd = 0;
					fds[i].events = 0;
					continue;
				}
				printf("recv:%s bits:%d\n",buf,ret);
				strcat(buf,":return");
				ret = send(fds[i].fd,buf,strlen(buf)+1,0);
				if(0 >= ret)
				{
					printf("客户端%d退出\n",fds[i].fd);
					fds[i].fd = 0;
					fds[i].events = 0;
				}
			}
		}
	}
}	

epoll(常用)

int epoll_create(int size);
功能:用于创建一个epoll对象,该对象可以保存被监控的描述符
size:epoll对象保存描述符的数量
返回值:epoll对象的描述符

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:控制epoll对象,添加、删除描述符
epfd:epoll对象描述符
op:
	EPOLL_CTL_ADD	添加描述符
	EPOLL_CTL_MOD	修改要监控的事件
	EPOLL_CTL_DEL	删除描述符
fd:
	要操作的描述符
event:
	要监控的事件
返回值:成功 0 	失败 -1
	
int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);
功能:监控文件描述符,并返回事件产生的文件描述符
epfd:epoll对象描述符
events:输出型参数,用于获取事件发生的文件描述符
maxevents:epoll对象的空间大小
timeout:超时时间
返回值:事件发生的描述符数量

优点:
	1、只需要拷贝一次描述符
	2、会把发生事件的描述符返回,不需要遍历所有的描述符
	3、编程结构简介
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define EPOLL_SIZE (10)
// TCP sever
int main(int argc,const char* argv[])
{
	// 创建socket对象 
	int svr_fd = socket(AF_INET,SOCK_STREAM,0);
	if(0 > svr_fd)
	{
		perror("socket");
		return EXIT_FAILURE;
	}

	// 准备通信地址
	struct sockaddr_in addr = {};
	addr.sin_family = AF_INET;
	addr.sin_port = htons(6788);
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	socklen_t addrlen = sizeof(addr);

	// 绑定
	if(bind(svr_fd,(struct sockaddr*)&addr,addrlen))
	{
		perror("bind");
		return EXIT_FAILURE;
	}

	// 网络监听
	if(listen(svr_fd,10))
	{
		perror("bind");
		return EXIT_FAILURE;
	}

	// 创建epoll对象
	int epfd = epoll_create(EPOLL_SIZE);
	if(0 > epfd)
	{
		perror("epoll_create");
		return EXIT_FAILURE;
	}


	// 向epoll对象中添加描述符
	struct epoll_event event;
	event.events = EPOLLIN;
	event.data.fd = svr_fd;

	if(epoll_ctl(epfd,EPOLL_CTL_ADD,svr_fd,&event))
	{
		perror("epoll_ctl");
		return EXIT_FAILURE;
	}

	// 定义存储监控结果的数组
	struct epoll_event events[EPOLL_SIZE] = {};

	char buf[4] = {};
	size_t buf_size = sizeof(buf);
	for(;;)
	{
		// 监听
		int event_cnt = epoll_wait(epfd,events,EPOLL_SIZE,10000);
		if(0 == event_cnt)continue;

		printf(" epoll_wait 有事件触发!\n");

		// 遍历监听的结果
		for(int i=0; i<event_cnt; i++)
		{
			if(svr_fd == events[i].data.fd)
			{
				int cli_fd = accept(svr_fd,(struct sockaddr*)&addr,&addrlen);
				if(0 < cli_fd)
				{
					event.events = EPOLLIN | EPOLLET;
					event.data.fd = cli_fd;
					epoll_ctl(epfd,EPOLL_CTL_ADD,cli_fd,&event);
				}
			}
			else
			{
				int ret = 0;
				while(-1 != (ret = recv(events[i].data.fd,buf,buf_size,MSG_DONTWAIT)))
				{
					if(0 == ret)
					{
						printf("客户端%d退出\n",events[i].data.fd);
						epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,NULL);
						continue;
					}
					printf("recv:%s bits:%d\n",buf,ret);
				}
				strcpy(buf,"return");
				ret = send(events[i].data.fd,buf,strlen(buf)+1,0);
				if(0 >= ret)
				{
					printf("客户端%d退出\n",events[i].data.fd);
					epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,NULL);
				}
			}
		}
	}
}

epoll的条件触发和边缘触发

​ 条件触发:当文件缓冲区中有要读取的数据就会触发事件

​ 边缘触发:当数据发送过来时触发一次事件

​ 1、事件设置为EPOLLET 边缘触发

​ 1、要循环读取

​ 2、要以非阻塞方式读取

​ 3、当返回-1时说明数据读取完毕

​ 优点:降低事件触发的次数

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值