程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> 一個簡單的IOCP(IO完成端口)服務器/客戶端類(1/2)

一個簡單的IOCP(IO完成端口)服務器/客戶端類(1/2)

編輯:關於C語言

作者:Amin Gholiha   翻譯:高慶余 文章來源:[url]http://www.codeproject.com/KB/IP/iocp_server_client.aspx[/url]   前言:源代碼使用比較高級的IOCP技術,它能夠有效的為多個客戶端服務,利用IOCP編程API,它也提供了一些實際問題的解決辦法,並且提供了一個簡單的帶回復的文件傳輸的客戶端/服務器。   1.1  要求: l         文章要求讀者熟悉C++, TCP/IP, 套接字(socket)編程, MFC, 和多線程。 l         源代碼使用Winsock 2.0和IOCP技術,並且要求: Ø         Windows NT/2000 or later: Requires Windows NT 3.5 or later. Ø         Windows 95/98/ME: 不支持 Ø         Visual C++ .NET, or a fully updated Visual C++ 6.0. 1.2  摘要: 在你開發不同類型的軟件,不久之後或者更晚,你必須得面對客戶端/服務器端的發展。對程序員來說,寫一個全面的客戶端/服務器的代碼是很困難的。這篇文章提供了一個簡單的,但卻強大的客戶端/服務器源代碼,它能夠被擴展到許多客戶端/服務器的應用程序中。源代碼使用高級的IOCP技術,這種技術能高效的為多個客戶端提供服務。IOCP技術提供了一種對 一個線程—一個客戶端one-thread-one client)這種瓶頸問題很多中問題的一個)的有效解決方案。它使用很少的一直運行的線程和異步輸入/輸出,發送/接收。IOCP技術被廣泛應用於各自高性能的服務器,像Apache等。源代碼也提供了一系列的函數,在處理通信、客戶端/服務器接收/發送文件函數、還有線程池處理等方面都會經常用到。文章主要關注利用IOCP應用API函數的實際解決方案,也提供了一個全面的代碼文檔。此外,也為你呈現了一個能處理多個連接、同時能夠進行文件傳輸的簡單回復客戶端/服務器。 2.1.     介紹: 這片文章提供了一個類,它是一個應用於客戶端和服務器的源代碼,這個類使用IOCP和異步函數,我們稍後會進行介紹。這個源代碼是根據很多代碼和文章得到的。 利用這些簡單的源代碼,你能夠: l         服務/連接多個客戶端和服務器。 l         異步發送和接收文件。 l         為了處理沉重的客戶端/服務器請求,創建並管理一個邏輯工作者線程池。logical worker thread pool)。 我們很難找到充分的,但簡單的能夠應對客戶端/服務器通信的源代碼。在網上發現的源代碼即復雜超過20個類),又不能提供足夠的功能。本問的代碼盡量簡單,也有好的文檔。我們將簡要介紹Winsock API 2.0提供的IOCP技術,編碼時遇到的疑難問題,以及這些問題的應對方案。 2.2.      異步輸入/輸出完成端口IOCP)簡介 一個服務器應用程序,假如不能夠同時為多個客戶端提供服務,那它就沒有什麼意義。通常的異步I/O調用,還有多線程都是這個目的。准確的說,一個異步I/O調用能夠立即返回,盡管有阻塞的I/O調用。同時,I/O異步調用的結果必須和主線程同步。這可以用很多種方法實現,同步可以通過下面方法實現: l         利用事件——當異步調用完成時設定的信號。這種方法的優點是線程必須檢查和等待這個信號被設定。 l         使用GetOverlappedResult函數——這個方法和上面方法有相同的優點。 l         使用異步程序調用APC)——這種方法有些缺點。第一,APC總是在正被調用的線程的上下文中被調用;第二,調用線程必須暫停,等待狀態的改變。 l         使用IOCP——這種方法的缺點是有些疑難問題必須解決。使用IOCP編碼多少有些挑戰。 2.2.1       為什麼使用IOCP 使用IOCP,我們能夠克服 一個線程 —— 一個客戶端 問題。我們知道,假如軟件不是運行在一個真實的多處理器機器上,它的性能會嚴重下降。線程是系統的資源,它們即不是無限的,也不便宜。 IOCP提供了一種利用有限的I/O工作線程)公平的處理多客戶端的輸入/輸出問題的解決辦法。線程並不被阻塞,在無事可作的情況下也不使CPU循環。 2.3.      什麼是IOCP 我們已經知道,IOCP僅僅是一個線程同步對象,有點像信號量semaphore),因此IOCP並不是一個難懂的概念。一個IOCP對象和很多支持異步I/O調用的I/O對象相聯系。線程有權阻塞IOCP對象,直到異步I/O調用完成。 3              IOCP如何工作 為了得到更多信息,建議你參考其它的文章1, 2, 3, see References)。 使用IOCP,你必須處理3件事情。將一個套接字綁定到一個完成端口,使用異步I/O調用,和使線程同步。為了從異步I/O調用得到結果,並知道一些事情,像哪個客戶端進行的調用,我們必須傳遞兩個參數:CompletionKey參數還有OVERLAPPED結構體 3.1.     CompletionKey參數 CompletionKey參數是第一個參數,是一個DWORD類型的變量。你可以給它傳遞你想要的任何值,這些值總是和這個參數聯系。通常,指向結構體的指針,或者包含客戶端指定對象的類的指針被傳遞給這個參數。在本文的源代碼中,一個ClientContext結構體的指針被傳遞給CompletionKey參數。 3.2.     OVERLAPPED參數 這個參數通常被用來傳遞被異步I/O調用的內存。要重點強調的是,這個數據要被加鎖,並且不要超出物理內存頁,我們之後進行討論。 3.3.     將套接字和完成端口進行綁定 一旦創建了完成端口,通過調用CreateIoCompletionPort函數可以將一個套接字和完成端口進行綁定,像下面的方法:  

BOOL IOCPS::AssociateSocketWithCompletionPort(SOCKET socket, 
               HANDLE hCompletionPort, DWORD dwCompletionKey)
   {
       HANDLE h = CreateIoCompletionPort((HANDLE) socket, 
             hCompletionPort, dwCompletionKey, m_nIOWorkers);
       return h == hCompletionPort;
   }
  3.4.     進行異步I/O調用 通過調用WSASend, WSARecv函數,進行實際的異步調用。這些函數也需要包含將要被用到的內存指針的參數WSABUF。通常情況下,當服務器/客戶端想要執行一個I/O調用操作,它們並不直接去做,而是發送到完成端口,這些操作被I/O工作線程執行。這是因為,要公平的分配CPU。通過給完成端口傳遞一個狀態,進行I/O調用。象下面這樣:
BOOL bSuccess = PostQueuedCompletionStatus(m_hCompletionPort, 
                       pOverlapBuff->GetUsed(), 
                       (DWORD) pContext, &pOverlapBuff->m_ol);
  3.5.     線程的同步 通過調用GetQueuedCompletionStatus函數進行線程的同步看下面)。這個函數也提供了CompletionKey 參數 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開發客戶端/服務器程序時會遇到它們。它們是: l       WSAENOBUFS出錯問題。 l         數據包的重排序問題。 l         訪問紊亂access violation)問題。   3.6.1   WSAENOBUFS出錯問題。 這個問題並不直觀,並且很難檢查。因為,乍一看,它很像普通的死鎖,或者內存洩露。假設你已經弄好了你的服務器並且能夠很好的運行。當你對服務器進行承受力測試的時候,它突然掛機了。如果你幸運,你會發現這和WSAENOBUFS出錯有關。 伴隨著每一次的重疊發送和接收操作,有數據的內存提交可能會被加鎖。當內存被鎖定時,它不能越過物理內存頁。操作系統會強行為能夠被鎖定的內存的大小設定一個上限。當達到上限時,重疊操作將失敗,並發送WSAENOBUFS錯誤。 假如一個服務器在在每個連接上提供了很多重疊接收,隨著連接數量的增長,很快就會達到這個極限。如果服務器能夠預計到要處理相當多的並發客戶端的話,服務器可以在每個連接上僅僅回復一個0字節的接收。這是因為沒有接收操作和內存無關,內存不需要被鎖定。利用這個方法,每一個套接字的接收內存都應該被完整的保留,這是因為,一旦0字節的接收操作完成,服務器僅僅為套接字的接收內存的所以數據內存返回一個非阻塞的接收。利用WSAEWOULDBLOCK當非阻塞接收失敗時,也沒有數據被阻塞。這種設計的目的是,在犧牲數據吞吐量的情況下,能夠處理最大量的並發連接。當然,對於客戶端如何和服務器交互,你知道的越多越好。在以前的例子中,每當0字節的接收完成,返回存儲了的數據,馬上執行非阻塞接收。假如服務器知道客戶端突然發送數據,當0字節接收一旦完成,為防止客戶端發送一定數量的數據大於每個套接字默認的8K內存大小),它可以投遞一個或多個重疊接收。 源代碼提供了一個簡單的解決WSAENOBUFS錯誤的可行方案。對於0字節內存,我們采用WSARead函數OnZeroByteRead))。當調用完成,我們知道數據在TCP/IP棧中,通過采用幾個異步WSARead函數讀取MAXIMUMPACKAGESIZE的內存。這個方法在數據達到時僅僅鎖定物理內存,解決了WSAENOBUFS問題。但是這個方案降低了服務器的吞吐量見第9部分的Q6和A6例子)。 3.6.2   數據包的重排序問題 在參考文獻3中也討論了這個問題。盡管使用IOCP,可以使數據按照它們被發送的順序被可靠的處理,但是線程表的結果是實際工作線程的完成順序是不確定的。例如,假如你有兩個I/O工作線程,並且你應該接收“字節數據塊1、字節數據塊2 、字節數據塊3”,你可以按照錯誤的順序處理它們,也就是“字節數據塊2、字節數據塊1 、字節數據塊3”。這也意味著,當你通過把發送請求投遞到IO完成端口來發送數據時,數據實際上是被重新排序後發送的。 這個問題的一個實際解決辦法是,為我們的內存類增加順序號,並按照順序號處理內存。意思是,具有不正確號的內存被保存備用,並且因為性能原因,我們將內存保存在希哈表中例如m_SendBufferMap和m_ReadBufferMap)。 要想得到更多這個方案的信息,請查看源代碼,並在IOCPS類中查看下面的函數: l         GetNextSendBuffer (..) 和GetNextReadBuffer(..), 為了得到排序的發送或接收內存。 l         IncreaseReadSequenceNumber(..)和IncreaseSendSequenceNumber(..), 為了增加順序號。 3.6.3   異步阻塞讀和字節塊包處理問題 大多數服務器協議是一個包,這個包的基礎是第一個X位的描述頭,它包含了完整包的長度等詳細信息。服務器可以解讀這個頭,可以算出還需要多少數據,並一直解讀,直到得到一個完整的包。在一個時間段內,服務器通過異步讀取調用是很好的。但是,假若我們想全部利用IOCP服務器的潛力,我們應該有很多的異步讀操作等待數據的到達。意思是很多異步讀無順序完成像在3.6.2討論的),通過異步讀操作無序的返回字節塊流。還有,一個字節塊流byte chunk streams)能包含一個或多個包,或者包的一部分,如圖1所示: 圖1 這個圖表明部分包綠色)和完整的包黃色)在字節塊流中是如何異步到達的。 這意味著我們要想成功解讀一個完整包,必須處理字節流數據塊byte stream chunks)。還有,我們必須處理部分包,這使得字節塊包的處理更加困難。完整的方案可以在IOCP類裡的ProcessPackage(..)函數中找到。 3.6.4   訪問紊亂access violation)問題。 這是一個次要問題,是編碼設計的結果,而不是IOCP的特有問題。倘若客戶端連接丟失,並且一個I/O調用返回了一個錯誤標識,這樣我們知道客戶端已經不在了。在CompletionKey參數中,我們為它傳遞一個包含了客戶端特定數據的結構體的指針。假如我們釋放被ClientContext結構體占用的內存,被同一個客戶端執行I/O調用所返回的錯誤碼,我們為ClientContext指針傳遞雙字節的CompletionKey變量,試圖訪問或刪除CompletionKey參數,這些情況下會發生什麼?一個訪問紊亂發生了。 這個問題的解決辦法是為ClientContext結構體增加一個阻塞I/O調用的計數(m_nNumberOfPendlingIO)當我們知道沒有阻塞I/O調用時我們刪除這個結構體。EnterIoLoop(..) 函數和 ReleaseClientContext(..).函數就是這樣做的。 3.7       源代碼總攬 源代碼的目標是提供一些能處理與IOCP有關的問題的代碼。源代碼也提供了一些函數,它們在處理通信、客戶端/服務器接收/發送文件函數、還有線程池處理等方面會經常用到。 圖2 源代碼IOCPS類函數總攬 我們有很多I/O工作線程,它們通過完成端口IOCP)處理異步I/O調用,這些工作線程調用一些能把需要大量計算的請求放到一個工作隊列著中的虛函數。邏輯工作線程從隊列中渠道任務,進行處理,並通過使用一些類提供的函數將結果返回。圖形用戶界面GUI)通常使用Windows消息,通過函數調用,或者使用共享的變量,和主要類進行通信。 圖3 圖3顯示了類的總攬。 圖3中的類歸納如下: l         CIOCPBuffer:管理被異步I/O調用使用的內存的類。 l         IOCPS:處理所有通信的主要類。 l         JobItem:包含被邏輯工作線程所執行工作的結構體。 l         ClientContext:保存客戶端特定信息的結構體例如:狀態、數據 )。   3.7.1   內存設計——CIOCPBuffer類 當使用異步I/O調用時,我們必須為I/O操作提供一個私有內存空間。當我們分配內存時要考慮下面一些情況: l         分配和釋放內存是很費時間的,因此我們要反復利用分配好的內存。所以,我們像下面所示將內存保存在一個連接表中。 ·                // 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. l         有時,當一個異步I/O調用完成時,我們可能在內存中有部分包,因此我們為了得到一個完整的消息,需要分離內存。在CIOCPS類中的函數SplitBuffer)可以實現這一目標。我們有時也需要在兩個內存間復制信息, CIOCPS類中的AddAndFlush)函數可以實現。 l         我們知道,我們為我們的內存增加序列號和狀態變量IOZeroReadCompleted))。 l         我們也需要字節流和數據相互轉換的方法,在CIOCPBuffer類中提供了這些函數。 在我們的CIOCPBuffer類中,有上面所有問題的解決辦法。 3.8       如何使用源代碼 從IOCP中派生你自己的類,使用虛函數,使用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);
l        nPortt :服務器將監聽的端口號在客戶端模式設為-1)。 l        iMaxNumConnections:最多允許連接數。 l         iMaxIOWorkers :輸入/輸出工作線程數。 l         nOfWorkers:邏輯工作者數在運行時能被改變)。 l         iMaxNumberOfFreeBuffer :保留的重復利用的內存的最大數量-1:無 ,0:無窮)。 l         iMaxNumberOfFreeContext :保留的重復利用的客戶端信息的最大數量-1:無 ,0:無窮)。 l         bOrderedRead :用來進行順序讀。 l         bOrderedSend :用來進行順序發送。 l         iNumberOfPendlingReads :等待數據的異步讀循環的數量。在連接到一個遠端的連接時調用下面的函數:
Connect(const CString &strIPAddr, int nPort)
l         strIPAddr :遠端服務器的IP地址。 l         nPort:端口。 關閉服務器,調用函數:ShutDown)。 例如:
MyIOCP m_iocp;
if(!m_iocp.Start(-1,1210,2,1,0,0))
AfxMessageBox("Error could not start the Client");
….
m_iocp.ShutDown();
備注: 1:以上為全文的第一部分,全文共兩部分。由於時間所限,第二部分要等兩天才能和大家見面,請大家關注。因此給大家造成的影響,還請見諒! 2:由於譯者水平所限,錯誤在所難免,歡迎指正,謝謝。 3:希望結交朋友,歡迎大家加我為好友。我的MSN:[email protected]

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved