1. 程式人生 > >專注於linux,網路安全

專注於linux,網路安全

1. 使用select改寫tcp伺服器

在此之前,回顧一下多程序併發型伺服器通訊過程,併發型伺服器的做法是針對每一個客戶端請求,伺服器的父程序就fork建立一個子程序來處理客戶端的請求。如果有大量的客戶端請求的話,那麼伺服器也需要建立大量的子程序來處理請求,這將會消耗伺服器大量的系統資源。

因此我們可以使用select改寫tcp伺服器,通過select來處理多個客戶端的請求,具體處理過程如下:

圖1
圖1

在客戶端連線伺服器之前,伺服器只建立了單個檔案描述符進行監聽,在圖1我們用一個方塊表示。

伺服器只維護一個讀檔案描述符集合rest,當伺服器啟動時描述符0,1,2分別表示標準輸入,標準輸出,標準出錯,且這三個檔案描述符被置為0,而伺服器監聽的描述符3被置為1,表示描述符3處於監聽。而client陣列中則記錄每個客戶端連線的描述符,開始時會將client陣列初始化為-1,FD_SETSIZE代表伺服器處理的最大客戶端連線的數量,即client陣列的大小。

圖2

在rest陣列中,描述符3處於監聽,因此select中的maxfd引數就是4。當第一個客戶端與伺服器建立tcp連線時,監聽的描述符會變為可讀,隨後伺服器呼叫accept,假設accept返回已連線的新描述符值是4,如圖3所示:

圖3

那麼在client陣列中必須記錄新的已連線描述符的值,同時把描述符4加入到rest集合中:

圖4

接著第二個客戶端與伺服器建立tcp連線:

圖5

同理,新的已連線描述符5也需要在client陣列中記錄,同時把描述符5加入到rest集合中去:

圖6

如果第一個客戶端傳送了FIN終止了tcp連線,那麼伺服器中的描述符4將會變的可讀,當伺服器讀取描述符4將會返回0,於是可以關閉該套接字並更新rest集合將描述符4置為0,同時將client陣列中client[0]的值置為-1,需要注意maxfd的值不變。

圖7

當有新的客戶端建立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;
}

程式執行結果: