socket為了實現(xiàn)非阻塞模式,winsock提供了幾種不同的套接字I/O模型對I/O進(jìn)行管理,包括:select(選擇),WSAAsyncSelect(異步選擇),WSAEventSelect(事件選擇),Overlapped(重疊)以及 Completion port(完成端口), 另可以用ioctlsocket(SOCKET s, FIOBIO, int &cmd )設(shè)置非阻塞模式,不過這樣會非常的復(fù)雜。
select是winsock中最常見的i/o模型。通過調(diào)用select函數(shù)可確定一個或多個套接字的狀態(tài),判斷套接字上是否存在數(shù)據(jù),或都能否向一個套接字寫入數(shù)據(jù)。它既能防止應(yīng)用程序在套接字處于阻塞模式時,在一次i/o操作后被阻塞,同時也防止在套接字處于非鎖定模式中時,產(chǎn)生WSAEWOULDBLOCK錯誤。
1.select函數(shù)
int select( int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, const struct timeval* timeout );
其中,參數(shù)nfds可忽略,僅僅起到與Berkeley套接字兼容的作用。 readfds,writefds,exceptfds三個fd_set數(shù)據(jù)類型的參數(shù)分別為指向等待可讀性檢查的套接字組,等待可寫性檢查的套接字組和指向等待錯誤檢查的套接字組的指針。在這三個fd_set參數(shù)中,至少有一個不為NULL,在任何不為空的集合中,必須包含至少一個套接字句柄,否則,select()函數(shù)便沒有任何東西可以等待了。參數(shù)timeout為一timeval結(jié)構(gòu)數(shù)據(jù),用于指定select()最多等待時間,對阻塞操作則為NULL。timeval結(jié)構(gòu)的格式為:
struct timeval
{
long tv_sec; //秒
long tv_usec; //毫秒
}
其中,tv_sec字段以秒為單位指定等待時間。tv_usec字段則以毫秒為單位指定等待時間。若將超時值設(shè)置為(0,0),則select()會立即返回,允許應(yīng)用程序?qū)elect操作進(jìn)行"輪詢"。出于對性能方面的考慮,應(yīng)避免這樣的設(shè)置。select()調(diào)用成功返回時,fd_set結(jié)構(gòu)中將存有滿足條件的套接字組的子集,并且select()返回滿足條件的套接字的數(shù)目。若調(diào)用超過timeval設(shè)定的時間,便會返回0;若調(diào)用失敗,則返回SOCKET_ERROR。
winsock提供了FD_SETSIZE變量用于確定一個集合中最多的套接字描述字?jǐn)?shù)目(FD_SETSIZE缺省值為64,可在包含winsock.h前用 #define FD_SETSIZE 來改變該值)。此外,還提供了四個宏對fd_set結(jié)構(gòu)進(jìn)行操作,它們分別為:
FD_CLR( s, *set );從集合set中 刪除套接字句柄s
FD_ISSET( s, *set ); 若s為集合中一員,非零;否則為零。
FD_SET( s, *set );向集合添加套接字句柄s
FD_ZERO( s, *set ); 將set初始化為空
若想測試一個套接字是否"可讀",則操作如下:
1.將該套接字增加到readfd集合中:
fd_set fdread;
FD_ZERO(&fdread);
FD_SET( s, &fdread );
2.調(diào)用select()函數(shù):
select( 0, &fdread, NULL, NULL, NULL );
3.等待select()返回,若調(diào)用成功,則判斷該套接字是否為集合一員,若答案是肯定的,便表明該套接字“可讀”,可立即從它上面讀取數(shù)據(jù):
if( FD_ISSET( s, &fdread )
{
//從套接字中讀取數(shù)據(jù)
}
2.WSAAsyncSelect模型
WSAAsyncSelect模型也是一個常用的異步I/O模型,利用這個模型,應(yīng)用程序可在一個套接字上接收以windows消息為基礎(chǔ)的網(wǎng)絡(luò)事件通知。該模型的實現(xiàn)方法是通過調(diào)用WSAAsyncSelect函數(shù)自動將套接字設(shè)置為非阻塞模式,并向winsock dll注冊一個或多個感興趣的網(wǎng)絡(luò)事件,并提供一個通知時使用的窗口句柄,當(dāng)注冊的網(wǎng)絡(luò)事件發(fā)生時,對應(yīng)窗口將收到一個基于消息的通知。WSAAsyncSelect函數(shù)的原型為:
int WSAAsyncSelect( SOCKET s, HWND hWnd, unsigned int wMsg, long lEvent );
其中,字段s用于標(biāo)識一個需要事件通知的套接字的句柄。hWnd標(biāo)識一個在網(wǎng)絡(luò)事件發(fā)生時需要接收消息的窗口句柄。wMsg在網(wǎng)絡(luò)事件發(fā)生時指定窗口要接收到的消息,它為一個自定義的消息,最后一個參數(shù)是lEvent,為位屏蔽碼,用于指明應(yīng)用程序感興趣的網(wǎng)絡(luò)事件集合。它可以為以下值:
FD_READ 欲接收讀準(zhǔn)備好的通知
FD_WRITE 欲接收寫準(zhǔn)備好的通知
FD_OOB 欲接收帶邊數(shù)據(jù)到達(dá)的通知
FD_ACCEPT 欲接收寫準(zhǔn)備好的通知
FD_CONNECT 欲接收已連接好的通知
FD_CLOSE 欲接收套接字關(guān)閉的通知
對不同的事件區(qū)分不同的消息是不可能的,困此下面的代碼將不會工作,第二個調(diào)用將會使第一次調(diào)用的作用失效,只有FD_WRITE會通過wMsg2消息通知到,也正困為如此,多個事件必須在套接字上一次注冊:
rc = WSAAsyncSelect( s, hWnd, wMsg1, FD_READ );
rc = WSAAsyncSelect( s, hWnd, wMsg2, FD_WRITE );
如果要取消所有的通知,即winsock的實現(xiàn)不再在套接字上發(fā)送任何和網(wǎng)絡(luò)事件相關(guān)的消息,則應(yīng)將lEvent參數(shù)置為0:
WSAAsyncSelect( s, hWnd, 0, 0 );
這樣,WSAAsyncSelect立即使傳給該套接字的事件消息無效,但仍有可能有消息等在應(yīng)用程序的消息隊列中,應(yīng)用程序困此也必須準(zhǔn)備好接收網(wǎng)絡(luò)消息,即使消息作廢。用closesocket關(guān)閉一個套接字也同樣使WSAAsyncSelect發(fā)送的消息作廢,但在closesocket這前隊列中的消息仍然起作用。
由于一個已調(diào)用accept的套接字和用來接收它的偵聽套接字有同樣的屬性,任何為偵聽套接字設(shè)置的WSAAsyncSelect事件也同樣對已接收的套接字起作用。例如,如果一個偵聽套接字有WSAAsyncSelect事件FD_ACCEPT,FD_READ,FD_WRITE,則任何在那個偵聽 的套接字上接收的套接字將也有FD_ACCEPT,FD_READ,FD_WRITE事件,以及同樣的wMsg的值。若需要不同的sMsg及事件,應(yīng)用程序應(yīng)調(diào)用WSAAsyncSelect,將已接收的套接字和想要發(fā)送的新消息作為參數(shù)傳遞。
當(dāng)某一套接字s上發(fā)生了一個已注冊的網(wǎng)絡(luò)事件,應(yīng)用程序窗口hWnd會接收到消息wMsg.wParam參數(shù)標(biāo)識了網(wǎng)絡(luò)事件發(fā)生的套接字,lParam的低字指明了發(fā)生的網(wǎng)絡(luò)事件,lParam的高字則含有一個錯誤代碼,該錯誤代碼可以是winsock.h中聲明的任何錯誤。
****應(yīng)用程序如果需要給偵聽的和調(diào)用 過accept()的套接字以不同的wMsg,它就應(yīng)該在偵聽的套接字上請求FD_ACCEPT事件,然后在accept()調(diào)用后設(shè)置相應(yīng)的事件。由于FD_ACCEPT從不發(fā)送給已連接的套接字,而FD_READ,FD_WRITE,FD_OOB及FD_CLOSE也從不發(fā)送給偵聽套接字,所以不會產(chǎn)生困難。
若應(yīng)用程序感興趣的網(wǎng)絡(luò)事件注冊成功,則返回0,否則返回SOCKET_ERROR.
3.WSAEventSelect模型
WSAEventSelect模型是winsock提供的另一個有用的異步I/O模型,它和WSAAsynSelect模型類似的是,它也允許應(yīng)用程序在一個或多個套接字上,接收以事件為基礎(chǔ)的網(wǎng)絡(luò)事件通知,并且它支持的網(wǎng)絡(luò)事件與WSAEventselect模型的一樣。它與WSAAsyncSelect模型最主要的差別在于網(wǎng)絡(luò)事件會被發(fā)送到一個事件對象句柄,而不是發(fā)送到一個窗口。
首先要調(diào)用函數(shù)WSACreateEvent()創(chuàng)建事件對象來接收網(wǎng)絡(luò)事件,該函數(shù)原型為:
WSAEVENT WSACreateEvent(void);
它的返回值是一個事件對象句柄,該事件具有兩種工作狀態(tài):"已傳信"(signaled)和"未傳信"(nosignaled)及兩種工作模式:"人工重設(shè)"(manual reset)和"自動重設(shè)"(auto reset).默認(rèn)狀態(tài)下事件處于未傳信的工作狀態(tài)和人工重設(shè)模式。
接下來就要調(diào)用WSAEventSelect()函數(shù)將所創(chuàng)建的事件對象與某個套接字關(guān)聯(lián)在一起,同時注冊感興趣的網(wǎng)絡(luò)事件類型,使事件對象的工作狀態(tài)從"未傳信"轉(zhuǎn)變成"已傳信"。 WSAEventSelect函數(shù)原型為:
int WSAEventSelect( SOCKET s, WSAEVENT hEventObject, long lNetworkEvents );
其中,參數(shù)s為一個標(biāo)識套接字句柄。hEventObject參數(shù)用于指定與所提供的FD_XXX網(wǎng)絡(luò)事件集合相關(guān)的一個事件對象句柄。lNetworkEvents參數(shù)是一個屏蔽位,用于指定感興趣FD_XXX網(wǎng)絡(luò)事件組合。
由于事件對象創(chuàng)建后默認(rèn)處于人工重設(shè)模式,所以在完成了一個I/O請求的處理后,應(yīng)用程序需要調(diào)用WSAResetEvent函數(shù)將事件對象的工作狀態(tài)從已傳信更改為未傳信。
WSAResetEvent函數(shù)的原型為:
BOOL WSAResetEvent(WSAEVENT hEvent );
該函數(shù)唯一的參數(shù)是一個事件句柄,成功則返回TRUE,失敗返回FALSE。
一個套接字同一個事件對象句柄關(guān)聯(lián)在一起后,應(yīng)用程序就可以通過WSAWaitForMultipleEvents函數(shù)等待網(wǎng)絡(luò)事件來觸發(fā)事件句柄的工作狀態(tài),進(jìn)行I/O處理;WSAWaitForMultipleEvents函數(shù)原型為:
DWORD WSAWaitForMultipleEvents(
DWORD cEvents,
const WSAEVENT FAR* lphEvents,
BOOL fWaitAll,
DWORD dwTimeout,
BOOL fAlertable );
其中,lphEvents參數(shù)為指向一個事件對象句柄數(shù)組的指針。cEvents參數(shù)指出所指向的數(shù)組事事件對象句柄的數(shù)目,事件對象句柄數(shù)組的最大值為WSA_MAXIMUM_WAIT_EVENTS。fWaitAll參數(shù)指定等待類型,若為TRUE,則當(dāng)lphEvents數(shù)組中的所有事件對象同時有信號時,函數(shù)返回;若為FALSE,則當(dāng)任意一個事件對象有信號時函數(shù)就返回。在后一種情況下,返回值指出是哪一個事件對象造成函數(shù)返回。通常把參數(shù)設(shè)為FALSE,一次只為一個套接字事件提供服務(wù)。dwTimeout參數(shù)指定超時等待時間(以毫秒計),當(dāng)超時間隔到,不論fWaitAll參數(shù)所指定的條件是否滿足,函數(shù)即返回。如果dwTimeout為零,則函數(shù)測試指定的時間對象的狀態(tài),并立即返回。如果dwTimeout是WSA_INFINITE,則函數(shù)的超時間隔永遠(yuǎn)不會到,最后一個參數(shù)fAlertable指定當(dāng)系統(tǒng)針一個輸入/輸出完成例程放入隊列以供執(zhí)行時,函數(shù)是否返回,若為TRUE,則函數(shù)返回且執(zhí)行完成例程,若為FALSE,函數(shù) 不返回,不執(zhí)行完成例程,在win16中應(yīng)忽略該參數(shù)。如果函數(shù)成功,返回值指出造成函數(shù)返回的事件對象,如果函數(shù)失敗,返回值為WSA_WAIT_FAILED。
這樣,應(yīng)用程序便可引用事件數(shù)組中已傳信的事件,并檢索與哪個事件對應(yīng)的套接字,判斷到底是在哪個套接字上,發(fā)生了什么網(wǎng)絡(luò)事件類型。對事件數(shù)組中的事件進(jìn)行引用時,應(yīng)該用WSAWaitForMultipleEvents的返回值,減去預(yù)聲明值WSA_WAIT_EVENT_0,得到具體的引用值(即索引位置)。如:
Index = WSAWaitForMultipleEvents(...);
MyEvent = EventArray[Index-WSA_WAIT_EVENT_0];
知道了產(chǎn)生網(wǎng)絡(luò)事件的套接字后,接下來可調(diào)用WSAEnumNetsorkEvents函數(shù)知道發(fā)生了什么類型的網(wǎng)絡(luò)事件。該函數(shù)原型為:
int WSAEnumNetworkEvents(
SOCKET s,
WSAEVENT hEventObject,
LPWSANETWORKEVENTS lpNetworkEvents
);
其中,s參數(shù)標(biāo)識套接字句柄,hEventObject對數(shù)是可選的,用于標(biāo)識需要重設(shè)的相應(yīng)事件對象,由于事件對象處在一個"已傳信"狀態(tài),所以可將它傳入,令其自動成為"未傳信"狀態(tài)。如果不想hEventObject參數(shù)來重設(shè)事件,那么可使用WSAResetEvent()函數(shù);最后一個參數(shù)是lpNetworkEvents是一個WSANETWORKEVENTS結(jié)構(gòu)的數(shù)組,每一個元素記錄了一個網(wǎng)絡(luò)事件和相應(yīng)的錯誤代碼。它WSANETWORKEVENTS結(jié)構(gòu)格式為:
typedef struct _WSANETWORKEVENTS {
long lNetworkEvents;
int iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS, *LPWSANETWORKEVENTS;
其中,lNetworkEvents參數(shù)用于指定套接字上發(fā)生的所有網(wǎng)絡(luò)事件類型。iErrorCode參數(shù)指定的是一個錯誤代碼數(shù)組,同lNetworkEvents中的事件關(guān)聯(lián)在一起。針對每個網(wǎng)絡(luò)事件類型,都存在著一個特殊的事件索引,名字與事件類型的名字類似,只是要在事件名字后添加 一個"_BIT"后綴字串即可。如,對FD_READ事件類型來說,iErrorCode數(shù)組的索引標(biāo)識符便是FD_READ_BIT。
完成了對WSANETWORKEVENTS結(jié)構(gòu)中的事件處理后,應(yīng)用程序可在所有可用的套接字上繼續(xù)等待更多的網(wǎng)絡(luò)事件.
4.重疊I/O模型
winsock2引入了重疊I/O模型的概念并要求所有的傳輸協(xié)議提供者都支持這一功能。它的基本原理是讓應(yīng)用程序使用一個重疊的數(shù)據(jù)結(jié)構(gòu),一次投遞一個或多個winsock I/O請求,針對那些提交的請求,在它們完成后,應(yīng)用程序可為它們提供服務(wù)。應(yīng)用程序可通過ReadFile和WriteFile兩個函數(shù)執(zhí)行I/O操作。重疊I/O僅能在由WSASocket函數(shù)(函數(shù)socket的增強版本)打開的套接實際上使用(使用 WSA_FLAG_OVERLAPPED標(biāo)記)。這種方式的使用將采用win32建立的模型。
對于接收,應(yīng)用程序使用WSARecv函數(shù)或WSARecvFrom函數(shù)來提供存放接收數(shù)據(jù)的緩沖區(qū)。如果數(shù)據(jù)在網(wǎng)絡(luò)接收前,應(yīng)用程序已經(jīng)提供了一個或多個數(shù)據(jù)緩沖區(qū),那么接收的數(shù)據(jù)就可以立即存放進(jìn)用戶緩沖區(qū)。這樣可省去使用recv函數(shù)和recvfrom函數(shù)時需要進(jìn)行的拷貝工作。如果在應(yīng)用程序提供數(shù)據(jù)區(qū)時,已經(jīng)有數(shù)據(jù)到來,那么接收的數(shù)據(jù)將被立即拷貝進(jìn)用戶緩沖區(qū)。如果數(shù)據(jù)到來時,應(yīng)用程序沒有提供接收緩沖區(qū),那么網(wǎng)絡(luò)將回到我們熟悉的同步操作方式--傳送來的數(shù)據(jù)將被存放進(jìn)內(nèi)部緩沖區(qū),直到應(yīng)用程序發(fā)出了接收調(diào)用并且提供了緩沖區(qū),這時接收的數(shù)據(jù)就被拷貝進(jìn)接收緩沖區(qū)。這種做法會有一個例外:就是當(dāng)應(yīng)用程序使用setsockopt函數(shù)把接收緩沖區(qū)長度軒為了0.在這種情況下,對于可靠輿協(xié)議,只有在應(yīng)用程序提供了接收數(shù)據(jù)緩沖區(qū)后,數(shù)據(jù)才會被接收;而對于不可靠傳輸協(xié)議,靈氣將會丟失。
對于發(fā)送的一方,應(yīng)用程序使用WSASend()函數(shù)或WSASendTo函數(shù)提供一個指向已填充的靈氣緩沖區(qū)的指針。應(yīng)用程序不應(yīng)在網(wǎng)絡(luò)使用完該緩沖區(qū)的數(shù)據(jù)以前以任何方式破壞該緩沖區(qū)的數(shù)據(jù)。
重疊發(fā)送和接收調(diào)用會立即返回。如果返回值是SOCKET_ERROR,并且錯誤代碼是WSA_IO_PENDING,那么表明重疊操作已經(jīng)被成功的初始化,今后發(fā)送緩沖區(qū)被用完或接收緩沖區(qū)被填滿時,將會有完成指示。任何其他的錯誤代碼表明了初始化沒有成功,今后也不會有什么完成指示。發(fā)送操作和接收操作都可以被重疊使用。接收函數(shù)可被多次調(diào)用,發(fā)出接收緩沖區(qū),準(zhǔn)備接收到來的數(shù)據(jù)。發(fā)送函數(shù)也可以被多次調(diào)用,組成 一個發(fā)送緩沖區(qū)隊列。要注意的是,應(yīng)用程序可以通過按順序提供發(fā)送緩沖區(qū)來確保一毓重疊發(fā)送操作的順序,但是對應(yīng)的完成指示有可能是按另外的順序排列的。同樣的,在接收數(shù)據(jù)的一方,緩沖區(qū)是按被提供的順序填充的,但完成指示也可能按另外的順序排列。
WSAIoctl函數(shù)(ioctolsocket函數(shù)的增強版本)還可以使用重疊I/O操作的延遲完成特性。