開(kāi)篇之前先放上本次講的IOCP project github地址:這里 缨叫。這個(gè)project中包含了IOCP和select拖刃,各自封裝成一個(gè)動(dòng)態(tài)鏈接庫(kù),可以直接使用脚翘。同時(shí)項(xiàng)目配有完整的glog支持,方便調(diào)試绍哎,并可以通過(guò)config控制server来农。如有bug,歡迎大家提出崇堰,正在完善過(guò)程中沃于,代碼可以優(yōu)化的地方也請(qǐng)大家隨時(shí)提出,一起進(jìn)步成長(zhǎng)海诲。
本文主要從以下幾方面講解IOCP使用及其原理繁莹。
為什么需要完成端口
完成端口能做什么
完成端口原理
如何使用完成端口
1.? 為什么需要完成端口
網(wǎng)絡(luò)通信模型是編寫(xiě)網(wǎng)絡(luò)程序的一個(gè)比較核心的模塊,也直接影響著程序的性能特幔,所以選擇合適的網(wǎng)絡(luò)模型是非常有必要的咨演。
IOCP是一種網(wǎng)絡(luò)通信模型,但是在IOCP出現(xiàn)之前已經(jīng)有相關(guān)網(wǎng)絡(luò)通信模型在使用了蚯斯,比較普遍的應(yīng)該就是select模型薄风,另外windows自己家也單獨(dú)實(shí)現(xiàn)了alertable I/O等。但是提到的select和alertable I/O都存在一些局限溉跃,比如select模型其并發(fā)處理量受FDSETSIZE宏大小限制村刨,在windows平臺(tái)上這個(gè)大小默認(rèn)是64告抄,當(dāng)然也可以自己在包含select之前手動(dòng)#define其值撰茎,但是如果在使用之前就定義一個(gè)很大的值難免有點(diǎn)造成資源浪費(fèi),libevent就提供了一種自由的方法來(lái)使用select打洼,這個(gè)在后面的文章中會(huì)詳細(xì)介紹龄糊。alertable I/O一個(gè)缺陷是多線程之間無(wú)法達(dá)到負(fù)載均衡的,同一個(gè)線程發(fā)出的IO請(qǐng)求必須由同一個(gè)線程來(lái)接收募疮,即使其他線程閑著沒(méi)事干炫惩。所以這不能充分利多核系統(tǒng)的強(qiáng)大資源。那么IOCP有沒(méi)有缺陷呢阿浓,當(dāng)然也有他嚷,首先是使用起來(lái)不夠簡(jiǎn)單明了,接口設(shè)計(jì)的不夠簡(jiǎn)潔直觀芭毙。但是呢性能還是杠杠的筋蓖。
上面簡(jiǎn)單的說(shuō)了IOCP模型與其他模型的一些對(duì)比,另外一點(diǎn)很大的區(qū)別是退敦,IOCP模型是一種真正意義上的異步通信模型粘咖,具體啥是異步啥是同步可以參考我之前的一篇文章。有一點(diǎn)需要說(shuō)明的是侈百,并不是所有網(wǎng)絡(luò)通信項(xiàng)目都必須要使用IOCP模型瓮下,對(duì)于一些已知的連接數(shù)較少的網(wǎng)絡(luò)程序,完全可以用select甚至是每個(gè)客戶端對(duì)應(yīng)一個(gè)線程這種方式。
2.? 完成端口能做什么
上面吹了一大波完成端口浓镜,那么完成端口究竟能做什么呢蜕企。
首先一點(diǎn)是:IOCP會(huì)主動(dòng)幫我們完成網(wǎng)絡(luò)IO數(shù)據(jù)復(fù)制。這一點(diǎn)其實(shí)也就是他與其他網(wǎng)絡(luò)模型最直接的區(qū)別了震缭,一般網(wǎng)絡(luò)操作包括兩個(gè)步驟赂毯,以recv來(lái)說(shuō)吧,如果是一般模型拣宰,那么其第一步是通知等待的線程有數(shù)據(jù)可以讀取党涕,這時(shí)候線程會(huì)調(diào)用recv或者recvfrom等函數(shù)將數(shù)據(jù)從讀緩沖區(qū)復(fù)制到用戶空間,然后再做下一步的處理巡社,而IOCP能幫我們的是膛堤,他會(huì)在內(nèi)核中幫我們監(jiān)聽(tīng)那些我們感興趣的的事件,例如我們希望接收客戶端數(shù)據(jù)晌该,那么我們向完成端口投遞一個(gè)讀事件肥荔,完成端口在監(jiān)測(cè)有讀事件到來(lái)的時(shí)候會(huì)主動(dòng)地去幫我們把數(shù)據(jù)從內(nèi)存空間復(fù)制到用戶空間,然后通知我們過(guò)來(lái)取數(shù)據(jù)就OK了朝群,這就是IOCP提供的方便之處燕耿。
另外一點(diǎn):IOCP在內(nèi)部管理線程,實(shí)現(xiàn)負(fù)載平衡姜胖。上面提到了windows的alertable I/O的負(fù)載均衡是他一個(gè)弊端誉帅,那么IOCP是如何自己管理線程調(diào)度的呢,簡(jiǎn)單的說(shuō)就是以棧的方式進(jìn)行管理右莱,具體內(nèi)容接下來(lái)一節(jié)會(huì)詳細(xì)描述蚜锨。
3.? 完成端口原理
overlapped
提到IOCP就不得不提到overlapped這個(gè)數(shù)據(jù)結(jié)構(gòu),這個(gè)數(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;
這個(gè)數(shù)據(jù)結(jié)構(gòu)原本windows是不打算公開(kāi)的亚再,隨著軟件編程的發(fā)展后來(lái)微軟的工程師發(fā)現(xiàn)編程人員需要用到這個(gè)數(shù)據(jù)結(jié)構(gòu),所以就把他公開(kāi)了晨抡,但是對(duì)于內(nèi)部的變量名卻沒(méi)有變動(dòng)氛悬,因?yàn)槲④泝?nèi)部使用這個(gè)變量名有太多地方了,如果該變量名可能會(huì)帶來(lái)很多其他的不好的影響耘柱,所以就沒(méi)有更換變量名如捅。具體的變量含義在微軟的官方文檔中已經(jīng)明確給出。
Internal: 這個(gè)變量用來(lái)表明當(dāng)前IO請(qǐng)求的狀態(tài)帆谍,當(dāng)我們向完成端口提交一個(gè)IO請(qǐng)求的時(shí)候如果請(qǐng)求還沒(méi)有響應(yīng)伪朽,這個(gè)值就會(huì)是STATUS_PENDING。
InternalHigh:這個(gè)變量用來(lái)當(dāng)前IO請(qǐng)求字節(jié)數(shù)汛蝙。
Offset:指定文件的起始偏移位置
OffsetHigh:指定開(kāi)始傳輸數(shù)據(jù)的字節(jié)數(shù)的的高位
hEvent:是事件句柄烈涮,在IO請(qǐng)求完成后處于信號(hào)狀態(tài)朴肺。
需要注意的是,在網(wǎng)絡(luò)通信過(guò)程中offser與offserhigh是被系統(tǒng)自動(dòng)忽略的坚洽,這兩個(gè)值在異步讀寫(xiě)文件時(shí)使用戈稿。
在異步IO過(guò)程中,只要向可以使用OVERLAPPED數(shù)據(jù)結(jié)構(gòu)的函數(shù)投遞一個(gè)overlapped數(shù)據(jù)機(jī)構(gòu)讶舰,系統(tǒng)內(nèi)核就會(huì)在后臺(tái)默默的幫你監(jiān)聽(tīng)你所投遞的IO事件鞍盗,當(dāng)你的感興趣的事件觸發(fā)的時(shí)候系統(tǒng)會(huì)在后臺(tái)幫你收集好數(shù)據(jù)然后通知你事件完成,并返回給你你投遞的Overlapped數(shù)據(jù)結(jié)構(gòu)的地址,由此可以看出在時(shí)間完成之前你是不能改變overlappd的地址的跳昼,否則會(huì)出現(xiàn)未定義的行為般甲。
在項(xiàng)目中使用Overlapped數(shù)據(jù)結(jié)構(gòu)一般有兩種方法:
① 使用結(jié)構(gòu)體包含,并通過(guò)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ù)的時(shí)候?qū)⑵鋸?qiáng)制轉(zhuǎn)換成(OVERLAPPED*)(TEST_OVERLAPPED), 另外OVERLAPPED數(shù)據(jù)結(jié)構(gòu)一定要放在新的數(shù)據(jù)結(jié)構(gòu)頭部敷存。operate_type_是自己定義的一個(gè)標(biāo)識(shí),用來(lái)表示這個(gè)IO請(qǐng)求是什么類(lèi)型的堪伍,當(dāng)然也可以將本次IO請(qǐng)求的socket句柄放進(jìn)去锚烦,用來(lái)表明具體是哪個(gè)client的IO。所有的這些數(shù)據(jù)都可以通過(guò)CONTAINING_RECORD宏來(lái)抽取帝雇,具體CONTAINING_RECORD是如何工作的我就不細(xì)講了涮俄,網(wǎng)上一大堆。
② C++類(lèi)繼承overlapped數(shù)據(jù)結(jié)構(gòu)
這種方法其實(shí)與第一種方法很類(lèi)似尸闸,不過(guò)比較方便的是不需要用CONTAINING_RECORD來(lái)抽取具體信息了彻亲。
class MyOverlapped : public OVERLAPPED{
WSABUF wsabuf_;
char data_[DATASIZE];
int data_len_;
OP operate_type_;
SOCKET client_;
}
同樣在使用的時(shí)候需要將其轉(zhuǎn)換為(OVERLAPPED*)傳入overlapped函數(shù),在IO事件完成后再將其轉(zhuǎn)換為我們的類(lèi)室叉,(MyOverlapped*)(&OVERLAPPED),之后直接讀取成員變量即可得出信息睹栖。
IOCP內(nèi)部工作原理
先上一張Jeffrey Richter在windows核心編程里的一個(gè)IOCP原理圖硫惕。一張好圖的效果比說(shuō)n句話效果要好多了茧痕。
雖然微軟沒(méi)有公開(kāi)完成端口具體的實(shí)現(xiàn)方式,但是從Jeffrey Richter的windows核心編程可以大概了解完成端口的大概實(shí)現(xiàn)恼除。
當(dāng)我們創(chuàng)建一個(gè)完成端口的時(shí)候(創(chuàng)建方式下一節(jié)具體講)windows底層會(huì)幫我們創(chuàng)建一系列底層設(shè)施踪旷,以輔助我們后來(lái)的通信過(guò)程。具體設(shè)施如上圖所示豁辉。
①設(shè)備列表
這里的設(shè)備列表我們可以簡(jiǎn)單的認(rèn)為就是所有連接的socket信息的列表令野,對(duì)于一個(gè)socket我們要將他與完成端口關(guān)聯(lián),完成端口才會(huì)在內(nèi)核中為我們監(jiān)聽(tīng)我們感興趣的事件徽级,這里有一個(gè)關(guān)鍵的數(shù)據(jù)結(jié)構(gòu)气破,dwCompletionKey,這個(gè)數(shù)據(jù)結(jié)構(gòu)需要在我們將socket綁定到完成端口時(shí)一起傳入內(nèi)核設(shè)備列表中餐抢,那么他有什么作用呢现使?我的理解是這個(gè)數(shù)據(jù)結(jié)構(gòu)主要是為了在內(nèi)核中標(biāo)記當(dāng)前所通信的socket對(duì)象具體是哪一個(gè)低匙,這個(gè)數(shù)據(jù)結(jié)構(gòu)是由我們自己定義的,所以在這個(gè)結(jié)構(gòu)體里我們可以加上我們一些自己想要的信息碳锈,因?yàn)楹髞?lái)通信過(guò)程中內(nèi)核會(huì)將這個(gè)數(shù)據(jù)傳送給我們顽冶,所以在這個(gè)結(jié)構(gòu)中定義一些你感興趣的字段可以方便后期的一些操作,比如你可以定義一個(gè)容器用來(lái)存放改設(shè)備投遞的所有IO操作售碳,這樣在后期該socket關(guān)閉的時(shí)候可以方便清理與其相關(guān)的內(nèi)存强重,以確保不會(huì)造成內(nèi)存泄漏。
②完成隊(duì)列
完成隊(duì)列中存放的是已經(jīng)完成的IO事件贸人,每一個(gè)列表項(xiàng)主要包括dwBytesTransferred, dwCompletionKey, pOverlapped, dwError四個(gè)數(shù)據(jù)间景,dwBytesTransferred顧名思義就是本次IO事件所傳輸?shù)淖止?jié)數(shù),dwCompletionKey就是上面我們所說(shuō)的用來(lái)標(biāo)識(shí)socket信息的數(shù)據(jù)結(jié)構(gòu)艺智,從這個(gè)數(shù)據(jù)結(jié)構(gòu)我們可以知道當(dāng)前的IO事件是發(fā)生在哪個(gè)socket上的拱燃,當(dāng)然你需要在completionKey中設(shè)置這個(gè)字段,否則你也不知道這個(gè)事件是發(fā)生在哪個(gè)socket上的力惯。pOverlapped數(shù)據(jù)結(jié)構(gòu)也就是我們之前提到的Overlapped數(shù)據(jù)結(jié)構(gòu)碗誉,這個(gè)數(shù)據(jù)結(jié)構(gòu)里包含了IO數(shù)據(jù)。
③線程管理設(shè)施
線程管理是完成端口的一大特點(diǎn)父晶,也就是內(nèi)核在內(nèi)部自己管理一個(gè)線程池哮缺。與這個(gè)線程池相關(guān)的基礎(chǔ)設(shè)施主要有等待線程隊(duì)列(棧)(以棧的方式管理),已釋放的線程列表甲喝,已暫停的線程列表尝苇。當(dāng)線程調(diào)用GetQueuedCompletionStatus的時(shí)候內(nèi)核會(huì)將該線程放入線程棧中,因?yàn)镚etQueuedCompletionStatus會(huì)將線程掛起埠胖,一旦有事件發(fā)生GetQueuedCompletion返回糠溜,這時(shí)候內(nèi)核會(huì)將線程放入已釋放列表中,如果這個(gè)已釋放的線程又調(diào)用了一些函數(shù)將線程掛起直撤,那么內(nèi)核會(huì)將其放入已暫停線程列表中非竿。當(dāng)GetQueuedCompletionStatus返回后線程處理完數(shù)據(jù)后再次調(diào)用GetQueuedCompletionStatus進(jìn)行等待時(shí),內(nèi)核會(huì)重新將該線程放到線程等待棧中谋竖,這樣的一個(gè)流程下來(lái)我們可以看出红柱,如果在IO事件處理比較慢的情況下一個(gè)線程就可以搞定所有的IO請(qǐng)求,這樣避免了線程之間的上下文切換帶來(lái)的性能開(kāi)銷(xiāo)蓖乘。
4. 如何使用完成端口
鋪墊了這么多锤悄,終于要講到如何使用IOCP了。先介紹一下使用完成端口需要用到的幾個(gè)比較重要的函數(shù)嘉抒。
(1)CreateIOCompletionPort
之前說(shuō)到過(guò)完成端口在API設(shè)計(jì)上不夠清晰零聚,現(xiàn)在提到的這個(gè)函數(shù)可以充分說(shuō)明這個(gè)問(wèn)題。這個(gè)函數(shù)有兩個(gè)用途。
①CreateIOCompletionPort(-1,NULL,NULL,0)
這種調(diào)用方法是用來(lái)創(chuàng)建一個(gè)新的完成端口時(shí)使用的方式隶症,最重要的是第四個(gè)參數(shù)容诬,第四個(gè)參數(shù)主要是用來(lái)確定在同一時(shí)間最多能有多少個(gè)線程運(yùn)行,設(shè)置為0代表數(shù)量與機(jī)器CPU個(gè)數(shù)一致沿腰。
②CreateIOCompletionPort(socket, completionport览徒, pcompletionkey, 0)
這種調(diào)用方式使用老將一個(gè)socket句柄與已創(chuàng)建好的完成端口相關(guān)聯(lián),第一個(gè)參數(shù)代表句柄創(chuàng)建的時(shí)候不需要傳入颂龙,這個(gè)主要是用于將一個(gè)句柄綁定到完成端口時(shí)時(shí)使用的习蓬,第二個(gè)參數(shù)代表已創(chuàng)建好的完成端口,第三個(gè)參數(shù)completionley上面已經(jīng)說(shuō)過(guò)了措嵌,是用來(lái)標(biāo)識(shí)一個(gè)socket句柄的躲叼。
(2)GetQueuedCompletionStatus(
_In_? HANDLE? ? ? CompletionPort,
_Out_ LPDWORD? ? ? lpNumberOfBytes,
_Out_ PULONG_PTR? lpCompletionKey,
_Out_ LPOVERLAPPED *lpOverlapped,
_In_? DWORD? ? ? ? dwMilliseconds)
這個(gè)函數(shù)主要是將當(dāng)前線程掛起等待IO事件完成。
CompletionPort:就是上面所創(chuàng)建的端口企巢,另外有一點(diǎn)要說(shuō)明的是這個(gè)端口與socket中使用的端口不是一個(gè)概念枫慷,你就把這個(gè)端口當(dāng)成一個(gè)內(nèi)核句柄就行。
lpNumberOfBytes:這個(gè)代表IO事件傳輸?shù)淖止?jié)數(shù)
lpCompletionKey:代表當(dāng)前IO事件所隸屬的句柄信息浪规,這個(gè)是我們?cè)诮壎ň浔酵瓿啥丝跁r(shí)自己傳進(jìn)去的或听。
lpOverlapped:這個(gè)值就包含了這次異步IO的數(shù)據(jù)信息。
dwMilliseconds:代表在等待一個(gè)IO事件完成時(shí)會(huì)等多久笋婿,如果這個(gè)值設(shè)為INFINITE誉裆,那么這個(gè)調(diào)用將永遠(yuǎn)不會(huì)超時(shí),如果傳入0缸濒,那么這個(gè)調(diào)用會(huì)立即返回足丢。
(3)PostQueuedCompletionStatus(
_In_? ? HANDLE? ? ? CompletionPort,
_In_? ? DWORD? ? ? ? dwNumberOfBytesTransferred,
_In_? ? ULONG_PTR? ? dwCompletionKey,
_In_opt_ LPOVERLAPPED lpOverlapped
)
這個(gè)函數(shù)可以用來(lái)模擬IO完成事件,經(jīng)常用于退出時(shí)發(fā)送一個(gè)模擬的IO完成事件來(lái)喚醒在等待中的線程庇配,參數(shù)信息之前都有提到就不解釋了斩跌。
以上三個(gè)就是使用完成端口時(shí)最主要的三個(gè)函數(shù),那么既然要用到Overlapped數(shù)據(jù)結(jié)構(gòu)來(lái)投遞IO請(qǐng)求事件捞慌,那么socket的發(fā)送接收函數(shù)也就不能用原來(lái)常用的send和recv了耀鸦,要用到WSAERecv,WSASend兩個(gè)函數(shù)了,因?yàn)檫@兩個(gè)函數(shù)都接收一個(gè)overlapped參數(shù)卿闹。另外還有一個(gè)要提到的是揭糕,accept沒(méi)有對(duì)應(yīng)的WSA版本萝快,但是由于accept是一個(gè)阻塞函數(shù)锻霎,所以如果想盡可能提高性能,可以使用微軟后期自己封裝的一個(gè)函數(shù)acceptex揪漩,這個(gè)函數(shù)與原始的accept之間的差別在于它是將已經(jīng)創(chuàng)建好的socket傳入函數(shù)旋恼,那么當(dāng)其IO事件返回,對(duì)應(yīng)的socket也就已經(jīng)與客戶端建立連接了奄容,這個(gè)函數(shù)也接受一個(gè)overlapped數(shù)據(jù)結(jié)構(gòu)冰更,所以我們可以把a(bǔ)ccept事件當(dāng)做一般的IO事件即可产徊,當(dāng)GetQueuedCompletionStatus返回時(shí)檢查completionkey中的socket句柄是不是listen socket,如果是listen socket說(shuō)明有新的連接接入蜀细,這時(shí)候需要調(diào)用微軟自己的函數(shù)GetAcceptExSockaddrs來(lái)獲取客戶端的地址信息舟铜。在創(chuàng)建好完成端口時(shí)可以先投遞幾個(gè)accept IO事件,這樣可以在高并發(fā)的時(shí)候處理的得心應(yīng)手奠衔,當(dāng)然了谆刨,完成端口本身就很強(qiáng)大。有一點(diǎn)要注意的是在每次處理完新的連接時(shí)要重新投遞新的accept事件归斤,為下一個(gè)連接做準(zhǔn)備痊夭。
IOCP具體如何使用可以看我的GitHub,里面有完整的完成端口項(xiàng)目,對(duì)overlapped與completionkey都做了封裝脏里,有資源管理器她我,測(cè)試還未發(fā)現(xiàn)資源泄露,有問(wèn)題可以直接提出來(lái)迫横,會(huì)及時(shí)改進(jìn)番舆。
完成端口使用流程圖: