.選擇模型

 

Select模型原理

利用select函數(shù),判斷套接字上是否存在數(shù)據(jù),或者能否向一個(gè)套接字寫入數(shù)據(jù)。目的是防止應(yīng)用程序在套接字處于鎖定模式時(shí),調(diào)用recv(或send)從沒有數(shù)據(jù)的套接字上接收數(shù)據(jù),被迫進(jìn)入阻塞狀態(tài)。

 

select參數(shù)和返回值意義如下:

int select (

 IN int nfds,                           //0,無意義

 IN OUT fd_set* readfds,      //檢查可讀性

 IN OUT fd_set* writefds,     //檢查可寫性

 IN OUT fd_set* exceptfds,  //例外數(shù)據(jù)

 IN const struct timeval* timeout);    //函數(shù)的返回時(shí)間

 

struct  timeval {

        long    tv_sec;        //

        long    tv_usec;     //毫秒

};

select返回fd_set中可用的套接字個(gè)數(shù)。

 

fd_set是一個(gè)SOCKET隊(duì)列,以下宏可以對該隊(duì)列進(jìn)行操作:

FD_CLR( s, *set) 從隊(duì)列set刪除句柄s;

FD_ISSET( s, *set) 檢查句柄s是否存在與隊(duì)列set;

FD_SET( s, *set )把句柄s添加到隊(duì)列set;

FD_ZERO( *set ) set隊(duì)列初始化成空隊(duì)列.

 

Select工作流程

1:用FD_ZERO宏來初始化我們感興趣的fd_set。

也就是select函數(shù)的第二三四個(gè)參數(shù)。

2:用FD_SET宏來將套接字句柄分配給相應(yīng)的fd_set。

如果想要檢查一個(gè)套接字是否有數(shù)據(jù)需要接收,可以用FD_SET宏把套接接字句柄加入可讀性檢查隊(duì)列中

3:調(diào)用select函數(shù)。

如果該套接字沒有數(shù)據(jù)需要接收,select函數(shù)會把該套接字從可讀性檢查隊(duì)列中刪除掉,

4:用FD_ISSET對套接字句柄進(jìn)行檢查。

如果我們所關(guān)注的那個(gè)套接字句柄仍然在開始分配的那個(gè)fd_set里,那么說明馬上可以進(jìn)行相應(yīng)的IO 作。比如一個(gè)分配給select第一個(gè)參數(shù)的套接字句柄在select返回后仍然在select第一個(gè)參數(shù)的fd_set里,那么說明當(dāng)前數(shù)據(jù)已經(jīng)來了, 馬上可以讀取成功而不會被阻塞。

 

 

/*************************************************************************************/

#include "stdafx.h"  

#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 lpParam);  

int main(int argc, char* argv[])  

{  

    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_family = AF_INET;  

    local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);  

    local.sin_port = htons(PORT);  

    bind(sListen, (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, (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);   //1清空隊(duì)列

        for (i = 0; i < g_iTotalConn; i++)   

        {  

            FD_SET(g_CliSocketArr[i], &fdread);   //2將要檢查的套接口加入隊(duì)列

        }  

        // We only care read event  

        ret = select(0, &fdread, NULL, NULL, &tv);   //3查詢滿足要求的套接字,不滿足要求,出隊(duì)

        if (ret == 0)   

        {  

            // Time expired  

            continue;  

        }  

        for (i = 0; i < g_iTotalConn; i++)   

        {  

            if (FD_ISSET(g_CliSocketArr[i], &fdread))    //4.是否依然在隊(duì)列

            {  

                // A read event happened on g_CliSocketArr  

                ret = recv(g_CliSocketArr[i], szMessage, MSGSIZE, 0);  

                if (ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET))   

                {  

                    // Client socket closed  

                    printf("Client socket %d closed.\n", g_CliSocketArr[i]);  

                    closesocket(g_CliSocketArr[i]);  

                    if (i < g_iTotalConn-1)   

                    {  

                        g_CliSocketArr[i--] = g_CliSocketArr[--g_iTotalConn];  

                    }  

                }   

                else   

                {  

                    // We reveived a message from client  

                    szMessage[ret] = '\0';  

                    send(g_CliSocketArr[i], szMessage, strlen(szMessage), 0);  

                }  

            }  

        }  

    }  

} 

服務(wù)器的幾個(gè)主要動作如下:

1.創(chuàng)建監(jiān)聽套接字,綁定,監(jiān)聽;

2.創(chuàng)建工作者線程;

3.創(chuàng)建一個(gè)套接字?jǐn)?shù)組,用來存放當(dāng)前所有活動的客戶端套接字,每accept一個(gè)連接就更新一次數(shù)組;

4.接受客戶端的連接。

 

這里有一點(diǎn)需要注意的,就是我沒有重新定義FD_SETSIZE宏,所以服務(wù)器最多支持的并發(fā)連接數(shù)為64。而且,這里決不能無條件的accept,服務(wù)器應(yīng)該根據(jù)當(dāng)前的連接數(shù)來決定是否接受來自某個(gè)客戶端的連接。一種比較好的實(shí)現(xiàn)方案就是采用WSAAccept函數(shù),而且讓WSAAccept回調(diào)自己實(shí)現(xiàn)的Condition Function。如下所示:

 

int CALLBACK ConditionFunc(LPWSABUF lpCallerId,LPWSABUF lpCallerData, LPQOS lpSQOS,LPQOS lpGQOS,LPWSABUF lpCalleeId, LPWSABUF lpCalleeData,GROUP FAR * g,DWORD dwCallbackData)

{

if (當(dāng)前連接數(shù) < FD_SETSIZE)

  return CF_ACCEPT;

else

  return CF_REJECT;

}

 

工作者線程里面是一個(gè)死循環(huán),一次循環(huán)完成的動作是:

1.將當(dāng)前所有的客戶端套接字加入到讀集fdread中;

2.調(diào)用select函數(shù);

3.查看某個(gè)套接字是否仍然處于讀集中,如果是,則接收數(shù)據(jù)。如果接收的數(shù)據(jù)長度為0,或者發(fā)生WSAECONNRESET錯(cuò)誤,則表示客戶端套接字主動關(guān)閉,這時(shí)需要將服務(wù)器中對應(yīng)的套接字所綁定的資源釋放掉,然后調(diào)整我們的套接字?jǐn)?shù)組(將數(shù)組中最后一個(gè)套接字挪到當(dāng)前的位置上)

 

除了需要有條件接受客戶端的連接外,還需要在連接數(shù)為0的情形下做特殊處理,因?yàn)槿绻x集中沒有任何套接字,select函數(shù)會立刻返回,這將導(dǎo)致工作者線程成為一個(gè)毫無停頓的死循環(huán),CPU的占用率馬上達(dá)到100%。

 

關(guān)系到套接字列表的操作都需要使用循環(huán),在輪詢的時(shí)候,需要遍歷一次,再新的一輪開始時(shí),將列表加入隊(duì)列又需要遍歷一次.也就是說,Select在工作一次時(shí),需要至少遍歷2次列表,這是它效率較低的原因之一.在大規(guī)模的網(wǎng)絡(luò)連接方面,還是推薦使用IOCPEPOLL模型.但是Select模型可以使用在諸如對戰(zhàn)類游戲上,比如類似星際這種,因?yàn)樗∏梢子趯?shí)現(xiàn),而且對戰(zhàn)類游戲的網(wǎng)絡(luò)連接量并不大.

 

對于Select模型想要突破Windows 64個(gè)限制的話,可以采取分段輪詢,一次輪詢64個(gè).例如套接字列表為128個(gè),在第一次輪詢時(shí),將前64個(gè)放入隊(duì)列中用Select進(jìn)行狀態(tài)查詢,待本次操作全部結(jié)束后.將后64個(gè)再加入輪詢隊(duì)列中進(jìn)行輪詢處理.這樣處理需要在非阻塞式下工作.以此類推,Select也能支持無限多個(gè).