服務器開發基礎-Tcp/Ip網絡模型—完成端口(Completion Port)模型


本文對於初學網絡編程的極為友好,文中所有代碼全部基於C語言實現,文中見解僅限於作者對於完成端口的初步認識,由於作者才疏學淺,出現的錯誤和紕漏,麻煩您一定要指出來,咱們共同進步。謝謝!!!


 

完成端口(completion Port)

 

前言:

網絡通信分為兩種:同步和異步。

  在同步通信中,每一次接受數據都會導致主線程的掛起,從而阻塞住了其他操作。為了解決這一問題,我們通常會采取同步通信+多線程的策略,即為每一個連入的Socket分配一個線程。然而隨着連入的Socket的數量的增加,線程的數量也在增加,這樣CPU則需要不停地進行線程的切換,因此難以成為高性能的服務器程序。
  異步通信則可以把接收數據這一操作交給內核,即在內核接收數據的時候,主線程可以不用被阻塞並且繼續執行其他操作,而一旦接收數據完成以后,再由內核通知主線程。而如何通知主線程是一個關鍵,不同的異步通信策略有着不同的通知方式。
  在這樣的情況下,完成端口這一I/O模型被提出,成為目前Windows下性能最好的I/O模型之一。

 (注:文中所有函數參數均已MSDN上的為標准,文中觀點僅代表個人理解,如有錯誤,還請多多包涵並及時留言,我會第一時間改正,謝謝!!!)

完成端口模型簡介:

上面所說的“初學”指你已經熟悉Socket進行TCP/IP編程的基本原理,前期基本的概念我這里就略過不提了,直入主題。

嗯~~!怎么說呢,完成端口是Windows的一種機制,這種機制是在重疊IO上的優化,所以說完成端口也是基於重疊結構的,換句話說如果對於重疊IO結構特別熟悉的話,那么完成端口對於你來說就特別簡單。為什么說完成端口是在重疊IO上的一種優化呢?對比一下下面第一張和第二轉張結構圖,一定會有人好奇,為什么兩張圖差不多一樣呢?仔細看會發現完成端口結構圖里面操作系統有一步操作是將通知放進隊列(第三張結構圖,模仿消息隊列原理系統會創建一個通知隊列)。到這就可以說明完成端口在重疊IO具體優化的是什么了,熟悉重疊IO的都知道,重疊IO最嚴重的問題就是線程數量,有多少的客戶端,那就得有多少根線程。肯定會有人說線程多了不是更好嗎?速度跟快嗎?程序執行時間更短碼?那就錯了,恰恰是相反的,上面我也大致提到了線程太多的問題。了解操作系統的都知道,線程在一個周期內分得的時間越多,那么執行就越快。換而言之如果線程數量增加,那么每根線程上所分得的時間就會變短,再加上切換線程的時間,這樣一來反而時間更久。而理論上最優的線程數就是和CPU核數一樣(還有其他的幾種:CPU核數*2、CPU核數*2+2。為什么會有這幾種情況,這里就不多多介紹了。)這樣以來就可以充分的利用CPU資源。不過這也要求線程函數中沒有調用諸如Sleep(),WSAWaitForMultipleEvents()...這類函數,這類函數會使線程掛起(但不占cpu時間片),從而使得CPU某個核空閑了,這就不好了,所以一般我們多建個兩三根,以解決此類情況,讓CPU不停歇,從而在整體上保證程序執行效率。本文采取的是和CPU核數一樣多。而對於重疊IO中的無序性問題,完成端口采用了上述所說的創建一個通知隊列(第三張結構圖)來進行管理,從而達到有序。所以說完成端口是對重疊IO的改進也不為過。

    

完成端口原理以及部分函數用法:

1.CreateIoCompletionPort()函數創建一個完成端口。

對於 CreateIoCompletionPort()函數它有兩個功能一個功能是創建完成端口,另一個功能就是將SOCKET與完成端口進行綁定,在這里就是創建完成端口。至於說功能不一樣,也就是參數不同而已。

 

 HANDLE WINAPI CreateIoCompletionPort(  
    __in      HANDLE  FileHandle,           
     __in_opt  HANDLE  ExistingCompletionPort, 
     __in      ULONG_PTR CompletionKey,       
     __in      DWORD NumberOfConcurrentThreads 
);  

 

參數(Parameters):

 

此函數若要是在不關聯I/O完成端口的情況下創建I/O完成端口,如果指定了參數FileHandleINVALID_HANDLE_VALUE,在這種情況下,ExistingCompletionPort參數必須為NULL,而CompletionKey參數則被忽略可填0;那么參數NumberOfConcurrentThreads是允許此端口上最多同時運行的線程數量,一般設置為零(這里的零並不是參數3中忽略的意思,而是自動獲取CPU核數。當然你也可以不用自動獲取自己去指定通過函數GetSystemInfo())。

(注:這里簡單介紹一下GetSystemInfo()函數的用法。這個函數也特別簡單,參數也就一個SYSTEM_INFO類型的結構體,在這里我們只需要專注這個結構體里面的DWORD dwNumberOfProcessors成員即可; )

返回值(Return value):

函數執行成功會返回一個可用的端口變量,否則返回0;這里可以用GetLastError()獲取錯誤碼。
(注意:這里為什么不用WSAGetLastError()獲取錯誤碼?
創建完成端口是Windows的一種機制,不是專門用於網絡的,和網絡是無關的。完成端口的模型只是利用了這種機制。)

2.用 CreateIoCompletionPort()函數將重疊套接字(客戶端SOCKET+服務器SOCKET)與完成端口進行綁定。

毋庸置疑這就是CreateIoCompletionPort()函數的第二個功能:綁定重疊套接字與完成端口

 

 HANDLE WINAPI CreateIoCompletionPort(  
    __in      HANDLE  FileHandle,           
     __in_opt  HANDLE  ExistingCompletionPort, 
     __in      ULONG_PTR CompletionKey,       
     __in      DWORD NumberOfConcurrentThreads 
);  

參數(Parameters):

FileHandle:要綁定的SOCKET。
ExistingCompletionPort:創建完成端口時返回的變量。
CompletionKey:這個參數就要和下面即將講到的一個函數GetQueuedCompletionStatus()的參數3關聯在一起比較着看,會很清楚。
      先大概說一下GetQueuedCompletionStatus()這個函數,上面我也提到過系統會把所有SOCKET上的通知放進通知隊列里面,而GetQueuedCompletionStatus()
      
函數就是從這個隊列里面依次往外拿出通知然后進行分類處理,而CreateIoCompletionPort()函數的參數3就是告知函數GetQueuedCompletionStatus()
      隊列里面拿出的事件通知具體是哪一個SOCKET上的發生的。
所以這里的參數就是要傳入具體發生事件通知的SOCKET(如果是把所有的SOCKET裝進數組里面的話,這里也可以傳具體SOCKET的下標)。
NumberOfConcurrentThreads:如果參數ExistingCompletionPort不是NULL,則忽略此參數。可填0。

返回值(Return value):

函數執行成功返回自己,也就是再返回參數2;如果執行不成功那肯定就不等於參數2了啊!

3.使用AcceptEx(),WSARecv(),WSASend()函數投遞請求。(這三個異步函數就偷個懶這里不過多的介紹了,因為是直接拿的重疊IO里面的函數,哈哈哈)

4.使用CreateThread()函數創建線程,使用GetSystemInfo()獲得操作系統相關信息,比如獲取CPU核數。

(GetSystemInfo()數上文已經大致介紹了一下,和網絡也沒有太大的關系這里就不詳細介紹了,想了解的可以看一下MSDN)

創建線程函數CreateThread()的功能就是一次創建一根線程,如果要創建多根線程,可以用循環

 

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES   lpThreadAttributes,
  SIZE_T                  dwStackSize,
  LPTHREAD_START_ROUTINE  lpStartAddress,
  __drv_aliasesMem LPVOID lpParameter,
  DWORD                   dwCreationFlags,
  LPDWORD                 lpThreadId
);

參數(Parameters):

lpThreadAttributes:線程句柄是否被繼承,不繼承就填NULL。如果不繼承就是子線程與父線程共享一份線程句柄,相當於全局變量;
          如果繼承的話子類復制一份父類的此時就會有兩份,相當於局部變量自己用自己的;
          還有一個功能就是指定線程的權限,默認權限就填NULL。
          所以此參數填NULL就好。
dwStackSize:線程大小(棧區大小),填0,默認大小為1M。可以指定大小以字節為單位。
lpStartAddress:線程函數地址;
        線程函數函數頭:DWORD WINAPI ThreadProc(LPVOID lpParameter); 這個函數的參數由函數CreateThread()的參數4傳入
lpParameter:外部給線程傳遞數據,把傳遞進來的數據傳遞給參數3中的線程函數中;
dwCreationFlags:線程創建出來的一種執行狀態;
         立即執行填0,也就是立即獲得時間片分得的時間;
         掛起狀態填CREATE_SUSPENDED(不占用時間周期)。調用ResumeThread()函數,激活掛起狀態的線程。
         如果填STACK_SIZE_PARAM_IS_A_RESERVATION,這個宏是和參數2關聯在一起的。如果想修改棧區大小,
         設置了這個宏,參數2就是修改的棧保留大小,即虛擬內存上棧得大小;如果沒有設置修改的就是棧提交大小,即物理內存上的大小。
lpThreadId:線程ID,每根線程的ID都不一樣。不用就填NULL。

返回值(Return value):

函數執行成功返回線程句柄,失敗返回NULL。可以用GetLastError()獲得錯誤碼。
線程句柄是內核對象,用完要釋放用CloseHandle()函數。

5.當系統異步處理完成后,會生成一個通知,這個通知就會放進通知隊列里面,而完成端口就可以理解為通知隊列的頭。該隊列由操作系統系統創建,維護。

6.通過GetQueuedCompletionStatus()函數從隊列頭一個一個往外拿,進行處理。

 如果通知隊列里沒有通知,那么會使線程處於掛起狀態,這樣就不會占用CPU時間。 

BOOL GetQueuedCompletionStatus(
  HANDLE       CompletionPort,
  LPDWORD      lpNumberOfBytesTransferred,
  PULONG_PTR   lpCompletionKey,
  LPOVERLAPPED *lpOverlapped,
  DWORD        dwMilliseconds
);

 

參數(Parameters):

CompletionPort:創建完成端口時返回的變量。
lpNumberOfBytesTransferred:收到或發送的字節數。如果是客戶端SOKCET發生事件通知並且此參數返回的是0,那就說明是客戶端退出。
lpCompletionKey:在上面寫綁定重疊套接字與完成端口的時候已經介紹到了此參數,這里就不過多說了。它就是接收綁定完成端口的時候傳進來的SOCKET。
lpOverlapped:返回一個發生事件通知的SOCKET上所綁定的那個重疊結構的地址。
dwMilliseconds:等待時間。可以是具體的等待時間以毫秒為單位;也可以一直等到有事件通知為止,一直等填INFINITE。

返回值(Return value):

 函數執行成功返回TRUE,失敗返回FALSE,可以用GetLastError()獲取錯誤碼。

 

完成端口代碼邏輯:

1.打開網絡庫(WSAStartup())

2.校驗版本(副版本:HIBYTE()、主版本:LOBYTE())

3.創建SOCKET(WSASocket())

4.綁定地址與端口號(bind())

5.創建完成端口(CreateIoCompletionPort())

6.將重疊套接字(客戶端SOCKET+服務器SOCKET)與完成端口進行綁定(CreateIoCompletionPort())

7.開始監聽(listen())

8.創建線程(CreteThread())

9.獲取事件通知(GetQueuedCompletionPort())進行分類處理

10.釋放

 

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM