轉(zhuǎn)自:http://www.vckbase.com/document/viewdoc/?id=1866
英文原文:http://www.codeproject.com/KB/IP/iocp_server_client.aspx
源代碼下載源碼使用了高級(jí)的完成端口(IOCP)技術(shù),該技術(shù)可以有效地服務(wù)于多客戶(hù)端。本文提出了一些IOCP編程中出現(xiàn)的實(shí)際問(wèn)題的解決方法,并提供了一個(gè)簡(jiǎn)單的echo版本的可以傳輸文件的客戶(hù)端/服務(wù)器程序。
程序截圖:
1.1 環(huán)境要求
本文讀者需要熟悉C++、TCP/IP、Socket編程、MFC,和多線(xiàn)程。
源碼使用Winsock 2.0和IOCP技術(shù),要求:
Windows NT/2000或以上:要求Windows NT3.5或以后版本
Windows 95/98/ME:不支持
Visual C++.NET,或完整更新過(guò)的Visual C++ 6.0
1.2 摘要
當(dāng)你開(kāi)發(fā)不同類(lèi)型的軟件,你遲早必須處理C/S的開(kāi)發(fā)。對(duì)一個(gè)程序員來(lái)說(shuō),寫(xiě)一個(gè)通用的C/S編碼是一項(xiàng)困難的工作。本文檔提供了一份簡(jiǎn)單但是功能強(qiáng)大的C/S源碼,可以擴(kuò)展到任何類(lèi)型的C/S應(yīng)用程序中。這份源碼使用了高級(jí)的IOCP技術(shù),該技術(shù)可以高效的服務(wù)于多客戶(hù)端。IOCP提供了解決“每個(gè)客戶(hù)端占用一個(gè)線(xiàn)程”的瓶頸問(wèn)題的辦法,只使用幾個(gè)處理線(xiàn)程,異步輸入/輸出來(lái)發(fā)送/接收。IOCP技術(shù)被廣泛應(yīng)用在各種類(lèi)型的高效服務(wù)端,例如Apache等。這份源碼也提供了一系列的在處理通信和C/S軟件中經(jīng)常使用的功能,如文件接收/傳送功能和邏輯線(xiàn)程池管理。本文重點(diǎn)在于出現(xiàn)在IOCP程序API中實(shí)用的解決方案,以及關(guān)于源碼的全面的文檔。另外,一份簡(jiǎn)單的echo版的可處理多連接和文件傳輸?shù)腃/S程序也在這里提供。
2.1 引言
本文提出了一個(gè)類(lèi),可以用在客戶(hù)端和服務(wù)端。這個(gè)類(lèi)使用IOCP(Input Output Completion Ports)和異步(非阻塞)機(jī)制。…
通過(guò)這些簡(jiǎn)單的源碼,你可以:
· 服務(wù)或連接多客戶(hù)端和服務(wù)端
· 異步發(fā)送或接收文件
· 創(chuàng)建并管理一個(gè)邏輯工作者線(xiàn)程池,用以處理繁重的客戶(hù)端/服務(wù)器請(qǐng)求或計(jì)算
找到一份全面但簡(jiǎn)單的解決客戶(hù)端/服務(wù)器通信的源碼是件困難的事情。在網(wǎng)絡(luò)上找到的源碼要么太復(fù)雜(超過(guò)20個(gè)類(lèi)),要命沒(méi)有提供足夠的效率。本源碼的設(shè)計(jì)盡可能簡(jiǎn)單,并提供了充足的文檔。在這篇文章中,我們簡(jiǎn)潔的呈現(xiàn)出了Winsock API 2.0支持的IOCP技術(shù),說(shuō)明了在編寫(xiě)過(guò)程中出現(xiàn)的棘手問(wèn)題,并提出了每一個(gè)問(wèn)題的解決方案。
2.2 異步完成端口介紹
如果一個(gè)服務(wù)器應(yīng)用程序不能同時(shí)支持多個(gè)客戶(hù)端,那是毫無(wú)意義的,為此,通常使用異步I/O請(qǐng)求和多線(xiàn)程。根據(jù)定義,一個(gè)異步I/O請(qǐng)求會(huì)立即返回,而留下I/O請(qǐng)求處于等待狀態(tài)。有時(shí),I/O異步請(qǐng)求的結(jié)果必須與主線(xiàn)程同步。這可以通過(guò)幾種不同方式解決。同步可以通過(guò)下面的方式實(shí)現(xiàn):
> 使用事件 – 當(dāng)異步請(qǐng)求結(jié)束時(shí)會(huì)馬上觸發(fā)一個(gè)信號(hào)。這種方式的缺點(diǎn)是線(xiàn)程必須檢查并等待事件被觸發(fā)
> 使用GetOverlappedResult函數(shù) – 這種方式與上一種方式有相同的缺點(diǎn)。
> 使用Asynchronous Procedure Calls(或APC) – 這種方式有幾個(gè)缺點(diǎn)。首先,APC總是在請(qǐng)求線(xiàn)程的上下文中被請(qǐng)求;第二,為了執(zhí)行APC,請(qǐng)求線(xiàn)程必須在可變等候狀態(tài)下掛起。
> 使用IOCP – 這種方式的缺點(diǎn)是必須解決很多實(shí)際的棘手的編程問(wèn)題。編寫(xiě)IOCP可能有點(diǎn)麻煩。
2.2.1 為什么使用IOCP?
通過(guò)使用IOCP,我們可以解決“每個(gè)客戶(hù)端占用一個(gè)線(xiàn)程”的問(wèn)題。通常普遍認(rèn)為如果軟件不能運(yùn)行在真正的多處理器機(jī)器上,執(zhí)行能力會(huì)嚴(yán)重降低。線(xiàn)程是系統(tǒng)資源,而這些資源既不是無(wú)限的,也不是低價(jià)的。
IOCP提供了一種方式來(lái)使用幾個(gè)線(xiàn)程“公平的”處理多客戶(hù)端的輸入/輸出。線(xiàn)程被掛起,不占用CPU周期直到有事可做。
2.3 什么是IOCP?
我們已經(jīng)看到IOCP只是一個(gè)線(xiàn)程同步對(duì)象,類(lèi)似于信號(hào)燈,因此IOCP并不是一個(gè)復(fù)雜的概念。一個(gè)IOCP對(duì)象與幾個(gè)支持待定異步I/O請(qǐng)求的I/O對(duì)象綁定。一個(gè)可以訪(fǎng)問(wèn)IOCP的線(xiàn)程可以被掛起,直到一個(gè)待定的異步I/O請(qǐng)求結(jié)束。
3 IOCP是怎樣工作的?
要使用IOCP,你必須處理三件事情,綁定一個(gè)socket到完成端口,創(chuàng)建異步I/O請(qǐng)求,并與線(xiàn)程同步。為從異步I/O請(qǐng)求獲得結(jié)果,如那個(gè)客戶(hù)端發(fā)出的請(qǐng)求,你必須傳遞兩個(gè)參數(shù):CompletionKey參數(shù)和OVERLAPPED結(jié)構(gòu)。
3.1 關(guān)鍵參數(shù)
第一個(gè)參數(shù):CompletionKey,是一個(gè)DWORD類(lèi)型的變量。你可以傳遞任何你想傳遞的唯一值,這個(gè)值將總是同該對(duì)象綁定。正常情況下會(huì)傳遞一個(gè)指向結(jié)構(gòu)或類(lèi)的指針,該結(jié)構(gòu)或類(lèi)包含了一些客戶(hù)端的指定對(duì)象。在源碼中,傳遞的是一個(gè)指向ClientContext的指針。
3.2 OVERLAPPED參數(shù)
這個(gè)參數(shù)通常用來(lái)傳遞異步I/O請(qǐng)求使用的內(nèi)存緩沖。很重要的一點(diǎn)是:該數(shù)據(jù)將會(huì)被鎖定并不允許從物理內(nèi)存中換出頁(yè)面(page out)。
3.3 綁定一個(gè)socket到完成端口
一旦創(chuàng)建完成一個(gè)完成端口,可以通過(guò)調(diào)用CreateIoCompletionPort函數(shù)來(lái)綁定socket到完成端口。形式如下:
BOOL IOCPS::AssociateSocketWithCompletionPort(SOCKET socket, HANDLE hCompletionPort, DWORD dwCompletionKey)
{
HANDLE h = CreateIoCompletionPort((HANDLE) socket, hCompletionPort, dwCompletionKey, m_nIOWorkers);
return h == hCompletionPort;
}
3.4 響應(yīng)異步I/O請(qǐng)求
響應(yīng)具體的異步請(qǐng)求,調(diào)用函數(shù)WSASend和WSARecv。他們也需要一個(gè)參數(shù):WSABUF,這個(gè)參數(shù)包含了一個(gè)指向緩沖的指針。一個(gè)重要的規(guī)則是:通常當(dāng)服務(wù)器/客戶(hù)端響應(yīng)一個(gè)I/O操作,不是直接響應(yīng),而是提交給完成端口,由I/O工作者線(xiàn)程來(lái)執(zhí)行。這么做的原因是:我們希望公平的分割CPU周期。通過(guò)發(fā)送狀態(tài)給完成端口來(lái)發(fā)出I/O請(qǐng)求,如下:
BOOL bSuccess = PostQueuedCompletionStatus(m_hCompletionPort,
pOverlapBuff->GetUsed(),
(DWORD) pContext,
&pOverlapBuff->m_ol);
3.5 與線(xiàn)程同步
與I/O工作者線(xiàn)程同步是通過(guò)調(diào)用GetQueuedCompletionStatus函數(shù)來(lái)實(shí)現(xiàn)的(如下)。這個(gè)函數(shù)也提供了CompletionKey參數(shù)和OVERLAPPED參數(shù),如下:
BOOL GetQueuedCompletionStatus( HANDLE CompletionPort, // handle to completion port
LPDWORD lpNumberOfBytes, // bytes transferred
PULONG_PTR lpCompletionKey, // file completion key
LPOVERLAPPED *lpOverlapped, // buffer
DWORD dwMilliseconds // optional timeout value
);
3.6 四個(gè)棘手的IOCP編碼問(wèn)題和解決方法
使用IOCP時(shí)會(huì)出現(xiàn)一些問(wèn)題,其中有一些不是很直觀的。在使用IOCP的多線(xiàn)程編程中,一個(gè)線(xiàn)程函數(shù)的控制流程不是筆直的,因?yàn)樵诰€(xiàn)程和通訊直接沒(méi)有關(guān)系。在這一章節(jié)中,我們將描述四個(gè)不同的問(wèn)題,可能在使用IOCP開(kāi)發(fā)客戶(hù)端/服務(wù)器應(yīng)用程序時(shí)會(huì)出現(xiàn),分別是:
The WSAENOBUFS error problem.(WSAENOBUFS錯(cuò)誤問(wèn)題)
The package reordering problem.(包重構(gòu)問(wèn)題)
The access violation problem.(訪(fǎng)問(wèn)非法問(wèn)題)
3.6.1 WSAENOBUFS問(wèn)題
這個(gè)問(wèn)題通常很難靠直覺(jué)發(fā)現(xiàn),因?yàn)楫?dāng)你第一次看見(jiàn)的時(shí)候你或許認(rèn)為是一個(gè)內(nèi)存泄露錯(cuò)誤。假定已經(jīng)開(kāi)發(fā)完成了你的完成端口服務(wù)器并且運(yùn)行的一切良好,但是當(dāng)你對(duì)其進(jìn)行壓力測(cè)試的時(shí)候突然發(fā)現(xiàn)服務(wù)器被中止而不處理任何請(qǐng)求了,如果你運(yùn)氣好的話(huà)你會(huì)很快發(fā)現(xiàn)是因?yàn)閃SAENOBUFS 錯(cuò)誤而影響了這一切。
每當(dāng)我們重疊提交一個(gè)send或receive操作的時(shí)候,其中指定的發(fā)送或接收緩沖區(qū)就被鎖定了。當(dāng)內(nèi)存緩沖區(qū)被鎖定后,將不能從物理內(nèi)存進(jìn)行分頁(yè)。操作系統(tǒng)有一個(gè)鎖定最大數(shù)的限制,一旦超過(guò)這個(gè)鎖定的限制,那么就會(huì)產(chǎn)生WSAENOBUFS 錯(cuò)誤了。
如果一個(gè)服務(wù)器提交了非常多的重疊的receive在每一個(gè)連接上,那么限制會(huì)隨著連接數(shù)的增長(zhǎng)而變化。如果一個(gè)服務(wù)器能夠預(yù)先估計(jì)可能會(huì)產(chǎn)生的最大并發(fā)連接數(shù),服務(wù)器可以投遞一個(gè)使用零緩沖區(qū)的receive在每一個(gè)連接上。因?yàn)楫?dāng)你提交操作沒(méi)有緩沖區(qū)時(shí),那么也不會(huì)存在內(nèi)存被鎖定了。使用這種辦法后,當(dāng)你的receive操作事件完成返回時(shí),該socket底層緩沖區(qū)的數(shù)據(jù)會(huì)原封不動(dòng)的還在其中而沒(méi)有被讀取到receive操作的緩沖區(qū)來(lái)。此時(shí),服務(wù)器可以簡(jiǎn)單的調(diào)用非阻塞式的recv將存在socket緩沖區(qū)中的數(shù)據(jù)全部讀出來(lái),一直到recv返回 WSAEWOULDBLOCK 為止。 這種設(shè)計(jì)非常適合那些可以犧牲數(shù)據(jù)吞吐量而換取巨大 并發(fā)連接數(shù)的服務(wù)器。當(dāng)然,你也需要意識(shí)到如何讓客戶(hù)端的行為盡量避免對(duì)服務(wù)器造成影響。在上一個(gè)例子中,當(dāng)一個(gè)零緩沖區(qū)的receive操作被返回后使 用一個(gè)非阻塞的recv去讀取socket緩沖區(qū)中的數(shù)據(jù),如果服務(wù)器此時(shí)可預(yù)計(jì)到將會(huì)有爆發(fā)的數(shù)據(jù)流,那么可以考慮此時(shí)投遞一個(gè)或者多個(gè)receive 來(lái)取代非阻塞的recv來(lái)進(jìn)行數(shù)據(jù)接收。(這比你使用1個(gè)缺省的8K緩沖區(qū)來(lái)接收要好的多。)
源碼中提供了一個(gè)簡(jiǎn)單實(shí)用的解決WSAENOBUF錯(cuò)誤的辦法。我們執(zhí)行了一個(gè)零字節(jié)緩沖的異步WSARead(...)(參見(jiàn) OnZeroByteRead(..))。當(dāng)這個(gè)請(qǐng)求完成,我們知道在TCP/IP棧中有數(shù)據(jù),然后我們通過(guò)執(zhí)行幾個(gè)有MAXIMUMPACKAGESIZE緩沖的異步WSARead(...)去讀,解決了WSAENOBUFS問(wèn)題。但是這種解決方法降低了服務(wù)器的吞吐量。
總結(jié):
解決方法一:
投遞使用空緩沖區(qū)的 receive操作,當(dāng)操作返回后,使用非阻塞的recv來(lái)進(jìn)行真實(shí)數(shù)據(jù)的讀取。因此在完成端口的每一個(gè)連接中需要使用一個(gè)循環(huán)的操作來(lái)不斷的來(lái)提交空緩沖區(qū)的receive操作。
解決方法二:
在投遞幾個(gè)普通含有緩沖區(qū)的receive操作后,進(jìn)接著開(kāi)始循環(huán)投遞一個(gè)空緩沖區(qū)的receive操作。這樣保證它們按照投遞順序依次返回,這樣我們就總能對(duì)被鎖定的內(nèi)存進(jìn)行解鎖。
3.6.2 包重構(gòu)問(wèn)題
... ... 盡管使用IO完成端口的待發(fā)操作將總是按照他們發(fā)送的順序來(lái)完成,線(xiàn)程調(diào)度安排可能使綁定到完成端口的實(shí)際工作不按指定的順序來(lái)處理。例如,如果你有兩個(gè)I/O工作者線(xiàn)程,你可能接收到“字節(jié)塊2,字節(jié)塊1,字節(jié)塊3”。這就意味著:當(dāng)你通過(guò)向I/O完成端口提交請(qǐng)求數(shù)據(jù)發(fā)送數(shù)據(jù)時(shí),數(shù)據(jù)實(shí)際上用重新排序過(guò)的順序發(fā)送了。
這可以通過(guò)只使用一個(gè)工作者線(xiàn)程來(lái)解決,并只提交一個(gè)I/O請(qǐng)求,等待它完成。但是如果這么做,我們就失去了IOCP的長(zhǎng)處。
解決這個(gè)問(wèn)題的一個(gè)簡(jiǎn)單實(shí)用辦法是給我們的緩沖類(lèi)添加一個(gè)順序數(shù)字,如果緩沖順序數(shù)字是正確的,則處理緩沖中的數(shù)據(jù)。這意味著:有不正確的數(shù)字的緩沖將被存下來(lái)以后再用,并且因?yàn)閳?zhí)行原因,我們保存緩存到一個(gè)HASH MAP對(duì)象中(如m_SendBufferMap 和 m_ReadBufferMap)。
獲取這種解決方法的更多信息,請(qǐng)查閱源碼,仔細(xì)查看IOCPS類(lèi)中如下的函數(shù):
GetNextSendBuffer (..) and GetNextReadBuffer(..), to get the ordered send or receive buffer.
IncreaseReadSequenceNumber(..) and IncreaseSendSequenceNumber(..), to increase the sequence numbers.
3.6.3 異步等待讀 和 字節(jié)塊包處理問(wèn)題
最通用的服務(wù)端協(xié)議是一個(gè)基于協(xié)議的包,首先X個(gè)字節(jié)代表包頭,包頭包含了詳細(xì)的完整的包的長(zhǎng)度。服務(wù)端可以讀包頭,計(jì)算出需要多少數(shù)據(jù),繼續(xù)讀取直到讀完一個(gè)完整的包。當(dāng)服務(wù)端同時(shí)只處理一個(gè)異步請(qǐng)求時(shí)工作的很好。但是,如果我們想發(fā)揮IOCP服務(wù)端的全部潛能,我們應(yīng)該啟用幾個(gè)等待的異步讀事件,等待數(shù)據(jù)到達(dá)。這意味著幾個(gè)異步讀操作是不按順序完成的,通過(guò)等待的讀事件返回的字節(jié)塊流將不會(huì)按順序處理。而且,一個(gè)字節(jié)塊流可以包含一個(gè)或幾個(gè)包,也可能包含部分包,如下圖所示:

這個(gè)圖形顯示了部分包(綠色)和完整包(黃色)是怎樣在不同字節(jié)塊流中異步到達(dá)的。
這意味著我們必須處理字節(jié)流來(lái)成功的讀取一個(gè)完整的包。而且,我們必須處理部分包(圖表中綠色的部分)。這就使得字節(jié)流的處理更加困難。這個(gè)問(wèn)題的完整解決方法在IOCPS類(lèi)的ProcessPackage(…)函數(shù)中。
3.6.4 訪(fǎng)問(wèn)非法問(wèn)題
這是一個(gè)較小的問(wèn)題,代碼設(shè)計(jì)導(dǎo)致的問(wèn)題更勝于IOCP的特定問(wèn)題。假設(shè)一個(gè)客戶(hù)端連接已經(jīng)關(guān)閉并且一個(gè)I/O請(qǐng)求返回一個(gè)錯(cuò)誤標(biāo)志,然后我們知道客戶(hù)端已經(jīng)關(guān)閉。在參數(shù)CompletionKey中,我們傳遞了一個(gè)指向結(jié)構(gòu)ClientContext的指針,該結(jié)構(gòu)中包含了客戶(hù)端的特定數(shù)據(jù)。如果我們釋放這個(gè)ClientContext結(jié)構(gòu)占用的內(nèi)存,并且同一個(gè)客戶(hù)端處理的一些其它I/O請(qǐng)求返回了錯(cuò)誤代碼,我們通過(guò)轉(zhuǎn)換參數(shù)CompletionKey為一個(gè)指向ClientContext結(jié)構(gòu)的指針并試圖訪(fǎng)問(wèn)或刪除它,會(huì)發(fā)生什么呢?一個(gè)非法訪(fǎng)問(wèn)出現(xiàn)了!
這個(gè)問(wèn)題的解決方法是添加一個(gè)數(shù)字到結(jié)構(gòu)中,包含等待的I/O請(qǐng)求的數(shù)量(m_nNumberOfPendingIO),然后當(dāng)我們知道沒(méi)有等待的I/O請(qǐng)求時(shí)刪除這個(gè)結(jié)構(gòu)。這個(gè)功能通過(guò)函數(shù)EnterIoLoop(…) 和ReleaseClientContext(…)來(lái)實(shí)現(xiàn)。
3.7 源碼略讀
源碼的目標(biāo)是提供一系列簡(jiǎn)單的類(lèi)來(lái)處理所有IOCP編碼中的問(wèn)題。源碼也提供了一系列通信和C/S軟件中經(jīng)常使用的函數(shù),如文件接收/傳送函數(shù),邏輯線(xiàn)程池處理,等等。

上圖功能性的圖解說(shuō)明了IOCP類(lèi)源碼。
我們有幾個(gè)IO工作者線(xiàn)程通過(guò)完成端口來(lái)處理異步IO請(qǐng)求,這些工作者線(xiàn)程調(diào)用一些虛函數(shù),這些虛函數(shù)可以把需要大量計(jì)算的請(qǐng)求放到一個(gè)工作隊(duì)列中。邏輯工作者通過(guò)類(lèi)中提供的這些函數(shù)從隊(duì)列中取出任務(wù)、處理并發(fā)回結(jié)果。GUI經(jīng)常與主類(lèi)通信,通過(guò)Windows消息(因?yàn)镸FC不是線(xiàn)程安全的)、通過(guò)調(diào)用函數(shù)或通過(guò)使用共享的變量。
圖三
上圖顯示了類(lèi)結(jié)構(gòu)縱覽。
圖3中的類(lèi)說(shuō)明如下:
> CIOCPBuffer:管理異步請(qǐng)求的緩存的類(lèi)。
> IOCPS:處理所有通信的主類(lèi)。
> JobItem:保存邏輯工作者線(xiàn)程要處理的任務(wù)的結(jié)構(gòu)。
> ClientContex:保存客戶(hù)端特定信息的結(jié)構(gòu)(如狀態(tài)、數(shù)據(jù),等等)。
3.7.1 緩沖設(shè)計(jì) - CIOCPBuffer類(lèi)
使用異步I/O調(diào)用時(shí),我們必須提供私有的緩沖區(qū)供I/O操作使用。
當(dāng)我們將帳號(hào)信息放入分配的緩沖供使用時(shí)有許多情況需要考慮:
.分配和釋放內(nèi)存代價(jià)高,因此我們應(yīng)重復(fù)使用以及分配的緩沖(內(nèi)存),
因此我們將緩沖保存在列表結(jié)構(gòu)中,如下所示:
// Free Buffer List..
CCriticalSection m_FreeBufferListLock;
CPtrList m_FreeBufferList;
// OccupiedBuffer List.. (Buffers that is currently used)
CCriticalSection m_BufferListLock;
CPtrList m_BufferList;
// Now we use the function AllocateBuffer(..)
// to allocate memory or reuse a buffer.
.有時(shí),當(dāng)異步I/O調(diào)用完成后,緩沖里可能不是完整的包,因此我們需要分割緩沖去取得完整的信息。在CIOCPS類(lèi)中提供了SplitBuffer函數(shù)。
同樣,有時(shí)候我們需要在緩沖間拷貝信息,IOCPS類(lèi)提供了AddAndFlush函數(shù)。
. 眾所周知,我們也需要添加序號(hào)和狀態(tài)(IOType 變量, IOZeroReadCompleted, 等等)到我們的緩沖中。
. 我們也需要有將數(shù)據(jù)轉(zhuǎn)換到字節(jié)流或?qū)⒆止?jié)流轉(zhuǎn)換到數(shù)據(jù)的方法,CIOCPBuffer也提供了這些函數(shù)。
以上所有問(wèn)題都在CIOCPBuffer中解決。
3.8 如何使用源代碼
從IOCP繼承你自己的類(lèi)(如圖3),實(shí)現(xiàn)IOCPS類(lèi)中的虛函數(shù)(例如,threadpool),
在任何類(lèi)型的服務(wù)端或客戶(hù)端中實(shí)現(xiàn)使用少量的線(xiàn)程有效地管理大量的連接。
3.8.1 啟動(dòng)和關(guān)閉服務(wù)端/客戶(hù)端
調(diào)用下面的函數(shù)啟動(dòng)服務(wù)端
BOOL Start(int nPort=999,int iMaxNumConnections=1201,
int iMaxIOWorkers=1,int nOfWorkers=1,
int iMaxNumberOfFreeBuffer=0,
int iMaxNumberOfFreeContext=0,
BOOL bOrderedSend=TRUE,
BOOL bOrderedRead=TRUE,
int iNumberOfPendlingReads=4);
nPort
服務(wù)端偵聽(tīng)的端口. ( -1 客戶(hù)端模式.)
iMaxNumConnections
允許最大的連接數(shù). (使用較大的數(shù).)
iMaxIOWorkers
I/O工作線(xiàn)程數(shù)
nOfWorkers
邏輯工作者數(shù)量Number of logical workers. (可以在運(yùn)行時(shí)改變.)
iMaxNumberOfFreeBuffer
重復(fù)使用的緩沖最大數(shù). (-1 不使用, 0= 不限)
iMaxNumberOfFreeContext
重復(fù)使用的客戶(hù)端信息對(duì)象數(shù) (-1 for 不使用, 0= 不限)
bOrderedRead
順序讀取. (我們已經(jīng)在 3.6.2. 處討論過(guò))
bOrderedSend
順序?qū)懭? (我們已經(jīng)在 3.6.2. 處討論過(guò))
iNumberOfPendlingReads
等待讀取數(shù)據(jù)時(shí)未決的異步讀取循環(huán)數(shù)
連接到遠(yuǎn)程服務(wù)器(客戶(hù)端模式nPort=-1),調(diào)用函數(shù):
CodeConnect(const CString &strIPAddr, int nPort)
.strIPAddr
遠(yuǎn)程服務(wù)器的IP地址
.nPort
端口
調(diào)用ShutDown()關(guān)閉連接
例如:
if(!m_iocp.Start(-1,1210,2,1,0,0))
AfxMessageBox("Error could not start the Client");
….
m_iocp.ShutDown();
4.1 源代碼描述
更多關(guān)于源代碼的信息請(qǐng)參考代碼里的注釋。
4.1.1 虛函數(shù)
NotifyNewConnection
新的連接已接受
NotifyNewClientContext
空的ClientContext結(jié)構(gòu)被分配
NotifyDisconnectedClient
客戶(hù)端連接斷開(kāi)
ProcessJob
邏輯工作者需要處理一個(gè)工作
NotifyReceivedPackage
新的包到達(dá)
NotifyFileCompleted
文件傳送完成。
4.1.2 重要變量
所有變量共享使用時(shí)必須加鎖避免存取違例,所有需要加鎖的變量,名稱(chēng)為XXX則鎖變量名稱(chēng)為XXXLock。
m_ContextMapLock;
保存所有客戶(hù)端數(shù)據(jù)(socket,客戶(hù)端數(shù)據(jù),等等)
ContextMap m_ContextMap;
m_NumberOfActiveConnections
保存已連接的連接數(shù)
4.1.3 重要函數(shù)
GetNumberOfConnections()
返回連接數(shù)
CString GetHostAdress(ClientContext* p)
提供客戶(hù)端上下文,返回主機(jī)地址
BOOL ASendToAll(CIOCPBuffer *pBuff);
發(fā)送緩沖上下文到所有連接的客戶(hù)端
DisconnectClient(CString sID)
根據(jù)客戶(hù)端唯一編號(hào),斷開(kāi)指定的客戶(hù)端
CString GetHostIP()
返回本地IP
JobItem* GetJob()
將JobItem從隊(duì)列中移出, 如果沒(méi)有job,返回 NULL
BOOL AddJob(JobItem *pJob)
添加Job到隊(duì)列
BOOL SetWorkers(int nThreads)
設(shè)置可以任何時(shí)候調(diào)用的邏輯工作者數(shù)量
DisconnectAll();
斷開(kāi)所有客戶(hù)端
ARead(…)
異步讀取
ASend(…)
異步發(fā)送,發(fā)送數(shù)據(jù)到客戶(hù)端
ClientContext* FindClient(CString strClient)
根據(jù)字符串ID尋找客戶(hù)(非線(xiàn)程安全)
DisconnectClient(ClientContext* pContext, BOOL bGraceful=FALSE);
端口客戶(hù)
DisconnectAll()
端口所有客戶(hù)
StartSendFile(ClientContext *pContext)
根據(jù)ClientContext結(jié)構(gòu)發(fā)送文件(使用經(jīng)優(yōu)化的transmitfile(..) 函數(shù))
PrepareReceiveFile(..)
接收文件準(zhǔn)備。調(diào)用該函數(shù)時(shí),所有進(jìn)入的字節(jié)流已被寫(xiě)入到文件。
PrepareSendFile(..)
打開(kāi)文件并發(fā)送包含文件信息的數(shù)據(jù)包。函數(shù)禁用ASend(..)函數(shù),直到文件傳送關(guān)閉或中斷。
DisableSendFile(..)
禁止發(fā)送文件模式
DisableRecevideFile(..)
禁止文件接收模式
5.1 文件傳輸
文件傳輸使用Winsock 2.0 中的TransmitFile函數(shù)。TransmitFile函數(shù)通過(guò)連接的socket句柄傳送文件數(shù)據(jù)。函數(shù)使用操作系統(tǒng)的高速緩沖管理器(cache manager)接收文件數(shù)據(jù),通過(guò)sockets提供高性能的文件數(shù)據(jù)傳輸。異步文件傳輸要點(diǎn):
在TransmitFile函數(shù)返回前,所有其他發(fā)送或?qū)懭氲皆搒ocket的操作都將無(wú)法執(zhí)行,因?yàn)檫@將使文件數(shù)據(jù)混亂。
因此,在PrepareSendFile()函數(shù)調(diào)用之后,所有ASend都被禁止。
因?yàn)椴僮飨到y(tǒng)連續(xù)讀取文件數(shù)據(jù),你可以使用FILE_FLAG_SEQUENTIAL_SCAN參數(shù)來(lái)優(yōu)化高速緩沖性能。
發(fā)送文件時(shí)我們使用了內(nèi)核異步操作(TF_USE_KERNEL_APC)。TF_USE_KERNEL_APC的使用可以更好地提升性能。有可能, 無(wú)論如何,TransmitFile在線(xiàn)程中的大量使用,這種情形可能會(huì)阻止APCs的調(diào)用.
文件傳輸按如下順序執(zhí)行:服務(wù)器調(diào)用PrepareSendFile(..)函數(shù)初始化文件傳輸。客戶(hù)端接收文件信息時(shí),調(diào)用PrepareReceiveFile(..)作接收前的準(zhǔn)備,并發(fā)送一個(gè)包到服務(wù)器告知開(kāi)始文件傳送。當(dāng)包到達(dá)服務(wù)器端,服務(wù)器端調(diào)用StartSendFile(..)采用高性能的TransmitFile函數(shù)發(fā)送指定文件。
6 源代碼示例
提供的源代碼演示代碼是一個(gè)echo客戶(hù)端/服務(wù)器端程序,并提供了對(duì)文件傳輸?shù)闹С郑▓D4)。在代碼中,MyIOCP類(lèi)從IOCP繼承,處理客戶(hù)端/服務(wù)器端的通訊,所涉及的虛函數(shù)可以參見(jiàn)4.1.1處。
客戶(hù)端或服務(wù)器端最重要的部分是虛函數(shù)NotifyReceivedPackage,定義如下:
void MyIOCP::NotifyReceivedPackage(CIOCPBuffer *pOverlapBuff,
int nSize,ClientContext *pContext)
{
BYTE PackageType=pOverlapBuff->GetPackageType();
switch (PackageType)
{
case Job_SendText2Client :
Packagetext(pOverlapBuff,nSize,pContext);
break;
case Job_SendFileInfo :
PackageFileTransfer(pOverlapBuff,nSize,pContext);
break;
case Job_StartFileTransfer:
PackageStartFileTransfer(pOverlapBuff,nSize,pContext);
break;
case Job_AbortFileTransfer:
DisableSendFile(pContext);
break;
};
}
函數(shù)接收進(jìn)入的信息并執(zhí)行遠(yuǎn)程連接發(fā)送的請(qǐng)求。這種情況,只是簡(jiǎn)單的echo或文件傳輸?shù)那樾巍7?wù)器端和客戶(hù)端源代碼分成兩個(gè)工程,IOCP和IOCPClient。
6.1 編譯問(wèn)題
使用VC++6.0或VC.NET編譯,你可能在編譯CFile時(shí)得到一些奇怪的錯(cuò)誤,如:
“if (pContext->m_File.m_hFile !=
INVALID_HANDLE_VALUE) <-error C2446: '!=' : no conversion "
"from 'void *' to 'unsigned int'”
這個(gè)問(wèn)題可以通過(guò)更新頭文件(*.h)或VC++ 6.0的版本或改變類(lèi)型轉(zhuǎn)換錯(cuò)誤來(lái)解決,在修正了錯(cuò)誤后,服務(wù)器端/客戶(hù)端源代碼可以在不需MFC的情況下使用。
7 特別的考慮和經(jīng)驗(yàn)總結(jié)
當(dāng)你在其他類(lèi)型的程序中使用本代碼,有一些可以避免的編程陷阱和多線(xiàn)程問(wèn)題。
非確定錯(cuò)誤指的是那些隨機(jī)出現(xiàn)的錯(cuò)誤,很難通過(guò)執(zhí)行相同順序的任務(wù)來(lái)重現(xiàn)這些錯(cuò)誤。
這是最壞的錯(cuò)誤類(lèi)型,通常,錯(cuò)誤出現(xiàn)在內(nèi)部源代碼的設(shè)計(jì)中。當(dāng)服務(wù)器端有多個(gè)IO工作線(xiàn)程在運(yùn)行,
為客戶(hù)端提供連接,如果程序員沒(méi)有考慮多線(xiàn)程環(huán)境,可能會(huì)發(fā)生存取違例等不確定錯(cuò)誤。
經(jīng)驗(yàn) #1:
在未對(duì)上下文加鎖時(shí),不要讀寫(xiě)客戶(hù)上下文(例如:ClientContext)。
通知函數(shù)(例如:Notify*(ClientContext *pContext))已經(jīng)是線(xiàn)程安全,處理成員變量ClientContext可以不需要解鎖、解鎖。
//不要這樣使用
// …
If(pContext->m_bSomeData)
pContext->m_iSomeData=0;
// …
// 應(yīng)該這樣使用
//….
pContext->m_ContextLock.Lock();
If(pContext->m_bSomeData)
pContext->m_iSomeData=0;
pContext->m_ContextLock.Unlock();
//…
大家都知道的,當(dāng)你鎖定上下文,其他線(xiàn)程或GUI都將等待它。
經(jīng)驗(yàn)#2:
避免使用復(fù)雜的和其他類(lèi)型的"上下文鎖",應(yīng)為容易造成死鎖(例如:A在等待B,B在等待C,C在等待A,A死鎖)
pContext-> m_ContextLock.Lock();
//… code code ..
pContext2-> m_ContextLock.Lock();
// code code..
pContext2-> m_ContextLock.Unlock();
// code code..
pContext-> m_ContextLock.Unlock();
以上代碼可能導(dǎo)致死鎖
經(jīng)驗(yàn) #3:
不要在通知函數(shù)(例如:Notify*(ClientContext *pContext))以外處理客戶(hù)上下文,如果你需要這樣做,你
要放入
m_ContextMapLock.Lock();
…
m_ContextMapLock.Unlock();
參考如下代碼:
ClientContext* pContext=NULL ;
m_ContextMapLock.Lock();
pContext = FindClient(ClientID);
// safe to access pContext, if it is not NULL
// and are Locked (Rule of thumbs#1:)
//code .. code..
m_ContextMapLock.Unlock();
// Here pContext can suddenly disappear because of disconnect.
// do not access pContext members here.
8 將來(lái)的工作
將來(lái),代碼將提供以下功能:
添加支持AcceptEx(..)接受新連接,處理短連接和DOS攻擊。
源代碼兼容Win32,STL,WTL等環(huán)境。