重叠I/O之事件对象通知
重疊I/O,Overlapped I/O。
一、 重疊I/O的優(yōu)點
?
1. 可以運行在支持Winsock2的所有Windows平臺 ,而不像完成端口只是支持NT系統(tǒng)。
2. 比起阻塞、select、WSAAsyncSelect以及WSAEventSelect等模型,重疊I/O使應(yīng)用程序能達到更佳的系統(tǒng)性能。因為它和這4種模型不同的是,使用重疊I/O的應(yīng)用程序通知緩沖區(qū)收發(fā)系統(tǒng)直接使用數(shù)據(jù),也就是說,如果應(yīng)用程序投遞了一個10KB大小的緩沖區(qū)來接收數(shù)據(jù),且數(shù)據(jù)已經(jīng)到達套接字,則該數(shù)據(jù)將直接被拷貝到投遞的緩沖區(qū)。而這4種模型種,數(shù)據(jù)到達并拷貝到單套接字接收緩沖區(qū)中,此時應(yīng)用程序會被告知可以讀入的容量。當應(yīng)用程序調(diào)用接收函數(shù)之后,數(shù)據(jù)才從單套接字緩沖區(qū)拷貝到應(yīng)用程序的緩沖區(qū),差別就體現(xiàn)出來了。
3. 從《windows網(wǎng)絡(luò)編程》中提供的試驗結(jié)果中可以看到,在使用了P4 1.7G Xero處理器(CPU很強啊)以及768MB的回應(yīng)服務(wù)器中,最大可以處理4萬多個SOCKET連接,在處理1萬2千個連接的時候CPU占用率才40% 左右 ―― 非常好的性能,已經(jīng)直逼完成端口了。
二、重疊I/O的基本原理
?
概括一點說,重疊I/O是讓應(yīng)用程序使用重疊數(shù)據(jù)結(jié)構(gòu)(WSAOVERLAPPED),一次投遞一個或多個Winsock I/O請求。針對這些提交的請求,在它們完成之后,應(yīng)用程序會收到通知,于是就可以通過自己另外的代碼來處理這些數(shù)據(jù)了。
?
需要注意的是,有兩個方法可以用來管理重疊I/O請求的完成情況(就是說接到重疊操作完成的通知):
?
1. 事件對象通知(Event Object Notification) 。
2. 完成例程(Completion Routines)(注意,這里并不是完成端口)
?
本文只是講述如何來使用事件通知的的方法實現(xiàn)重疊I/O,如沒有特殊說明,本文的重疊I/O默認就是指基于事件通知的重疊I/O。
?
既然是基于事件通知,就要求將Windows事件對象與WSAOVERLAPPED結(jié)構(gòu)關(guān)聯(lián)在一起(WSAOVERLAPPED結(jié)構(gòu)中專門有對應(yīng)的參數(shù)),既然要使用重疊結(jié)構(gòu),我們常用的send, sendto, recv, recvfrom也都要被WSASend,WSASendto, WSARecv, WSARecvFrom替換掉了,它們的用法我后面會講到,這里只需要注意一點,它們的參數(shù)中都有一個Overlapped參數(shù),我們可以假設(shè)是把我們的WSARecv這樣的操作操作“綁定”到這個重疊結(jié)構(gòu)上,提交一個請求,其他的事情就交給重疊結(jié)構(gòu)去操心,而其中重疊結(jié)構(gòu)又要與Windows的事件對象“綁定”在一起,這樣我們調(diào)用完WSARecv以后就可以“坐享其成”,等到重疊操作完成以后,自然會有與之對應(yīng)的事件來通知我們操作完成,然后我們就可以來根據(jù)重疊操作的結(jié)果取得我們想要德數(shù)據(jù)了。
?
三、關(guān)于重疊I/O的基礎(chǔ)知識
?
下面來介紹并舉例說明一下編寫重疊I/O的程序中將會使用到的幾個關(guān)鍵數(shù)據(jù)結(jié)構(gòu)和函數(shù)。
?
1. WSAOVERLAPPED結(jié)構(gòu)
?
這個結(jié)構(gòu)自然是重疊I/O里的核心,它是這么定義的:
?
typedef struct _WSAOVERLAPPED
{
DWORD Internal;
DWORD InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
WSAEVENT hEvent; //?唯一需要關(guān)注的參數(shù),用來關(guān)聯(lián)WSAEvent對象
} WSAOVERLAPPED, *LPWSAOVERLAPPED;
我們需要把WSARecv等操作投遞到一個重疊結(jié)構(gòu)上,而我們又需要一個與重疊結(jié)構(gòu)“綁定”在一起的事件對象來通知我們操作的完成,看到了和hEvent參數(shù),不用我說你們也該知道如何來來把事件對象綁定到重疊結(jié)構(gòu)上吧?大致如下:
WSAEVENT event; // 定義事件
WSAOVERLAPPED AcceptOverlapped ; // 定義重疊結(jié)構(gòu)
event = WSACreateEvent(); // 建立一個事件對象句柄
ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED)); // 初始化重疊結(jié)構(gòu)
AcceptOverlapped.hEvent = event;
?
2. WSARecv系列函數(shù)
?
在重疊I/O中,接收數(shù)據(jù)就要靠它了,它的參數(shù)也比recv要多,因為要用刀重疊結(jié)構(gòu)嘛,它是這樣定義的:
?
int WSARecv
(
SOCKET s, //?當然是投遞這個操作的套接字
LPWSABUF lpBuffers, //?接收緩沖區(qū),與Recv函數(shù)不同這里需要一個由WSABUF結(jié)構(gòu)構(gòu)成的數(shù)組
DWORD dwBufferCount, // 數(shù)組中WSABUF結(jié)構(gòu)的數(shù)量
LPDWORD lpNumberOfBytesRecvd, // 如果接收操作立即完成,這里會返回所接收到的字節(jié)數(shù)
LPDWORD lpFlags, // 說來話長了,我們這里設(shè)置為0 即可
LPWSAOVERLAPPED lpOverlapped, // “綁定”的重疊結(jié)構(gòu)
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine // 完成例程中將會用到的參數(shù),我們這里設(shè)置為 NULL
);
返回值:
WSA_IO_PENDING?: 最常見的返回值,這是說明我們的WSARecv操作成功了。
?
舉個例子:(變量的定義順序和上面的說明的順序是對應(yīng)的,下同)
SOCKET s;
WSABUF DataBuf; // 定義WSABUF結(jié)構(gòu)的緩沖區(qū)
// 初始化一下DataBuf
#define DATA_BUFSIZE 5096
char buffer[DATA_BUFSIZE];
ZeroMemory(buffer, DATA_BUFSIZE);
DataBuf.len = DATA_BUFSIZE;
DataBuf.buf = buffer;
DWORD dwBufferCount = 1, dwRecvBytes = 0, Flags = 0;
// 重疊結(jié)構(gòu),如果要處理多個操作,這里當然需要一個WSAOVERLAPPED數(shù)組。
WSAOVERLAPPED AcceptOverlapped ;
//?事件,如果要多個事件,這里當然也需要一個WSAEVENT數(shù)組。需要注意的是,可能一個SOCKET同時會有一個以上的重疊請求,也就會對應(yīng)一個以上的WSAEVENT。
WSAEVENT event;?
Event = WSACreateEvent();
ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED));
AcceptOverlapped.hEvent = event; // 關(guān)鍵的一步,把事件句柄“綁定”到重疊結(jié)構(gòu)上。
作了這么多工作,終于可以使用WSARecv來把我們的請求投遞到重疊結(jié)構(gòu)上了,呼…
WSARecv(s, &DataBuf, dwBufferCount, &dwRecvBytes, &Flags, &AcceptOverlapped, NULL);
其他的函數(shù)我這里就不一一介紹了,因為我們畢竟還有MSDN這么個好幫手,而且在講后面的完成例程和完成端口的時候我還會講到一些 ^_^
?
3. WSAWaitForMultipleEvents函數(shù)
?
熟悉WSAEventSelect模型的朋友對這個函數(shù)肯定不會陌生,不對,其實大家都不應(yīng)該陌生,這個函數(shù)與線程中常用的WaitForMultipleObjects函數(shù)有些地方還是比較像的,因為都是在等待某個事件的觸發(fā)嘛。
?
因為我們需要事件來通知我們重疊操作的完成,所以自然需要這個等待事件的函數(shù)與之配套。
?
DWORD WSAWaitForMultipleEvents(
DWORD cEvents, // 等候事件的總數(shù)量
const WSAEVENT* lphEvents, // 事件數(shù)組的指針
BOOL fWaitAll, // 這個要多說兩句:如果設(shè)置為 TRUE,則事件數(shù)組中所有事件被傳信的時候函數(shù)才會返回;FALSE則任何一個事件被傳信函數(shù)都要返回。我們這里肯定是要設(shè)置為FALSE的。
DWORD dwTimeout, // 超時時間,如果超時,函數(shù)會返回。
WSA_WAIT_TIMEOUT? // 如果設(shè)置為0,函數(shù)會立即返回;如果設(shè)置為 WSA_INFINITE只有在某一個事件被傳信后才會返回。在這里不建議設(shè)置為WSA_INFINITE,因為…后面再講吧。
?
BOOL fAlertable // 在完成例程中會用到這個參數(shù),這里我們先設(shè)置為FALSE。
);
返回值:
WSA_WAIT_TIMEOUT :最常見的返回值,我們需要做的就是繼續(xù)等待。
WSA_WAIT_FAILED : 出現(xiàn)了錯誤,請檢查cEvents和lphEvents兩個參數(shù)是否有效如果事件數(shù)組中有某一個事件被傳信了,函數(shù)會返回這個事件的索引值,但是這個索引值需要減去預(yù)定義值 WSA_WAIT_EVENT_0才是這個事件在事件數(shù)組中的位置。具體的例子就先不在這里舉了,后面還會講到。
注意:WSAWaitForMultipleEvents函數(shù)只能支持由WSA_MAXIMUM_WAIT_EVENTS對象定義的一個最大值,是 64,就是說WSAWaitForMultipleEvents只能等待64個事件,如果想同時等待多于64個事件,就要創(chuàng)建額外的工作者線程,就不得不去管理一個線程池,這一點就不如下一篇要講到的完成例程模型了。
?
4. WSAGetOverlappedResult函數(shù)
?
既然我們可以通過WSAWaitForMultipleEvents函數(shù)來得到重疊操作完成的通知,那么我們自然也需要一個函數(shù)來查詢一下重疊操作的結(jié)果,定義如下:
?
BOOL WSAGetOverlappedResult(
SOCKET s, // 套接字,不用說了
LPWSAOVERLAPPED lpOverlapped, // 要查詢的那個重疊結(jié)構(gòu)的指針
LPDWORD lpcbTransfer, // 本次重疊操作的實際接收(或發(fā)送)的字節(jié)數(shù)
… //其他參數(shù)暫略,以后補充。
);
如果WSAGetOverlappedResult完成以后,第三個參數(shù)返回是 0 ,則說明通信對方已經(jīng)關(guān)閉連接,我們這邊的SOCKET, Event之類的也就可以關(guān)閉了。
?
四、實現(xiàn)重疊I/O的步驟
?
下面我們配合代碼,來一步步的講解如何親手完成一個重疊I/O。
?
第一步:定義變量
?
#define DATA_BUFSIZE 4096 // 接收緩沖區(qū)大小
SOCKET ListenSocket, // 監(jiān)聽套接字
SOCKET AcceptSocket; // 與客戶端通信的套接字
WSAOVERLAPPED AcceptOverlapped; // 重疊結(jié)構(gòu)一個
WSAEVENT EventArray[WSA_MAXIMUM_WAIT_EVENTS]; // 用來通知重疊操作完成的事件句柄數(shù)組
WSABUF DataBuf[DATA_BUFSIZE] ;?
DWORD dwEventTotal = 0, // 程序中事件的總數(shù)
dwRecvBytes = 0, // 接收到的字符長度
Flags = 0; // WSARecv的參數(shù)
?
?
第二步:創(chuàng)建一個套接字,開始在指定的端口上監(jiān)聽連接請求。
?
和其他SOCKET初始化一樣,直接照搬即可。
?
WSADATA wsaData;
WSAStartup(MAKEWORD(2,2),&wsaData);
?
ListenSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); //創(chuàng)建TCP套接字
SOCKADDR_IN ServerAddr; //分配端口及協(xié)議族并綁定
ServerAddr.sin_family=AF_INET;?
ServerAddr.sin_addr.S_un.S_addr =htonl(INADDR_ANY);?
ServerAddr.sin_port=htons(11111);
bind(ListenSocket,(LPSOCKADDR)&ServerAddr, sizeof(ServerAddr)); // 綁定套接字
listen(ListenSocket, 5); //開始監(jiān)聽
?
第三步:接受一個入站的連接請求
?
AcceptSocket = accept (ListenSocket, NULL,NULL) ;?
當然,這里是偷懶,如果想要獲得連入客戶端的信息(記得論壇上也常有人問到),accept的后兩個參數(shù)就不要用NULL,而是這樣:
?
SOCKADDR_IN ClientAddr; // 定義一個客戶端得地址結(jié)構(gòu)作為參數(shù)
int addr_length=sizeof(ClientAddr);
AcceptSocket = accept(ListenSocket,(SOCKADDR*)&ClientAddr, &addr_length);
// 于是乎,我們就可以輕松得知連入客戶端的信息了
LPCTSTR lpIP = inet_ntoa(ClientAddr.sin_addr); // IP
UINT nPort = ClientAddr.sin_port; // Port
?
第四步:建立并初始化重疊結(jié)構(gòu)
?
為連入的這個套接字新建立一個WSAOVERLAPPED重疊結(jié)構(gòu),并且象前面講到的那樣,為這個重疊結(jié)構(gòu)從事件句柄數(shù)組里挑出一個空閑的對象句柄“綁定”上去。
?
// 創(chuàng)建一個事件,dwEventTotal可以暫時先作為Event數(shù)組的索引。
EventArray[dwEventTotal] = WSACreateEvent();?
?
ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED)); // 置零
AcceptOverlapped.hEvent = EventArray[dwEventTotal]; // 關(guān)聯(lián)事件
?
char buffer[DATA_BUFSIZE];
ZeroMemory(buffer, DATA_BUFSIZE);
DataBuf.len = DATA_BUFSIZE;
DataBuf.buf = buffer; // 初始化一個WSABUF結(jié)構(gòu)
dwEventTotal ++; // 總數(shù)加一
?
第五步:以WSAOVERLAPPED結(jié)構(gòu)為參數(shù),在套接字上投遞WSARecv請求。
?
各個變量都已經(jīng)初始化OK以后,我們就可以開始Socket操作了,然后讓WSAOVERLAPPED結(jié)構(gòu)來替我們管理I/O 請求,我們只須等待事件的觸發(fā)就可以了。
?
if(SOCKET_ERROR==WSARecv(AcceptSocket ,&DataBuf,1,&dwRecvBytes,&Flags,&AcceptOverlapped, NULL))
{?
// 返回WSA_IO_PENDING是正常情況,表示IO操作正在進行,不能立即完成。如果不是WSA_IO_PENDING錯誤,就大事不好了!
if(WSAGetLastError() != WSA_IO_PENDING)?
{
// 那就只能關(guān)閉大吉了
closesocket(AcceptSocket);
WSACloseEvent(EventArray[dwEventTotal]);
}
}
?
第六步:用WSAWaitForMultipleEvents函數(shù)等待重疊操作返回的結(jié)果
?
因為前面已經(jīng)把事件和Overlapped綁定在一起,重疊操作完成后我們會接到事件通知。
DWORD dwIndex;
//?等候重疊I/O調(diào)用結(jié)束
dwIndex=WSAWaitForMultipleEvents(dwEventTotal,EventArray ,FALSE ,WSA_INFINITE,FALSE);
// 注意這里返回的Index并非是事件在數(shù)組里的Index,而是需要減去WSA_WAIT_EVENT_0
dwIndex = dwIndex – WSA_WAIT_EVENT_0;
?
第七步:使用WSAResetEvent函數(shù)重設(shè)當前這個用完的事件對象
?
事件已經(jīng)被觸發(fā)了之后,它對于我們來說已經(jīng)沒有利用價值了,所以要將它重置一下留待下一次使用,很簡單,就一步,連返回值都不用考慮。
WSAResetEvent(EventArray[dwIndex]);
?
第八步:使用WSAGetOverlappedResult函數(shù)取得重疊調(diào)用的返回狀態(tài)
?
這是我們最關(guān)心的事情,費了那么大勁投遞的這個重疊操作究竟是個什么結(jié)果呢?其實對于本模型來說,唯一需要檢查一下的就是對方的Socket連接是否已經(jīng)關(guān)閉了。
?
DWORD dwBytesTransferred;
WSAGetOverlappedResult( AcceptSocket,AcceptOverlapped,&dwBytesTransferred, FALSE, &Flags);
// 先檢查通信對方是否已經(jīng)關(guān)閉連接,如果dwBytesTransferred等于0則表示連接已經(jīng)關(guān)閉,就關(guān)閉套接字。
if(dwBytesTransferred == 0)
{
closesocket(AcceptSocket);
WSACloseEvent(EventArray[dwIndex]); // 關(guān)閉事件
return;
}
?
?
第九步:“享受”接收到的數(shù)據(jù)
?
如果程序執(zhí)行到了這里,那么就說明一切正常,WSABUF結(jié)構(gòu)里面就存有我們WSARecv來的數(shù)據(jù)了,終于到了盡情享用成果的時候了!喝杯茶,休息一下吧~~~^_^
?
DataBuf.buf就是一個char*字符串指針,聽憑你的處理吧,這里就不多說了。
?
?
第十步:同第五步一樣,在套接字上繼續(xù)投遞WSARecv請求,然后重復(fù)步驟 6 ~ 9。
?
這樣一路下來,我們終于可以從客戶端接收到數(shù)據(jù)了,但是回想起來,呀~~~~~,這豈不是只能收到一次數(shù)據(jù),然后程序不就Over了?所以我們接下來不得不重復(fù)一遍第五步到第九步的工作,再次在這個套接字上投遞另一個WSARecv請求,并且使整個過程循環(huán)起來,are u clear?
?
五、多客戶端情況的注意事項
?
完成了上面的循環(huán)以后,重疊I/O就已經(jīng)基本上搭建好了80%了,為什么不是100%呢?因為仔細一回想起來,呀~~~~~~~,這樣豈不是只能連接一個客戶端?是的,如果只處理一個客戶端,那重疊I/O就半點優(yōu)勢也沒有了,我們正是要使用重疊I/O來處理多個客戶端。
?
所以我們不得不再對結(jié)構(gòu)作一些改動。
?
首先需要一個SOCKET數(shù)組 ,分別用來和每一個SOCKET通信。其次,因為重疊I/O中每一個SOCKET操作都是要“綁定”一個重疊結(jié)構(gòu)的,所以需要為每一個SOCKET操作搭配一個WSAOVERLAPPED結(jié)構(gòu),但是這樣說并不嚴格,因為如果每一個SOCKET同時只有一個操作,比如WSARecv,那么一個SOCKET就可以對應(yīng)一個WSAOVERLAPPED結(jié)構(gòu),但是如果一個SOCKET上會有WSARecv 和WSASend兩個操作,那么一個SOCKET肯定就要對應(yīng)兩個WSAOVERLAPPED結(jié)構(gòu),所以有多少個SOCKET操作就會有多少個WSAOVERLAPPED結(jié)構(gòu)。
?
然后,同樣是為每一個WSAOVERLAPPED結(jié)構(gòu)都要搭配一個WSAEVENT事件,所以說有多少個SOCKET操作就應(yīng)該有多少個WSAOVERLAPPED結(jié)構(gòu),有多少個WSAOVERLAPPED結(jié)構(gòu)就應(yīng)該有多少個WSAEVENT事件,最好把SOCKET、WSAOVERLAPPED 、WSAEVENT三者的關(guān)聯(lián)起來,到了關(guān)鍵時刻才會臨危不亂。
?
須要開兩個線程。一個線程用來循環(huán)監(jiān)聽端口,接收請求的連接,然后給在這個套接字上配合一個WSAOVERLAPPED結(jié)構(gòu)投遞第一個WSARecv請求。另一個個線程用來不停的對WSAEVENT數(shù)組WSAWaitForMultipleEvents,等待任何一個重疊操作的完成,然后根據(jù)返回的索引值進行處理,處理完畢以后再繼續(xù)投遞下一個WSARecv請求。
?
這里需要注意一點的是,前面是把WSAWaitForMultipleEvents函數(shù)的參數(shù)設(shè)置為WSA_INFINITE的,但是在多客戶端的時候這樣就不好了,需要設(shè)定一個超時時間,如果等待超時了再重新WSAWaitForMultipleEvents。因為WSAWaitForMultipleEvents函數(shù)在沒有觸發(fā)的時候是阻塞在那里的,我們可以設(shè)想一下,這時如果監(jiān)聽線程中接入了新的連接,自然也會為這個連接增加一個Event,但是WSAWaitForMultipleEvents還是阻塞在那里就不會處理這個新連接的Event了。
?
總結(jié)
以上是生活随笔為你收集整理的重叠I/O之事件对象通知的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: UDP丢包的原因
- 下一篇: Windows Sockets 2.0