Linux之多路I/O转接服务器(一)——select模型

select模型

一、模型概述

select系统调用的的用途是:在一段指定的时间内,监听用户感兴趣的文件描述符上可读、可写和异常等事件。

select 机制的优势

为什么会出现select模型?
先看一下下面的这句代码:

int iResult = recv(s, buffer,1024);

这是用来接收数据的,在默认的阻塞模式下的套接字里,recv会阻塞在那里,直到套接字连接上有数据可读,把数据读到buffer里后recv函数才会返回,不然就会一直阻塞在那里。在单线程的程序里出现这种情况会导致主线程(单线程程序里只有一个默认的主线程)被阻塞,这样整个程序被锁死在这里,如果永远没数据发送过来,那么程序就会被永远锁死。这个问题可以用多线程解决,但是在有多个套接字连接的情况下,这不是一个好的选择,扩展性很差

再看代码:

int iResult = ioctlsocket(s, FIOBIO, (unsigned long *)&ul);
iResult = recv(s, buffer,1024);

这一次recv的调用不管套接字连接上有没有数据可以接收都会马上返回。原因就在于我们用ioctlsocket把套接字设置为非阻塞模式了。不过你跟踪一下就会发现,在没有数据的情况下,recv确实是马上返回了,但是也返回了一个错误:WSAEWOULDBLOCK,意思就是请求的操作没有成功完成。

看到这里很多人可能会说,那么就重复调用recv并检查返回值,直到成功为止,但是这样做效率很成问题,开销太大

select模型的出现就是为了解决上述问题
select模型的关键是使用一种有序的方式,对多个套接字进行统一管理与调度
在这里插入图片描述
如上所示,用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行
使用select以后最大的优势用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

二、select函数

2.1select函数的功能和调用顺序

使用select函数时统一监视多个文件描述符的:
1、 是否存在套接字接收数据?
2、 无需阻塞传输数据的套接字有哪些?
3、 哪些套接字发生了异常?

select函数调用过程:
在这里插入图片描述
由上图知,调用select函数需要一些准备工作,调用后还需要查看结果。

2.2设置文件描述符

select可以同时监视多个文件描述符(套接字)。
此时需要先将文件描述符集中到一起。集中时也要按照监视项(接收,传输,异常)进行区分,即按照上述3种监视项分成三类。
使用fd_set数组变量执行此项操作,该数组是存有0和1的位数组。
在这里插入图片描述
最左端的位表示文件描述符0(位置)。如果该位值为1,则表示该文件描述符是监视对象
图上显然监视对象为fd1和fd3。

操作fd_set的值由如下宏来完成:

FD_ZERO(fd_set* fdset): 将fd_set变量的所有位初始化为0。
FD_SET(int fd, fd_set* fdset):在参数fdset集合中注册文件描述符fd的信息。
FD_CLR(int fd, fd_set* fdset):参数fdset集合中清除文件描述符fd的信息。
FD_ISSET(int fd, fd_set* fdset): 文件描述符fd是否被置位。

2.3 设置监视范围及超时

#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);

参数详情:
maxfdp:被监听的文件描述符的总数,它比所有文件描述符集合中的文件描述符的最大值大1,因为文件描述符是从0开始计数的。(监视范围
readset:要检查读事件的容器,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读文件,则根据timeout参数判断是否超时,若超时则返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。
writeset:要检查写事件的容器,主要关心写事件。
exceptset:用来监视文件错误信息。
timeout超时时间

  • 永远等待下去:仅在有一个描述字准备好I/O时才返回,为此,我们将timeout设置为空指针NULL
  • 等待固定时间:在有一个描述字准备好I/O是返回,但不超过由timeout参数所指timeval结构中指定的秒数和微秒数
struct timeval
{      
    long tv_sec;   /*秒 */
    long tv_usec;  /*微秒 */   
};
  • 根本不等待:检查描述字后立即返回,这称为轮询。定时器的值必须为0

本来select函数只有在监视文件描述符发生变化时才返回,未发生变化会进入阻塞状态。指定超时时间就是为了防止这种情况发生。
将上述结构体填入时间值,然后将结构体地址值传给select函数的最后一个参数,此时,即使文件描述符中未发生变化,只要过了指定时间,也可以从函数返回。不过这种情况下,select函数返回0。

返回值:错误返回-1,超时返回0。因关注的事件返回时,返回大于0的值,该值是发生事件的文件描述符数。

三、举例(服务器与客户端)

/*selectServer.c*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAXLINE 80
#define SERV_PORT 8000
#define INET_ADDRSTRLEN 16
 
void perr_exit(const char *s)
{
	perror(s);
	exit(1);
}

int main(int argc, char *argv[])
{
	int i, maxi, maxfd, listenfd, connfd, sockfd;
	//自定义数组 
	int nready, client[FD_SETSIZE]; /* FD_SETSIZE 默认为 1024 ,所监听的文件描述符*/
	ssize_t n;
	//用到集合 关心读事件 allset保存原来的样子 
	fd_set rset, allset;
	//缓冲区 
	char buf[MAXLINE];
	char str[INET_ADDRSTRLEN]; /* #define INET_ADDRSTRLEN 16 */
	
	socklen_t cliaddr_len;
	struct sockaddr_in cliaddr, servaddr;
	//创建套接字 
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	
	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(SERV_PORT);
	//绑定
	bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
	//监听 
	listen(listenfd, 20); /* 默认最大128 */
	//初始值 开始的最大 
	maxfd = listenfd; /* 初始化 */
	maxi = -1; /* client[]的下标 */
	
	for (i = 0; i < FD_SETSIZE; i++)
		client[i] = -1; /* 用-1初始化client[] */
		
	FD_ZERO(&allset);//将allset集合清零
	FD_SET(listenfd, &allset); /* 构造select监控文件描述符集 */
	
	for ( ; ; ) 
	{
		rset = allset; /* 每次循环时都从新设置select监控信号集 */
		//nready 总数 超时NULL 永久等待满足才返回 
		nready = select(maxfd+1, &rset, NULL, NULL, NULL);
		if (nready < 0)
			perr_exit("select error");		
		//是哪一个要连接请求,是否在读集合里, 传入传出参数 出来的时候也是一个 
		if (FD_ISSET(listenfd, &rset)) 
		{ /* new client connection */
			cliaddr_len = sizeof(cliaddr);
			//会阻塞吗?直接调用accept 不会阻塞  返回新的文件描述符 
			connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
			// 打印哪个IP PORT 
			printf("received from %s at PORT %d\n",
			inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port));
			
			//监听的时候加到自定义的集合中,找到任意一个为-1的塞进去 
			for (i = 0; i < FD_SETSIZE; i++)
				if (client[i] < 0) 
				{
					client[i] = connfd; /* 保存accept返回的文件描述符到client[]里 */
					break;
				}
			/* 达到select能监控的文件个数上限 1024 */
			if (i == FD_SETSIZE)
			{
				fputs("too many clients\n", stderr);
				exit(1);
			}
			//allset之前放listened 是不是有两个了 
			FD_SET(connfd, &allset); /* 添加一个新的文件描述符到监控信号集里 */
			
			if (connfd > maxfd)
				maxfd = connfd; /* select第一个参数需要 */
			
			if (i > maxi)
				maxi = i; /* 更新client[]最大下标值 */
			if (--nready == 0)
				continue; /* 如果没有更多的就绪文件描述符继续回到上面select阻塞监听,负责处理未
						   处理完的就绪文件描述符 */
		}
		
		for (i = 0; i <= maxi; i++)
		{ 
			if ( (sockfd = client[i]) < 0)/* 检测哪个clients 有数据就绪 */
				continue;
			if (FD_ISSET(sockfd, &rset))//返回值非零是否在集合里 
			{
				if ( (n = read(sockfd, buf, MAXLINE)) == 0)  
				{
					/* 当client关闭链接时,服务器端也关闭对应链接 */
					close(sockfd);
					FD_CLR(sockfd, &allset); /* 解除select监控此文件描述符 */
					client[i] = -1;
				} 
				else 
				{
					int j;
					for (j = 0; j < n; j++)
						buf[j] = toupper(buf[j]);//小写转大写 
					write(sockfd, buf, n);//写回去 
				}
				if (--nready == 0)
				break;
			}
		}
	}
	close(listenfd);
	return 0;
}

/*selectClient.c*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>

#define MAXLINE 80
#define SERV_PORT 8000


int main(int argc, char *argv[])
{
    struct sockaddr_in servaddr;
    char buf[MAXLINE];
    int sockfd, n;
    
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
    servaddr.sin_port = htons(SERV_PORT);
    
    connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    
    while (fgets(buf, MAXLINE, stdin) != NULL)
	{
        write(sockfd, buf, strlen(buf));
        n = read(sockfd, buf, MAXLINE);
        if (n == 0)
            printf("the other side has been closed.\n");
        else
            write(STDOUT_FILENO, buf, n);
    }
    close(sockfd);
    return 0;
}

参考:
1、https://www.cnblogs.com/skyfsm/p/7079458.html
2、https://blog.csdn.net/y396397735/article/details/55004775

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值