Windows socket之WSAEventSelect模型
WSAEventSelect模型
WSAEventSelect模型是Windows socekts提供的另一個有用非同步IO模型。該模型允許在一個或多個套接字上接收以事件為基礎的網路事件通知。Windows sockets應用程式可以通過呼叫WSAEventSelect函式,將一個事件與網路事件集合關聯起來。當網路事件發生時,應用程式以事件的形式接收網路事件通知。
WSAEventSelect模型與WSAAsyncSelect模型很相似。它們最主要的差別就是當網路事件發生時通知應用程式的形式不同。雖然它們都是非同步的,但WSAAsyncSelect
與select模型相比較,WSAAsyncSelect與WSAEventSelect模型都是被動接受的。網路事件發生時,系統通知應用程式。而select模型是主動的,應用程式主動呼叫select函式來檢查是否發生了網路事件。
WSAEventSelect函式。
該函式功能是為套接字註冊網路事件。該函式將事件物件與網路事件關聯起來。當在該套接字上發生一個或多個網路事件時,應用程式便以事件的形式接收這些網路事件通知。
int WSAEventSelect( SOCKET s, WSAEVENT hEvent, Long lNetworkEvents);
s為套接字控制代碼。
hEvent為事件物件控制代碼。
lNetworkEvents為應用程式感興趣的網路事件集合。
如果應用程式為套接字註冊網路事件成功,函式返回0。否則返回SOCKET_ERROR。可以呼叫WSAGetLastError來獲取具體的錯誤程式碼。
呼叫該函式後,套接字自動被設為非阻塞的工作模式。如果應用程式要將套接字設定為阻塞模式,必須將lNetwork引數設為0,再次呼叫WSAEventSelect函式。
Windows sockets宣告的網路事件與前面介紹的WSAAsyncSelect介紹的是一樣的,此處不再介紹。
WSACreateEvent()函式。
應用程式在呼叫WSAEventSelect
WSAEVENT WSACreateEvent(void);
呼叫成功則返回事件物件控制代碼。否則返回WSA_INVALID_EVENT。返回的事件物件的初始態為未觸發態手工重置的物件。
當網路事件到來時,與套接字關聯的事件物件由未觸發變為觸發態。由於它是手工重置事件,應用程式需要手動將事件的狀態設定為未觸發態。這可以呼叫WSAResetEvent函式:
bool WSAResetEvent(WSAEVENT hEvent);
該函式的引數為事件物件。呼叫成功則返回TRUE,否則false。
不再使用事件物件時要將其關閉。這可以呼叫WSACloseEvent函式:
bool WSACloseEvent(WSAEVENT hEvent);
呼叫成功返回TRUE,否則false。
WSAWaitForMultipleEvents函式:
該函式可以等待網路事件的發生。它的目的是等待一個或是所有的事件物件變為已觸發狀態。
DWORD WSAWaitForMultipleEvents(
DWORD cEvents,
const WSAEVENT *plhEvent,
BOOL fWaitAll,
DWORD dwTimeOUT,
BOOL fAlertable);
cEvents:為事件物件控制代碼個數。至少為1,最多為WSA_MAXIMUM_WAIT_EVENTS,64個。
lphEvents為指向物件控制代碼陣列指標。
fWaitAll:如果為TRUE,則該函式在所有事件物件都轉變為已觸發時才返回。如為false,只要有一個物件被觸發,函式即返回。
dwTimeOUT:函式阻塞事件。單位為毫秒。在時間用完後函式會返回。如果為WSA_INFINITE則函式會一直等待下去。如果超時返回,函式返回WSA_WAIT_TIMEOUT。
fAlterable:該引數說明當完成例程在系統佇列中排隊等待執行時,該函式是否返回。這主要應用於重疊IO模型,以後還會介紹。此處將其設定為false即可。
WSAWaitForMultipleEvents返回時,返回值會指出它返回的原因。
當bWaitAll為TRUE時:
如果返回值為WSA_TIMEOUT則表明等待超時。
WSA_EVENT_0表明所有物件都已變成觸發態。等待成功。
WAIT_IO_COMPLETION說明一個或多個完成例程已經排隊等待執行。
如果bWaitAll為false時:
WSA_WAIT_EVENT_0到WSA_WAIT_EVENT_0+cEvent-1範圍內的值,說明有一個物件變為觸發態。它在陣列中的下標為:返回值-WSA_EVENT_0。
如果函式呼叫失敗,則返回WSA_WAIT_FAILED。
下面的程式程式碼,演示了當WSAWaitForMultipleEvents返回值,如何確定事件物件和發生網路事件的套接字:
SOCKET socketArray[WSA_MAXIMUM_WAIT_EVENTS];
WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS];
int dwIndex=WSAWaitForMultipleEvents(num,eventArray,false,false);
//已觸發的網路事件物件為:
WSAEVENT cur=eventArray[dwIndex-WSA_WAIT_EVENT_0];
//當前套接字為:
SOCKET curSocket=socektArray[dwIndex-WSA_WAIT_EVENT-0];
WSAEnumNetworkEvents函式。
通過WSAWaitForMultipleEvents的返回值可以獲得發生網路事件的套接字。但是,應用程式需要判斷在該套接字上究竟發生了什麼網路事件。這可以通過呼叫WSAEnumNetworkEvents來實現:
int WSAEnumNetworkEvents(
SOCKET s,
WSAEVENT hEvent,
LPWSANETWORKEVENTS lpNetworkEvents);
該函式可以查詢發生在套接字上的網路事件,並清除系統內部的網路事件記錄,重置事件物件。
s為發生網路事件的套接字控制代碼。
hEvent為被重置的事件物件控制代碼(可選)。
lpNetworkEvents為指向WSANETWORKEVENTS結構指標。
如果hEvent不為NULL,則該事件被重置。如果為NULL,需要呼叫WSAResetEvent函式設定事件為非觸發狀態。
該結構中包含發生網路事件的記錄和相關錯誤程式碼。
呼叫成功返回0,否則為SOCKETS_ERROR。
WSANETWORKEVENTS結構如下:
typedef struct _WSANETWORKEVENTS
{
long lNetworkEvents,
int iErrorCode[FD_MAX_EVENTS];
}WSANETWORKEVENTS,*LPWSANETWORKEVENTS;
lNetworkEvents指示發生的網路事件。一個物件再變為觸發態時,可能在套接字上發生了多個網路事件。
iErrorCode為包含網路事件錯誤程式碼的陣列。錯誤程式碼與lNetworkEvents欄位中的網路事件對應。
在應用程式中,使用網路事件事件錯誤識別符號對iErrorCode陣列進行索引,檢查是否發生了網路錯誤。這些識別符號的命名規則是對應的網路事件後面新增_BIT.例如,對於FD_READ事件的網路事件錯誤識別符號為FD_READ_BIT。
下面的程式碼演示了,如何判斷FD_READ網路事件的發生:
SOCKET s;
WSAEVENT hNetworkEvent;
WSANETWORKEVENT networkEvents;
if(0==WSAEnumNetworkEvents(h,hNetworkEvent,&networkEvents);
{
//發生FD_WRITE網路事件。
if(networkEvents.lNetworkEvents&FD_READ)
{
if(0==networkEvent.iErrorCode[FD_READ_BIT])
{
//接收資料。
}
else
{
//獲取錯誤程式碼。
int nErrorCode=networkEvents.iErrorCode[FD_READ_BIT];
//處理錯誤。
}
}
}
本例演示利用WSAEventSelect模型開發一個伺服器應用程式的步驟。
主要步驟:
程式開始時會建立監聽套接字,利用WSAEventSelect函式為套接字註冊FD_ACCEPT和FD_CLOSE事件,然後套接字進入監聽狀態。在while迴圈內,迴圈呼叫WSAWaitForMultipleEvents函式等待網路事件的發生,當網路事件發生時函式返回,並通過該函式的返回值得到發生網路事件的套接字。呼叫WSAEnumNetworkEvents函式檢查在該套接字上到底發生什麼網路事件。
如果發生FD_ACCEPT網路事件,則呼叫accept函式接受客戶端連線。將該套接字加入套接字陣列。建立事件物件並加入事件陣列。事件物件數量加一。然後呼叫WSAEventSelect函式為該套接字關聯事件物件,註冊FD_READ,FD_WRITE和FD_CLOSE網路事件。如果發生FD_READ網路事件,則呼叫recv函式接收資料。
如果發生FD_WRITE網路事件,則呼叫send函式傳送資料。
如果發生FD_CLOSE網路事件,將該套接字從套接字陣列清除,同時將對應事件從事件陣列刪除。事件物件數量減一,並關閉該套接字。
在應用程式中,對發生的每種網路事件,都首先判斷是否發生了網路錯誤。如果發生錯誤,則伺服器退出。
步驟一:定義事件物件陣列和套接字陣列。
這兩個陣列的最大程度為WSA_MAXIMUM_WAIT_EVENTS。這兩個陣列的成員存在一一對應關係。
DWORD totalEvent;//事件物件數量。
WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS];
SOCKETS socketArray[WSA_MAXIMUM_WAIT_EVENTS];
步驟二:建立套接字:
WSADATA wsaData;
WSAStartup(MAKEWORD(2,2),&wsaData);
if((sListen==socket(AF_INET,SOCK_STREAM,0))
{
//建立失敗。
}
步驟三:為監聽套接字註冊網路事件:
if(eventArray[totalEvent]=WSACreateEvent()==WSA_INVALID_EVENT)
{
//呼叫失敗。
}
//為監聽套接字註冊FD_READ,和FD_CLOSE網路事件。
if(WSAEventSelect(sListen,eventArray[totalEvent],FD_ACCEPT|FD_CLOSE)==SOCKETS_ERROR)
{
//呼叫失敗。
}
步驟四:開始監聽:
sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_addr.S_addr=htons(INADDR_ANY);
addr.sin_port=htons(4000);
if(bind(sListen,(SOCKADDR*)&addr,sizeof(addr))==SOCKETS_ERROR)
{
//繫結失敗。
}
if(!listen(sListen,10))
{
//監聽失敗。
}
步驟五:等待網路事件。
while(true)
{
if(dwIndex=WSAWaitForMultipleEvents(totalEvent,eventArray,
false,WSA_INFINITE,false)==
WSA_WAIT_FAILED)
{
//等待失敗。
}
步驟六:獲取發生的網路事件。
當網路事件發生時WSAWaitForMultipleEvents函式返回。呼叫WSAEnumNetworkEvents函式獲取發生在套接字上的網路事件。
socketArray[dwIndex-WSA_WAIT_EVENT_0]為當前發生網路事件的套接字。
eventArray[dwIndex-WSA_WAIT_EVENT_0]為當前被投遞網路事件的事件物件。
當函式返回時networkEvents變數中儲存了網路事件的記錄。同時事件物件的工作狀態,由未觸發態變為觸發態。
WSANETWORKEVENTS networkEvents;
if(WSAEnumNetworkEvents(socketArray[dwIndex-WSA_WAIT_EVENT_0],eventArray[dwIndex-WSA_WAIT_EVENT_0],&networkEvents)==SOCKETS_ERROR)
{
//呼叫失敗。
}
步驟七:判斷是否是各網路事件發生。
WSAEnumNetworkEvents函式返回時,首先檢查是否發生了FD_ACCEPT網路事件。如果該網路事件發生,則說明此時客戶端的連線請求被等待。檢查是否發生了網路錯誤,如果沒有錯誤發生,則執行下面的步驟:
1:呼叫accept接受客戶端請求。
2:判斷當前套接字數量是否超過了最大值。如果超過則關閉該套接字。
3:將客戶端套接字介入套接字陣列。
4:建立套接字事件物件,並將該事件物件加入事件物件陣列。
5:為該套接字註冊FD_READ,FD_WRITE和FD_CLOSE網路事件。
6:事件物件數量加一,將該套接字加入管理客戶端套接字連結串列中。
使用WSAEventSelect應該注意的問題。
1:如果在一個套接字上多次呼叫WSAEventSelect函式,那麼最後一次函式呼叫將會取消前一次的呼叫效果。
2:一個套接字不要關聯多個事件物件 。在一個套接字上為不同網路事件註冊不同的事件物件是不可能的。一個套接字關聯一個物件,當該物件被觸發時,獲得對應的套接字,然後呼叫WSAEnumNetworkEvents來獲得發生在此套節字上的事件。
3:如果要取消事件物件與網路事件的關聯,以及為套接字註冊的網路事件。應用程式可以在呼叫WSAEventSelect時將lNetworkEvent設定為0。另外,呼叫closesocket關閉套接字時,也會取消這種關聯和為套接字註冊的網路事件。
4:呼叫accept接受的套接字與監聽套接字具有同樣的屬性。
如:在建立監聽套接字時為其設定感興趣的網路事件為FD_ACCEPT和FD_CLOSE。那麼accept返回的套接字同樣具有這些屬性。它與監聽套接字感興趣的網路事件相同且使用同一個事件物件。一般情況下,我們都會為新套接字重新呼叫WSAEventSelect。後面的程式碼中在accept後會新套接字呼叫WSAEventSelect函式就不足為奇了!!
5:接收FD_CLOSE網路事件時,錯誤程式碼指出套接字是從容關閉還是硬關閉。如果錯誤程式碼為0,則為從容關閉;若錯誤程式碼為WSAECONNRESET錯誤,則是硬關閉。當應用程式接收到該網路事件時,說明對方在該套接字上執行了shutdown或者是closesocket函式呼叫。
WSAEventSelect模型的優勢和不足。
WSAEventSelect模型的優勢是可以應用在一個非視窗的Windows sockets程式中,實現對多個套接字的管理。
不足是:每個WSAEventSelect模型最多隻能管理64個套接字。當應用程式需要管理多於64個套接字時,就需要額外建立執行緒。由於該模型需要呼叫多個函式,這增加了開發的難度。
以下為詳細程式碼:
#include<iostream>
#include<windows.h>
#include"winsock2.h"
SOCKET sListen;
#pragma comment(lib,"WS2_32.lib")
#define MAX_NUM_SOCKET 20
u_int totalEvent=0;
//構造事件物件陣列和套接字陣列。
WSAEVENT eventArray[MAX_NUM_SOCKET];
SOCKET socketArray[MAX_NUM_SOCKET];
bool InitSocket()
{
WSAData wsa;
WSAStartup(MAKEWORD(2,2),&wsa);
sListen=socket(AF_INET,SOCK_STREAM,0);
if(sListen==INVALID_SOCKET)
{
return false;
}
WSAEVENT hEvent=WSACreateEvent();
eventArray[totalEvent]=hEvent;
//可用事件加一。
totalEvent++;
int ret=WSAEventSelect(sListen,hEvent,FD_CLOSE|FD_ACCEPT);//監聽套接字只能收到這兩種訊息。
if(!ret)
{
return false;
}
sockaddr_in addr;
addr.sin_addr.S_un=inet_addr("192.168.1.100");
addr.sin_family=AF_INET;
addr.sin_port=htons(4000);
ret=bind(sListen,(SOCKADDR*)&addr,sizeof(addr));
if(ret==SOCKET_ERROR)
{
return false;
}
ret=listen(sListen,10);
if(SOCKET_ERROR==ret)
{
return false;
}
return true;
}
int main(int argc,char**argv)
{
InitSocket();
while(true)
{
//有一個事件被觸發等待函式即返回。
int dwIndex=WSAWaitForMultipleEvents(totalEvent,eventArray,false,WSA_INFINITE,false);
if(dwIndex==WSA_WAIT_FAILED)
{
break;
}
else
{
//有網路事件發生。
WSANETWORKEVENTS wsanetwork;
SOCKET s=socketArray[dwIndex-WSA_WAIT_EVENT_0];
//傳hEventObject為被觸發的套接字,WSAEnumNetworkEvents函式,會將其設定為非觸發態。無需手工設定。
int ret=WSAEnumNetworkEvents(s,eventArray[dwIndex-WSA_WAIT_EVENT_0],&wsanetwork);
if(ret==SOCKET_ERROR)//函式呼叫失敗。
{
break;
}
//發生FD_ACCEPT網路事件。
else if(wsanetwork.lNetworkEvents&FD_ACCEPT)
{
if(wsanetwork.iErrorCode[FD_ACCEPT_BIT]!=0)//發生網路錯誤。
{
break;
}
else //接受連線請求。
{
SOCKET sAccept;
if((sAccept=accept(socketArray[dwIndex-WSA_WAIT_EVENT_0],NULL,NULL))==INVALID_SOCKET)
{
break;
}
//超過最大值。
if(totalEvent>WSA_MAXIMUM_WAIT_EVENTS)
{
closesocket(sAccept);
break;
}
//將新接受的套接字加入套接字陣列。
socketArray[totalEvent]=sAccept;
//建立套接字事件物件。
if((eventArray[totalEvent]=WSACreateEvent())==WSA_INVALID_EVENT)
{
break;
}
//為新接受的套接字重新註冊網路事件,重新關聯事件物件。不使用與監聽套接字同樣的屬性,這點要注意!!!!。
if(WSAEventSelect(sAccept,eventArray[totalEvent],FD_READ|FD_WRITE|FD_CLOSE)==SOCKET_ERROR)//接受的套接字,用於收發資料。
{
break;
}
totalEvent++;//總數加一。
//將套接字加入連結串列。
}
}
//發生FD_CLOSE網路事件。
else if(wsanetwork.lNetworkEvents&FD_CLOSE)
{
if(wsanetwork.iErrorCode[FD_CLOSE_BIT]!=0)//發生網路錯誤。
{
break;
}
else //連線關閉。
{
//刪除連結串列中的該套接字。
//關閉網路事件物件。
WSACloseEvent(eventArray[dwIndex-WSA_WAIT_EVENT_0]);
//將此套節字和事件物件從陣列中清除。
for(int i=dwIndex-WSA_WAIT_EVENT_0;i<totalEvent-1;i++)
{
eventArray[i]=eventArray[i+1];
socketArray[i]=socketArray[i+1];
}
totalEvent--;//總數減一。
}
}
//發生FD_READ網路事件。
else if(wsanetwork.lNetworkEvents&FD_READ)
{
if(wsanetwork.iErrorCode[FD_READ_BIT]!=0)//發生網路錯誤。
{
break;
}
else //套接字可讀。
{
//接收資料。
}
}
//發生FD_WRITE網路事件。
else if(wsanetwork.lNetworkEvents&FD_WRITE)
{
if(wsanetwork.iErrorCode[FD_WRITE_BIT]!=0)//發生網路錯誤。
{
break;
}
else //套接字可寫。
{
//傳送資料。
}
}
}
}
return 0;
}
以上參考自《精通Windows sockets網路開發-基於Visual C++實現》如有紕漏,請不吝指教!
2013.1.7于山西大同