1. 程式人生 > >windows下的IO模型之選擇(select)模型

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

工作者線程 dfs read 沒有 應用 ould 設置 spa fine

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是一個空指針,那麽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)模型