資料整理--socket之select函數

Select在Socket編程中還是比較重要的,它能夠監視我們需要監視的文件描述符的變化情況——讀寫或是異常。
Select的函數格式(Unix系統下的伯克利socket編程,和windows下的略有區別,體現兩個方面:一是select函數的第一個參數,在windows下可以忽略,但在linux下必須設爲最大文件描述符加1;二是結構fd_set在兩個系統裏定義不一樣):
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
先說明兩個結構體:
第一,struct fd_set可以理解爲一個集合,這個集合中存放的是文件描述符(file descriptor),即文件句柄,這可以是我們所說的普通意義的文件,當然Unix下任何設備、管道、FIFO等都是文件形式,全部包括在內,所以毫無疑問一個socket就是一個文件,socket句柄就是一個文件描述符。fd_set集合可以通過一些宏由人爲來操作,比如清空集合 FD_ZERO(fd_set *),將一個給定的文件描述符加入集合之中FD_SET(int ,fd_set *),將一個給定的文件描述符從集合中刪除FD_CLR(int ,fd_set*),檢查集合中指定的文件描述符是否可以讀寫FD_ISSET(int ,fd_set* )。一會兒舉例說明。
第二,struct timeval是一個大家常用的結構,用來代表時間值,有兩個成員,一個是秒數,另一個是毫秒數。
具體解釋select的參數:
int maxfdp是一個整數值,是指集合中所有文件描述符的範圍,即所有文件描述符的最大值加1,不能錯!在Windows中這個參數的值無所謂,可以設置不正確。
fd_set *readfds是指向fd_set結構的指針,這個集合中應該包括文件描述符,我們是要監視這些文件描述符的讀變化的,即我們關心是否可以從這些文件中讀取數據了,如果這個集合中有一個文件可讀,select就會返回一個大於0的值,表示有文件可讀,如果沒有可讀的文件,則根據timeout參數再判斷是否超時,若超出timeout的時間,select返回0,若發生錯誤返回負值。可以傳入NULL值,表示不關心任何文件的讀變化。
fd_set *writefds是指向fd_set結構的指針,這個集合中應該包括文件描述符,我們是要監視這些文件描述符的寫變化的,即我們關心是否可以向這些文件中寫入數據了,如果這個集合中有一個文件可寫,select就會返回一個大於0的值,表示有文件可寫,如果沒有可寫的文件,則根據timeout參數再判斷是否超時,若超出timeout的時間,select返回0,若發生錯誤返回負值。可以傳入NULL值,表示不關心任何文件的寫變化。
fd_set *errorfds同上面兩個參數的意圖,用來監視文件錯誤異常。
struct timeval* timeout是select的超時時間,這個參數至關重要,它可以使select處於三種狀態,第一,若將NULL以形參傳入,即不傳入時間結構,就是將select置於阻塞狀態,一定等到監視文件描述符集合中某個文件描述符發生變化爲止;第二,若將時間值設爲0秒0毫秒,就變成一個純粹的非阻塞函數,不管文件描述符是否有變化,都立刻返回繼續執行,文件無變化返回0,有變化返回一個正值;第三,timeout的值大於0,這就是等待的超時時間,即 select在timeout時間內阻塞,超時時間之內有事件到來就返回了,否則在超時後不管怎樣一定返回,返回值同上述。
返回值:
負值:select錯誤 正值:某些文件可讀寫或出錯 0:等待超時,沒有可讀寫或錯誤的文件
在有了select後可以寫出像樣的網絡程序來!舉個簡單的例子,就是從網絡上接受數據寫入一個文件中。
    #include <winsock.h>
    #include <stdio.h>
    #define PORT       5150
    #define MSGSIZE    1024
    #pragma comment(lib, "ws2_32.lib")
    int    g_iTotalConn = 0;
    SOCKET g_CliSocketArr[FD_SETSIZE];
    DWORD WINAPI WorkerThread(LPVOID lpParameter);
    int main()
    {  
        WSADATA     wsaData;  
        SOCKET      sListen, sClient;  
        SOCKADDR_IN local, client;  
        int         iaddrSize = sizeof(SOCKADDR_IN);  
        DWORD       dwThreadId;  
        // Initialize Windows socket library  
        WSAStartup(0x0202, &wsaData);  
        // Create listening socket  
        sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  
        // Bind          
        local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
        local.sin_family = AF_INET;
        local.sin_port = htons(PORT);  
        bind(sListen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN));  
        // Listen   listen(sListen, 3);  
        // Create worker thread  
        CreateThread(NULL, 0, WorkerThread, NULL, 0, &dwThreadId);    
        while (TRUE)  
        {              // Accept a connection    
            sClient = accept(sListen, (struct sockaddr *)&client, &iaddrSize);    
            printf("Accepted client:%s:%d/n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));    
            // Add socket to g_CliSocketArr    
            g_CliSocketArr[g_iTotalConn++] = sClient;  
        }    
        return 0;
    }
    DWORD WINAPI WorkerThread(LPVOID lpParam)
    {  
        int            i;  
        fd_set         fdread;  
        int            ret;  
        struct timeval tv = {1, 0};  
        char           szMessage[MSGSIZE];    
        while (TRUE)  
        {    
            FD_ZERO(&fdread);    
            for (i = 0; i < g_iTotalConn; i++)
             {
                 FD_SET(g_CliSocketArr, &fdread);
             }                     // We only care read event
             ret = select(0, &fdread, NULL, NULL, &tv);
             if (ret == 0)
             {       // Time expired
                 continue;
             }
             for (i = 0; i < g_iTotalConn; i++)
             {
                 if (FD_ISSET(g_CliSocketArr, &fdread))
                  {         // A read event happened on g_CliSocketArr
                       ret = recv(g_CliSocketArr, szMessage, MSGSIZE, 0);
                       if (ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET))
                        {
                             // Client socket closed          
                             printf("Client socket %d closed./n", g_CliSocketArr);
                             closesocket(g_CliSocketArr);
                             if (i < g_iTotalConn - 1)
                             {
                                 g_CliSocketArr[i--] = g_CliSocketArr[--g_iTotalConn];
                             }
                         }
                         else
                         {
                              // We received a message from client
                              szMessage[ret] = '/0';
                              send(g_CliSocketArr, szMessage, strlen(szMessage), 0);
                         }
                   } //if
             }//for
         }//while    
         return 0;
     }
    服務器的幾個主要動作如下:
    1.創建監聽套接字,綁定,監聽;
    2.創建工作者線程;
    3.創建一個套接字數組,用來存放當前所有活動的客戶端套接字,每accept一個連接就更新一次數組;
    4.接受客戶端的連接。
    這裏有一點需要注意的,就是我沒有重新定義FD_SETSIZE宏,所以服務器最多支持的併發連接數爲64。而且,這裏決不能無條件的ccept,服務器應該根據當前的連接數來決定
是否接受來自某個客戶端的連接。一種比較好的實現方案就是採用WSAAccept函數,而且讓WSAAccept回調自己實現的Condition Function。
    如下所示:
    int CALLBACK ConditionFunc(LPWSABUF lpCallerId,LPWSABUF lpCallerData, LPQOS lpSQOS,LPQOS lpGQOS,LPWSABUF lpCalleeId, LPWSABUF lpCalleeData,GROUP FAR *
g,DWORD dwCallbackData)
    {
        if (當前連接數 < FD_SETSIZE)
            return CF_ACCEPT;
        else  
            return CF_REJECT;
    }
    工作者線程裏面是一個死循環,一次循環完成的動作是:
    1.將當前所有的客戶端套接字加入到讀集fdread中;
    2.調用select函數;
    3.查看某個套接字是否仍然處於讀集中,如果是,則接收數據。如果接收的數據長度爲0,或者發生WSAECONNRESET錯誤,則表示客戶端套接字主動關閉,這時需要將服務器中
對應的套接字所綁定的資源釋放掉,然後調整我們的套接字數組(將數組中最後一個套接字挪到當前的位置上)。
    除了需要有條件接受客戶端的連接外,還需要在連接數爲0的情形下做特殊處理,因爲如果讀集中沒有任何套接字,select函數會立刻返回,這將導致工作者線程成爲一個毫無
停頓的死循環,CPU的佔用率馬上達到100%。
    關係到套接字列表的操作都需要使用循環,在輪詢的時候,需要遍歷一次,再新的一輪開始時,將列表加入隊列又需要遍歷一次.也就是說,Select在工作一次時,需要至少遍歷2次
列表,這是它效率較低的原因之一.
    在大規模的網絡連接方面,還是推薦使用IOCP或EPOLL模型.但是Select模型可以使用在諸如對戰類遊戲上,比如類似星際這種,因爲它小巧易於實現,且對戰類遊戲的網絡連接量
並不大. 對於Select模型想要突破Windows 64個限制的話,可以採取分段輪詢,一次輪詢64個.例如套接字列表爲128個,在第一次輪詢時,將前64個放入隊列中用Select進行狀態查詢,
待本次操作全部結束後.將後64個再加入輪詢隊列中進行輪詢處理.這樣處理需要在非阻塞式下工作.以此類推,Select也能支持無限多個.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章