完成端口聽起來好像很神祕和複雜,其實並無想象的那麼難。這方面的文章在論壇上能找到的我差很少都看過,寫得好點的就是CSDN.NET上看到的一組系 列文章,不過我認爲它只是簡單的翻譯了一下Network Programming for Microsoft Windows 2nd 中的相關內容,附上的代碼好像不是原書中的,多是另外一本外文書裏的。我看了之後,以爲還不如看原版的更容易理解。因此在個人開始部分,我主要帶領初學者 理解一下完成端口的有關內容,是我開發的經驗,其餘的請參考原書的相關內容。
採用完成端口的好處是,操做系統的內部重疊機制能夠保證大量的網絡請求都被服務器處理,而不是像WSAAsyncSelect 和WSAEventSelect的那樣對併發的網絡請求有限制,這一點從上一章的測試表格中能夠清楚的看出。
完成端口就像一種消息通知的機制,咱們建立一個線程來不斷讀取完成端口狀態,接收到相應的完成通知後,就進行相應的處理。其實感受就像 WSAAsyncSelect同樣,不過仍是有一些的不一樣。好比咱們想接收消息,WSAAsyncSelect會在消息到來的時候直接通知Windows 消息循環,而後就能夠調用WSARecv來接收消息了;而完成端口則首先調用一個WSARecv表示程序須要接收消息(這時可能尚未任何消息到來),但 是隻有當消息來的時候WSARecv纔算完成,用戶就能夠處理消息了,而後再調用一個WSARecv表示等待下一個消息,如此不停循環,我想這就是完成端 口的最大特色吧。
Per-handle Data 和 Per-I/O Operation Data 是兩個比較重要的概念,Per-handle Data用來把客戶端數據和對應的完成通知關聯起來,這樣每次咱們處理完成通知的時候,就能知道它是哪一個客戶端的消息,而且能夠根據客戶端的信息做出相應 的反應,我想也能夠理解爲Per-Client handle Data吧。Per-I/O Operation Data則不一樣,它記錄了每次I/O通知的信息,好比接收消息時咱們就能夠從中讀出消息的內容,也就是和I/O操做有關的信息都記錄在裏面了。當你親手實 現完成端口的時候就能夠理解他們的不一樣和用途了。
CreateIoCompletionPort函數中有個參數NumberOfConcurrentThreads,完成端口編程裏有個概念Worker Threads。這裏比較容易引發混亂,NumberOfConcurrentThreads須要設置多少,又須要建立多少個Worker Threads纔算合適?NumberOfConcurrentThreads的數目和CPU數量同樣最好,由於少了就無法利用多CPU的優點,而多了則 會由於線程切換形成性能降低。Worker Threads的數量是否是也要同樣多呢,固然不是,它的數量取決於應用程序的須要。舉例來講,咱們在Worker Threads裏進行消息處理,若是這個過程當中有可能會形成線程阻塞,那若是咱們只有一個Worker Thread,咱們就不能很快響應其餘客戶端的請求了,而只有當這個阻塞操做完成了後才能繼續處理下一個完成消息。可是若是咱們還有其餘的Worker Thread,咱們就能繼續處理其餘客戶端的請求,因此到底須要多少的Worker Thread,須要根據應用程序來定,而不是能夠事先估算出來的。若是工做者線程裏沒有阻塞操做,對於某些狀況來講,一個工做者線程就能夠知足須要了。編程
===========================================================windows
「完成端口」模型是迄今爲止最爲複雜的—種I/O模型。然而。倘若—個應用程序同時須要管理爲數衆多的套接字,那麼採用這種模型。每每能夠達到最佳的系統性能,然而不幸的是,該模型只適用於如下操做系統(微軟的):Windows NT和Windows 2000操做系統。因其設計的複雜性,只有在你的應用程序須要同時管理數百乃至上千個套接字的時候、並且但願隨着系統內安裝的CPU數量的增多、應用程序的性能也能夠線性提高,才應考慮採用「完成端口」模型。要記住的一個基本準則是,假如要爲Windows NT或windows 2000開發高性能的服務器應用,同時但願爲大量套接字I/O請求提供服務(Web服務器即是這方面的典型例子),那麼I/O完成端口模型即是最佳選擇.服務器
從本質上說,完成端口模型要求咱們建立一個Win32完成端口對象,經過指定數量的線程對重疊I/O請求進行管理。以便爲已經完成的重疊I/O請求提供服務。要注意的是。所謂「完成端口」,實際是Win3二、Windows NT以及windows 2000採用的一種I/O構造機制,除套接字句柄以外,實際上還可接受其餘東西。然而,本節只打算講述如何使用套接字句柄,來發揮完成端口模型的巨大威力。使用這種模型以前,首先要建立一個I/O完成端口對象,用它面向任意數量的套接字句柄。管理多個I/O請求。要作到這—點,須要調用CreateIoCompletionPort函數。該函數定義以下: 網絡
HANDLE CreateIoCompletionPort(數據結構
HANDLE FileHandle,併發
HANDLEExistingCompletionPort,框架
DWORD CompletionKey,異步
DWORD NumberOfConcurrentThreads函數
);性能
在咱們深刻探討其中的各個參數以前,首先要注意意該函數實際用於兩個明顯有別的目的:
■用於建立—個完成端口對象。
■將一個句柄同完成端口關聯到一塊兒。
最開始建立—個完成端口的時候,惟一感興趣的參數即是NumberOfConcurrentThreads 併發線程的數量);前面三個參數都會被忽略。NumberOfConcurrentThreads 參數的特殊之處在於.它定義了在一個完成端口上,同時容許執行的線程數量。理想狀況下咱們但願每一個處理器各自負責—個線程的運行,爲完成端口提供服務,避免過於頻繁的線程「場景」切換。若將該參數設爲0,說明系統內安裝了多少個處理器,便容許同時運行多少個線程!可用下述代碼建立一個I/O完成端口:
CompetionPort=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0)
該語加的做用是返問一個句柄.在爲完成端口分配了—個套接字句柄後,用來對那個端
口進行標定(引用)。
1.工做者線程與完成端口
成功建立一個完成端口後,即可開始將套接字句柄與對象關聯到一塊兒。但在關聯套接字以前、首先必須建立—個或多個「工做者線程」,以便在I/O請求投遞給完成端口對象後。爲完成端口提供服務。在這個時候,你們或許會以爲奇怪、到底應建立多少個線程。以便爲完成端口提供服務呢?這實際正是完成端口模型顯得頗爲「複雜」的—個方面, 由於服務I/O請求所需的數量取決於應用程序的整體設計狀況。在此要記住的—個重點在於,在咱們調用CreateIoComletionPort時指定的併發線程數量,與打算建立的工做者線程數量相比,它們表明的並不是同—件事情。早些時候,咱們曾建議你們用CreateIoCompletionPort函數爲每一個處理器都指定一個線程(處理器的數量有多少,便指定多少線程)以免因爲頻繁的線程「場景」交換活動,從而影響系統的總體性能。CreateIoCompletionPort函數的NumberofConcurrentThreads參數明確指示系統: 在一個完成端口上,一次只容許n個工做者線程運行。假如在完成端門上建立的工做者線程數量超出n個.那麼在同一時刻,最多隻容許n個線程運行。但實際上,在—段較短的時間內,系統有可能超過這個值。但很快便會把它減小至事先在CreateIoCompletionPort函數中設定的值。那麼,爲什麼實際建立的工做者線程數最有時要比CreateIoCompletionPort函數設定的多—些呢?這樣作有必要嗎?如先前所述。這主要取決於應用程序的整體設計狀況,假設咱們的工做者線程調用了一個函數,好比Sleep()或者WaitForSingleobject(),但卻進入了暫停(鎖定或掛起)狀態、那麼容許另—個線程代替它的位置。換行之,咱們但願隨時都能執行儘量多的線程;固然,最大的線程數量是事先在CreateIoCompletonPort調用裏設定好的。這樣—來。假如事先預料到本身的線程有可能暫時處於停頓狀態,那麼最好可以建立比CreateIoCompletionPort的NumberofConcurrentThreads參數的值多的線程.以便到時候充分發揮系統的潛力。—旦在完成端口上擁有足夠多的工做者線程來爲I/O請求提供服務,即可着手將套接字句柄同完成端口關聯到一塊兒。這要求咱們在—個現有的完成端口上調用CreateIoCompletionPort函數,同時爲前三個參數: FileHandle,ExistingCompletionPort和CompletionKey——提供套接字的信息。其中,FileHandle參數指定—個要同完成端口關聯在—一塊兒的套接字句柄。
ExistingCompletionPort參數指定的是一個現有的完成端口。CompletionKey(完成鍵)參數則指定要與某個特定套接字句柄關聯在—起的「單句柄數據」,在這個參數中,應用程序可保存與—個套接字對應的任意類型的信息。之因此把它叫做「單句柄數據」,是因爲它只對應着與那個套接字句柄關聯在—起的數據。可將其做爲指向一個數據結構的指針、來保存套接字句柄;在那個結構中,同時包含了套接字的句柄,以及與那個套接字有關的其餘信息。就象本章稍後還會講述的那樣,爲完成端口提供服務的線程例程可經過這個參數。取得與其套字句柄有關的信息。
根據咱們到目前爲止學到的東西。首先來構建—個基本的應用程序框架。
程序清單8—9向人家闡述瞭如何使用完成端口模型。來開發—個迴應(或「反射’)服務器應用。
在這個程序中。咱們基本上按下述步驟行事:
1) 建立一個完成端口。第四個參數保持爲0,指定在完成端口上,每一個處理器一次只容許執行一個工做者線程。
2) 判斷系統內到底安裝了多少個處理器。
3) 建立工做者線程,根據步驟2)獲得的處理器信息,在完成端口上,爲已完成的I/O請求提供服務。在這個簡單的例子中,咱們爲每一個處理器都只建立—個工做者線程。這是出於事先已經預計到,到時候不會有任何線程進入「掛起」狀態,形成因爲線程數量的不足,而使處理器空閒的局面(沒有足夠的線程可供執行)。調用CreateThread函數時,必須同時提供—個工做者線程,由線程在建立好執行。本節稍後還會詳細討論線程的職責。
4) 準備好—個監聽套接字。在端口5150上監聽進入的鏈接請求。
5) 使用accept函數,接受進入的鏈接請求。
6) 建立—個數據結構,用於容納「單句柄數據」。 同時在結構中存入接受的套接字句柄。
7) 調用CreateIoCompletionPort將自accept返回的新套接字句柄向完成端口關聯到 一塊兒,經過完成鍵(CompletionKey)參數,將但句柄數據結構傳遞給CreateIoCompletionPort。
8) 開始在已接受的鏈接上進行I/O操做。在此,咱們但願經過重疊I/O機制,在新建的套接字上投遞一個或多個異步WSARecv或WSASend請求。這些I/O請求完成後,一個工做者線程會爲I/O請求提供服務,同時繼續處理將來的I/O請求,稍後便會在步驟3)指定的工做者例程中。體驗到這一點。
9) 重複步驟5)—8)。直到服務器終止。
程序清單8。9 完成端口的創建
StartWinsock() //步驟一,建立一個完成端口 CompletionPort=CreateIoCompletionPort(INVALI_HANDLE_VALUE,NULL,0,0); //步驟二判斷有多少個處理器 GetSystemInfo(&SystemInfo); //步驟三:根據處理器的數量建立工做線程,本例當中,工做線程的數目和處理器數目是相同的 for(i = 0; i < SystemInfo.dwNumberOfProcessers,i++){ HANDLE ThreadHandle; //建立工做者線程,並把完成端口做爲參數傳給線程 ThreadHandle=CreateThread(NULL,0, ServerWorkerThread,CompletionPort, 0, &ThreadID); //關閉線程句柄(僅僅關閉句柄,並不是關閉線程自己) CloseHandle(ThreadHandle); } //步驟四:建立監聽套接字 Listen=WSASocket(AF_INET,S0CK_STREAM,0,NULL, WSA_FLAG_OVERLAPPED); InternetAddr.sin_famlly=AF_INET; InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY); InternetAddr.sln_port = htons(5150); bind(Listen,(PSOCKADDR)&InternetAddr,sizeof(InternetAddr)); //準備監聽套接字 listen(Listen,5); while(TRUE){ //步驟五,接入Socket,並和完成端口關聯 Accept = WSAAccept(Listen,NULL,NULL,NULL,0); //步驟六 建立一個perhandle結構,並和端口關聯 PerHandleData=(LPPER_HANDLE_DATA)GlobalAlloc(GPTR,sizeof(PER_HANDLE_DATA)); printf("Socket number %d connected\n",Accept); PerHandleData->Socket=Accept; //步驟七,接入套接字和完成端口關聯 CreateIoCompletionPort((HANDLE)Accept, CompletionPort,(DWORD)PerHandleData,0); //步驟八 //開始進行I/O操做,用重疊I/O發送一些WSASend()和WSARecv() WSARecv(...)