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

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

1.選擇(select)模型:
選擇模型:通過一個fd_set集合管理套接字,在滿足套接字需求後,通知套接字。讓套接字進行工作。

選擇模型的核心是FD_SET集合和select函式。通過該函式,我們可以們判斷套接字上是否存在資料,或者能否向一個套接字寫入資料。

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

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

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

b我們可以想到用ioctlsocket()函式把socket設定成非阻塞的,然後用迴圈逐個socket檢視當前套接字是否有資料,輪詢進行。

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

c.於是想辦法用只切換一次狀態就知道所有socket的接受緩衝區是否有資料,於是有了select模型,select是阻塞的,Select的好處是可以同時處理若干個Socket,

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,如果是就應該重讀,一般是用迴圈。如果你不是一定要用非阻塞就不要設成這樣,這就是為什麼系統的預設模式是阻塞。

通過完善select模型可以得到IO複用模型,詳情請看:http://www.cnblogs.com/curo0119/p/8461520.html

一個IO模型的阻塞非阻塞指的是資料訪問過程,而不是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並用fd_zero巨集初始化為空

(2)用FD_SET巨集,把套接字控制代碼加入到fd_set集合

(3)呼叫select函式,檢查每個套接字的可讀可寫性,select完成後,會返回所有在fd_set集合中有資料到達的socket的socket控制代碼總數,並對每個集合進行更新,即沒有資料到達的socket在原集合中會被置成空。
(4)根據select的返回值以及FD_ISSET巨集,對FD_SET集合進行檢查
(5)知道了每個集合中“待決”的I/O操作後,對相應I/O操作進行處理,返回步驟1,繼續select

select函式返回後,會修改FD_SET的結構,刪除不存在待決IO操作的套接字,這也就是為什麼我們之後要用FD_ISSET判斷是否還在集合中的原因。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 boolUDPNet::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。 { returnfalse; } returntrue; }

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 {
longtv_sec;
longtv_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優缺點:

優點:可實現單執行緒處理多個任務

缺點:

a.等待資料到達的過程以及將資料從核心拷貝到使用者的過程總也存在一定阻塞

b.管理的set陣列有一定上限,最多是64個(可通過重置fd_setsize將上限擴大到1024)

c.select低效是因為每次它都需要輪詢。

完整程式碼參考:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 #include "stdafx.h" #include <WinSock2.h> #include <iostream> usingnamespacestd; #include <stdio.h> #pragma comment(lib,"ws2_32.lib") #define PORT 8000 #define MSGSIZE 255 #define SRV_IP "127.0.0.1" intg_nSockConn = 0;//請求連線的數目 //FD_SETSIZE是在winsocket2.h標頭檔案裡定義的,這裡windows預設最大為64 //在包含winsocket2.h標頭檔案前使用巨集定義可以修改這個值 structClientInfo { SOCKET sockClient; SOCKADDR_IN addrClient; }; ClientInfo g_Client[FD_SETSIZE]; DWORDWINAPI WorkThread(LPVOIDlpParameter); int_tmain(intargc, _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); DWORDdwThreadIDRecv = 0; DWORDdwThreadIDWrite = 0; HANDLEhand = CreateThread(NULL,0, WorkThread,NULL,0,&dwThreadIDRecv);//用來處理手法訊息的程序 if(hand == NULL) { cout<<"Create work thread failed\n"; getchar(); return-1; } SOCKET sockClient; SOCKADDR_IN addrClient; intnLenAddrClient =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(); return0; } DWORDWINAPI WorkThread(LPVOIDlpParameter) { FD_SET fdRead; intnRet = 0;//記錄傳送或者接受的位元組數 TIMEVAL tv;//設定超時等待時間 tv.tv_sec = 1; tv.tv_usec = 0; charbuf[MSGSIZE] =""; while(true) { FD_ZERO(&fdRead); for(inti = 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(inti = 0;i < g_nSockConn;i++) { if(FD_ISSET(g_Client[i].sockClient,&fdRead)) {<br>          //如果在集合中,向下進行相應的IO操作 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;<br>              //i--是因為要重新判斷新的i的位置的socket是否失效 } } 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); } } } } return0; }

  

伺服器的主要步驟:

1.建立監聽套接字,繫結,監聽

2.建立工作者執行緒

3.建立一個套接字組,用來存放當前所有活動的客戶端套接字,沒accept一個連線就更新一次陣列

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

工作執行緒

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

1.將當前客戶端套接字加入到fd_read集中

2.呼叫select函式

3.用FD_ISSET檢視時候套接字還在讀集中,如果是就接收資料。如果接收的資料長度為0,或者發生WSAECONNRESET錯誤,,則

表示客戶端套接字主動關閉,我們要釋放這個套接字資源,調整我們的套接字陣列(讓下一個補上)。上面還有個nRet==0的判斷,

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