轉載自:http://blog.csdn.net/ithzhang/article/details/8496232
Windows socket重疊IO模型開發
利用套接字重疊IO模型,應用程序能一次投遞一個或多個IO請求,當系統完成IO操作後通知應用程序。該模型以win32異步IO機制爲基礎。與前面介紹的所有IO模型相比較,該模型是真正意義上的異步IO模型,它能使Windows socket應用程序達到更高的性能。
關於異步IO機制可以參考:《Windows核心編程系列》十談談同步設備IO與異步設備IO之異步設備IO。
Windows socket重疊IO延續了win32 IO模型。從發送和接收的角度來看,重疊IO模型與前面介紹的Select模型、WSAAsyncSelect模型和WSAEventSelect模型都不同。因爲在這三個模型中IO操作還是同步的,例如:在應用程序調用recv函數時,都會在recv函數內阻塞,直到接收數據完畢後才返回。而重疊IO模型會在調用recv後立即返回。等數據準備好後再通知應用程序。
系統嚮應用程序發送通知的形式有兩種:一是事件通知。二是完成例程。後面將會介紹這兩種形式。
注意:套接字的重疊IO屬性不會對套接字的當前工作模式產生影響。創建具有重疊屬性的套接字執行重疊IO操作,並不會改變套接字的阻塞模式。套接字的阻塞模式與重疊IO操作不相關。重疊IO模型僅僅對WSASend和WSARecv的行爲有影響。 對listen,如果是阻塞模式,直到有客戶請求到達時纔會返回。這點要特別注意。
與其他模型的區別
在Windows socket中,接收數據的過程可以分爲:等待數據和將數據從系統複製到用戶空間兩個階段。各種IO模型的區別在於這兩個階段上。
前三個模型的第一個階段的區別: select模型利用select函數主動檢查系統中套接字是否滿足可讀條件。WSAAsyncSelect模型和WSAEventSelect模型則被動等待系統的通知。
第二個階段的區別:此階段前三個模型基本相同,在將數據從系統複製到用戶緩衝區時,線程阻塞。而重疊IO與它們都不同,應用程序在調用輸入函數後,只需等待接收系統完成的IO操作完成通知。
套接字重疊IO模型主要有一下相關函數:
WSASocket() //創建套接字。
WSASend 和 WSASendTo //發送數據。
WSARecv 和 WSARecvFrom //接收數據。
WSAIoctl //控制套接字模式。
AcceptEx //接受連接。
創建套接字
要在套接字上使用重疊IO模型,在創建套接字時,必須使用WSA_FLAG_OVERLAPPED標誌創建。
SOCKET s = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
當使用socket函數創建套接字時,會默認設置WSA_FLAG_OVERLAPPED標誌。
接收數據
應用程序調用WSARecv和WSARecvFrom函數接收數據。
WSARecv函數 介紹如下:
int WSARecv(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd,
LPDWORD lpFlags,
LPWSWOVERLAPPED lpOverlapped,
LPWSOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUNTINE);
- s :爲接收套接字。
- lpBuffers :接受緩衝區。
- dwBufferCount :爲接收緩衝區數組元素的個數。如果就一個緩衝區,就指定爲1.
- lpNumberOfBytesRecvd :如果接收操作立即完成,該參數指明接收數據的字節數。
- lpFlags :標誌位,一般爲0.
- lpOverlapped :指向WSAOVERLAPPED結構指針。
- lpCompletionROUTINE :完成例程。
該函數可以使用一個或多個緩衝區接收數據。如果接收操作未能立即完成。應用程序利用完成例程或是事件對象獲得通知。
如果操作立即完成,該函數返回值爲0。lpNumberOfBytesRecvd參數指明接收數據的字節數。如果未能立即完成,函數返回SOCKET_ERROR值。WSAGetLastError返回WSA_IO_PENDING。
如果lpOverlapped和lpCompletionROUTINE都爲NULL,則該套接字作爲同步IO套接字使用。
如果lpCompletionRoutine參數爲NULL,當接收完成時,lpOverlapped參數的事件變爲觸發狀態。在應用程序中可以調用WSAWaitForMultipleEvents或者是WSAGetOverlappedResult等待該事件。
重疊IO採用事件或者是完成例程通知程序異步操作已完成。這可以通過調用WSASend或WSARecv來實現。之所以前面首先介紹WSARecv是因爲:我們並不知道客戶發送的數據何時到來。當在有客戶請求進來時,我們就調用了WSARecv函數,並採用重疊IO模型,並且在收到數據後,再次調用WSARecv,這樣在任何時刻只要此客戶的數據到來我們就會收到通知。如果應用程序調用WSARecv的時間先於數據到達的時間,則數據到達時會直接放入用戶的接收緩衝區。這就避免了從系統緩衝區向用戶緩衝區的複製操作。對於提高性能很有幫助。
WSBUF定義如下:
typedef struct _WSABUF
{
u_long len; //緩衝區長度。
har *buf; //緩衝區。
} WSABUF, *LPWSABUF;
WSAOVERLAPPED結構。
WSARecv函數使用WSAOVERLAPPED結構作爲參數,將事件對象和重疊IO關聯在一起。該結構聲明如下:
typdef struct _WSAOVERLAPPED
{
DWROD Internal; //錯誤代碼。
DWORD InternalHigh; //已傳輸字節數
DWORD Offset; //低32位文件偏移。
DWORD OffsetHigh; //高32位文件偏移
WSAEVENT hEvent; //事件對象句柄。
} WSAOVERLAPPED, *LPWSAOVERLAPPED;
應用程序可以執行一下步驟將一個事件對象與套接字關聯起來:
- 調用WSACreateEvent創建事件對象。
- 將該事件賦值給WSAOVERLAPPED結構的hEvent字段。
- 使用該重疊結構,調用WSASend或WSARecv函數。
當重疊操作完成時,重疊IO結構的事件對象變爲已觸發狀態。可以調用WSAWaitForMultipleEvents函數等待該事件發生。關於該函數,前面在介紹WSAEventSelect模型時有詳細的介紹,此處不再贅述。注意它最多隻能等待64個事件對象。
WSAGetOverlappedResult函數。
該函數返回在套接字重疊IO的結構:
bool WSAGetOverlappedResult(
SOCKET s,
LPWSAOVERLAPPED lpOverlapped,
LPDWORD lpcbTransfer,
BOOL fWait,
LPDWORD lpdwFlags);
- s爲發起重疊操作的套接字。
- lpOverlapped發起重疊操作的WSAOVERLAPPED結構指針。
- lpcbTransfer:實際發送或接收的字節數。
- fWait:函數返回的方式。如果爲TRUE,該函數直到重疊IO完成時才返回。當爲false時,如果操作仍然處於等待執行狀態,則函數返回false。錯誤代碼爲WSA_IO_INCOMPLELE。
- lpdwFlags:接收完成狀態的附加標誌。
當函數返回TRUE時,重疊IO已經完成,lpOverlapped指明實際返回的數據。當函數返回false時,重疊IO還未完成。
注意:由於微軟已經公佈了WSAOVERLAPPED和OVERLAPPED結構的個字節的意義。Internal表示錯誤代碼。InternalHigh表示已傳輸字節數。因此在調用WSAWaitForMultipleEvents等待事件對象返回後,得到此事件對應的WSAOVERLAPPED結構後,就可以根據這兩個字段判斷異步IO的執行結果。無需再調用GetOverlappedResult結構。具體請參考《Windows核心編程》。
發送數據
應用程序調用WSASend或者WSASendTo發送數據。
WSASend函數聲明如下:
int WSASend(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent,
DWORD dwFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTINE);
- s:爲發送套接字。
- lpBuffer:指向WSABUF結構指針,用於發送數據。
- dwBufferCount : lpBuffers數組中元素數量。
- lpNumberOfBytesSent:如果發送立即完成,該參數指明發送字節數。
- lpFlags:標誌位。
- lpOverlapped : 指向WSAOVERLAPPED結構指針。
- lpCompletionROUTINE:完成例程。
事件通知
套接字重疊IO的事件通知方法要求事件對象與WSAOVERLAPPED結構關聯在一起。當IO操作完成後,該事件對象從未觸發狀態變爲觸發狀態。在應用程序中先調用WSAWaitForMultipleEvents函數等待該事件的發生。獲得該事件對象對應的WSAOVERLAPPED結構後可以根據Internal和InternalHigh字段(也可以調用WSAGetOverlappedResult函數)判斷IO完成的情況。
分爲以下步驟:
- 創建具有WSAOVERLAPPED標誌的套接字。如果調用socket()函數,則默認創建具有WSAOVERLAPPED標誌的套接字。如果調用WSASocket函數,需要指定WSAOVERLAPPED標誌。
- 爲套接字定義WSAOVERLAPPED結構,並清零。
- 調用WSACreateEvent函數創建事件對象,並將該事件句柄分配給WSAOVERLAPPED結構的hEvent字段。
- 調用輸入或者輸出函數。
- 調用WSAWaitForMultipleEvents函數等待與重疊IO關聯在一起的事件變爲已觸發狀態。
- WSAWaitForMultipleEvents返回後,調用WSAResetEvent函數,將該事件對象恢復爲未觸發態。
- 調用WSAGetOverlappedResult函數判斷重疊IO的完成狀態。
下面一個實例演示了使用socket 重疊IO模型開發服務程序的步驟。該程序設計兩個線程,接收線程用於接受客戶端連接請求,初始化重疊IO操作。服務線程用於重疊IO處理。
CClient:該類在前面各模型的介紹中都涉及過。它主要用於管理連接後的客戶端。
管理客戶端鏈表:存儲CClient*,用於管理連接的客戶端。
接受線程:
#include"iostream"
#include"Client.h"
#pragma comment(lib,"WS2_32.lib")
SOCKET sListenSocket;
UINT totalEvent; //事件數組元素個數。
WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS]; //事件對象數組。
bool InitSocket()
{
WSAData wsa;
WSAStartup(MAKEWORD(2,2),&wsa);
//ServSocket = socket(AF_INET,SOCK_STREAM,0);
sListenSocket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
if(sListenSocket == INVALID_SOCKET)
{
return false;
}
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.S_un.S_addr = inet_addr("192.168.1.100");
addr.sin_port = htons(4000);
int ret = bind(sListenSocket, (SOCKADDR*)&addr, sizeof(addr));
if(ret == SOCKET_ERROR)
{
return false;
}
ret = listen(sListenSocket,10);
if(ret == SOCKET_ERROR)
{
return false;
}
return true;
}
DWORD WINAPI AcceptThread(PVOID ppram)
{
SOCKET newSocket;
while(true)
{
newSocket = accept(sListenSocket, NULL, NULL);
if(totalEvent > WSA_MAXIMUM_WAIT_EVENTS)
{
return -1;
}
if(WSA_INVALID_EVENT == ([totalEvent] = WSACreateEvent()))
{
return -1;
}
CClient* pClient = new CClient(newSocket, eventArray[totalEvent]);
//m_list.add(newSocket)//新接受的套接字加入套接字鏈表,用以對客戶端的管理。
totalEvent++;
}
//DeleteAllNode();
closesocket(sListenSocket);
WSACleanup();
return 0;
}
服務線程:
DWORD WINAPI ServiceThread(PVOID pparam)
{
int dwIndex;
while(true)
{
if((dwIndex = WSAWaitForMultipleEvents(totalEvent, eventArray, false, WSA_INFINITE, false)) == WSA_WAIT_FAILED)
{
return -1;
}
WSAEVENT h = eventArray[dwIndex-WSA_WAIT_EVENT_0];
WSAResetEvent(h);
CClient* pClient;
//獲得與該套接字對應的CClient指針。
pClient = GetClient(h);
//判斷IO操作完成情況。
if(pClient->m_ol.InternalHigh == 0)//發生錯誤或客戶端關閉了連接。
{
//將此節點從客戶端鏈表中刪除。
//將此節點對應的事件對象從事件對象數組中刪除。
}
else
{
//1,數據已經到達緩衝區。
//2,處理數據。
//3,投遞接收數據重疊IO請求,確保任何數據到來前都有一個異步接收IO請求。
pClient->RecvData();
}
}
return 0;
}
如有紕漏,請不吝賜教!!!
2013.1.12于山西大同