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

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

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