windows下的IO模型之選擇(select)模型

來源:互聯網
上載者:User

標籤:getc   send   模型   解釋   鎖定   eva   成員   事件   為什麼   

1.選擇(select)模型:
選擇模型:通過一個fd_set集合管理通訊端,在滿足通訊端需求後,通知通訊端。讓通訊端進行工作。避免通訊端進入阻塞模式,進行無謂的等待。選擇模型的核心的FD_SET集合和select函數。通過該函數,我們可以們判斷通訊端上是否存在資料,或者能否向一個通訊端寫入資料。

用途:如果我們想接受多個SOCKET的資料,該怎麼處理呢?

由於當前socket是阻塞的,直接處理是一定完成不了要求的

a.我們會想到多線程,的確可以解決線程的阻塞問題,但開闢大量的線程並不是什麼好的選擇;

b我們可以想到用ioctlsocket()函數把socket設定成非阻塞的,然後用迴圈逐個socket查看當前通訊端是否有資料,輪詢進行。

這種是可以解決問題的,但是會導致頻繁切換狀態到核心去查看是否有資料到達,浪費時間。

c.於是想辦法用只切換一次狀態就知道所有socket的接受緩衝區是否有資料,於是有了select模型

2.select函數:
int select(
    int nfds,//忽略,只是為了保持與早期的Berkeley通訊端應用程式的相容

    fd_set FAR* readfds,//可讀性檢查(有資料可讀入,串連關閉,重設,終止)
    fd_set FAR* writefds,//可寫性檢查(有資料可發出)
    fd+set FAR* exceptfds,//帶外資料檢查(帶外資料)
    const struct timeval FAR* timeout//逾時
    );


3.select模型的工作步驟:
(1)定義一個集合fd_set並初始化為空白

(2)把通訊端加入到fd_set集合

(3)檢查通訊端的可讀寫性
(4)檢查通訊端是否還在fd_set集合上
(5)處理資料

bool UDPNet::SelectSocket(){timeval tv;tv.tv_sec =0;tv.tv_usec = 100;fd_set fdsets;//建立集合FD_ZERO(&fdsets); //初始化集合FD_SET(m_socklisten,&fdsets);//將socket加入到集合中(此例子是一個socket),將多個socket加入時,可以用數組加for迴圈select(NULL,&fdsets,NULL,NULL,&tv);//只檢查可讀性,即fd_set中的fd_read進行操作if(!FD_ISSET(m_socklisten,&fdsets))//檢查 s是否s e t集合的一名成員;如答案是肯定的是,則返回 T R U E。{return false;}return true;}

4.select函數參數詳解:  

三個 fd_set參數:一個用於檢查可讀性(readfds),一個用於檢查可寫性(writefds),另一個用於例外資料( excepfds)。

從根本上說,fdset資料類型代表著一系列特定通訊端的集合。其中,

readfds集合包括符合下述任何一個條件的通訊端:

■ 有資料可以讀入。
■ 串連已經關閉、重設或中止。
■ 假如已調用了listen,而且一個串連正在建立,那麼accept函數調用會成功。

writefds集合包括符合下述任何一個條件的通訊端:

■ 有資料可以發出。
■ 如果已完成了對一個非鎖定串連調用的處理,串連就會成功。
最後,exceptfds集合包括符合下述任何一個條件的通訊端:
■ 假如已完成了對一個非鎖定串連調用的處理,串連嘗試就會失敗。
■ 有帶外(out-of-band,OOB)資料可供讀取。

最後一個參數timeout:

對應的是一個指標,它指向一個timeval結構,用於決定select最多等待 I / O操作完成多久的時間。

如 timeout是一個null 指標,那麼select調用會無限期地“鎖定”或停頓下去,直到至少有一個描述符符合指定的條件後結束。

對timeval結構的定義如下:

struct timeval {
long tv_sec;
long tv_usec;

} ;

若將逾時值設定為(0,0),表明select會立即返回,允許應用程式對 select操作進行“輪詢”。出於對效能方面的考慮,應避免這樣的設定。

select成功完成後,會在 fd_set結構中,返回剛好有未完成的I/O操作的所有通訊端控制代碼的總量。

若超過timeval設定的時間,便會返回0。

如何測試一個通訊端是否“可讀”?

必須將自己的通訊端增添到readfds集合,再等待select函數完成。

select完成之後,必須判斷自己的通訊端是否仍為readfds集合的一部分。若答案是肯定的,便表明該通訊端“可讀”,可立即著手從它上面讀取資料。

在三個參數中(readfds、writedfss和exceptfds),任何兩個都可以是空值(NULL);但是,至少有一個不可為空值!在任何不為空白的集合中,必須包含至少一個通訊端控制代碼;

否則, select函數便沒有任何東西可以等待。

不管由於什麼原因,假如select調用失敗,都會返回SOCKET_ERROR

5.select

一個通訊端阻塞或者不阻塞,select就在那裡,它可以針對這2種通訊端使用,對任何一種通訊端的輪詢檢測,逾時時間都是有效,區別就在於:

當select完畢,認為該通訊端可讀時,

1 .阻塞的通訊端,會讓read阻塞,直到讀到所需要的所有位元組;

2 .非阻塞的通訊端,會讓read讀完fd中的資料後就返回,但如果原本你要求讀10個資料,這時唯讀了8個資料,如果你不再次使用select來判斷它是否可讀,而是直接read,很可能返回EAGAIN或=EWOULDBLOCK(BSD風格) ,
     此錯誤由在非阻塞通訊端上不能立即完成的操作返回,例如,當通訊端上沒有排隊資料可讀時調用了recv()函數。此錯誤不是嚴重錯誤,相應操作應該稍後重試。對於在非阻塞   SOCK_STREAM通訊端上調用connect()函數來說,報告EWOULDBLOCK是正常的,因為建立一個串連必須花費一些時間。

     EWOULDBLOCK的意思是如果你不把socket設成非阻塞(即阻塞)模式時,這個讀操作將阻塞,也就是說資料還未準備好(但系統知道資料來了,所以select告訴你那個socket可讀)。使用非阻塞模式做I/O操作的細心的人會檢查errno是不是EAGAIN、EWOULDBLOCK、EINTR,如果是就應該重讀,一般是用迴圈。如果你不是一定要用非阻塞就不要設成這樣,這就是為什麼系統的預設模式是阻塞。

完整代碼參考:

#include "stdafx.h"#include <WinSock2.h>#include <iostream>using namespace std;#include <stdio.h>#pragma comment(lib,"ws2_32.lib")#define PORT 8000#define MSGSIZE 255#define SRV_IP "127.0.0.1"int g_nSockConn = 0;//請求串連的數目//FD_SETSIZE是在winsocket2.h標頭檔裡定義的,這裡windows預設最大為64//在包含winsocket2.h標頭檔前使用宏定義可以修改這個值struct ClientInfo{    SOCKET sockClient;    SOCKADDR_IN addrClient;};ClientInfo g_Client[FD_SETSIZE];DWORD WINAPI WorkThread(LPVOID lpParameter);int _tmain(int argc, _TCHAR* argv[]){//基本步驟就不解釋了,網路編程基礎那篇部落格裡講的很詳細了    WSADATA wsaData;    WSAStartup(MAKEWORD(2,2),&wsaData);    SOCKET sockListen = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);    SOCKADDR_IN addrSrv;    addrSrv.sin_addr.S_un.S_addr = inet_addr(SRV_IP);    addrSrv.sin_family = AF_INET;    addrSrv.sin_port = htons(PORT);    bind(sockListen,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));    listen(sockListen,64);    DWORD dwThreadIDRecv = 0;    DWORD dwThreadIDWrite = 0;    HANDLE hand = CreateThread(NULL,0, WorkThread,NULL,0,&dwThreadIDRecv);//用來處理手法訊息的進程    if (hand == NULL)    {        cout<<"Create work thread failed\n";        getchar();        return -1;    }    SOCKET sockClient;    SOCKADDR_IN addrClient;    int nLenAddrClient = sizeof(SOCKADDR);//這裡用0初試化找了半天才找出錯誤    while (true)    {        sockClient = accept(sockListen,(SOCKADDR*)&addrClient,&nLenAddrClient);//第三個參數一定要按照addrClient大小初始化        //輸出串連者的地址資訊        //cout<<inet_ntoa(addrClient.sin_addr)<<":"<<ntohs(addrClient.sin_port)<<"has connect !"<<endl;        if (sockClient != INVALID_SOCKET)        {            g_Client[g_nSockConn].addrClient = addrClient;//儲存串連端地址資訊            g_Client[g_nSockConn].sockClient = sockClient;//加入串連者隊列            g_nSockConn++;        }    }    closesocket(sockListen);    WSACleanup();    return 0;}DWORD WINAPI WorkThread(LPVOID lpParameter){    FD_SET fdRead;    int nRet = 0;//記錄發送或者接受的位元組數    TIMEVAL tv;//設定逾時等待時間    tv.tv_sec = 1;    tv.tv_usec = 0;    char buf[MSGSIZE] = "";    while (true)    {        FD_ZERO(&fdRead);        for (int i = 0;i < g_nSockConn;i++)        {            FD_SET(g_Client[i].sockClient,&fdRead);        }        //只處理read事件,不過後面還是會有讀寫訊息發送的        nRet = select(0,&fdRead,NULL,NULL,&tv);        if (nRet == 0)        {//沒有串連或者沒有讀事件            continue;        }        for (int i = 0;i < g_nSockConn;i++)        {            if (FD_ISSET(g_Client[i].sockClient,&fdRead))            {                nRet = recv(g_Client[i].sockClient,buf,sizeof(buf),0);                if (nRet == 0 || (nRet == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET))                {                    cout<<"Client "<<inet_ntoa(g_Client[i].addrClient.sin_addr)<<"closed"<<endl;                    closesocket(g_Client[i].sockClient);                    if (i < g_nSockConn-1)                    {                        //將失效的sockClient剔除,用數組的最後一個補上去                        g_Client[i--].sockClient = g_Client[--g_nSockConn].sockClient;                    }                }                else                {                    cout<<inet_ntoa(g_Client[i].addrClient.sin_addr)<<": "<<endl;                    cout<<buf<<endl;                    cout<<"Server:"<<endl;                    //gets(buf);                    strcpy(buf,"Hello!");                    nRet = send(g_Client[i].sockClient,buf,strlen(buf)+1,0);                }            }        }    }    return 0;}

  

伺服器的主要步驟:

1.建立監聽通訊端,綁定,監聽

2.建立工作者線程

3.建立一個通訊端組,用來存放當前所有活動的用戶端通訊端,沒accept一個串連就更新一次數組

4.接收用戶端的串連,因為沒有重新定義FD_SIZE宏,伺服器最多支援64個並發串連。最好是記錄下串連數,不要無條件的接受串連

 

背景工作執行緒

背景工作執行緒是一個死迴圈,依次迴圈完成的動作是:

1.將當前用戶端通訊端加入到fd_read集中

2.調用select函數

3.用FD_ISSET查看時候通訊端還在讀集中,如果是就接收資料。如果接收的資料長度為0,或者發生WSAECONNRESET錯誤,,則

   表示用戶端通訊端主動關閉,我們要釋放這個通訊端資源,調整我們的通訊端數組(讓下一個補上)。上面還有個nRet==0的判斷,

   就是因為select函數會立即返回,串連數為0會陷入死迴圈。

本文參考:http://blog.csdn.net/rheostat/article/details/9815725

windows下的IO模型之選擇(select)模型

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.