1. 程式人生 > >一個簡單的IOCP(IO完成埠)伺服器/客戶端類(中文版)

一個簡單的IOCP(IO完成埠)伺服器/客戶端類(中文版)

一個簡單的IOCPIO完成埠)伺服器/客戶端類

——A simple IOCP Server/Client Class By spinoza

原文【選自CodeProject

原始碼:

——譯: Ocean Email: [email protected]

This source code uses the advanced IOCP technology which can efficiently serve multiple clients. It also presents some solutions to practical problems that arise with the IOCP programming API, and provides a simple echo client/server with file transfer.

1.1要求

l  本文希望讀者對C++TCP/IPSocket程式設計,MFC以及多執行緒比較熟悉

l  原始碼使用Winsock2.0以及IOCP技術,因此需要:

Windows NT/2000 or later: Requires Windows NT 3.5 or later.

Windows 95/98/ME: Not supported.

Visual C++ .NET, or a fully updated Visual C++ 6.0.

1.2 摘要

當你開發不同型別的軟體時,你總會需要進行C/S的開發。完成一個完善的C/S程式碼對於編碼人員來說是一件困難的事情。本文給出了一個簡單的但是卻是卻十分強大的C/S

原始碼,他可以擴充套件成任何型別的C/S程式。原始碼使用了IOCP技術,該技術可以有效地處理多客戶端。 IOCP 對於“一個客戶端一個執行緒”所有面臨的瓶頸(或者其他)問題提出了一種有效的解決方案,他只使用少量的執行執行緒以及非同步的輸入輸出、接受傳送。IOCP計數被廣泛的用於各種高效能的伺服器,如Apache等。原始碼同時也提供了一組用於處理通訊的常用功能以及在C/S軟體中經常用到功能,如檔案接受/傳輸功能以及邏輯執行緒池操作。本文將主要關注一種圍繞IOCP API在實際中的解決方案,以及呈現原始碼的完整文件。隨後,我將展示一個可以處理多連線和檔案傳輸的echo C/S程式。

2.1 介紹

本文闡述了一個類,他可以被同時用於客戶端和伺服器端程式碼。這個類使用IOCP(Input Output Completion Ports)

以及非同步(non-blocking) 功能呼叫。原始碼是基於很多其他原始碼和文章的。

使用此原始碼,你可以:

·為多主機進行連結、或者連結到多主機的客戶端和伺服器

·非同步的傳送和接受檔案

·建立和管理一個邏輯工作執行緒池,他可以處理繁重的C/S請求或計算

找到一段完善的卻又簡單的、可以處理C/S通訊的程式碼是一件困難的事情。在網路上找到的程式碼要麼太過於複雜(可能多於20個類),或者不能提供有效的效率。本程式碼就是以簡單為設計理念的,文件也儘可能的完善。在本文中,我們可以很簡單的使用由Winsock 2.0提供的IOCP技術,我也會談到一些在編碼時會遇到的棘手的問題以及他們的解決方法。

2.2 非同步輸入輸出完成埠(IOCP)的介紹

一個伺服器程式要是不能同時處理多客戶端,那麼我們可以說這個程式是毫無意義的,而我們為此一般會使用非同步I/O呼叫或者多執行緒技術去實現。從定義上來看,一個非同步I/O呼叫可以及時返回而讓I/O掛起。在同一時間點上,I/O非同步呼叫必須與主執行緒進行同步。這可以使用各種方式,同步主要可以通過以下實現:

·使用事件(events) 只要非同步呼叫完成,一個Signal就會被Set。這種方式主要的缺點是執行緒必須去檢查或者等待這個eventSet

·使用GetOverlappedResult功能。這種方式和上面的方式有同樣的缺點。

·使用非同步例程呼叫(APC)。對於這種方式有幾個缺點。首先,APC總是在呼叫執行緒的上下文被呼叫的,其次,為了執行APCs,呼叫執行緒必須被掛起,這被成為alterable wait state

·使用IOCP。這種方式的缺點是很多棘手的編碼問題必須得以解決。編寫IOCP可能一件讓人持續痛苦的事情。

2.2.1 為什麼使用IOCP?

使用IOCP,我們可以克服”一個客戶端一個執行緒”的問題。我們知道,這樣做的話,如果軟體不是執行在一個多核及其上效能就會急劇下降。執行緒是系統資源,他們既不是無限制的、也不是代價低廉的。

IOCP提供了一種只使用一些(I/O worker)執行緒去“相對公平地”完成多客戶端的”輸入輸出”。執行緒會一直被掛起,而不會使用CPU時間片,直到有事情做為止。

2.3 什麼是IOCP?

我們已經提到IOCP 只不過是一個執行緒同步物件,和訊號量(semaphore)相似,因此IOCP並不是一個複雜的概念。一個IOCP 物件是與多個I/O物件關聯的,這些物件支援掛起非同步IO呼叫。知道一個掛起的非同步IO呼叫結束為止,一個訪問IOCP的執行緒都有可能被掛起。

3. IOCP是如何工作的?

要獲得更多的資訊,我推薦其他的一些文章(譯者注,在CodeProject

當使用IOCP時,你必須處理三件事情:將一個Socket關聯到完成埠,建立一個非同步I/O呼叫,與執行緒進行同步。為了獲得非同步IO呼叫的結果,比如,那個客戶端執行了呼叫,你必須傳入兩個引數:the CompletionKey 引數, OVERLAPPED結構。

3.1 CompletionKey引數

第一個引數是CompletionKey,一個DWORD型別值。你可以任何你希望的標識值,這將會和物件關聯。一般的,一個包含一些客戶端特定物件的結構體或者物件的指標可以使用此引數傳入。在原始碼中,一個指向ClientContext結構被傳到CompletionKey引數中。

3.2 OVERLAPPED 引數

這個引數一般用於傳入被非同步IO呼叫使用的記憶體buffer。我們必須主意這個資料必須是鎖在記憶體的,不能被換頁出實體記憶體。我們稍後會討論這個。

3.3 socket與完成埠繫結

一旦一個完成埠建立,我們就可以使用CreateToCompletionPort方法去將socket繫結到完成埠,這看起來像這面這樣:

BOOL IOCPS::AssociateSocketWithCompletionPort(SOCKET socket,

HANDLE hCompletionPort, DWORD dwCompletionKey)

{

HANDLE h = CreateIoCompletionPort((HANDLE) socket,

hCompletionPort, dwCompletionKey, m_nIOWorkers);

return h == hCompletionPort;

}

3.4 建立非同步IO呼叫

建立真正的非同步呼叫:可以呼叫WSASend,WSARecv。他們也需要一個WSABUF引數,這個引數包含一個指向被使用的buffer的指標。首要規則是,當伺服器/客戶端試圖呼叫一個IO操作時,他們不是直接的操作,而是先被傳送到完成埠,這將會被IO工作執行緒去完成操作。之所以要這樣做,我們是想要CPU呼叫更加公平。 IO呼叫可以通過傳送(Post)狀態到完成埠來實現,看一下程式碼:

         BOOL bSuccess = PostQueuedCompletionStatus(m_hCompletionPort,

pOverlapBuff->GetUsed(),

(DWORD) pContext, &pOverlapBuff->m_ol);

3.5 與執行緒同步

IO工作執行緒同步是通過GetQueuedCompletionStatus方法完成的(程式碼如下)。該方法也提供了CompleteKey引數以及OVERLAPPED引數:

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時我們會遇到一些問題,這其中的有一些是不那麼直觀的。在使用IOCP的多執行緒場合中,執行緒的控制並不是很直觀,因為通訊與執行緒之間是沒有關係的。在本節中,我們將展示四個使用IOCP在開發C/S程式時不同的問題。他們是:

·WSAENOBUGS 錯誤問題

·資料包重排序問題

·非法訪問問題

3.6.1 WSAENOBUGS 錯誤問題

這個問題不是那麼直觀並且也很難發現,因為第一感覺是,他看起來像是一個平常的死鎖或者記憶體洩露bug。假如你開發了你的伺服器,他也能工作的很好。當你對他進行壓力測試時,他突然掛起了。如果你比較幸運,你可以發現他是與WSAENOBUGS錯誤相關的。

每次重疊傳送或者接受操作時,被提交的資料buffer都是有可能被鎖住的。當記憶體鎖住時,他就不能被換頁到實體記憶體外。一個作業系統限制了可以被鎖住的記憶體大小。當超出了限制時,重疊操作就會因WSAENOBUGS錯誤失敗。

如果一個伺服器在每個連線上進行了許多Overlapped接收,隨著連線數量的增加,我們就可能達到這個限制。如果一個伺服器希望處理非常大的突發使用者,伺服器POST可以從每個連結上接收到0位元組的資料,因為已經沒有buffer與接收操作關聯了,沒有記憶體需要被鎖住了。使用這種方式,每個socket的接收buffer應該被保持完整因為一旦0位元組的接收操作完成,伺服器可以簡單的進行非阻塞的接收去獲取socket接收buffer中的所有快取資料。當非阻塞因為WSAWOULDBLOCK錯誤失敗時,這裡就不再會有被掛起的資料了。這種設計可以用於那種需要最大可能的處理突發訪問連結,這是以犧牲吞吐量作為代價的。當然,你對客戶端如何與伺服器端進行互動知道的越多越好。在前一個例子中,一個非阻塞的Receive將會在0位元組接收完成後馬上進行以便去取得快取的資料。如果伺服器知道客戶端突然傳送了很多資料,那麼在接收0位元組資料的Receive完成後,他應該POST一個或者多個Overlapped Reveives以便接收客戶端傳送的一些資料(大於每個socket接收buffer的最大緩衝buffer,預設是8k)。

一個針對WSAENOBUFFERS錯誤問題的簡單而實際的解決方式在原始碼中已經提供了。我們進行一個使用0位元組Buffer的非同步WSARead()(請檢視OnZeroByteRead())。當這個呼叫完成後,我們知道在TCP/IP棧中存在資料,然後我們使用大小為MAXIMUMPACKAGE buffer進行幾個非同步的WSARead。這種解決方法只是在有資料來到時才鎖住實體記憶體,這樣可以解決WSAENOBUFS問題。但是這種解決方式會降低伺服器的吞吐量。

3.6.2 資料包重排序問題

這個問題也在參考文獻【3】中提到。雖然使用IO完成埠的提交操作總是按照他們被提交的順序完成,執行緒排程問題可能會導致與完成埠繫結的真正任務是以未知的順序完成的。例如,如果你有兩個IO工作執行緒,然後你應該接收到“byte chunk 1, byte chunk 2, byte chunk 3”,你可能會以錯誤的順序去處理byte chunk,如“byte chunk 2, byte chunk 1 byte chunk 3”。這也就是意味著當你POST一個傳送請求到IO完成埠進行傳送資料時,資料可能會被以另外的順序進行傳送。

這可以通過只使用單個工作執行緒來解決,只提交一個IO呼叫,直到他完成,但是如果我們這樣做的話,我們將會失去IOCP所有的好處。

一個實際的解決方式是新增一個順序號給我們的buffer類,只處理buffer中的順序號正確的buffer資料。這意味著,buffer如果有不正確的號碼必須儲存起來以便之後用到,因為效能的原因,我們將會將buffers儲存到一個hash map物件中( m_SendBufferMap and m_ReadBufferMap)

要獲得這種解決方式更多的資訊,請閱讀原始碼,然後在IOCPS類中檢視下面的函式:

·GetNextSendBuffer (..) and GetNextReadBuffer(..), to get the ordered send or receive buffer.

·IncreaseReadSequenceNumber(..) and IncreaseSendSequenceNumber(..), to increase the sequence numbers.

3.6.3 非同步掛起讀以及byte chunk資料包處理問題

最常用的伺服器協議是基於包的協議,該協議中前X位元組表示頭部,頭部包含了一個完整的包的長度。伺服器可以讀取頭部,檢視多少資料需要的,然後繼續讀取資料直到讀完一個包。這在伺服器在一個時間上只進行一個非同步呼叫時可以工作的很好。但是如果我們想挖掘IOCP伺服器的所有潛力,我們需要有多個非同步Reads去等待資料的到來。這意味這幾個非同步Reads 是完全亂序的(如前所述), byte chunk流被掛起的reads操作返回來將不再是順序的了(譯者注,實際上這幾個Reads操作是資源競爭的,同時讀取資料,返回時的順序不定)。並且,一個byte chunk流可以包含一個或者多個數據包,或者半包。如下圖所示:

1展示了部分包(綠色)以及完整包(黃色)可能會在不同的byte chunk流中非同步到達

這意味著我們不得對byte chunk進行處理以便獲得完整的資料包。進一步,我們不得不處理部分包(圖中綠色)。這會使得包的處理變得更加麻煩。該問題的完整解決方法可以在IOCPS類的ProcessPackage方法中找到。

3.6.4 非法訪問問題

這是一個小問題,一般是由於程式碼的設計導致的,而不是IOCP特定的問題。假如一個客戶端連結丟失了,而一個IO呼叫返回了一個錯誤flag,隨後我們知道客戶端不存在了。在CompleteKey引數中,我們將一個DWORD型別指標轉型為ClientContext指標,接下來去訪問或者刪除他?訪問異常就是這麼發生的!

這個問題的解決方式是為包含掛起IO呼叫的結構體新增一個數字(nNumberOfPendlingIO),然後當我們知道這將不會有掛起的IO呼叫時才去刪除結構體。這是在方法EnterIoLoop(..) function ReleaseClientContext(..).完成的

3.7 原始碼架構

整個原始碼是提供一些簡單的類去處理在IOCP中需要面對的棘手的問題。原始碼也提供了一些方法,這些方法經常被通訊或者軟體中用到的檔案接收傳輸、邏輯執行緒池處理等

2:上面的圖片展示了IOCP類原始碼功能

我們擁有幾個工作執行緒去處理來自IOCP的非同步IO呼叫,這些工作執行緒呼叫某些虛方法去把需要大量計算的請求放入到工作佇列中。邏輯工作執行緒從佇列中取得這些任務,處理、然後把結果通過類提供的一些方法傳送回去。 GUI通常是通過Windows訊息與主class通訊的(MFC不是執行緒安全的),然後呼叫方法或者使用共享變數。

3:上圖展示了類的框架

我們在圖3中可以看到以下的類:

· CIOCPBuffer: 一個用於管理被非同步IO呼叫使用的buffers

· IOCPS: 用於通訊的主要類。

· JobItem:一個包含需要被邏輯工作執行緒執行的任務的結構。

· ClientContext :一個包含了客戶端特定資訊(狀態、資料等等)的結構

3.7.1 Buffer設計—— CIOCPBuffer

當時用非同步IO呼叫時,我們必須提供一個私有的buffer去被IO操作使用。當我們分配buffers時,需要考慮幾個問題:

·分配與釋放記憶體時很昂貴的,所以我們應該重用已經分配的buffers(記憶體)。所以,我們可以使用連結串列結構去節省buffers:

         // 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.

·某些時候,當一個IO呼叫完成時,我們可能會在buffer中得到部分的包資料,所以我們需要將buffer分割以便得到完整的訊息。這是通過IOCPS類中的SpiltBuffer方法來實現的。同時,有時候我們需要在buffer之間拷貝資訊,而這是通過IOCPS類中的AddAndFlush()來完成的。

·我們知道,我們同時需要為我們的buffer新增一個序列號以及一個狀態(IOType變數,IOZeroReadCompleted等等)

·我們同時還需要一些方法去把byte資料流轉換成資料,有些方法也在CIOCPBuffer類中被提供了

我們先前提到的所有問題的解決方案都已經在CIOCPBuffer類中得到支援了。

3.8 如何使用本原始碼?

通過從IOCP(見圖3)派生你自己的類以及使用虛方法、使用IOCPS類提供的功能(如執行緒池),這就可以實現任何型別的伺服器和客戶端,我們可以使用有限數量的執行緒來有效的應對大量的連線。

3.8.1伺服器/客戶端的啟動和關閉

要啟動伺服器,呼叫以下的方法:

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);

·nPortt

伺服器將進行監聽的埠號(如果是客戶端的話,我們可以讓他是-1

·iMaxNumConnections

最大可允許的連線數(使用一個很大的數字)

·iMaxIOWorkers

輸入輸出工作執行緒的數量

·nOfWorkers

邏輯工作執行緒的數量

·iMaxNumberOfFreeBuffer

我們將要節省下來進行重用的buffer最大數量(-1表示不重用,0表示不限制數量)

·iMaxNumberOfFreeContext

我們將要節省下來進行重用的客戶端資訊物件的最大數量(-1表示不重用,0表示不限制數量)

·bOrderedRead

是否需要有序讀取(我們已經在3.6.2中討論過這個)

·bOrderedSend

是夠需要有序的寫入(我們已經在3.6.2中討論過這個)

·iNumberOfPendlingReads

等待資料而掛起的非同步讀取迴圈的數量

建立一個遠端連線(客戶端模式下nPort = -1)呼叫下面的方法:

Connect(const CString &strIPAddr, int nPort)

·strIPAddr

遠端伺服器的IP地址

·nPort

關閉時請確認伺服器呼叫以下的方法:ShutDown().

For example:

MyIOCP m_iocp;

if(!m_iocp.Start(-1,1210,2,1,0,0))

AfxMessageBox("Error could not start the Client");

.

m_iocp.ShutDown();

4.1 程式碼描述

更多的程式碼細節,請閱讀原始碼中的註釋。

4.1.1 虛方法

·NotifyNewConnection

當新的連線被建立時被呼叫

·NotifyNewClientContext

當一個空的ClientContext結構被分配時被呼叫

·NotifyDisconnectedClient

當一個客戶端斷線時被呼叫

·ProcessJob

當一個工作執行緒試圖執行一個任務時呼叫

·NotifyReceivedPackage

當一個新的包到達時的提示

·NotifyFileCompleted

當一個檔案傳輸完成時的提示

4.1.2 重要的變數

請注意,所有需要使用共享變數的方法將對進行額外的加鎖,這對於避免非法訪問和重疊寫入非常重要的。所有需要加鎖且使用XXX名字的變數需要加鎖,他們有一個XXXLock變數

·m_ContextMapLock;

維護所有的客戶端資料(Socket,客戶端資料等等)

·ContextMap m_ContextMap;

·m_NumberOfActiveConnections

維護已有的連線數量

4.1.2 重要的方法

·GetNumberOfConnections()

返回連線數

·CString GetHostAdress(ClientContext* p)

返回給定客戶端Context的主機地址

·BOOL ASendToAll(CIOCPBuffer *pBuff);

傳送buffer中的內容給所有的客戶端。

·DisconnectClient(CString sID)

與一個給定唯一標示號的客戶端斷開連線

·CString GetHostIP()

返回本地IP地址

·JobItem* GetJob()

從佇列中移除JobItem,如果沒有任務的話將會返回NULL

·BOOL AddJob(JobItem *pJob)

新增任務到佇列中

·BOOL SetWorkers(int nThreads)

設定在任何時刻能被呼叫的邏輯工作執行緒數量

·DisconnectAll();

斷開所有的客戶端

·ARead(…)

建立一個非同步讀取

·ASend(…)

建立一個非同步傳送。傳送資料到客戶端

·ClientContext* FindClient(CString strClient)

根據給定的字串ID查詢客戶端。不是執行緒安全的

·DisconnectClient(ClientContext* pContext, BOOL bGraceful=FALSE);

斷開一個客戶端

·DisconnectAll()

斷開所有已有的連線

·StartSendFile(ClientContext *pContext)

傳送ClientContext結構中宣告的檔案,通過使用優化的transmitfile()方法

·PrepareReceiveFile(..)

準備一個接收檔案的連線,當你呼叫這個方法時,所有接收的位元組將寫入到一個檔案

·PrepareSendFile(..)

開啟一個檔案以及傳送一個包含檔案資訊的包到遠端連線。這個方法也會把Asread()禁用掉,直到檔案被傳送完成或者終止。

·DisableSendFile(..)

禁用檔案傳送模式

·DisableRecevideFile(..)

禁用接收模式

5.1 檔案傳輸

檔案傳輸使用過Winsock 2.0TransmitFile方法完成的。 TransmitFile方法使用一個已經連線的socket控制代碼進行傳輸檔案資料。這個方法使用作業系統的快取管理器(cache manager)來接收檔案資料,他提供了基於socket的高效能檔案資料傳輸。當我們使用非同步檔案傳輸時,這裡幾個需要注意的地方:

·除非TransmitFile方法返回,不能在該socket上進行讀取和寫入,因為這樣會損壞檔案。因此,所有在PrepareSendFile()之後對ASend的呼叫都會禁用掉。

·因為作業系統是有序讀取檔案資料的,你可以通過使用FILE_FLAG_SEQUENTIAL_SCAN去開啟檔案控制代碼來提高快取的效能。

·當傳送檔案時,我們使用核心的非同步例程呼叫(TF_USE_KERNEL_APC.使用TF_USE_KERNEL_APC可以獲得很好的效能。當我們在Context TransmitFile初始化時使用的執行緒使用非常繁重的計算任務時,這有可能會阻止APCs的執行。

檔案傳輸是以下面的順序運作的:伺服器通過呼叫PrepareSendFile()初始化檔案傳輸。當客戶端接收到檔案的資訊時,他會呼叫PrepareReceiveFile(...) ,然後傳送一個包給伺服器去開始檔案傳輸。當一個包到達伺服器時,伺服器呼叫StartSendFile()方法,這個犯非法使用了高效能的TransmitFile()方法去傳輸特定的檔案。

6 原始碼例子

提供的原始碼例子時一個echo客戶端/伺服器應用程式,他可以支援檔案傳輸(見圖4)。在原始碼中,類MyIOCP繼承自IOCP,他通過在4.1.1節中提到的使用虛方法處理客戶端和伺服器端的互動。

客戶端或者伺服器端最重要的部分是NotifyReceivePackage,如下所示:

         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;};

}

該方法處理接收到的訊息以及執行由遠端連線傳送的請求。在本例中,他只是簡單的echo或者檔案傳輸而已。原始碼被非常兩個專案,IOCP IOCPClient, 一個是伺服器端的連線而另外一個時客戶端的連線。

6.1 編譯上的問題

當使用VC++ 6.0 或者 .NET時,你可能會在使用CFile時得到一些奇怪的錯誤,如:

if (pContext->m_File.m_hFile !=

INVALID_HANDLE_VALUE) <-error C2446: '!=' : no conversion "

"from 'void *' to 'unsigned int'”

這個問題可以通過更新標頭檔案或者你的VC++6.0版本來避免,或者改一下型別轉換錯誤。在一些修改之後,伺服器/客戶端程式碼是可以在沒有MFC時使用的。

7. 特殊的考慮以及首要原則

當你在其他型別的應用程式中使用這個程式碼時,你可能會遇到一些跟本程式碼相關的陷阱以及“多執行緒程式設計”的陷阱。非確定性的錯誤時那些隨機發生的錯誤,通過相同的一系列操作是很難重現這些非確定性的錯誤的。這些錯誤是已存在的錯誤中最糟糕的型別,同時通常他們時因為在核心設計的編碼實現時產生的。當伺服器執行多個IO工作執行緒時,為連線的客戶端服務,如果程式設計師沒有很好的搞清楚多執行緒環境下的編碼問題,非確定性錯誤如非法訪問可能就會發生。

原則1

在沒有使用context lock(如下面的例子)對客戶端Context(ClientContext)進行加鎖時,不要讀取/寫入。提示(Notification)方法( nofity* (ClientContext * pContext))已經是“執行緒安全”的了,你可以在不對context進行加鎖的情況下訪問ClientContext的成員。

//…同時,記住當你加鎖一個Context時,其他的執行緒或者GUI可能會進行等待。

原則2

避免或者在”context lock”中具有複雜的“context locks”或者其他型別鎖的程式碼中“特殊考慮”,因為這有可能會引起死鎖(比如,A等待B,而B等待C,C等待A =>死鎖)。

上面的程式碼會引起死鎖。

原則3

不要在Notification方法外訪問Client Context(比如,Notify*(ClientContext * pContext)).如果你你要這麼做,你必須使用m_ContextMapLock.Lock();m_ContstMapLock.Unlock();程式碼如下:

8 改進

將來該程式碼會做出以下的更新:

1.AcceptEX()方法,接收一個新的連線將會被新增到原始碼,該方法去處理短連線以及DOS攻擊。

2.原始碼將會被移植到其他平臺,如Win32,STL,WTL

9 FAQ

Q1: 記憶體使用量(伺服器程式)將會在客戶端連線增加的時候穩定的增加,這可以從工作管理員看到。然而即使客戶端斷線時,記憶體使用量還是沒有下降,這是怎麼回事?

A1:程式碼會重用已經分配的buffers而不是不斷釋放和分配。你可以通過改變引數iMaxNumberOfFreeBuffer iMaxNumberOfFreeContext來改變這種方式,請閱讀3.8.1節。

Q2:我在.NET環境下編譯時遇到以下的錯誤:“error C2446:!= no conversion from 'unsigned int' to 'HANDLE'”等等,這是怎麼回事?

A2:這是因為SDK不同的標頭檔案造成的。只要把他轉換成HANDLE編譯器就可以讓你通過了。你也可以這是刪除一行程式碼 #define TRANSFERFILEFUNCTIONALITY然後再編譯一下。

Q3:原始碼可以在沒有MFC的情況下使用嗎?純Win32或者在一個服務裡面?

A3:原始碼只是暫時使用了GUI開發的。我開發這個客戶端/伺服器解決方案時使用了MFC環境作為GUI。當然,你可以在一個通常的伺服器環境下使用他。很多人已經這麼做了。只要把MFC相關的東西,如CString,CPtrList等等移走,用Win32的類去替換。我其實也不喜歡MFC,如果你改變的程式碼,請發一份給我,謝謝。

Q4:做得太好了!謝謝你所做的工作,你會在什麼時候不是在監聽執行緒中實現AcceptEX()

A4:當代碼穩定後。現在他已經很穩定了,但是我知道一些IO工作執行緒和掛起的讀操作的整合可能會導致一些問題。我很高興你喜歡我的程式碼,請投我一票!

Q5:為什麼啟動多個IO工作執行緒?如果你沒有多執行緒機器的話就沒有必要了?

A5:不,沒有必要開啟多個IO工作執行緒。只要一個執行緒就能處理所有的連線。一般的家庭計算機中,一個工作執行緒就可以有最佳的表現。你也不需要考慮潛在的非法訪問。但是當計算機變得越來越強大的時候(比如超執行緒,雙核等等)多執行緒的可能性為什麼不會有?

Q6:為什麼使用多個掛起的讀操作?他有什麼優勢?

A6:這取決都與開發者進行伺服器開發採取的策略,也就是說“許多併發連線”還是“高吞吐量伺服器”。擁有多個掛起的讀操作增加了伺服器的吞吐量,這是因為TCP/IP包將會被直接寫到我們傳入的buffer而不是TCP/IP棧(不會有雙緩衝)。如果伺服器知道客戶端突然傳送了大量資料,多個掛起的讀操作可以提高效能(高吞吐量)。然而,每個掛起的接收操作(使用WSARevc())會強迫核心去鎖住接收buffers進入非換頁池。這在實體記憶體滿時(很多併發連線時產生)就會引起WSAENBUFFERS錯誤。這必須被考慮進去。再者,如果你使用多於一個IO工作執行緒,訪問包的順序就會被打亂(因為IOCP的結構),這樣就需要額外的工作去維護順序以便不用多個掛起的讀操作。在這個設計中,當IO工作執行緒的數量大於1個時,多個掛起的讀操作是被關閉的,這樣就可以不需要處理重排序(重排序的話,序列號是必須在負載中存在的)。

Q7:在先前的文章中,你提到我們使用VirtualAlloc方法而不是new實現記憶體管理,為什麼你沒有實現呢?

A7:當你使用new來分配記憶體時,記憶體會被分配在虛擬記憶體或者實體記憶體。到底記憶體被分配到什麼地方時不知道的,記憶體可以被分配到兩個頁面上。這意味著當我們訪問一個特定的資料時,我們載入了太多的記憶體到實體記憶體。再者,你不知道記憶體是在虛擬記憶體還是實體記憶體,你也不能夠高數系統什麼時候“寫回到磁碟中是不需要的(如果我們在記憶體中已經不再關心該資料)。但是請注意!任何使用VirtualAlloc*new分配都將會填滿到64kB(頁面檔案大小)所以你如果你分配一個新的VAS繫結到實體記憶體,作業系統將會消耗一定量的實體記憶體去達到頁面大小,這將會消耗VAS去執行填滿到64kB。使用VirtualAlloc會比較麻煩: new malloc 在內部使用了 virtualAlloc,但是每次你使用new/delete分配記憶體時,很多其他的計算就會被完成,而你不需要控制你的資料(彼此關聯的資料)剛好在相同的頁面(而不是跨越了兩個頁面)。我發現相對於程式碼的複雜度來說,我能獲得的效能提高的非常小的。

10 References

Developing a Truly Scalable Winsock Server using IO Completion Ports”, norm.NET, Writing scalable server applications using IOCP, 22/03/2005.

Windows Sockets 2.0: Write Scalable Winsock Apps Using Completion Ports”, Anthony Jones & Amol Deshpande, 22/02/2005.

A reusable, high performance, socket server class - Part 1-6”, Len Holgate, A reusable, high performance, socket server class - Part 1, JetByte Limited, jetbyte.

11 Revision History

Version 1.0 - 2005-05-10

Initial public release.

Version 1.1 - 2005-06-13

Fixed some memory leakage (e.g., ~CIOCPBuffer()).

TransmitFile is now optional in the source code (by using #define TRANSFERFILEFUNCTIONALITY).

Some extra functions are added (by using #define SIMPLESECURITY).

Version 1.11 - 2005-06-18

Changes in IOCPS::ProcessPackage(…) to avoid access violation.

Error in CIOCPBuffer::Flush(..) fixed.

Changes in IOCPS::Connect(..) to release socket when an error occurs.

Version 1.12 - 2005-11-29

Changes in IOCPS::OnWrite(….) to avoiding entering an infinite loop.

Changes in OnRead(…) and OnZeroByteRead (…) to avoid access violation if memory is full and AllocateBuffer fails.

Changes in OnReadCompleted(…) to avoid access violation.

Changes in AcceptIncomingClient(..) to better handle a new connection when the maximum number of connections is reached.

Version 1.13 - 2005-12-29

ReleaseBuffer(…) added to ARead(..), ASend(..), AZeroByteRead(..) to avoid memory leakage.

Changes in DisconnectClient(…) and ReleaseClientContext(…) to avoid “duplicate key” error when clients rapidly connect and disconnect.

Changes in IOWorkerThreadProc(…), OnWrite(ClientContext *pContext,…), etc. to avoid buffer leakage.

Changes in DisconnectClient( unsigned int iID) to avoid access violation.

Added EnterIOLoop(..)/ExitIPLoop(..) to StartSendFile(..) and OnTransmitFileCompleted(..) to avoid access violation.

Some unessential error messages removed from the release mode, and additional debug information (e.g., TRACE(..) ) added to the source code in debug mode.

The function AcceptIncomingClients(..) changed and replaced with AssociateIncomingClientWithContext(..).

The Connect(..) function now uses AssociateIncomingClientWithContext(..).

Transfer file functions are now completely optional by making #define TRANSFERFILEFUNCTIONALITY.

Changes in DisableSendFile(..)and other file tr