Linux网络编程——高并发服务器之select模型

1、前言

1.1、IO模型

下面用服务器比成车站,客户端比喻成小明。

  1. 传统阻塞模型:小明去车站买票,没买到票就在车站等待,直到有车票为止。
  2. 非阻塞模型:小明去车站买票,没票的话,他没过一段时间就去看看有没有票,没有票就回去。他消耗了来回的这一个过程,但是不用等待。
  3. 多路转接IO复用:委托黄牛来购票。select模型就是属于这一类。

下面我用两张图来描述两种
Linux网络编程——高并发服务器之select模型_第1张图片 Linux网络编程——高并发服务器之select模型_第2张图片

1.2、select模型概念

  • select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程 打开的文件描述符个数,并不能改变select监听文件个数。
  • 解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用 的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力(即超过了这个1024就不适合使用这个模型了)

1.3、select模型使用场景

一般常用在局域网,一些部分公司有在用,现在一般不常用,但是要学其他模型的话,这个模型是基础。

2、select模型需要用到的函数

2.1、模型建立主要函数

  • 需要的头文件:
    • #include
    • #include
    • #include
  • 函数:int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • 参数解析:
    1. nfds: 监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态 。比如在下面的文件描述符表中,最大的是在28位,那么我们在这个参数就要写28+1个
      Linux网络编程——高并发服务器之select模型_第3张图片

    2. readfds:监控有读数据到达文件描述符集合,传入传出参数

    3. writefds:监控写数据到达文件描述符集合,传入传出参数

    4. exceptfds:监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数

    5. timeout:定时阻塞监控时间,3种情况

      1. NULL,永远等下去
      2. 设置timeval,等待固定时间
      3. 设置timeval里时间均为0,检查描述字后立即返回(意思就是轮询)
  • 返回值:所监听的所有文件描述符 满足的总数,返回的是int类型。
        比如在下图中,2、3、4分别表示的是第二、第三、第四个参数,他们都传进去的A、B、C三个字符集,画黄色圈的就是符合对应功能的(比如2集合的,b就符合可读)。那么这个函数的返回值将会是4。
    Linux网络编程——高并发服务器之select模型_第4张图片

2.2、文件描述符集合处理函数

这一小节主要就是要为第2、3、4个参数服务的,我们要做的就是将文件操作符加入到字符集中。

void FD_CLR(int fd, fd_set *set); 	把文件描述符集合里fd清0

int FD_ISSET(int fd, fd_set *set); 	测试文件描述符集合里fd是否置1,如果有返回1,无则返回0

void FD_SET(int fd, fd_set *set); 	把文件描述符集合里fd位置1

void FD_ZERO(fd_set *set); 		把文件描述符集合里所有位清0

使用步骤:

  1. 建立fd_set 类型文件描述符集合 变量
  2. 使用FD_ZERO将该集合清零(就类似于初始化的步骤)
  3. 使用FD_SET将客户端的文件操作符加入指定集合
  4. 使用上文2.1中的select函数测得符合条件的总数。
  5. 遍历循环,使用FD_ISSET判断某个文件描述符,在指定的集合中是否符合条件。(看谁就绪了)

3、示例代码——生产消费者模型

在下面的代码会有点多,我主要分成3大部分

#include  
#include  
#include  
#include  
#include 

#define MAXLINE 80 
#define SERV_PORT 8000	//端口号
int main()
{
	int i, maxi, maxfd, connfd, sockfd; 
	int listenfd;	//监听套接字建立
	int nready, client[FD_SETSIZE]; //FD_SETSIZE 时FD的一个宏,默认为 1024
	ssize_t n; 
	fd_set rset, allset; 
	char buf[MAXLINE]; 
	char str[INET_ADDRSTRLEN]; //#define INET_ADDRSTRLEN 16
	socklen_t cliaddr_len; 
	struct sockaddr_in cliaddr, servaddr;	//socket需要的sockaddr_in变量

/*第一部分,socket基础部分*/
	//套接字初始化
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	
	//绑定
	bzero(&servaddr, sizeof(servaddr)); 
	servaddr.sin_family = AF_INET; 
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 	//任意ip号
	servaddr.sin_port = htons(SERV_PORT);	//绑定端口号
	bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
	
	//监听
	listen(listenfd, 20); //最多监听20个用户

	//寻找最大的文件描述符,并且清空一下client数组
	maxfd = listenfd; //在文件创建符里,目前最大的是listenfd,因为你这个代码里面现在只创建了一个listenfd文件操作符
	maxi = -1;  // client[]的下标,具体请看后面的解析
     	for (i = 0; i < FD_SETSIZE; i++)
     	{
        	client[i] = -1; // 用-1初始化client[]
        }
	
	//FD_ZERO初始化集合,把监听套接字加入FD_SET 
     	FD_ZERO(&allset); //清空allset字符集
     	FD_SET(listenfd, &allset); //把listenfd加入到allset字符集,后面用于select函数的监听
	
/*第二部分,判断谁就绪了*/
    	for ( ; ; ) 
    	{ 
    		rset = allset; /* 每次循环时都重新设置select监控信号集 */ 
    		nready = select(maxfd+1, &rset, NULL, NULL, NULL); //用nready 来接收select返回的总数,这里我们只监听一个rset文件操作符集,符合可读条件的文件操作符个数将会返回
    		//如果没有符合的就报错
    		if (nready < 0) 
    		{
    			perror("select error");
    		}
    		//如果listenfd在rset里为1的话,就代表有客户端要对服务器访问
    		if (FD_ISSET(listenfd, &rset)) 
    		{ 
    		/*新的client连接 */ 
    			cliaddr_len = sizeof(cliaddr); 
    			//客户端连接上,并且记录IP和端口号
    			connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
    			printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));
    			for (i = 0; i < FD_SETSIZE; i++) 
    			{
    				//在client[]找到空位,保存,因为我前面有给client[]初始化过全部为-1
    				if (client[i] < 0) 
    				{ 
    					client[i] = connfd; //保存accept返回的文件描述符到client[]里 
    					break; 
    				} 
    			}
    			// 达到select能监控的文件个数上限 1024
    			if (i == FD_SETSIZE) 
    			{ 
    				fputs("too many clients\n", stderr); 
    				exit(1); 
    			}
    			FD_SET(connfd, &allset); // 添加一个新的文件描述符到监控信号集里
    			if (connfd > maxfd) 
    			{
    				/* select第一个参数需要,因为你刚刚添加了connfd文件操作符,
    				 那么在client[]中最大的文件操作符将会换成connfd*/ 
    				maxfd = connfd; 
    			}
    			if (i > maxi) 
    			{
    				maxi = i; /* 更新client[]最大下标值 */
    			}
    			//重置一下nready 给下一次使用
    			if (--nready == 0) 
    			{
    				continue; 
    			}
    		}
    /*第三部分,对就绪者进行读写数据*/
   		for (i = 0; i <= maxi; i++) 
    		{ 
    			//检测哪个clients 有数据就绪,将clients 赋值给临时变量sockfd 
    			if ( (sockfd = client[i]) < 0)
    		 		continue; 
    		 	
    		 	//判断一下有数据的clients 在不在rset里
			if (FD_ISSET(sockfd, &rset)) 
    			{ 
    				//读数据到buf
    				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;
			}
		}//for (i = 0; i <= maxi; i++)结束 
    	}//for ( ; ; ) 结束
	close(listenfd);
	return 0;
}

你可能感兴趣的:(Linux)