理解I/O Completion Port

linglongyouzhi發表於2020-04-07

理解I/O Completion Port         nonocast原作  

 

歡迎閱讀此篇IOCP教程。我將先給出IOCP的定義然後給出它的實現方法,最後剖析一個Echo程式來為您撥開IOCP的謎雲,除去你心中對IOCP的煩惱。OK,雖然我不能保證你明白IOCP的一切,但我會盡我最大的努力。以下是我會在這篇文章中提到的相關技術:  

I/O

同步/非同步

堵塞/非堵塞

服務端/客戶端

多執行緒程式設計

Winsock API 2.0

在這之前,我曾經開發過一個專案,其中一塊需要網路支援,當時還考慮到了程式碼的可移植性,只要使用select, connect, accept, listen, send還有recv,再加上幾個#ifdef的封裝以用來處理WinsockBSD套接字[socket]中間的不相容性,一個網路子系統只用了幾個小時很少的程式碼就寫出來了,至今還讓我很回味。那以後很長時間也就沒再碰了。

前些日子,我們策劃做一個網路遊戲,我主動承擔下網路這一塊,想想這還不是小case, 心裡偷著樂啊。網路遊戲好啊,網路遊戲為成百上千的玩家提供了樂趣和令人著祕的遊戲體驗,他們線上上互相戰鬥或是加入隊伍去戰勝共同的敵人。我信心滿滿的準備開寫我的網路,於是乎,發現過去的阻塞同步模式模式根本不能拿到一個巨量多玩家[MMP]的架構中去,直接被否定掉了。於是乎,就有了IOCP,如果能夠很輕而易舉的搞掂IOCP,也就不會有這篇教程了。下面請諸位跟隨我進入正題。

 

什麼是IOCP

先讓我們看看對IOCP的評價

I/O完成埠可能是Win32提供的最複雜的核心物件。   [Advanced   Windows   3rd]   Jeffrey   Richter

這是[IOCP]實現高容量網路伺服器的最佳方法。[Windows Sockets2.0: Write Scalable Winsock Apps Using Completion Ports]  Microsoft Corporation

完成埠模型提供了最好的伸縮性。這個模型非常適用來處理數百乃至上千個套接字。[Windows網路程式設計2nd] Anthony Jones & Jim Ohlund

I/O completion ports特別顯得重要,因為它們是唯一適用於高負載伺服器[必須同時維護許多連線線路]的一個技術。Completion ports利用一些執行緒,幫助平衡由I/O請求所引起的負載。這樣的架構特別適合用在SMP系統中產生的”scalable”伺服器。 [Win32多執行緒程式設計] Jim Beveridge & Robert Wiener

看來我們完全有理由相信IOCP是大型網路架構的首選。那IOCP到底是什麼呢?

微軟在Winsock2中引入了IOCP這一概念。IOCP全稱I/O Completion Port,中文譯為I/O完成埠。IOCP是一個非同步I/OAPI,它可以高效地將I/O事件通知給應用程式。與使用select()或是其它非同步方法不同的是,一個套接字[socket]與一個完成埠關聯了起來,然後就可繼續進行正常的Winsock操作了。然而,當一個事件發生的時候,此完成埠就將被作業系統加入一個佇列中。然後應用程式可以對核心層進行查詢以得到此完成埠。

這裡我要對上面的一些概念略作補充,在解釋[完成]兩字之前,我想先簡單的提一下同步和非同步這兩個概念,邏輯上來講做完一件事後再去做另一件事就是同步,而同時一起做兩件或兩件以上事的話就是非同步了。你也可以拿單執行緒和多執行緒來作比喻。但是我們一定要將同步和堵塞,非同步和非堵塞區分開來,所謂的堵塞函式諸如accept(…),當呼叫此函式後,此時執行緒將掛起,直到作業系統來通知它,”HEY兄弟,有人連進來了,那個掛起的執行緒將繼續進行工作,也就符合生產者-消費者模型。堵塞和同步看上去有兩分相似,但卻是完全不同的概念。大家都知道I/O裝置是個相對慢速的裝置,不論印表機,調變解調器,甚至硬碟,與CPU相比都是奇慢無比的,坐下來等I/O的完成是一件不甚明智的事情,有時候資料的流動率非常驚人,把資料從你的檔案伺服器中以Ethernet速度搬走,其速度可能高達每秒一百萬位元組,如果你嘗試從檔案伺服器中讀取100KB,在使用者的眼光來看幾乎是瞬間完成,但是,要知道,你的執行緒執行這個命令,已經浪費了10個一百萬次CPU週期。所以說,我們一般使用另一個執行緒來進行I/O。重疊IO[overlapped   I/O]Win32的一項技術,你可以要求作業系統為你傳送資料,並且在傳送完畢時通知你。這也就是[完成]的含義。這項技術使你的程式在I/O進行過程中仍然能夠繼續處理事務。事實上,作業系統內部正是以執行緒來完成Overlapped I/O。你可以獲得執行緒所有利益,而不需要付出什麼痛苦的代價。

完成埠中所謂的[]並不是我們在TCP/IP中所提到的埠,可以說是完全沒有關係。我到現在也沒想通一個I/O裝置[I/O Device]和埠[IOCP中的Port]有什麼關係。估計這個埠也迷惑了不少人。IOCP只不過是用來進行讀寫操作,和檔案I/O倒是有些類似。既然是一個讀寫裝置,我們所能要求它的只是在處理讀與寫上的高效。在文章的第三部分你會輕而易舉的發現IOCP設計的真正用意。

 

IOCP和網路又有什麼關係?

int main()

{

     WSAStartup(MAKEWORD(2, 2), &wsaData);

     ListeningSocket = socket(AF_INET, SOCK_STREAM, 0);

     bind(ListeningSocket, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr));

     listen(ListeningSocket, 5);

     int nlistenAddrLen = sizeof(ClientAddr);

     while(TRUE)

     {

         NewConnection = accept(ListeningSocket, (SOCKADDR*)&ClientAddr, &nlistenAddrLen);

         HANDLE hThread = CreateThread(NULL,0,ThreadFunc,(void*)NewConnection, 0, &dwTreadId);

         CloseHandle(hThread);

     }

     return 0;

}

相信只要寫過網路的朋友,應該對這樣的結構在熟悉不過了。accept後執行緒被掛起,等待一個客戶發出請求,而後建立新執行緒來處理請求。當新執行緒處理客戶請求時,起初的執行緒迴圈回去等待另一個客戶請求。處理客戶請求的執行緒處理完畢後終結。

在上述的併發模型中,對每個客戶請求都建立了一個執行緒。其優點在於等待請求的執行緒只需做很少的工作。大多數時間中,該執行緒在休眠[因為recv處於堵塞狀態]

但是當併發模型應用在伺服器端[基於Windows NT] Windows NT小組注意到這些應用程式的效能沒有預料的那麼高。特別的,處理很多同時的客戶請求意味著很多執行緒併發地執行在系統中。因為所有這些執行緒都是可執行的[沒有被掛起和等待發生什麼事]Microsoft意識到NT核心花費了太多的時間來轉換執行執行緒的上下文[Context],執行緒就沒有得到很多CPU時間來做它們的工作。

大家可能也都感覺到並行模型的瓶頸在於它為每一個客戶請求都建立了一個新執行緒。建立執行緒比起建立程式開銷要小,但也遠不是沒有開銷的。

我們不妨設想一下:如果事先開好N個執行緒,讓它們在那hold[堵塞],然後可以將所有使用者的請求都投遞到一個訊息佇列中去。然後那N個執行緒逐一從訊息佇列中去取出訊息並加以處理。就可以避免針對每一個使用者請求都開執行緒。不僅減少了執行緒的資源,也提高了執行緒的利用率。理論上很不錯,你想我等泛泛之輩都能想出來的問題,Microsoft又怎會沒有考慮到呢?!

這個問題的解決方法就是一個稱為I/O完成埠的核心物件,他首次在Windows NT3.5中被引入。

其實我們上面的構想應該就差不多是IOCP的設計機理。其實說穿了IOCP不就是一個訊息佇列嘛!你說這和[]這兩字有何聯絡。我的理解就是IOCP最多是應用程式和作業系統溝通的一個介面罷了。

至於IOCP的具體設計那我也很難說得上來,畢竟我沒看過實現的程式碼,但你完全可以進行模擬,只不過效能可能,如果想深入理解IOCPJeffrey RitchterAdvanced Windows 3rd其中第13章和第14張有很多寶貴的內容,你可以拿來窺視一下系統是如何完成這一切的。

 

實現方法

MicrosoftIOCP提供了相應的API函式,主要的就兩個,我們逐一的來看一下:

HANDLE   CreateIoCompletionPort   (

HANDLE   FileHandle,                         // handle to file

HANDLE   ExistingCompletionPort,           // handle to I/O completion port

ULONG_PTR   CompletionKey,                // completion key

DWORD   NumberOfConcurrentThreads     // number of threads to execute concurrently

);

在討論各引數之前,首先要注意該函式實際用於兩個截然不同的目的:

1.用於建立一個完成埠物件

2.將一個控制程式碼[HANDLE]和完成埠關聯到一起

在建立一個完成一個埠的時候,我們只需要填寫一下NumberOfConcurrentThreads這個引數就可以了。它告訴系統一個完成埠上同時允許執行的執行緒最大數。在預設情況下,所開執行緒數和CPU數量相同,但經驗給我們一個公式:

執行緒數 = CPU * 2 + 2

要使完成埠有用,你必須把它同一個或多個裝置相關聯。這也是呼叫CreateIoCompletionPort完成的。你要向該函式傳遞一個已有的完成埠的控制程式碼,我們既然要處理網路事件,那也就是將客戶的socket作為HANDLE傳進去。和一個完成鍵[對你有意義的一個32位值,也就是一個指標,作業系統並不關心你傳什麼]。每當你向埠關聯一個裝置時,系統向該完成埠的裝置列表中加入一條資訊紀錄。

另一個API就是

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

);

第一個引數指出了執行緒要監視哪一個完成埠。很多服務應用程式只是使用一個I/O完成埠,所有的I/O請求完成以後的通知都將發給該埠。簡單的說,GetQueuedCompletionStatus使呼叫執行緒掛起,直到指定的埠的I/O完成佇列中出現了一項或直到超時。同I/O完成埠相關聯的第3個資料結構是使執行緒得到完成I/O項中的資訊:傳輸的位元組數,完成鍵和OVERLAPPED結構的地址。該資訊是通過傳遞給GetQueuedCompletionSatatuslpdwNumberOfBytesTransferredlpdwCompletionKeylpOverlapped引數返回給執行緒的。

根據到目前為止已經講到的東西,首先來構建一個frame。下面為您說明了如何使用完成埠來開發一個echo伺服器。大致如下:

1.初始化Winsock

2.建立一個完成埠

3.根據伺服器執行緒數建立一定量的執行緒數

4.準備好一個socket進行bind然後listen

5.進入迴圈accept等待客戶請求

6.建立一個資料結構容納socket和其他相關資訊

7.將連進來的socket同完成埠相關聯

8.投遞一個準備接受的請求

以後就不斷的重複58的過程

那好,我們用具體的程式碼來展示一下細節的操作。

WOW,程式的程式碼若是貼在此處,實在大煞風景,又不能CTRL+V還不能F7,如果大家需要原始碼可以發信給我o_nono@163.net

至此文章也該告一段落了,我帶著您做了一趟旋風般的旅遊,遊覽了所謂的完成埠。

很多細節由於篇幅的關係無法細細道來。但希望這篇文章能帶給您更多的思考。如有任何問題,可以發信至o_nono@163.net

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

【【【【上面的是windows網路程式設計英文原版,中文是我收集的。】】】】

完成埠模型是迄今為止最為複雜的I/O模型。然而。假若個應用程式同時需要管理為數眾多的套接字,那麼採用這種模型。往往可以達到最佳的系統效能,然而不幸的是,該模型只適用於以下作業系統(微軟的)Windows   NTWindows   2000作業系統。因其設計的複雜性,只有在你的應用程式需要同時管理數百乃至上千個套接字的時候、而且希望隨著系統內安裝的CPU數量的增多、應用程式的效能也可以線性提升,才應考慮採用完成埠模型。要記住的一個基本準則是,假如要為Windows   NTwindows   2000開發高效能的伺服器應用,同時希望為大量套接字I/O請求提供服務(Web伺服器便是這方面的典型例子),那麼I/O完成埠模型便是最佳選擇.

從本質上說,完成埠模型要求我們建立一個Win32完成埠物件, 通過指定數量的執行緒對重疊I/O請求進行管理。以便為已經完成的重疊I/O請求提供服務。要注意的是。所謂完成埠”,實際是Win32Windows NT以及windows 2000採用的一種I/O構造機制, 除套接字控制程式碼之外, 實際上還可接受其他東西。然而,本節只打算講述如何使用套接字控制程式碼,來發揮完成埠模型的巨大威力。使用這種模型之前,首先要建立一個I/O完成埠物件,用它面向任意數量的套接字控制程式碼。管理多個I/O請求。要做到這,需要呼叫CreateIoCompletionPort函式。該函式定義如下:

HANDLE   CreateIoCompletionPort(

          HANDLE   FileHandle,

          HANDLE   ExistingCompletionPort,

          DWORD   CompletionKey,

          DWORD   NumberOfConcurrentThreads

  );

在我們深入探討其中的各個引數之前,首先要注意意該函式實際用於兩個明顯有別的目的:

用於建立個完成埠物件。

將一個控制程式碼同完成埠關聯到一起。

最開始建立個完成埠的時候,唯一感興趣的引數便是NumberOfConcurrentThreads(併發執行緒的數量);前面三個引數都會被忽略。NumberOfConcurrentThreads引數的特殊之處在於.它定義了在一個完成埠上,同時允許執行的執行緒數量。理想情況下我們希望每個處理器各自負責個執行緒的執行,為完成埠提供服務, 避免過於頻繁的執行緒場景切換。若將該引數設為0, 說明系統內安裝了多少個處理器, 便允許同時執行多少個執行緒!可用下述程式碼建立一個I/O完成埠:

CompletionPort CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

該語加的作用是返問一個控制程式碼.在為完成埠分配了個套接字控制程式碼後,用來對那個埠進行標定(引用)

1.工作者執行緒與完成埠

成功建立一個完成埠後,便可開始將套接字控制程式碼與物件關聯到一起。但在關聯套接字之前、首先必須建立個或多個工作者執行緒,以便在I/O請求投遞給完成埠物件後。為完成埠提供服務。在這個時候,大家或許會覺得奇怪、到底應建立多少個執行緒。以便為完成埠提供服務呢? 這實際正是完成埠模型顯得頗為複雜個方面, 因為服務I/O請求所需的數量取決於應用程式的總體設計情況。在此要記住的個重點在於,在我們呼叫CreateIoComletionPort時指定的併發執行緒數量, 與打算建立的工作者執行緒數量相比, 它們代表的並非同件事情。早些時候,我們曾建議大家用CreateIoCompletionPort函式為每個處理器都指定一個執行緒(處理器的數量有多少,便指定多少執行緒)以避免由於頻繁的執行緒場景交換活動,從而影響系統的整體效能。

CreateIoCompletionPort函式的NumberofConcurrentThreads引數明確指示系統:在一個完成埠上,一次只允許n個工作者執行緒執行。假如在完成端門上建立的工作者執行緒數量超出n個.那麼在同一時刻,最多隻允許n個執行緒執行。但實際上,在段較短的時間內, 系統有可能超過這個值。但很快便會把它減少至事先在CreateIoCompletionPort函式中設定的值。那麼, 為何實際建立的工作者執行緒數最有時要比CreateIoCompletionPort函式設定的多些呢? 這樣做有必要嗎? 如先前所述。這主要取決於應用程式的總體設計情況,假設我們的工作者執行緒呼叫了一個函式,比如Sleep()或者WaitForSingleobject(),但卻進入了暫停(鎖定或掛起)狀態、那麼允許另個執行緒代替它的位置。換言之,我們希望隨時都能執行儘可能多的執行緒;當然,最大的執行緒數量是事先在CreateIoCompletonPort呼叫裡設定好的。這樣來。假如事先預料到自己的執行緒有可能暫時處於停頓狀態,那麼最好能夠建立比CreateIoCompletionPortNumberofConcurrentThreads引數的值多的執行緒.以便到時候充分發揮系統的潛力。旦在完成埠上擁有足夠多的工作者執行緒來為I/O請求提供服務,便可著手將套接字控制程式碼同完成埠關聯到一起。這要求我們在個現有的完成埠上呼叫CreateIoCompletionPort函式,同時為前三個引數: FileHandle, ExistingCompletionPortCompletionKey——提供套接字的資訊。其中, 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機制, 在新建的套接字上投遞一個或多個非同步WSARecvWSASend請求。這些I/O請求完成後, 一個工作者執行緒會為I/O請求提供服務, 同時繼續處理未來的I/O請求, 稍後便會在步驟3)指定的工作者例程中。體驗到這一點。

9)重複步驟5)—8)。直到伺服器終止。

程式清單89     完成埠的建立

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)&InternetAddrsizeof(InternetAddr));

//準備監聽套接字

listen(Listen5);

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

}

 

2.完成埠和重疊I/O

將套接字控制程式碼與一個完成埠關聯在一起後,便可以套接字控制程式碼為基礎。投遞傳送或接收請求。開始I/O請求的處理。接下來,可開始依賴完成埠,來接收有關I/O操作完成情況的通知。從本質上說、完成瑞口模型利用了Win32重疊I/O機制。在這種機制中。象WSASendWSARecv這樣的Winsock API呼叫會立即返回。此時, 需要由我們的應用程式負責在以後的某個時間。通過一個OVERLAPPED結構, 來接收呼叫的結果。在完成埠模型中。要想做到這一點,需要使用GetQueuedCompletionStatus(獲取排隊完成狀態)函式。讓一個或者多個工作者執行緒在完成埠上等待。該函式的定義如下:

B00L   GetQueuedCompletionStatus(

          HANDLE   CompletionPort

          LPDWORD     lpNumber0fBytesTransferred,

          LPDWORD     lpCompletionKey

          LPOVERLAPPED     *lpOverlapped,

          DWORD   dwMilliseconds

  };

其中,CompletionPort引數對應與要在上面等待的完成埠.lpNumberOfBytesTransferred引數負責在完成了I/O操作後(WSASendWSARecv)、接收實際傳輸的位元組數。lpCompletionKey引數為原先傳遞進入CreateCompletionPort函式的套接字返回單控制程式碼資料。如我們早先所述,大家最好將套接字控制程式碼儲存在這個”(Key)中。lpOverlapped引數用於接收完成的I/O操作的重疊結果。這實際是一個相當重要的引數,因為要用它獲取每個I/O操作的資料。而最後一個引數。DwMilliseconds用於指定呼叫者希望等待一個完成資料包在完成埠上出現的時間。假如將其設為INFINITE。呼叫會無休止地等持下去。

3.單控制程式碼資料和單I/O運算元據

個工作者執行緒從GetQueuedCompletionStatus這個API呼叫接收到I/O完成通知後。在lpCompletionKeylpOverlapped引數中,會包含些必要的套接字資訊。利用這些資訊,可通過完成埠,繼續在一個套接字上的I/O處理,通過這些引數。可獲得兩方面重要的套接字資料:單控制程式碼資料,以及單I/O運算元據。

其中,lpCompletionKey引數包含了單控制程式碼資料”, 因為在個套接字首次與完成埠關聯到起的時候。那些資料便與一個特定的套接字控制程式碼對應起來了。這些資料正是我們在進行CreateIoCompletionPort API呼叫的時候,通過CompletionKey引數傳遞的。如早先所述。應用程式可通過該引數傳遞任意型別的資料。通常情況下,應用程式會將與I/O請求有關的套接字控制程式碼儲存在這裡。lpOVerlapped引數則包含了OVERLAPPED結構,在它後邊跟隨I/O運算元據

我們的工作者執行緒處理個完成資料包時(將資料原封不動打轉回去,接受連線,投遞另個執行緒,等等). 這些資訊是它必須要知道的. I/O運算元據可以是追加到一個OVERLAPPED結構末尾的任意數量的位元組。假如一個函式要求用到一個OVERLAPPED結構,我們便必須將這樣的個結構傳遞進去,以滿足它的耍求。要想做到這一點,一個簡單的方法是定義個結構。然後將OVERLAPPED結構作為新結構的第一個元素使用。舉個例子來說。

可定義上述資料結構,實現對單I/O運算元據的管理:

  typedef     struct     {

      OVERLAPPED          Overlapped;

      WSABUF                  DataBuf;

      CHAR                   Bufferl[DATA—BUFSIZE];

      BOOL                 OperationType;

  }PER_IO_OPERATION_DATA;

該結構演示了通常要與I/O操作關聯在起的某些重要資料元素,比如剛才完成的那個I/O操作的型別(傳送或接收請求).在這個結構中。我們認為用於已完成I/O操作的資料緩衝區是非常有用的。要想呼叫Winsock   API函式,同時為其分配一個OVERLAPPED結構,既可將自己的結構造型為一個OVERLAPPED指標,亦可簡單地撤消對結構中的OVBRLAPPED元素的引用。如下例所示:

PER_IO_OPERATION_DATA   PerIoData;

可以象下邊這樣呼叫一個函式

WSARecv(socket,…,(OVERLAPPED   *)&PerIoData;

或者象下邊這樣

WSARecv(socket…,&(   PerIoData.Overlapped));

在工作執行緒的後面部分。等GetQueuedCompletionStatus函式返回了個重疊結構(和完成鍵)後。便可通過撤消對OperationType成員的引用。調查到底是哪個操作投遞到了這個控制程式碼之上(只需將返回的重疊結構造型為自的PER_IO_OPERATlON_DATA結構)。對單I/O運算元據來說,它最大的個優點便是允許我們在同一個控制程式碼上。同時管理多個I/O操作(/寫、多個讀、多個寫,等等)。大家此時或許會產生這樣的疑問:在同個套接字上,真的有必要同時投遞多個I/O操作嗎?答案在於系統的伸縮性”,或者說擴充套件能力。例如,假定我們的機器安裝了多箇中央處理器。每個處理器都在遠行一個工作者執行緒,那麼在同一個時候、完全可能有幾個不同的處理器在同一個套接字上,進行資料的收發操作。

為了完成前述的簡單回應伺服器示例,我們需要提供一個ServerWorkerThread(伺服器工作者執行緒)函式。在程式消單8.10中,我們展示瞭如何設計一個工作者執行緒例程,令其使用單控制程式碼資料以及單I/O運算元據,I/O請求提供服務。

程式清單8—10     完成埠工作者執行緒

DWORD     WINAPI     ServerWokerThread(LPVOID   CompletionPortID)

  {

  HANDLE     CompletionPort(HANDLE)ComleTionPortID;

  DWORD     BytesTransferred;

  LPOVERLAPPED   Overlapped;

  LPPER_HANDLE_DATA   PerHandleData;

  LPPER_IO_OPERATION_DATA   PerIoData;

  DWORD     SendBytes,RecvBytes;

  DWORD Flages;

  while(TRUE)

  {

  //wait for I/O to Complete on any socket associated with the completionport

  GetQueuedCompletionStatus(CompletionPort,

                           &BytesTransferred,

 (LPDWORD)&PerHandleData,

 (LPOVERLAPPED *)&PerIoData,

INFINITE);

  //first check to see whether an error has occurred on the socket, if so, close the socke and clearup the

  //per-handle and Per-I/O operation data associated with socket

  if (BytesTransferred == 0)&& (PerIoData->OperationType == RECV_POSTED)&& (PerIoData->OperationType == SEND_POSTED)

  {

  //A Zero BytesTransferred indicates that the socke has been closed by the peer, so you should close the socket

  //Note: Per-handle Data was used to refresence the socket associated with the I/O operation;

  closesocket(PerHandleData->Socket);

  GlobalFree(PerHandleData);

  GlobalFree(PerIoData);

  continue;

  }

  //service the completed I/O request; You determine which I/O request has just completed

  //by looking as the operationType field contained the per-I/O operation data

  if (PerIoData->OperationType == RECV_POSTED)

  {

  //do something with the received data in PerIoData->Buffer

 }

  //Post another WSASend or WSARecv operation as a example we will post another WSARecv()

   

  Flags   =   0;

 

  //Set up the Per-I/O Operation Data for a next overlapped call

  ZeroMemory(&(PerIoData->Overlapped),sizeof(OVERLAPPED));

  PerIoData->DataBuf.len   =   DATA_BUFFER_LEN;

  PerIoData->DataBuf.buf   =   PerIoData->Buffer;

  PerIoData->OperationType   =   RECV_POSTED;

 

  WSARecv(PerHandleData->Socket,

  &(PerIoData->DataBuf),1,&RecvBytes,

  &Flags,&(PerIoData->Overlapped),NULL);

  }

}

在程式清單8-9和程式清單8-10列出的簡單伺服器示例中(配套光碟也有),最後要注意的處理細節是如何正確地關閉I/O完成埠一特別是同時執行了一個或多個執行緒,在幾個不同的套接字上執行I/O操作的時候。要避免的一個重要問題是在進行重疊I/O操作的同時,強行釋放OVERLAPPED結構。要想避免出現這種情況,最好的辦法是針對每個套接字控制程式碼,呼叫closesocket函式。任何尚未進行的重疊I/O操作都會完成。旦所有套接字控制程式碼都已關閉。便需在完成埠上,終止所有工作者執行緒的執行。要想做到這一點,需要使用PostQueuedCompletionStatus函式,向每個工作者執行緒都傳送個特殊的完成資料包。該函式會指示每個執行緒都立即結束並退出”.下面是PostQueuedCompletionStatus函式的定義:

  BOOL   PostQueuedCompletionStatus(

          HANDLE   CompletlonPort,

          DW0RD     dwNumberOfBytesTrlansferred,

          DWORD     dwCompletlonKey,

LPOVERLAPPED   lpoverlapped,

);

其中,CompletionPort引數指定想向其傳送一個完成資料包的完成埠物件。而就dwNumberOfBytesTransferred, dwCompletionKeylpOverlapped這三個引數來說.個都允許我們指定個值,直接傳遞給GetQueuedCompletionStatus函式中對應的引數。這樣來。個工作者執行緒收到傳遞過來的三個GetQueuedCompletionStatus函式引數後,便可根據由這三個引數的某一個設定的特殊值,決定何時應該退出。例如,可用dwCompletionPort引數傳遞0,個工作者執行緒會將其解釋成中止指令。一旦所有工作者執行緒都已關閉,便可使用CloseHandle函式,關閉完成埠。最終安全退出程式。

4.其他問題

另外還有幾種頗有價值的技術。可用來進步改善套接字應用程式的總體I/O效能。值得考慮的一項技術是試驗不同的套接字緩衝區大小,以改善I/O效能和應用程式的擴充套件能力。例如,假如某個程式只採用了個比較大的緩衝區,僅能支援wSARecv請求,而不是同時設定了三個較小的緩衝區。提供對三個WSARecv請求的支援,那麼該程式的擴充套件能力並不是很好,特別是在轉移到安裝了多個處理器的機器上之後。這是由於單獨一個緩衝區每次只能處理一個執行緒!除此以外,單緩衝區設計還會對效能造成一定的干擾,假如次僅能進行次接收操作。網路協議驅動程式的潛力使不能得到充分發揮(它經常都會很”)。換言之,假如在接收更多的資料前、需要等待WSARecv操作的完成,那麼在WSARecv完成和下一次接收之間,整個協議實際上處於休息狀態。

個值得考慮的效能改進措施是用套接宇選項SO_SNDBUFSO_RCVBUF對內部套接字緩衝區的大小進行控制。利用這些選項,應用程式可更改個套接字的內部資料緩衝區的大小。如將該設為0,Winsock便會在重疊I/O呼叫中直接使用應用程式的緩衝區、進行資料在協議堆疊裡的傳人,傳出。這樣一來,在應用程式與Winsock之間,便避免了進行次緩衝區複製的必要。下述程式碼片斷闡釋瞭如何使用SO_SNDBUF選項,來進行setsockopt函式的呼叫:

setsockopt(socketSOL_S0CKET,SO_SNDBUF(char   *)&nZero,sizeof(nZero));

要注意的是,將這些緩衝區的大小設為0,只有在一段給定的時間內,存在著多個I/O請求的前提下才會產生積極作用。等到第9章,我們會向大家更深入地講述套接字選項的知識。提升效能的最後一項措施是使用AcceptEx這個API調

相關文章