windows IOCP完成端口實用詳解

開篇之前先放上本次講的IOCP project github地址:這里 。這個project中包含了IOCP和select,各自封裝成一個動態(tài)鏈接庫,可以直接使用。同時項目配有完整的glog支持,方便調(diào)試,并可以通過config控制server。如有bug,歡迎大家提出,正在完善過程中,代碼可以優(yōu)化的地方也請大家隨時提出,一起進(jìn)步成長。

本文主要從以下幾方面講解IOCP使用及其原理。

為什么需要完成端口

完成端口能做什么

完成端口原理

如何使用完成端口

1.? 為什么需要完成端口

網(wǎng)絡(luò)通信模型是編寫網(wǎng)絡(luò)程序的一個比較核心的模塊,也直接影響著程序的性能,所以選擇合適的網(wǎng)絡(luò)模型是非常有必要的。

IOCP是一種網(wǎng)絡(luò)通信模型,但是在IOCP出現(xiàn)之前已經(jīng)有相關(guān)網(wǎng)絡(luò)通信模型在使用了,比較普遍的應(yīng)該就是select模型,另外windows自己家也單獨實現(xiàn)了alertable I/O等。但是提到的select和alertable I/O都存在一些局限,比如select模型其并發(fā)處理量受FDSETSIZE宏大小限制,在windows平臺上這個大小默認(rèn)是64,當(dāng)然也可以自己在包含select之前手動#define其值,但是如果在使用之前就定義一個很大的值難免有點造成資源浪費,libevent就提供了一種自由的方法來使用select,這個在后面的文章中會詳細(xì)介紹。alertable I/O一個缺陷是多線程之間無法達(dá)到負(fù)載均衡的,同一個線程發(fā)出的IO請求必須由同一個線程來接收,即使其他線程閑著沒事干。所以這不能充分利多核系統(tǒng)的強(qiáng)大資源。那么IOCP有沒有缺陷呢,當(dāng)然也有,首先是使用起來不夠簡單明了,接口設(shè)計的不夠簡潔直觀。但是呢性能還是杠杠的。

上面簡單的說了IOCP模型與其他模型的一些對比,另外一點很大的區(qū)別是,IOCP模型是一種真正意義上的異步通信模型,具體啥是異步啥是同步可以參考我之前的一篇文章。有一點需要說明的是,并不是所有網(wǎng)絡(luò)通信項目都必須要使用IOCP模型,對于一些已知的連接數(shù)較少的網(wǎng)絡(luò)程序,完全可以用select甚至是每個客戶端對應(yīng)一個線程這種方式。

2.? 完成端口能做什么

上面吹了一大波完成端口,那么完成端口究竟能做什么呢。

首先一點是:IOCP會主動幫我們完成網(wǎng)絡(luò)IO數(shù)據(jù)復(fù)制。這一點其實也就是他與其他網(wǎng)絡(luò)模型最直接的區(qū)別了,一般網(wǎng)絡(luò)操作包括兩個步驟,以recv來說吧,如果是一般模型,那么其第一步是通知等待的線程有數(shù)據(jù)可以讀取,這時候線程會調(diào)用recv或者recvfrom等函數(shù)將數(shù)據(jù)從讀緩沖區(qū)復(fù)制到用戶空間,然后再做下一步的處理,而IOCP能幫我們的是,他會在內(nèi)核中幫我們監(jiān)聽那些我們感興趣的的事件,例如我們希望接收客戶端數(shù)據(jù),那么我們向完成端口投遞一個讀事件,完成端口在監(jiān)測有讀事件到來的時候會主動地去幫我們把數(shù)據(jù)從內(nèi)存空間復(fù)制到用戶空間,然后通知我們過來取數(shù)據(jù)就OK了,這就是IOCP提供的方便之處。

另外一點:IOCP在內(nèi)部管理線程,實現(xiàn)負(fù)載平衡。上面提到了windows的alertable I/O的負(fù)載均衡是他一個弊端,那么IOCP是如何自己管理線程調(diào)度的呢,簡單的說就是以棧的方式進(jìn)行管理,具體內(nèi)容接下來一節(jié)會詳細(xì)描述。

3.? 完成端口原理

overlapped

提到IOCP就不得不提到overlapped這個數(shù)據(jù)結(jié)構(gòu),這個數(shù)據(jù)結(jié)構(gòu)是IOCP進(jìn)行異步通信的關(guān)鍵。

typedef struct _OVERLAPPED {

ULONG_PTR Internal;

ULONG_PTR InternalHigh;

union {

struct {

DWORD Offset;

DWORD OffsetHigh;

};

PVOID? Pointer;

};

HANDLE? ? hEvent;

} OVERLAPPED, *LPOVERLAPPED;

這個數(shù)據(jù)結(jié)構(gòu)原本windows是不打算公開的,隨著軟件編程的發(fā)展后來微軟的工程師發(fā)現(xiàn)編程人員需要用到這個數(shù)據(jù)結(jié)構(gòu),所以就把他公開了,但是對于內(nèi)部的變量名卻沒有變動,因為微軟內(nèi)部使用這個變量名有太多地方了,如果該變量名可能會帶來很多其他的不好的影響,所以就沒有更換變量名。具體的變量含義在微軟的官方文檔中已經(jīng)明確給出。

Internal: 這個變量用來表明當(dāng)前IO請求的狀態(tài),當(dāng)我們向完成端口提交一個IO請求的時候如果請求還沒有響應(yīng),這個值就會是STATUS_PENDING。

InternalHigh:這個變量用來當(dāng)前IO請求字節(jié)數(shù)。

Offset:指定文件的起始偏移位置

OffsetHigh:指定開始傳輸數(shù)據(jù)的字節(jié)數(shù)的的高位

hEvent:是事件句柄,在IO請求完成后處于信號狀態(tài)。

需要注意的是,在網(wǎng)絡(luò)通信過程中offser與offserhigh是被系統(tǒng)自動忽略的,這兩個值在異步讀寫文件時使用。

在異步IO過程中,只要向可以使用OVERLAPPED數(shù)據(jù)結(jié)構(gòu)的函數(shù)投遞一個overlapped數(shù)據(jù)機(jī)構(gòu),系統(tǒng)內(nèi)核就會在后臺默默的幫你監(jiān)聽你所投遞的IO事件,當(dāng)你的感興趣的事件觸發(fā)的時候系統(tǒng)會在后臺幫你收集好數(shù)據(jù)然后通知你事件完成,并返回給你你投遞的Overlapped數(shù)據(jù)結(jié)構(gòu)的地址,由此可以看出在時間完成之前你是不能改變overlappd的地址的,否則會出現(xiàn)未定義的行為。

在項目中使用Overlapped數(shù)據(jù)結(jié)構(gòu)一般有兩種方法:

① 使用結(jié)構(gòu)體包含,并通過CONTAINING_RECORD抽取IO數(shù)據(jù)

struct TEST_OVERLAPPED{

OVERLAPPED overlapped_;

WSABUF? wsabuf_;

char data[SIZE];

int data_len_;

OP operate_type_;

}

定義這樣的數(shù)據(jù)結(jié)構(gòu),在傳入overlapped函數(shù)的時候?qū)⑵鋸?qiáng)制轉(zhuǎn)換成(OVERLAPPED*)(TEST_OVERLAPPED), 另外OVERLAPPED數(shù)據(jù)結(jié)構(gòu)一定要放在新的數(shù)據(jù)結(jié)構(gòu)頭部。operate_type_是自己定義的一個標(biāo)識,用來表示這個IO請求是什么類型的,當(dāng)然也可以將本次IO請求的socket句柄放進(jìn)去,用來表明具體是哪個client的IO。所有的這些數(shù)據(jù)都可以通過CONTAINING_RECORD宏來抽取,具體CONTAINING_RECORD是如何工作的我就不細(xì)講了,網(wǎng)上一大堆。

② C++類繼承overlapped數(shù)據(jù)結(jié)構(gòu)

這種方法其實與第一種方法很類似,不過比較方便的是不需要用CONTAINING_RECORD來抽取具體信息了。

class MyOverlapped : public OVERLAPPED{

WSABUF wsabuf_;

char data_[DATASIZE];

int data_len_;

OP operate_type_;

SOCKET client_;

}

同樣在使用的時候需要將其轉(zhuǎn)換為(OVERLAPPED*)傳入overlapped函數(shù),在IO事件完成后再將其轉(zhuǎn)換為我們的類,(MyOverlapped*)(&OVERLAPPED),之后直接讀取成員變量即可得出信息。

IOCP內(nèi)部工作原理

先上一張Jeffrey Richter在windows核心編程里的一個IOCP原理圖。一張好圖的效果比說n句話效果要好多了。


雖然微軟沒有公開完成端口具體的實現(xiàn)方式,但是從Jeffrey Richter的windows核心編程可以大概了解完成端口的大概實現(xiàn)。

當(dāng)我們創(chuàng)建一個完成端口的時候(創(chuàng)建方式下一節(jié)具體講)windows底層會幫我們創(chuàng)建一系列底層設(shè)施,以輔助我們后來的通信過程。具體設(shè)施如上圖所示。

①設(shè)備列表

這里的設(shè)備列表我們可以簡單的認(rèn)為就是所有連接的socket信息的列表,對于一個socket我們要將他與完成端口關(guān)聯(lián),完成端口才會在內(nèi)核中為我們監(jiān)聽我們感興趣的事件,這里有一個關(guān)鍵的數(shù)據(jù)結(jié)構(gòu),dwCompletionKey,這個數(shù)據(jù)結(jié)構(gòu)需要在我們將socket綁定到完成端口時一起傳入內(nèi)核設(shè)備列表中,那么他有什么作用呢?我的理解是這個數(shù)據(jù)結(jié)構(gòu)主要是為了在內(nèi)核中標(biāo)記當(dāng)前所通信的socket對象具體是哪一個,這個數(shù)據(jù)結(jié)構(gòu)是由我們自己定義的,所以在這個結(jié)構(gòu)體里我們可以加上我們一些自己想要的信息,因為后來通信過程中內(nèi)核會將這個數(shù)據(jù)傳送給我們,所以在這個結(jié)構(gòu)中定義一些你感興趣的字段可以方便后期的一些操作,比如你可以定義一個容器用來存放改設(shè)備投遞的所有IO操作,這樣在后期該socket關(guān)閉的時候可以方便清理與其相關(guān)的內(nèi)存,以確保不會造成內(nèi)存泄漏。

②完成隊列

完成隊列中存放的是已經(jīng)完成的IO事件,每一個列表項主要包括dwBytesTransferred, dwCompletionKey, pOverlapped, dwError四個數(shù)據(jù),dwBytesTransferred顧名思義就是本次IO事件所傳輸?shù)淖止?jié)數(shù),dwCompletionKey就是上面我們所說的用來標(biāo)識socket信息的數(shù)據(jù)結(jié)構(gòu),從這個數(shù)據(jù)結(jié)構(gòu)我們可以知道當(dāng)前的IO事件是發(fā)生在哪個socket上的,當(dāng)然你需要在completionKey中設(shè)置這個字段,否則你也不知道這個事件是發(fā)生在哪個socket上的。pOverlapped數(shù)據(jù)結(jié)構(gòu)也就是我們之前提到的Overlapped數(shù)據(jù)結(jié)構(gòu),這個數(shù)據(jù)結(jié)構(gòu)里包含了IO數(shù)據(jù)。

③線程管理設(shè)施

線程管理是完成端口的一大特點,也就是內(nèi)核在內(nèi)部自己管理一個線程池。與這個線程池相關(guān)的基礎(chǔ)設(shè)施主要有等待線程隊列(棧)(以棧的方式管理),已釋放的線程列表,已暫停的線程列表。當(dāng)線程調(diào)用GetQueuedCompletionStatus的時候內(nèi)核會將該線程放入線程棧中,因為GetQueuedCompletionStatus會將線程掛起,一旦有事件發(fā)生GetQueuedCompletion返回,這時候內(nèi)核會將線程放入已釋放列表中,如果這個已釋放的線程又調(diào)用了一些函數(shù)將線程掛起,那么內(nèi)核會將其放入已暫停線程列表中。當(dāng)GetQueuedCompletionStatus返回后線程處理完數(shù)據(jù)后再次調(diào)用GetQueuedCompletionStatus進(jìn)行等待時,內(nèi)核會重新將該線程放到線程等待棧中,這樣的一個流程下來我們可以看出,如果在IO事件處理比較慢的情況下一個線程就可以搞定所有的IO請求,這樣避免了線程之間的上下文切換帶來的性能開銷。

4. 如何使用完成端口

鋪墊了這么多,終于要講到如何使用IOCP了。先介紹一下使用完成端口需要用到的幾個比較重要的函數(shù)。

(1)CreateIOCompletionPort

之前說到過完成端口在API設(shè)計上不夠清晰,現(xiàn)在提到的這個函數(shù)可以充分說明這個問題。這個函數(shù)有兩個用途。

①CreateIOCompletionPort(-1,NULL,NULL,0)

這種調(diào)用方法是用來創(chuàng)建一個新的完成端口時使用的方式,最重要的是第四個參數(shù),第四個參數(shù)主要是用來確定在同一時間最多能有多少個線程運(yùn)行,設(shè)置為0代表數(shù)量與機(jī)器CPU個數(shù)一致。

②CreateIOCompletionPort(socket, completionport, pcompletionkey, 0)

這種調(diào)用方式使用老將一個socket句柄與已創(chuàng)建好的完成端口相關(guān)聯(lián),第一個參數(shù)代表句柄創(chuàng)建的時候不需要傳入,這個主要是用于將一個句柄綁定到完成端口時時使用的,第二個參數(shù)代表已創(chuàng)建好的完成端口,第三個參數(shù)completionley上面已經(jīng)說過了,是用來標(biāo)識一個socket句柄的。

(2)GetQueuedCompletionStatus(

_In_? HANDLE? ? ? CompletionPort,

_Out_ LPDWORD? ? ? lpNumberOfBytes,

_Out_ PULONG_PTR? lpCompletionKey,

_Out_ LPOVERLAPPED *lpOverlapped,

_In_? DWORD? ? ? ? dwMilliseconds)

這個函數(shù)主要是將當(dāng)前線程掛起等待IO事件完成。

CompletionPort:就是上面所創(chuàng)建的端口,另外有一點要說明的是這個端口與socket中使用的端口不是一個概念,你就把這個端口當(dāng)成一個內(nèi)核句柄就行。

lpNumberOfBytes:這個代表IO事件傳輸?shù)淖止?jié)數(shù)

lpCompletionKey:代表當(dāng)前IO事件所隸屬的句柄信息,這個是我們在綁定句柄到完成端口時自己傳進(jìn)去的。

lpOverlapped:這個值就包含了這次異步IO的數(shù)據(jù)信息。

dwMilliseconds:代表在等待一個IO事件完成時會等多久,如果這個值設(shè)為INFINITE,那么這個調(diào)用將永遠(yuǎn)不會超時,如果傳入0,那么這個調(diào)用會立即返回。

(3)PostQueuedCompletionStatus(

_In_? ? HANDLE? ? ? CompletionPort,

_In_? ? DWORD? ? ? ? dwNumberOfBytesTransferred,

_In_? ? ULONG_PTR? ? dwCompletionKey,

_In_opt_ LPOVERLAPPED lpOverlapped

)

這個函數(shù)可以用來模擬IO完成事件,經(jīng)常用于退出時發(fā)送一個模擬的IO完成事件來喚醒在等待中的線程,參數(shù)信息之前都有提到就不解釋了。

以上三個就是使用完成端口時最主要的三個函數(shù),那么既然要用到Overlapped數(shù)據(jù)結(jié)構(gòu)來投遞IO請求事件,那么socket的發(fā)送接收函數(shù)也就不能用原來常用的send和recv了,要用到WSAERecv,WSASend兩個函數(shù)了,因為這兩個函數(shù)都接收一個overlapped參數(shù)。另外還有一個要提到的是,accept沒有對應(yīng)的WSA版本,但是由于accept是一個阻塞函數(shù),所以如果想盡可能提高性能,可以使用微軟后期自己封裝的一個函數(shù)acceptex,這個函數(shù)與原始的accept之間的差別在于它是將已經(jīng)創(chuàng)建好的socket傳入函數(shù),那么當(dāng)其IO事件返回,對應(yīng)的socket也就已經(jīng)與客戶端建立連接了,這個函數(shù)也接受一個overlapped數(shù)據(jù)結(jié)構(gòu),所以我們可以把a(bǔ)ccept事件當(dāng)做一般的IO事件即可,當(dāng)GetQueuedCompletionStatus返回時檢查completionkey中的socket句柄是不是listen socket,如果是listen socket說明有新的連接接入,這時候需要調(diào)用微軟自己的函數(shù)GetAcceptExSockaddrs來獲取客戶端的地址信息。在創(chuàng)建好完成端口時可以先投遞幾個accept IO事件,這樣可以在高并發(fā)的時候處理的得心應(yīng)手,當(dāng)然了,完成端口本身就很強(qiáng)大。有一點要注意的是在每次處理完新的連接時要重新投遞新的accept事件,為下一個連接做準(zhǔn)備。

IOCP具體如何使用可以看我的GitHub,里面有完整的完成端口項目,對overlapped與completionkey都做了封裝,有資源管理器,測試還未發(fā)現(xiàn)資源泄露,有問題可以直接提出來,會及時改進(jìn)。

完成端口使用流程圖:


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 非原創(chuàng)文章,網(wǎng)絡(luò)收集,如遇原作者,請私聊會標(biāo)明出處! 1--11 tcp協(xié)議中三次握手和四次揮手建立TCP需要三次...
    Juinjonn閱讀 2,186評論 0 28
  • iOS面試小貼士 ———————————————回答好下面的足夠了------------------------...
    不言不愛閱讀 2,014評論 0 7
  • NIO(Non-blocking I/O,在Java領(lǐng)域,也稱為New I/O),是一種同步非阻塞的I/O模型,也...
    閃電是只貓閱讀 3,157評論 0 7
  • 那個時候并不知道映山紅就是杜鵑,也不知道望帝春心托杜鵑,杜鵑啼血的故事。 只知道,清明節(jié)祭祖時映山紅就都開了,一叢...
    林風(fēng)起閱讀 309評論 0 1
  • JS里面調(diào)用OC 對應(yīng)的Oc代碼為: 注意 在OC的實現(xiàn)中,如果方法的參數(shù)需要使用float、int、bool的,...
    大熱天曬太陽閱讀 1,350評論 0 0