Linux網絡編程——高併發服務器之select模型

1、前言

1.1、IO模型

下面用服務器比成車站,客戶端比喻成小明。

  1. 傳統阻塞模型:小明去車站買票,沒買到票就在車站等待,直到有車票爲止。
  2. 非阻塞模型:小明去車站買票,沒票的話,他沒過一段時間就去看看有沒有票,沒有票就回去。他消耗了來回的這一個過程,但是不用等待。
  3. 多路轉接IO複用:委託黃牛來購票。select模型就是屬於這一類。

下面我用兩張圖來描述兩種

1.2、select模型概念

  • select能監聽的文件描述符個數受限於FD_SETSIZE,一般爲1024,單純改變進程 打開的文件描述符個數,並不能改變select監聽文件個數。
  • 解決1024以下客戶端時使用select是很合適的,但如果鏈接客戶端過多,select採用 的是輪詢模型,會大大降低服務器響應效率,不應在select上投入更多精力(即超過了這個1024就不適合使用這個模型了)

1.3、select模型使用場景

一般常用在局域網,一些部分公司有在用,現在一般不常用,但是要學其他模型的話,這個模型是基礎。

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

2.1、模型建立主要函數

  • 需要的頭文件:
    • #include <sys/time.h>
    • #include <sys/types.h>
    • #include <unistd.h>
  • 函數:int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • 參數解析:
    1. nfds: 監控的文件描述符集裏最大文件描述符加1,因爲此參數會告訴內核檢測前多少個文件描述符的狀態 。比如在下面的文件描述符表中,最大的是在28位,那麼我們在這個參數就要寫28+1個
      在這裏插入圖片描述

    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。
    在這裏插入圖片描述

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 <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <netinet/in.h> 
#include <arpa/inet.h>

#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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章