1.Winsock同步阻塞方式的問題
在異步非阻塞模式下,像accept(WSAAccept),recv(recv,WSARecv,WSARecvFrom)等這樣的winsock函數調用後馬上返回,而不是等待可用的連接和數據。在阻塞模式下,server往往這樣等待client的連接:
while(TRUE)
{
//wait for a connection
ClientSocket = accept(ListenSocket,NULL,NULL);
if(ClientSocket == INVALID_SOCKET)
{
ERRORHANDLE
}
else
DoSomething
}
上述代碼簡單易用,但是缺點在於如果沒有client連接的話,accept一直不會返回,而且即使accept成功創建會話套接字,在阻塞方式下,C/S間傳輸數據依然要將recv,send這類函數放到一個循環中,反覆等待數據的到來,這種輪詢的方式效率很低。爲此,Winsock提供了異步模式的5種I/O模型,這些模型會在有網絡事件(如socket收到連接請求,讀取收到的數據請求等等)時通過監視集合(select),事件對象(WSAEventSelect,重疊I/O),窗口消息(WSAAsyncSelect),回調函數(重疊I/O),完成端口的方式通知程序,告訴我們可以“幹活了”,這樣的話大大的提高了執行效率,程序只需枕戈待旦,兵來將擋水來土掩,通知我們來什麼網絡事件,就做相應的處理即可。
2.WSAEventSelect模型的使用
WSAEventSelect模型其實很簡單,就是將一個事件對象同一個socket綁定並設置要監視的網絡事件,當這個socket有我們感興趣的網絡事件到達時,ws2_32.dll就將這個事件對象置爲受信狀態(signaled),在程序中等待這個事件對象受信後,根據網絡事件類型做不同的處理。如果對線程同步機制有些瞭解的話,這個模型很容易理解,其實就是CreateEvent系列的winsock版。
無代碼無真相,具體API的參數含義可以參考MSDN,MSDN上對這個模型解釋的非常詳盡。
// 使用WSAEventSelect的代碼片段,百度貼吧字數限制,略去錯誤處理及界面操作
// 爲了能和多個客戶端通信,使用兩個數組分別記錄所有通信的會話套接字
// 以及和這些套接字綁定的事件對象
// WSA_MAXIMUM_WAIT_EVENTS是系統內部定義的宏,值爲64
SOCKET g_sockArray[WSA_MAXIMUM_WAIT_EVENTS];
WSAEVENT g_eventArray[WSA_MAXIMUM_WAIT_EVENTS];
// 事件對象計數器
int nEventTotal = 0;
// 創建監聽套接字sListenSocket,並對其綁定端口和本機ip 代碼省去
........
// 設置sListenSocket爲監聽狀態
listen(sListenSocket, 5);
// 創建事件對象,同CreateEvent一樣,event創建後被置爲非受信狀態
WSAEVENT acceptEvent = WSACreateEvent();
// 將sListenSocket和acceptEvent關聯起來
// 並註冊程序感興趣的網絡事件FD_ACCEPT 和 FD_CLOSE
// 這裏由於是在等待客戶端connect,所以FD_ACCEPT和FD_CLOSE是我們關心的
WSAEventSelect(sListenSocket, acceptEvent, FD_ACCEPT|FD_CLOSE);
// 添加到數組中
g_eventArray[nEventTotal] = acceptEvent;
g_sockArray[nEventTotal] = sListenSocket;
nEventTotal++;
// 處理網絡事件
while(TRUE)
{
// 由於第三個參數是 FALSE,所以 g_eventArray 數組中有一個元素受信 WSAWaitForMultipleEvents 就返回
// 注意 返回值 nIndex 減去 WSA_WAIT_EVENT_0 的值纔是受信事件在數組中的索引。
// 如果有多個事件同時受信,函數返回索引值最小的那個。
// 由於第四個參數指定 WSA_INFINITE ,所以沒有對象受信時會無限等待。
int nIndex = WSAWaitForMultipleEvents(nEventTotal, g_eventArray, FALSE, WSA_INFINITE, FALSE);
// 取得受信事件在數組中的位置。
nIndex = nIndex - WSA_WAIT_EVENT_0;
// 判斷受信事件 g_eventArray[nIndex] 所關聯的套接字 g_sockArray[nIndex] 的網絡事件類型
// MSDN中說如果事件對象不是NULL, WSAEnumNetworkEvents 會幫咱重置該事件對象爲非受信,方便等待新的網絡事件
// 也就是說這裏的 g_eventArray[nIndex] 變爲非受信了,所以程序中不用再調用 WSAResetEvent了
// WSANETWORKEVENTS 這個結構中 記錄了關於g_sockArray[nIndex] 的網絡事件和錯誤碼
WSANETWORKEVENTS event;
WSAEnumNetworkEvents(g_sockArray[nIndex], g_eventArray[nIndex], &event);
// 這裏處理 FD_ACCEPT 這個網絡事件
// event.lNetWorkEvents中記錄的是網絡事件類型
if(event.lNetworkEvents & FD_ACCEPT)
{
// event.iErrorCode是錯誤代碼數組,event.iErrorCode[FD_ACCEPT_BIT] 爲0表示正常
if(event.iErrorCode[FD_ACCEPT_BIT] == 0)
{
// 連接數超過系統約定的範圍
if(nEventTotal > WSA_MAXIMUM_WAIT_EVENTS)
{
ErrorHandle...
continue;
}
// 沒有問題就可以accept了
SOCKET sAcceptSocket = accept(g_sockArray[nIndex], NULL, NULL);
// 新建的會話套接字用於C/S間的數據傳輸,所以這裏關心FD_READ,FD_CLOSE,FD_WRITE三個事件
WSAEVENT event = WSACreateEvent();
WSAEventSelect(sAcceptSocket, event, FD_READ|FD_CLOSE|FD_WRITE);
// 將新建的會話套接字及與該套接字關聯的事件對象添加到數組中
g_eventArray[nEventTotal] = event;
g_sockArray[nEventTotal] = sAcceptSocket;
nEventTotal++;
}
//event.iErrorCode[FD_ACCEPT_BIT] != 0 出錯了
else
{
ErrorHandle...
break;
}
}
// 這裏處理FD_READ通知消息,當會話套接字上有數據到來時,ws2_32.dll會記錄該事件
else if(event.lNetworkEvents & FD_READ)
{
if(event.iErrorCode[FD_READ_BIT] == 0)
{
int nRecv = recv(g_sockArray[nIndex], buffer, nbuffersize, 0);
if(nRecv == SOCKET_ERROR)
{
// 爲了程序更魯棒,這裏要特別處理一下WSAEWOULDBLOCK這個錯誤
// MSDN中說在異步模式下有時recv(WSARecv)讀取時winsock的緩衝區中沒有數據,導致recv立即返回
// 錯誤碼就是 WSAEWOULDBLOCK,但這時程序並沒有出問題,在有新的數據到來時recv還是可以讀到數據的
// 所以不能僅僅根據recv返回值是SOCKET_ERROR就認爲出錯從而執行退出操作。
//如果錯誤碼不是WSAEWOULDBLOCK 則表示真的出錯了
if(WSAGetLastError() != WSAEWOULDBLOCK)
{
ErrorHandle...
break;
}
}
// 沒出任何錯誤
else
DoSomeThing...
}
// event.iErrorCode[FD_READ_BIT] != 0
else
{
ErrorHandle...
break;
}
}
// 這裏處理FD_CLOSE通知消息
// 當連接被關閉時,ws2_32.dll會記錄FD_CLOSE事件
else if(event.lNetworkEvents & FD_CLOSE)
{
if(event.iErrorCode[FD_CLOSE_BIT] == 0)
{
closesocket(g_sockArray[nIndex]);
// 將g_sockArray[nIndex]從g_sockArray數組中刪除
for(int j=nIndex; j<nEventTotal-1; j++)
g_sockArray[j] = g_sockArray[j+1];
nEventTotal--;
}
// event.iErrorCode[FD_CLOSE_BIT] != 0
else
{
ErrorHandle...
break;
}
}
// 處理FD_WRITE通知消息
// FD_WRITE事件其實就是ws2_32.dll告訴我們winsock的緩衝區已經ok,可以發送數據了
// 同recv一樣,send(WSASend)的返回值也要對SOCKET_ERROR特殊判斷一下 WSAEWOULDBLOCK
else if(event.lNetworkEvents & FD_WRITE)
{
//關於FD_WRITE的討論在下面。
}
}
// 如果出錯退出循環 則將套接字數組中的套接字與事件對象統統解除關聯
// 給WSAEventSelect的最後一個參數傳0可以解除g_sockArray[nIndex]和g_eventArray[nIndex]的關聯
// 解除關聯後,ws2_32.dll將停止記錄g_sockArray[nIndex]這個套接字的網絡事件
// 退出時還要關閉所有創建的套接字和事件對象
for(int i = 0; i < nEventTotal; i++)
{
WSAEventSelect(g_sockArray[i], g_eventArray[i], 0);
closesocket(g_sockArray[i]);
WSACloseEvent(g_eventArray[i]);
}
nEventTotal = 0;
DoSomethingElse....
3.FD_WRITE 事件的觸發
常見的網絡事件中,FD_ACCEPT和FD_READ都比較好理解。一開始我唯一困惑的就是FD_WRITE,搞不清楚到底什麼時候纔會觸發這個網絡事件,後來仔細查了MSDN又看了一些文章並測試了下,終於搞懂了FD_WRITE的觸發機制。
下面是MSDN中對FD_WRITE觸發機制的解釋:
The FD_WRITE network event is handled slightly differently. An FD_WRITE network event is recorded when a socket is first connected with connect/WSAConnect or accepted with accept/WSAAccept, and then after a send fails with WSAEWOULDBLOCK and buffer space becomes available. Therefore, an application can assume that sends are possible starting from the first FD_WRITE network event setting and lasting until a send returns WSAEWOULDBLOCK. After such a failure the application will find out that sends are again possible when an FD_WRITE network event is recorded and the associated event object is set
FD_WRITE事件只有在以下三種情況下才會觸發
①client 通過connect(WSAConnect)首次和server建立連接時,在client端會觸發FD_WRITE事件
②server通過accept(WSAAccept)接受client連接請求時,在server端會觸發FD_WRITE事件
③send(WSASend)/sendto(WSASendTo)發送失敗返回WSAEWOULDBLOCK,並且當緩衝區有可用空間時,則會觸發FD_WRITE事件
①②其實是同一種情況,在第一次建立連接時,C/S端都會觸發一個FD_WRITE事件。
主要是③這種情況:send出去的數據其實都先存在winsock的發送緩衝區中,然後才發送出去,如果緩衝區滿了,那麼再調用send(WSASend,sendto,WSASendTo)的話,就會返回一個 WSAEWOULDBLOCK的錯誤碼,接下來隨着發送緩衝區中的數據被髮送出去,緩衝區中出現可用空間時,一個 FD_WRITE 事件纔會被觸發,這裏比較容易混淆的是 FD_WRITE 觸發的前提是 緩衝區要先被充滿然後隨着數據的發送又出現可用空間,而不是緩衝區中有可用空間,也就是說像如下的調用方式可能出現問題
else if(event.lNetworkEvents & FD_WRITE)
{
if(event.iErrorCode[FD_WRITE_BIT] == 0)
{
send(g_sockArray[nIndex], buffer, buffersize);
....
}
else
{
}
}
問題在於建立連接後 FD_WRITE 第一次被觸發, 如果send發送的數據不足以充滿緩衝區,雖然緩衝區中仍有空閒空間,但是 FD_WRITE 不會再被觸發,程序永遠也等不到可以發送的網絡事件。
基於以上原因,在收到FD_WRITE事件時,程序就用循環或線程不停的send數據,直至send返回WSAEWOULDBLOCK,表明緩衝區已滿,再退出循環或線程。當緩衝區中又有新的空閒空間時,FD_WRITE 事件又被觸發,程序被通知後又可發送數據了。
上面代碼片段中省略的對 FD_WRITE 事件處理
else if(event.lNetworkEvents & FD_WRITE)
{
if(event.iErrorCode[FD_WRITE_BIT] == 0)
{
while(TRUE)
{
// 得到要發送的buffer,可以是用戶的輸入,從文件中讀取等
GetBuffer....
if(send(g_sockArray[nIndex], buffer, buffersize, 0) == SOCKET_ERROR)
{
// 發送緩衝區已滿
if(WSAGetLastError() == WSAEWOULDBLOCK)
break;
else
ErrorHandle...
}
}
}
else
{
ErrorHandle..
break;
}
}
補充:
1.WSAWaitForMultipleEvents內部調用的還是WaitForMulipleObjectsEx,MSDN中說使用WSAEventSelect模型等待時是不佔cpu時間的,這也是效率比阻塞winsock高的原因。
2.WSAAsycSelect的用法和WSAEventSelect類似,不同的是網絡事件的通知是以windows消息的方式發送到指定的窗口。
來自:http://oliver258.blog.51cto.com/750330/423813