多路复用-select

select 的原理

 在网络IO一篇中我们讲到了5种的IO网络模型。而select则是多路复用中的一种。它把等待数据就绪和读取数据区分开,实现了单线程操作多个网络IO的功能。
 select如何实现单线程中对多个网络IO读取操作的呢?

内核根据io的3种状态,将各个状态准备好的io放入对应的位图中去。而后,我们根据对应的位图,去遍历io获取或写入数据。由此实现同时操作多个io。

重点小黑板:

  1. io的3种状态 可读、可写、是否出错
  2. 位图 fd_set,最大为1024位
  3. socket创建出来的fd是一个int型,而且是逐次递增的。如果中间关闭了一个fd,则下次创建的时候,新建的fd为之前关闭的fd值。

基本函数

fd_set: 该类型可以简单的理解为按 bit 位标记句柄的队列,例如标记一个值为8的句柄,则该fd_set的第8位标记为1.
头文件#include <arpa/inet.h>

select

  • 原型
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,struct timeval *timeout)
  • 参数介绍
参数名说明
nfds遍历到的最大socketfd
readfds获取到的可读的io列表
writefds获取到的可写的io列表
exceptfds获取到的出错的io列表
timeout读取io状态的间隔时间

timeout为NULL时,表示置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;
为0时,表示不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
大于0时,该值为等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回,否则在超时后一定返回,文件无变化返回0,有变化返回一个正值。

  • 函数功能
     用来获取出可读、可写、出错的io列表

  • 注意事项
    当新客户端connect的时候也会触发select的可读事件,故也需要处理

对fd_set 操作

函数名函数原型函数作用
FD_ISSETFD_ISSET(int fd, fd_set* fds)判断fd是否存在于fds
FD_SETFD_SET(int fd, fd_set* fds)在fds中标记fd的句柄
FD_CLRFD_CLR(int fd, fd_set* fds)将fd的标记从fds集合中清除
FD_ZEROFD_ZERO(fd_set* fds)清空fds里面所有的标记

用select搭建一个简单的服务端

#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>

#include <errno.h>

#define BUFFER_LEN	1024

int main()
{
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd < 0)
	{
		printf("create socket error!\n");
		return -1;
	}

	struct sockaddr_in addr;
	memset(&addr, 0, sizeof(struct sockaddr_in));

	addr.sin_family = AF_INET;
	addr.sin_port = htons(4018);
	addr.sin_addr.s_addr = INADDR_ANY;
	
	if(bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0)
	{
		printf("bind socket error! \n");
		return -1;
	}

	if (listen(sockfd, 5) < 0)
	{
		printf("listen socket error! \n");
		return -1;
	}
	
	fd_set rReadset,rfds;
	FD_ZERO(&rfds);
	FD_SET(sockfd, &rfds);
	int maxfd = sockfd;

	while (1)
	{
		rReadset = rfds;
		// 这里只获取可读状态的io
		int nready = select(maxfd + 1, &rReadset, NULL, NULL, NULL);

		if (nready < 0)
		{
			printf("select error!\n");
			continue;
		}

		if (FD_ISSET(sockfd, &rReadset))
		{
			struct sockaddr_in clientAddr;
			memset(&clientAddr, 0, sizeof(struct sockaddr_in));
			int cliLen = sizeof(struct sockaddr_in);

			int clientfd = accept(sockfd, (struct sockaddr*)&clientAddr, (socklen_t *)&cliLen);
			if (clientfd < 0)
			{
				printf("accept client error\n");
				continue;
			}

			char str[INET_ADDRSTRLEN] = {0};
			printf("recv from %s at port %d, sockfd:%d, clientfd:%d\n", inet_ntop(AF_INET, &clientAddr.sin_addr, str, sizeof(str)),ntohs(clientAddr.sin_port), sockfd, clientfd);

			if (maxfd == FD_SETSIZE)
			{
				printf("clientfd out of range\n");
				break;
			}
			
			FD_SET(clientfd, &rfds);
			maxfd = clientfd > maxfd ? clientfd : maxfd;
			
			printf("sockfd:%d, max_fd:%d, clientfd:%d\n", sockfd, maxfd, clientfd);

			if (--nready == 0)
			{
				continue;
			}
		}

		for (int i = sockfd + 1; i < maxfd + 1; i++)
		{
			if (!FD_ISSET(i, &rReadset))
			{
				continue;
			}

			char recvBuf[BUFFER_LEN] = {0};
			int ret = read(i, recvBuf, BUFFER_LEN);
			
			if (ret < 0)
			{
				if (errno == EAGAIN || errno == EWOULDBLOCK)
				{
					printf("read all data early\n");
				}

				FD_CLR(i, &rfds);
				close(i);
			}else if (ret == 0)
			{
				printf("connet is close %d\n", i);
				FD_CLR(i, &rfds);
				close(i);
			}
			else
			{
				printf("Recv: %s, %d Bytes\n", recvBuf, ret);
			}
			
			if (-- nready == 0)
			{
				break;
			}
		}
	}

	return 0;
}

总结

fd_set 数据结构中只有1024位,故通常说法是select最高支持1024个连接。
select在select调用的时候也是阻塞的,因为kernel 会“监视”所有 select 负责的 socket,当任何一个 socket 中的数据准备好了,select 就会返回。

优点:只用单线程执行,占用资源少,不消耗太大的cpu资源,能够同时为多个客户端提供服务
缺点:当句柄值太大的时候,本身需要消耗大量的时间去轮询。很多操作系统提供了更为高效的接口,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll

  • 21
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值