天天看點

IO多路轉接之Select詳解及實作select網絡伺服器

select

在五種IO模型中我們認識了select,用于實作多路複用輸入/輸出模型。

回憶一下其作用:

1. select系統調用是用來讓我們的程式監視多個檔案描述符的狀态變化的;

2. 程式會停在select這裡等待,直到被監視的檔案描述符有一個或多個發生了狀态改變。

函數原型

IO多路轉接之Select詳解及實作select網絡伺服器

參數解釋:

  1. 參數nfds是需要監視的最大的檔案描述符值+1;
  2. readfds,writefds,exceptfds分别對應于需要檢測的可讀檔案描述符的集合,可寫檔案描述符的集合及異常檔案描述符的集合;
  3. 參數timeout為結構timeval,用來設定select()的等待時間

參數timeout取值:

  1. NULL:則表示select()沒有timeout,select将一直被阻塞,直到某個檔案描述符上發生了事件;
  2. 0:僅檢測描述符集合的狀态,然後立即傳回,并不等待外部事件的發生。
  3. 特定的時間值:如果在指定的時間段裡沒有事件發生,select将逾時傳回.

fd_set結構

注意到select函數中參數類型出現了fd_set結構,其實它是一個”位圖”,使用位圖中對應的位來表示要監視的檔案描述符。

它的大小由系統決定,是有上限的,是以表示的檔案描述符也是有上限的。

系統中提供了一組操作fd_set的接口, 來比較友善的操作位圖:

IO多路轉接之Select詳解及實作select網絡伺服器
函數 作用
FD_CLR 用來清除描述詞組set中相關fd的位
FD_ISSET 用來測試描述詞組set中相關fd的位是否為真
FD_SET 用來設定描述詞組set中相關fd的位
FD_ZERO 用來清除描述詞組set的全部位

函數傳回值:

特定的數:執行成功則傳回檔案描述詞狀态已改變的個數

0:如果傳回0代表在描述詞狀态改變前已超過timeout時間,沒有傳回

-1:當有錯誤發生時則傳回-1,錯誤原因存于errno,此時參數readfds,writefds, exceptfds和timeout 的值變成不可預測。

舉例select執行過程

上文提到過fd_set為位圖,用來監視檔案描述符集。要了解select模型,我們首先要了解fd_set。

比如:fd_set長度為1位元組,fd_set中的每一bit可以對應一個檔案描述符fd。則1位元組長的fd_set最大可以對應8個fd.

(1)執行fdset set; FDZERO(&set);則set用位表示是0000,0000。

(2)若fd=5,執行 FD_SET(fd,&set);後set變為0001,0000(第5位置為1)

(3)若再加入fd=2,fd=1,則set變為0001,0011

(4)執行select(6,&set,0,0,0)阻塞等待

(5)若fd=1,fd=2上都發生可讀事件,則select傳回,此時set變為0000,0011。

注意:沒有事件發生的fd=5被清空。且fd_set的大小由系統決定,是有上限的,是以表示的檔案描述符也是有上限的

socket就緒條件

從select函數原型我們可知,select監視讀、寫、或者異常條件就緒。

那麼在什麼情況下,滿足就緒條件呢?

讀就緒:

  1. socket核心中,接收緩沖區中的位元組數,大于等于低水位标記SO_RCVLOWAT. 此時可以無阻塞的讀該檔案描述符, 并且傳回值大于0;
  2. socket TCP通信中, 對端關閉連接配接, 此時對該socket讀, 則傳回0;
  3. 監聽的socket上有新的連接配接請求;
  4. socket上有未處理的錯誤;

寫就緒:

  1. socket核心中, 發送緩沖區中的可用位元組數(發送緩沖區的空閑位置大小), 大于等于低水位标記 SO_SNDLOWAT, 此時可以無阻塞的寫, 并且傳回值大于0;
  2. socket的寫操作被關閉(close或者shutdown).對一個寫操作被關閉的socket進行寫操作, 會觸發 SIGPIPE信号;
  3. socket使用非阻塞connect連接配接成功或失敗之後;
  4. socket上有未讀取的錯誤

異常就緒

socket上收到帶外資料. 關于帶外資料, 和TCP緊急模式相關。

select的特點

  1. 可監控的檔案描述符個數取決與sizeof(fdset) 的值 ,是有上限的,每bit表示一個檔案描述符
  2. 将fd加入select監控集的同時,還要再使用一個資料結構array儲存放到select監控集中的fd,其作用為:
  1. 用于在select 傳回後,array作為源資料和fdset 進行FDISSET判斷,是否已經就緒。
  2. select傳回後會把以前加入的但并無事件發生的fd清空,則每次開始select前都要重新從array取得fd加入都fd_set(FD_ZERO最先);掃描array的同時取得fd最大值maxfd,用于select的第一個參數。

select的優缺點

優點

比較與多線程和多程序,效率較高。

缺點

  1. 每次調用select, 都需要手動設定fd集合, 從接口使用角度來說非常不便.
  2. 每次調用select,都需要把fd集合從使用者态拷貝到核心态,這個開銷在fd很多時會很大
  3. 同時每次調用select都需要在核心周遊傳遞進來的所有fd,這個開銷在fd很多時也很大
  4. select支援的檔案描述符數量太小

使用select編寫網絡伺服器

我們在代碼裡隻是為了示範select的用法,假設資料可以一次從用戶端取完,不用考慮資料粘包等問題,代碼如下:

1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<string.h>
  5 #include<sys/socket.h>
  6 #include<netinet/in.h>
  7 #include<arpa/inet.h>
  8 #include<sys/select.h>
  9 #define MAX_FDS sizeof(fd_set)*8
 10 #define INIT_DATA -1
 11 
 12 static void initArray(int arr[],int num){
 13     int i=0;
 14     for(;i<num;i++){
 15         arr[i]=INIT_DATA;
 16     }
 17 }
 18 
 19 static int addSockToArray(int arr[],int num,int fd){
 20     int i=0;
 21     for(;i<num;i++){
 22         if(arr[i]<0){
 23             arr[i]=fd;
 24             return i;
 25         }
 26     }
 27     return -1;  //full
 28 }
 29 
 30 int setArrayToFdSet(int arr[],int num,fd_set *rfds){
 31     int i=0;
 32     int max_fd=INIT_DATA;
 33     for(;i<num;i++){
 34         if(arr[i]>=0){
 35             FD_SET(arr[i],rfds);
 36 
 37             if(max_fd<arr[i]){
 38                 max_fd=arr[i];
 39             }
 40         }
 41     }
 42     return max_fd;
 43 }
 44 
 45 
 46 static void serviceIO(int arr[],int num,fd_set *rfds){
 47     int i=0;
 48     for(;i<num;i++){
 49         if(arr[i]>INIT_DATA){
 50             int fd=arr[i];
 51             if(i==0&&FD_ISSET(arr[i],rfds)){
 52                 //listen ready
 53                 struct sockaddr_in client;
 54                 socklen_t len=sizeof(client);
 55                 int sock=accept(fd,(struct sockaddr*)&client,&len);
 56                 if(sock<0){
 57                     perror("accept");
 58                     continue;
 59                 }
 60                 printf("get a new client [%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port)    );
 61                 //addArray
 62                 if(addSockToArray(arr,num,sock)==-1)
 63                 {
 64                     close(sock);
 65                 }
 66 
 67             }
 68             else if(i!=0&&FD_ISSET(arr[i],rfds)){
 69                 //normal fd ready
 70                 char buf[1024];
 71                 ssize_t s=read(fd,buf,sizeof(buf)-1);
 72                 if(s>0){
 73                     buf[s]=0;
 74                     printf("client:> %s\n",buf);
 75                 }
 76                 else if(s==0){
 77                     close(fd);
 78                     arr[i]=INIT_DATA;
 79                     printf("client quit!\n");
 80                 }
 81                 else{
 82                     perror("read\n");
 83                     close(fd);
 84                     arr[i]=INIT_DATA;
 85                 }
 86             }
 87             else{
 88                 //do nothing
 89             }
 90         }
 91     }
 92 }
 93 
 94 
 95 int startup(int port){
 96     int sock=socket(AF_INET,SOCK_STREAM,0);
 97     if(sock<0){
 98         perror("sock");
 99         exit(2);
100     }
101 
102     int opt=1;
103     setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
104 
105     struct sockaddr_in local;
106     local.sin_family=AF_INET;
107     local.sin_addr.s_addr=htonl(INADDR_ANY);
108     local.sin_port=htons(port);
109 
110     if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){
111         perror("bind");
112         exit(3);
113     }
114     if(listen(sock,5)<0){
115         perror("listen");
116         exit(4);
117     }
118     return sock;
119 }
120 // ./select_server port
121 int main(int argc,char *argv[]){
122     if(argc!=2){
123         printf("Usage:%s [port]\n",argv[0]);
124         return 1;
125     }
126 
127     int listen_sock=startup(atoi(argv[1]));
128 
129     fd_set rfds;
130     int fd_array[MAX_FDS];
131     initArray(fd_array,MAX_FDS);
132     addSockToArray(fd_array,MAX_FDS,listen_sock);
133     for(;;){
134         FD_ZERO(&rfds);
135 
136         int max_fd=setArrayToFdSet(fd_array,MAX_FDS,&rfds);
137 
138         struct timeval timeout={3,0};
139         switch(select(max_fd+1,&rfds,NULL,NULL,NULL)){
140             case -1:
141                 perror("select\n");
142                 break;
143             case 0:
144                 printf("timeout...\n");
145                 break;
146             default:
147                 serviceIO(fd_array,MAX_FDS,&rfds);
148                 break;
149         }
150     }
151 }      

測試:

用遠端登入工具telnet測試,需要安裝:

yum install telnet-server

yum install telnet

service xinetd restart