專注於linux,網路安全
1. 使用select改寫tcp伺服器
在此之前,回顧一下多程序併發型伺服器通訊過程,併發型伺服器的做法是針對每一個客戶端請求,伺服器的父程序就fork建立一個子程序來處理客戶端的請求。如果有大量的客戶端請求的話,那麼伺服器也需要建立大量的子程序來處理請求,這將會消耗伺服器大量的系統資源。
因此我們可以使用select改寫tcp伺服器,通過select來處理多個客戶端的請求,具體處理過程如下:
在客戶端連線伺服器之前,伺服器只建立了單個檔案描述符進行監聽,在圖1我們用一個方塊表示。
伺服器只維護一個讀檔案描述符集合rest,當伺服器啟動時描述符0,1,2分別表示標準輸入,標準輸出,標準出錯,且這三個檔案描述符被置為0,而伺服器監聽的描述符3被置為1,表示描述符3處於監聽。而client陣列中則記錄每個客戶端連線的描述符,開始時會將client陣列初始化為-1,FD_SETSIZE代表伺服器處理的最大客戶端連線的數量,即client陣列的大小。
在rest陣列中,描述符3處於監聽,因此select中的maxfd引數就是4。當第一個客戶端與伺服器建立tcp連線時,監聽的描述符會變為可讀,隨後伺服器呼叫accept,假設accept返回已連線的新描述符值是4,如圖3所示:
那麼在client陣列中必須記錄新的已連線描述符的值,同時把描述符4加入到rest集合中:
接著第二個客戶端與伺服器建立tcp連線:
同理,新的已連線描述符5也需要在client陣列中記錄,同時把描述符5加入到rest集合中去:
如果第一個客戶端傳送了FIN終止了tcp連線,那麼伺服器中的描述符4將會變的可讀,當伺服器讀取描述符4將會返回0,於是可以關閉該套接字並更新rest集合將描述符4置為0,同時將client陣列中client[0]的值置為-1,需要注意maxfd的值不變。
當有新的客戶端建立tcp連線時,就可以在client陣列中的第一項記錄新的已連線描述符,並將新的已連線描述符新增到rest集合中。變數maxi是client陣列中當前使用項的最大下標,變數maxfd(加1之後)則表示select函式的第一個引數的值。
2. 伺服器程式
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #define MAXLINE 1024 #define SERV_PORT 10001 int main(void) { int i, n ,maxi, maxfd, listenfd, connfd, sockfd; int nready; int client[FD_SETSIZE]; fd_set rset, allset; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; 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); //初始化maxfd maxfd = listenfd; //client陣列的下標 maxi = -1; //初始化client陣列 for (i = 0; i < FD_SETSIZE; i++){ client[i] = -1; } FD_ZERO(&allset); FD_SET(listenfd, &allset); /* 構造select檢測檔案描述符集 */ while(1){ /* 每次迴圈時都重新設定select檢測的描述符集合 */ rset = allset; nready = select(maxfd+1, &rset, NULL, NULL, NULL); if (nready < 0) perror("select error:"); /* 判斷是否有新的客戶端連線建立完成 */ if (FD_ISSET(listenfd, &rset)) { cliaddr_len = sizeof(cliaddr); 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)); //在client陣列中必須記錄新的已連線描述符的值 for (i = 0; i < FD_SETSIZE; i++) { if(client[i] < 0){ client[i] = connfd; break; } } /* 判斷是否達到select能監控的檔案個數上限 1024 */ if(i == FD_SETSIZE){ puts("too many clients"); exit(1); } /* 新增一個已連線的新描述符到監控訊號集裡監聽 */ FD_SET(connfd, &allset); /* 更新maxfd為最大描述符的值 */ if(connfd > maxfd) maxfd = connfd; /* 一旦i大於maxi的話,同時更新client陣列中當前使用項的最大下標 */ if(i > maxi) maxi = i; /* 如果nready為0,說明I/O事件處理完畢 */ /* 如果沒有更多的就緒檔案描述符繼續回到上面select阻塞監聽,負責處理未處理完的就緒檔案描述符 */ if (--nready == 0) continue; } /* 檢測哪個clients有資料就緒 */ for (i = 0; i <= maxi; i++) { if ( (sockfd = client[i]) < 0) continue; if (FD_ISSET(sockfd, &rset)) { //讀取資料,有可能會讀到0 if ( (n = read(sockfd, buf, MAXLINE)) == 0) { /* 當client關閉連結時,伺服器端也關閉對應連結 */ close(sockfd); /* 取消select監聽該檔案描述符 */ FD_CLR(sockfd, &allset); /* client陣列中置為-1 */ client[i] = -1; }else{ //讀到資料後就處理資料 int j; for (j = 0; j < n; j++) buf[j] = toupper(buf[j]); write(sockfd, buf, n); } /* 判斷select返回的I/O事件是否處理完畢 */ if (--nready == 0) break; } } } close(listenfd); return 0; }
程式執行結果: