完成端口(Completion Port)详解----- By PiggyXP(小猪)
? 本系列里完成端口的代碼在兩年前就已經(jīng)寫(xiě)好了,但是由于許久沒(méi)有寫(xiě)東西了,不知該如何提筆,所以這篇文檔總是在醞釀之中……醞釀了兩年之后,終于決定開(kāi)始動(dòng)筆了,但愿還不算晚…..
??????? 這篇文檔我非常詳細(xì)并且圖文并茂的介紹了關(guān)于網(wǎng)絡(luò)編程模型中完成端口的方方面面的信息,從API的用法到使用的步驟,從完成端口的實(shí)現(xiàn)機(jī)理到實(shí)際使用的注意事項(xiàng),都有所涉及,并且為了讓朋友們更直觀的體會(huì)完成端口的用法,本文附帶了有詳盡注釋的使用MFC編寫(xiě)的圖形界面的示例代碼。
??????? 我的初衷是希望寫(xiě)一份互聯(lián)網(wǎng)上能找到的最詳盡的關(guān)于完成端口的教學(xué)文檔,而且讓對(duì)Socket編程略有了解的人都能夠看得懂,都能學(xué)會(huì)如何來(lái)使用完成端口這么優(yōu)異的網(wǎng)絡(luò)編程模型,但是由于本人水平所限,不知道我的初衷是否實(shí)現(xiàn)了,但還是希望各位需要的朋友能夠喜歡。
??????? 由于篇幅原因,本文假設(shè)你已經(jīng)熟悉了利用Socket進(jìn)行TCP/IP編程的基本原理,并且也熟練的掌握了多線程編程技術(shù),太基本的概念我這里就略過(guò)不提了,網(wǎng)上的資料應(yīng)該遍地都是。
??????? 本文檔凝聚著筆者心血,如要轉(zhuǎn)載,請(qǐng)指明原作者及出處,謝謝!不過(guò)代碼沒(méi)有版權(quán),可以隨便散播使用,歡迎改進(jìn),特別是非常歡迎能夠幫助我發(fā)現(xiàn)Bug的朋友,以更好的造福大家。^_^
??????? 本文配套的示例源碼下載地址(在我的下載空間里,已經(jīng)補(bǔ)充上了客戶端的代碼)
????????http://piggyxp.download.csdn.net/
?????? (里面的代碼包括VC++2008/VC++2010編寫(xiě)的完成端口服務(wù)器端和客戶端的代碼,還包括一個(gè)對(duì)服務(wù)器端進(jìn)行壓力測(cè)試的客戶端,都是經(jīng)過(guò)我精心調(diào)試過(guò),并且?guī)в蟹浅T敱M的代碼注釋的。當(dāng)然,作為教學(xué)代碼,為了能夠使得代碼結(jié)構(gòu)清晰明了,我還是對(duì)代碼有所簡(jiǎn)化,如果想要用于產(chǎn)品開(kāi)發(fā),最好還是需要自己再完善一下,另外我的工程是用2010編寫(xiě)的,附帶的2008工程不知道有沒(méi)有問(wèn)題,但是其中代碼都是一樣的,暫未測(cè)試)
??????? 忘了囑咐一下了,文章篇幅很長(zhǎng)很長(zhǎng),基本涉及到了與完成端口有關(guān)的方方面面,一次看不完可以分好幾次,中間注意休息,好身體才是咱們程序員最大的本錢(qián)!
?????? 對(duì)了,還忘了囑咐一下,因?yàn)楸救说乃接邢?#xff0c;雖然我反復(fù)修正了數(shù)遍,但文章和示例代碼里肯定還有我沒(méi)發(fā)現(xiàn)的錯(cuò)誤和紕漏,希望各位一定要指出來(lái),拍磚、噴我,我都能Hold住,但是一定要指出來(lái),我會(huì)及時(shí)修正,因?yàn)槲也幌胱屛闹械腻e(cuò)誤傳遍互聯(lián)網(wǎng),禍害大家。
????? OK, Let’s go ! Have fun !
?
目錄:
1. 完成端口的優(yōu)點(diǎn)
2. 完成端口程序的運(yùn)行演示
3. 完成端口的相關(guān)概念
4. 完成端口的基本流程
5. 完成端口的使用詳解
6. 實(shí)際應(yīng)用中應(yīng)該要注意的地方
?
一. 完成端口的優(yōu)點(diǎn)
??????? 1. 我想只要是寫(xiě)過(guò)或者想要寫(xiě)C/S模式網(wǎng)絡(luò)服務(wù)器端的朋友,都應(yīng)該或多或少的聽(tīng)過(guò)完成端口的大名吧,完成端口會(huì)充分利用Windows內(nèi)核來(lái)進(jìn)行I/O的調(diào)度,是用于C/S通信模式中性能最好的網(wǎng)絡(luò)通信模型,沒(méi)有之一;甚至連和它性能接近的通信模型都沒(méi)有。
??????? 2. 完成端口和其他網(wǎng)絡(luò)通信方式最大的區(qū)別在哪里呢?
??????? (1) 首先,如果使用“同步”的方式來(lái)通信的話,這里說(shuō)的同步的方式就是說(shuō)所有的操作都在一個(gè)線程內(nèi)順序執(zhí)行完成,這么做缺點(diǎn)是很明顯的:因?yàn)橥降耐ㄐ挪僮鲿?huì)阻塞住來(lái)自同一個(gè)線程的任何其他操作,只有這個(gè)操作完成了之后,后續(xù)的操作才可以完成;一個(gè)最明顯的例子就是咱們?cè)贛FC的界面代碼中,直接使用阻塞Socket調(diào)用的代碼,整個(gè)界面都會(huì)因此而阻塞住沒(méi)有響應(yīng)!所以我們不得不為每一個(gè)通信的Socket都要建立一個(gè)線程,多麻煩?這不坑爹呢么?所以要寫(xiě)高性能的服務(wù)器程序,要求通信一定要是異步的。
??????? (2) 各位讀者肯定知道,可以使用使用“同步通信(阻塞通信)+多線程”的方式來(lái)改善(1)的情況,那么好,想一下,我們好不容易實(shí)現(xiàn)了讓服務(wù)器端在每一個(gè)客戶端連入之后,都要啟動(dòng)一個(gè)新的Thread和客戶端進(jìn)行通信,有多少個(gè)客戶端,就需要啟動(dòng)多少個(gè)線程,對(duì)吧;但是由于這些線程都是處于運(yùn)行狀態(tài),所以系統(tǒng)不得不在所有可運(yùn)行的線程之間進(jìn)行上下文的切換,我們自己是沒(méi)啥感覺(jué),但是CPU卻痛苦不堪了,因?yàn)榫€程切換是相當(dāng)浪費(fèi)CPU時(shí)間的,如果客戶端的連入線程過(guò)多,這就會(huì)弄得CPU都忙著去切換線程了,根本沒(méi)有多少時(shí)間去執(zhí)行線程體了,所以效率是非常低下的,承認(rèn)坑爹了不?
??????? (3) 而微軟提出完成端口模型的初衷,就是為了解決這種"one-thread-per-client"的缺點(diǎn)的,它充分利用內(nèi)核對(duì)象的調(diào)度,只使用少量的幾個(gè)線程來(lái)處理和客戶端的所有通信,消除了無(wú)謂的線程上下文切換,最大限度的提高了網(wǎng)絡(luò)通信的性能,這種神奇的效果具體是如何實(shí)現(xiàn)的請(qǐng)看下文。
??????? 3. 完成端口被廣泛的應(yīng)用于各個(gè)高性能服務(wù)器程序上,例如著名的Apache….如果你想要編寫(xiě)的服務(wù)器端需要同時(shí)處理的并發(fā)客戶端連接數(shù)量有數(shù)百上千個(gè)的話,那不用糾結(jié)了,就是它了。
?
二. 完成端口程序的運(yùn)行演示
??????? 首先,我們先來(lái)看一下完成端口在筆者的PC機(jī)上的運(yùn)行表現(xiàn),筆者的PC配置如下:
????????????????????????
??????? 大體就是i7 2600 + 16GB內(nèi)存,我以這臺(tái)PC作為服務(wù)器,簡(jiǎn)單的進(jìn)行了如下的測(cè)試,通過(guò)Client生成3萬(wàn)個(gè)并發(fā)線程同時(shí)連接至Server,然后每個(gè)線程每隔3秒鐘發(fā)送一次數(shù)據(jù),一共發(fā)送3次,然后觀察服務(wù)器端的CPU和內(nèi)存的占用情況。
??????? 如圖2所示,是客戶端3萬(wàn)個(gè)并發(fā)線程發(fā)送共發(fā)送9萬(wàn)條數(shù)據(jù)的log截圖
?????????????????????????????
??????? 圖3是服務(wù)器端接收完畢3萬(wàn)個(gè)并發(fā)線程和每個(gè)線程的3份數(shù)據(jù)后的log截圖
???????????????????????????????
??????? 最關(guān)鍵是圖4,圖4是服務(wù)器端在接收到28000個(gè)并發(fā)線程的時(shí)候,CPU占用率的截圖,使用的軟件是大名鼎鼎的Process Explorer,因?yàn)橄鄬?duì)來(lái)講這個(gè)比自帶的任務(wù)管理器要準(zhǔn)確和精確一些。
??????????????????????? ???????????
???????? 我們可以發(fā)現(xiàn)一個(gè)令人驚訝的結(jié)果,采用了完成端口的Server程序(藍(lán)色橫線所示)所占用的CPU才為 3.82%,整個(gè)運(yùn)行過(guò)程中的峰值也沒(méi)有超過(guò)4%,是相當(dāng)氣定神閑的……哦,對(duì)了,這還是在Debug環(huán)境下運(yùn)行的情況,如果采用Release方式執(zhí)行,性能肯定還會(huì)更高一些,除此以外,在UI上顯示信息也很大成都上影響了性能。
???????? 相反采用了多個(gè)并發(fā)線程的Client程序(紫色橫線所示)居然占用的CPU高達(dá)11.53%,甚至超過(guò)了Server程序的數(shù)倍……
???????? 其實(shí)無(wú)論是哪種網(wǎng)絡(luò)操模型,對(duì)于內(nèi)存占用都是差不多的,真正的差別就在于CPU的占用,其他的網(wǎng)絡(luò)模型都需要更多的CPU動(dòng)力來(lái)支撐同樣的連接數(shù)據(jù)。
??????? ?雖然這遠(yuǎn)遠(yuǎn)算不上服務(wù)器極限壓力測(cè)試,但是從中也可以看出來(lái)完成端口的實(shí)力,而且這種方式比純粹靠多線程的方式實(shí)現(xiàn)并發(fā)資源占用率要低得多。
?
三. 完成端口的相關(guān)概念
???????? 在開(kāi)始編碼之前,我們先來(lái)討論一下和完成端口相關(guān)的一些概念,如果你沒(méi)有耐心看完這段大段的文字的話,也可以跳過(guò)這一節(jié)直接去看下下一節(jié)的具體實(shí)現(xiàn)部分,但是這一節(jié)中涉及到的基本概念你還是有必要了解一下的,而且你也更能知道為什么有那么多的網(wǎng)絡(luò)編程模式不用,非得要用這么又復(fù)雜又難以理解的完成端口呢??也會(huì)堅(jiān)定你繼續(xù)學(xué)習(xí)下去的信心^_^
???????? 3.1 異步通信機(jī)制及其幾種實(shí)現(xiàn)方式的比較
???????? 我們從前面的文字中了解到,高性能服務(wù)器程序使用異步通信機(jī)制是必須的。
???????? 而對(duì)于異步的概念,為了方便后面文字的理解,這里還是再次簡(jiǎn)單的描述一下:
???????? 異步通信就是在咱們與外部的I/O設(shè)備進(jìn)行打交道的時(shí)候,我們都知道外部設(shè)備的I/O和CPU比起來(lái)簡(jiǎn)直是龜速,比如硬盤(pán)讀寫(xiě)、網(wǎng)絡(luò)通信等等,我們沒(méi)有必要在咱們自己的線程里面等待著I/O操作完成再執(zhí)行后續(xù)的代碼,而是將這個(gè)請(qǐng)求交給設(shè)備的驅(qū)動(dòng)程序自己去處理,我們的線程可以繼續(xù)做其他更重要的事情,大體的流程如下圖所示:
????????????????????????
??????? 我可以從圖中看到一個(gè)很明顯的并行操作的過(guò)程,而“同步”的通信方式是在進(jìn)行網(wǎng)絡(luò)操作的時(shí)候,主線程就掛起了,主線程要等待網(wǎng)絡(luò)操作完成之后,才能繼續(xù)執(zhí)行后續(xù)的代碼,就是說(shuō)要末執(zhí)行主線程,要末執(zhí)行網(wǎng)絡(luò)操作,是沒(méi)法這樣并行的;
??????? “異步”方式無(wú)疑比 “阻塞模式+多線程”的方式效率要高的多,這也是前者為什么叫“異步”,后者為什么叫“同步”的原因了,因?yàn)椴恍枰却W(wǎng)絡(luò)操作完成再執(zhí)行別的操作。
????????而在Windows中實(shí)現(xiàn)異步的機(jī)制同樣有好幾種,而這其中的區(qū)別,關(guān)鍵就在于圖1中的最后一步“通知應(yīng)用程序處理網(wǎng)絡(luò)數(shù)據(jù)”上了,因?yàn)閷?shí)現(xiàn)操作系統(tǒng)調(diào)用設(shè)備驅(qū)動(dòng)程序去接收數(shù)據(jù)的操作都是一樣的,關(guān)鍵就是在于如何去通知應(yīng)用程序來(lái)拿數(shù)據(jù)。它們之間的具體區(qū)別我這里多講幾點(diǎn),文字有點(diǎn)多,如果沒(méi)興趣深入研究的朋友可以跳過(guò)下一面的這一段,不影響的:)
??????? (1)?設(shè)備內(nèi)核對(duì)象,使用設(shè)備內(nèi)核對(duì)象來(lái)協(xié)調(diào)數(shù)據(jù)的發(fā)送請(qǐng)求和接收數(shù)據(jù)協(xié)調(diào),也就是說(shuō)通過(guò)設(shè)置設(shè)備內(nèi)核對(duì)象的狀態(tài),在設(shè)備接收數(shù)據(jù)完成后,馬上觸發(fā)這個(gè)內(nèi)核對(duì)象,然后讓接收數(shù)據(jù)的線程收到通知,但是這種方式太原始了,接收數(shù)據(jù)的線程為了能夠知道內(nèi)核對(duì)象是否被觸發(fā)了,還是得不停的掛起等待,這簡(jiǎn)直是根本就沒(méi)有用嘛,太低級(jí)了,有木有?所以在這里就略過(guò)不提了,各位讀者要是沒(méi)明白是怎么回事也不用深究了,總之沒(méi)有什么用。
??????? (2)?事件內(nèi)核對(duì)象,利用事件內(nèi)核對(duì)象來(lái)實(shí)現(xiàn)I/O操作完成的通知,其實(shí)這種方式其實(shí)就是我以前寫(xiě)文章的時(shí)候提到的《基于事件通知的重疊I/O模型》,鏈接在這里,這種機(jī)制就先進(jìn)得多,可以同時(shí)等待多個(gè)I/O操作的完成,實(shí)現(xiàn)真正的異步,但是缺點(diǎn)也是很明顯的,既然用WaitForMultipleObjects()來(lái)等待Event的話,就會(huì)受到64個(gè)Event等待上限的限制,但是這可不是說(shuō)我們只能處理來(lái)自于64個(gè)客戶端的Socket,而是這是屬于在一個(gè)設(shè)備內(nèi)核對(duì)象上等待的64個(gè)事件內(nèi)核對(duì)象,也就是說(shuō),我們?cè)谝粋€(gè)線程內(nèi),可以同時(shí)監(jiān)控64個(gè)重疊I/O操作的完成狀態(tài),當(dāng)然我們同樣可以使用多個(gè)線程的方式來(lái)滿足無(wú)限多個(gè)重疊I/O的需求,比如如果想要支持3萬(wàn)個(gè)連接,就得需要500多個(gè)線程…用起來(lái)太麻煩讓人感覺(jué)不爽;
??????? (3) 使用APC( Asynchronous Procedure Call,異步過(guò)程調(diào)用)來(lái)完成,這個(gè)也就是我以前在文章里提到的《基于完成例程的重疊I/O模型》,鏈接在這里,這種方式的好處就是在于擺脫了基于事件通知方式的64個(gè)事件上限的限制,但是缺點(diǎn)也是有的,就是發(fā)出請(qǐng)求的線程必須得要自己去處理接收請(qǐng)求,哪怕是這個(gè)線程發(fā)出了很多發(fā)送或者接收數(shù)據(jù)的請(qǐng)求,但是其他的線程都閑著…,這個(gè)線程也還是得自己來(lái)處理自己發(fā)出去的這些請(qǐng)求,沒(méi)有人來(lái)幫忙…這就有一個(gè)負(fù)載均衡問(wèn)題,顯然性能沒(méi)有達(dá)到最優(yōu)化。
??????? (4)?完成端口,不用說(shuō)大家也知道了,最后的壓軸戲就是使用完成端口,對(duì)比上面幾種機(jī)制,完成端口的做法是這樣的:事先開(kāi)好幾個(gè)線程,你有幾個(gè)CPU我就開(kāi)幾個(gè),首先是避免了線程的上下文切換,因?yàn)榫€程想要執(zhí)行的時(shí)候,總有CPU資源可用,然后讓這幾個(gè)線程等著,等到有用戶請(qǐng)求來(lái)到的時(shí)候,就把這些請(qǐng)求都加入到一個(gè)公共消息隊(duì)列中去,然后這幾個(gè)開(kāi)好的線程就排隊(duì)逐一去從消息隊(duì)列中取出消息并加以處理,這種方式就很優(yōu)雅的實(shí)現(xiàn)了異步通信和負(fù)載均衡的問(wèn)題,因為它提供了一種機(jī)制來(lái)使用幾個(gè)線程“公平的”處理來(lái)自于多個(gè)客戶端的輸入/輸出,并且線程如果沒(méi)事干的時(shí)候也會(huì)被系統(tǒng)掛起,不會(huì)占用CPU周期,挺完美的一個(gè)解決方案,不是嗎?哦,對(duì)了,這個(gè)關(guān)鍵的作為交換的消息隊(duì)列,就是完成端口。
??????? 比較完畢之后,熟悉網(wǎng)絡(luò)編程的朋友可能會(huì)問(wèn)到,為什么沒(méi)有提到WSAAsyncSelect或者是WSAEventSelect這兩個(gè)異步模型呢,對(duì)于這兩個(gè)模型,我不知道其內(nèi)部是如何實(shí)現(xiàn)的,但是這其中一定沒(méi)有用到Overlapped機(jī)制,就不能算作是真正的異步,可能是其內(nèi)部自己在維護(hù)一個(gè)消息隊(duì)列吧,總之這兩個(gè)模式雖然實(shí)現(xiàn)了異步的接收,但是卻不能進(jìn)行異步的發(fā)送,這就很明顯說(shuō)明問(wèn)題了,我想其內(nèi)部的實(shí)現(xiàn)一定和完成端口是迥異的,并且,完成端口非常厚道,因?yàn)樗窍劝延脩魯?shù)據(jù)接收回來(lái)之后再通知用戶直接來(lái)取就好了,而WSAAsyncSelect和WSAEventSelect之流只是會(huì)接收到數(shù)據(jù)到達(dá)的通知,而只能由應(yīng)用程序自己再另外去recv數(shù)據(jù),性能上的差距就更明顯了。
??????? 最后,我的建議是,想要使用 基于事件通知的重疊I/O和基于完成例程的重疊I/O的朋友,如果不是特別必要,就不要去使用了,因?yàn)檫@兩種方式不僅使用和理解起來(lái)也不算簡(jiǎn)單,而且還有性能上的明顯瓶頸,何不就再努力一下使用完成端口呢?
??????? 3.2 重疊結(jié)構(gòu)(OVERLAPPED)
???????? 我們從上一小節(jié)中得知,要實(shí)現(xiàn)異步通信,必須要用到一個(gè)很風(fēng)騷的I/O數(shù)據(jù)結(jié)構(gòu),叫重疊結(jié)構(gòu)“Overlapped”,Windows里所有的異步通信都是基于它的,完成端口也不例外。
???????? 至于為什么叫Overlapped?Jeffrey Richter的解釋是因?yàn)椤皥?zhí)行I/O請(qǐng)求的時(shí)間與線程執(zhí)行其他任務(wù)的時(shí)間是重疊(overlapped)的”,從這個(gè)名字我們也可能看得出來(lái)重疊結(jié)構(gòu)發(fā)明的初衷了,對(duì)于重疊結(jié)構(gòu)的內(nèi)部細(xì)節(jié)我這里就不過(guò)多的解釋了,就把它當(dāng)成和其他內(nèi)核對(duì)象一樣,不需要深究其實(shí)現(xiàn)機(jī)制,只要會(huì)使用就可以了,想要了解更多重疊結(jié)構(gòu)內(nèi)部的朋友,請(qǐng)去翻閱Jeffrey Richter的《Windows via C/C++》 5th?的292頁(yè),如果沒(méi)有機(jī)會(huì)的話,也可以隨便翻翻我以前寫(xiě)的Overlapped的東西,不過(guò)寫(xiě)得比較淺顯……
???????? 這里我想要解釋的是,這個(gè)重疊結(jié)構(gòu)是異步通信機(jī)制實(shí)現(xiàn)的一個(gè)核心數(shù)據(jù)結(jié)構(gòu),因?yàn)槟憧吹胶竺娴拇a你會(huì)發(fā)現(xiàn),幾乎所有的網(wǎng)絡(luò)操作例如發(fā)送/接收之類(lèi)的,都會(huì)用WSASend()和WSARecv()代替,參數(shù)里面都會(huì)附帶一個(gè)重疊結(jié)構(gòu),這是為什么呢?因?yàn)橹丿B結(jié)構(gòu)我們就可以理解成為是一個(gè)網(wǎng)絡(luò)操作的ID號(hào),也就是說(shuō)我們要利用重疊I/O提供的異步機(jī)制的話,每一個(gè)網(wǎng)絡(luò)操作都要有一個(gè)唯一的ID號(hào),因?yàn)檫M(jìn)了系統(tǒng)內(nèi)核,里面黑燈瞎火的,也不了解上面出了什么狀況,一看到有重疊I/O的調(diào)用進(jìn)來(lái)了,就會(huì)使用其異步機(jī)制,并且操作系統(tǒng)就只能靠這個(gè)重疊結(jié)構(gòu)帶有的ID號(hào)來(lái)區(qū)分是哪一個(gè)網(wǎng)絡(luò)操作了,然后內(nèi)核里面處理完畢之后,根據(jù)這個(gè)ID號(hào),把對(duì)應(yīng)的數(shù)據(jù)傳上去。
???????? 你要是實(shí)在不理解這是個(gè)什么玩意,那就直接看后面的代碼吧,慢慢就明白了……
???????? 3.3 完成端口(CompletionPort)
??????? 對(duì)于完成端口這個(gè)概念,我一直不知道為什么它的名字是叫“完成端口”,我個(gè)人的感覺(jué)應(yīng)該叫它“完成隊(duì)列”似乎更合適一些,總之這個(gè)“端口”和我們平常所說(shuō)的用于網(wǎng)絡(luò)通信的“端口”完全不是一個(gè)東西,我們不要混淆了。
??????? 首先,它之所以叫“完成”端口,就是說(shuō)系統(tǒng)會(huì)在網(wǎng)絡(luò)I/O操作“完成”之后才會(huì)通知我們,也就是說(shuō),我們?cè)诮拥较到y(tǒng)的通知的時(shí)候,其實(shí)網(wǎng)絡(luò)操作已經(jīng)完成了,就是比如說(shuō)在系統(tǒng)通知我們的時(shí)候,并非是有數(shù)據(jù)從網(wǎng)絡(luò)上到來(lái),而是來(lái)自于網(wǎng)絡(luò)上的數(shù)據(jù)已經(jīng)接收完畢了;或者是客戶端的連入請(qǐng)求已經(jīng)被系統(tǒng)接入完畢了等等,我們只需要處理后面的事情就好了。
??????? 各位朋友可能會(huì)很開(kāi)心,什么?已經(jīng)處理完畢了才通知我們,那豈不是很爽?其實(shí)也沒(méi)什么爽的,那是因?yàn)槲覀冊(cè)谥敖o系統(tǒng)分派工作的時(shí)候,都囑咐好了,我們會(huì)通過(guò)代碼告訴系統(tǒng)“你給我做這個(gè)做那個(gè),等待做完了再通知我”,只是這些工作是做在之前還是之后的區(qū)別而已。
??????? 其次,我們需要知道,所謂的完成端口,其實(shí)和HANDLE一樣,也是一個(gè)內(nèi)核對(duì)象,雖然Jeff Richter嚇唬我們說(shuō):“完成端口可能是最為復(fù)雜的內(nèi)核對(duì)象了”,但是我們也不用去管他,因?yàn)樗唧w的內(nèi)部如何實(shí)現(xiàn)的和我們無(wú)關(guān),只要我們能夠?qū)W會(huì)用它相關(guān)的API把這個(gè)完成端口的框架搭建起來(lái)就可以了。我們暫時(shí)只用把它大體理解為一個(gè)容納網(wǎng)絡(luò)通信操作的隊(duì)列就好了,它會(huì)把網(wǎng)絡(luò)操作完成的通知,都放在這個(gè)隊(duì)列里面,咱們只用從這個(gè)隊(duì)列里面取就行了,取走一個(gè)就少一個(gè)…。
??????? 關(guān)于完成端口內(nèi)核對(duì)象的具體更多內(nèi)部細(xì)節(jié)我會(huì)在后面的“完成端口的基本原理”一節(jié)更詳細(xì)的和朋友們一起來(lái)研究,當(dāng)然,要是你們?cè)谖恼轮袥](méi)有看到這一節(jié)的話,就是說(shuō)明我又犯懶了沒(méi)寫(xiě)…在后續(xù)的文章里我會(huì)補(bǔ)上。這里就暫時(shí)說(shuō)這么多了,到時(shí)候我們也可以看到它的機(jī)制也并非有那么的復(fù)雜,可能只是因?yàn)椴僮飨到y(tǒng)其他的內(nèi)核對(duì)象相比較而言實(shí)現(xiàn)起來(lái)太容易了吧^_^
?
四. 使用完成端口的基本流程
???????? 說(shuō)了這么多的廢話,大家都等不及了吧,我們終于到了具體編碼的時(shí)候了。
??????? 使用完成端口,說(shuō)難也難,但是說(shuō)簡(jiǎn)單,其實(shí)也簡(jiǎn)單 ---- 又說(shuō)了一句廢話=。=
??????? 大體上來(lái)講,使用完成端口只用遵循如下幾個(gè)步驟:
????? ? (1) 調(diào)用 CreateIoCompletionPort() 函數(shù)創(chuàng)建一個(gè)完成端口,而且在一般情況下,我們需要且只需要建立這一個(gè)完成端口,把它的句柄保存好,我們今后會(huì)經(jīng)常用到它……
?????? ?(2) 根據(jù)系統(tǒng)中有多少個(gè)處理器,就建立多少個(gè)工作者(為了醒目起見(jiàn),下面直接說(shuō)Worker)線程,這幾個(gè)線程是專(zhuān)門(mén)用來(lái)和客戶端進(jìn)行通信的,目前暫時(shí)沒(méi)什么工作;
??????? (3) 下面就是接收連入的Socket連接了,這里有兩種實(shí)現(xiàn)方式:一是和別的編程模型一樣,還需要啟動(dòng)一個(gè)獨(dú)立的線程,專(zhuān)門(mén)用來(lái)accept客戶端的連接請(qǐng)求;二是用性能更高更好的異步AcceptEx()請(qǐng)求,因?yàn)楦魑粚?duì)accept用法應(yīng)該非常熟悉了,而且網(wǎng)上資料也會(huì)很多,所以為了更全面起見(jiàn),本文采用的是性能更好的AcceptEx,至于兩者代碼編寫(xiě)上的區(qū)別,我接下來(lái)會(huì)詳細(xì)的講。
?????? ?(4) 每當(dāng)有客戶端連入的時(shí)候,我們就還是得調(diào)用CreateIoCompletionPort()函數(shù),這里卻不是新建立完成端口了,而是把新連入的Socket(也就是前面所謂的設(shè)備句柄),與目前的完成端口綁定在一起。
??????? 至此,我們其實(shí)就已經(jīng)完成了完成端口的相關(guān)部署工作了,嗯,是的,完事了,后面的代碼里我們就可以充分享受完成端口帶給我們的巨大優(yōu)勢(shì),坐享其成了,是不是很簡(jiǎn)單呢?
?????? (5) 例如,客戶端連入之后,我們可以在這個(gè)Socket上提交一個(gè)網(wǎng)絡(luò)請(qǐng)求,例如WSARecv(),然后系統(tǒng)就會(huì)幫咱們乖乖的去執(zhí)行接收數(shù)據(jù)的操作,我們大可以放心的去干別的事情了;
?????? (6) 而此時(shí),我們預(yù)先準(zhǔn)備的那幾個(gè)Worker線程就不能閑著了, 我們?cè)谇懊娼⒌膸讉€(gè)Worker就要忙活起來(lái)了,都需要分別調(diào)用GetQueuedCompletionStatus() 函數(shù)在掃描完成端口的隊(duì)列里是否有網(wǎng)絡(luò)通信的請(qǐng)求存在(例如讀取數(shù)據(jù),發(fā)送數(shù)據(jù)等),一旦有的話,就將這個(gè)請(qǐng)求從完成端口的隊(duì)列中取回來(lái),繼續(xù)執(zhí)行本線程中后面的處理代碼,處理完畢之后,我們?cè)倮^續(xù)投遞下一個(gè)網(wǎng)絡(luò)通信的請(qǐng)求就OK了,如此循環(huán)。
??????? 關(guān)于完成端口的使用步驟,用文字來(lái)表述就是這么多了,很簡(jiǎn)單吧?如果你還是不理解,我再配合一個(gè)流程圖來(lái)表示一下:
??????? 當(dāng)然,我這里假設(shè)你已經(jīng)對(duì)網(wǎng)絡(luò)編程的基本套路有了解了,所以略去了很多基本的細(xì)節(jié),并且為了配合朋友們更好的理解我的代碼,在流程圖我標(biāo)出了一些函數(shù)的名字,并且畫(huà)得非常詳細(xì)。
??????? 另外需要注意的是由于對(duì)于客戶端的連入有兩種方式,一種是普通阻塞的accept,另外一種是性能更好的AcceptEx,為了能夠方面朋友們從別的網(wǎng)絡(luò)編程的方式中過(guò)渡,我這里畫(huà)了兩種方式的流程圖,方便朋友們對(duì)比學(xué)習(xí),圖a是使用accept的方式,當(dāng)然配套的源代碼我默認(rèn)就不提供了,如果需要的話,我倒是也可以發(fā)上來(lái);圖b是使用AcceptEx的,并配有配套的源碼。
??????? 采用accept方式的流程示意圖如下:
??????????????????????????
???????? 采用AcceptEx方式的流程示意圖如下:
???????????????????????????
????????
???????? 兩個(gè)圖中最大的相同點(diǎn)是什么?是的,最大的相同點(diǎn)就是主線程無(wú)所事事,閑得蛋疼……
??????? ?為什么呢?因?yàn)槲覀兪褂昧水惒降耐ㄐ艡C(jī)制,這些瑣碎重復(fù)的事情完全沒(méi)有必要交給主線程自己來(lái)做了,只用在初始化的時(shí)候和Worker線程交待好就可以了,用一句話來(lái)形容就是,主線程永遠(yuǎn)也體會(huì)不到Worker線程有多忙,而Worker線程也永遠(yuǎn)體會(huì)不到主線程在初始化建立起這個(gè)通信框架的時(shí)候操了多少的心……
??????? ?圖a中是由 _AcceptThread()負(fù)責(zé)接入連接,并把連入的Socket和完成端口綁定,另外的多個(gè)_WorkerThread()就負(fù)責(zé)監(jiān)控完成端口上的情況,一旦有情況了,就取出來(lái)處理,如果CPU有多核的話,就可以多個(gè)線程輪著來(lái)處理完成端口上的信息,很明顯效率就提高了。
???????? 圖b中最明顯的區(qū)別,也就是AcceptEx和傳統(tǒng)的accept之間最大的區(qū)別,就是取消了阻塞方式的accept調(diào)用,也就是說(shuō),AcceptEx也是通過(guò)完成端口來(lái)異步完成的,所以就取消了專(zhuān)門(mén)用于accept連接的線程,用了完成端口來(lái)進(jìn)行異步的AcceptEx調(diào)用;然后在檢索完成端口隊(duì)列的Worker函數(shù)中,根據(jù)用戶投遞的完成操作的類(lèi)型,再來(lái)找出其中的投遞的Accept請(qǐng)求,加以對(duì)應(yīng)的處理。
???????? 讀者一定會(huì)問(wèn),這樣做的好處在哪里?為什么還要異步的投遞AcceptEx連接的操作呢?
???????? 首先,我可以很明確的告訴各位,如果短時(shí)間內(nèi)客戶端的并發(fā)連接請(qǐng)求不是特別多的話,用accept和AcceptEx在性能上來(lái)講是沒(méi)什么區(qū)別的。
??????? 按照我們目前主流的PC來(lái)講,如果客戶端只進(jìn)行連接請(qǐng)求,而什么都不做的話,我們的Server只能接收大約3萬(wàn)-4萬(wàn)個(gè)左右的并發(fā)連接,然后客戶端其余的連入請(qǐng)求就只能收到WSAENOBUFS (10055)了,因?yàn)橄到y(tǒng)來(lái)不及為新連入的客戶端準(zhǔn)備資源了。
??????? 需要準(zhǔn)備什么資源?當(dāng)然是準(zhǔn)備Socket了……雖然我們創(chuàng)建Socket只用一行SOCKET s= socket(…) 這么一行的代碼就OK了,但是系統(tǒng)內(nèi)部建立一個(gè)Socket是相當(dāng)耗費(fèi)資源的,因?yàn)閃insock2是分層的機(jī)構(gòu)體系,創(chuàng)建一個(gè)Socket需要到多個(gè)Provider之間進(jìn)行處理,最終形成一個(gè)可用的套接字。總之,系統(tǒng)創(chuàng)建一個(gè)Socket的開(kāi)銷(xiāo)是相當(dāng)高的,所以用accept的話,系統(tǒng)可能來(lái)不及為更多的并發(fā)客戶端現(xiàn)場(chǎng)準(zhǔn)備Socket了。
??????? 而AcceptEx比Accept又強(qiáng)大在哪里呢?是有三點(diǎn):
??????? ?(1)?這個(gè)好處是最關(guān)鍵的,是因?yàn)锳cceptEx是在客戶端連入之前,就把客戶端的Socket建立好了,也就是說(shuō),AcceptEx是先建立的Socket,然后才發(fā)出的AcceptEx調(diào)用,也就是說(shuō),在進(jìn)行客戶端的通信之前,無(wú)論是否有客戶端連入,Socket都是提前建立好了;而不需要像accept是在客戶端連入了之后,再現(xiàn)場(chǎng)去花費(fèi)時(shí)間建立Socket。如果各位不清楚是如何實(shí)現(xiàn)的,請(qǐng)看后面的實(shí)現(xiàn)部分。
?????? ??(2) 相比accept只能阻塞方式建立一個(gè)連入的入口,對(duì)于大量的并發(fā)客戶端來(lái)講,入口實(shí)在是有點(diǎn)擠;而AcceptEx可以同時(shí)在完成端口上投遞多個(gè)請(qǐng)求,這樣有客戶端連入的時(shí)候,就非常優(yōu)雅而且從容不迫的邊喝茶邊處理連入請(qǐng)求了。
?????? ? (3) AcceptEx還有一個(gè)非常體貼的優(yōu)點(diǎn),就是在投遞AcceptEx的時(shí)候,我們還可以順便在AcceptEx的同時(shí),收取客戶端發(fā)來(lái)的第一組數(shù)據(jù),這個(gè)是同時(shí)進(jìn)行的,也就是說(shuō),在我們收到AcceptEx完成的通知的時(shí)候,我們就已經(jīng)把這第一組數(shù)據(jù)接完畢了;但是這也意味著,如果客戶端只是連入但是不發(fā)送數(shù)據(jù)的話,我們就不會(huì)收到這個(gè)AcceptEx完成的通知……這個(gè)我們?cè)诤竺娴膶?shí)現(xiàn)部分,也可以詳細(xì)看到。
???????? 最后,各位要有一個(gè)心里準(zhǔn)備,相比accept,異步的AcceptEx使用起來(lái)要麻煩得多……
?
五. 完成端口的實(shí)現(xiàn)詳解
??????? 又說(shuō)了一節(jié)的廢話,終于到了該動(dòng)手實(shí)現(xiàn)的時(shí)候了……
??????? 這里我把完成端口的詳細(xì)實(shí)現(xiàn)步驟以及會(huì)涉及到的函數(shù),按照出現(xiàn)的先后步驟,都和大家詳細(xì)的說(shuō)明解釋一下,當(dāng)然,文檔中為了讓大家便于閱讀,這里去掉了其中的錯(cuò)誤處理的內(nèi)容,當(dāng)然,這些內(nèi)容在示例代碼中是會(huì)有的。
?????? 【第一步】創(chuàng)建一個(gè)完成端口
???????? 首先,我們先把完成端口建好再說(shuō)。
??????? 我們正常情況下,我們需要且只需要建立這一個(gè)完成端口,代碼很簡(jiǎn)單:
[cpp]?view plaincopy
??????? 呵呵,看到CreateIoCompletionPort()的參數(shù)不要奇怪,參數(shù)就是一個(gè)INVALID,一個(gè)NULL,兩個(gè)0…,說(shuō)白了就是一個(gè)-1,三個(gè)0……簡(jiǎn)直就和什么都沒(méi)傳一樣,但是Windows系統(tǒng)內(nèi)部卻是好一頓忙活,把完成端口相關(guān)的資源和數(shù)據(jù)結(jié)構(gòu)都已經(jīng)定義好了(在后面的原理部分我們會(huì)看到,完成端口相關(guān)的數(shù)據(jù)結(jié)構(gòu)大部分都是一些用來(lái)協(xié)調(diào)各種網(wǎng)絡(luò)I/O的隊(duì)列),然后系統(tǒng)會(huì)給我們返回一個(gè)有意義的HANDLE,只要返回值不是NULL,就說(shuō)明建立完成端口成功了,就這么簡(jiǎn)單,不是嗎?
??????? 有的時(shí)候我真的很贊嘆Windows API的封裝,把很多其實(shí)是很復(fù)雜的事整得這么簡(jiǎn)單……
????? ? 至于里面各個(gè)參數(shù)的具體含義,我會(huì)放到后面的步驟中去講,反正這里只要知道創(chuàng)建我們唯一的這個(gè)完成端口,就只是需要這么幾個(gè)參數(shù)。
??????? 但是對(duì)于最后一個(gè)參數(shù) 0,我這里要簡(jiǎn)單的說(shuō)兩句,這個(gè)0可不是一個(gè)普通的0,它代表的是NumberOfConcurrentThreads,也就是說(shuō),允許應(yīng)用程序同時(shí)執(zhí)行的線程數(shù)量。當(dāng)然,我們這里為了避免上下文切換,最理想的狀態(tài)就是每個(gè)處理器上只運(yùn)行一個(gè)線程了,所以我們?cè)O(shè)置為0,就是說(shuō)有多少個(gè)處理器,就允許同時(shí)多少個(gè)線程運(yùn)行。
??????? 因?yàn)楸热缫慌_(tái)機(jī)器只有兩個(gè)CPU(或者兩個(gè)核心),如果讓系統(tǒng)同時(shí)運(yùn)行的線程多于本機(jī)的CPU數(shù)量的話,那其實(shí)是沒(méi)有什么意義的事情,因?yàn)檫@樣CPU就不得不在多個(gè)線程之間執(zhí)行上下文切換,這會(huì)浪費(fèi)寶貴的CPU周期,反而降低的效率,我們要牢記這個(gè)原則。
????? 【第二步】根據(jù)系統(tǒng)中CPU核心的數(shù)量建立對(duì)應(yīng)的Worker線程
??????? 我們前面已經(jīng)提到,這個(gè)Worker線程很重要,是用來(lái)具體處理網(wǎng)絡(luò)請(qǐng)求、具體和客戶端通信的線程,而且對(duì)于線程數(shù)量的設(shè)置很有意思,要等于系統(tǒng)中CPU的數(shù)量,那么我們就要首先獲取系統(tǒng)中CPU的數(shù)量,這個(gè)是基本功,我就不多說(shuō)了,代碼如下:
[cpp]?view plaincopy
??????? 這樣我們根據(jù)系統(tǒng)中CPU的核心數(shù)量來(lái)建立對(duì)應(yīng)的線程就好了,下圖是在我的 i7 2600k CPU上初始化的情況,因?yàn)槲业腃PU是8核,一共啟動(dòng)了16個(gè)Worker線程,如下圖所示
?????????????????
??????? ?啊,等等!各位沒(méi)發(fā)現(xiàn)什么問(wèn)題么?為什么我8核的CPU卻啟動(dòng)了16個(gè)線程?這個(gè)不是和我們第二步中說(shuō)的原則自相矛盾了么?
???????? 哈哈,有個(gè)小秘密忘了告訴各位了,江湖上都流傳著這么一個(gè)公式,就是:
??????? 我們最好是建立CPU核心數(shù)量*2那么多的線程,這樣更可以充分利用CPU資源,因?yàn)橥瓿啥丝诘恼{(diào)度是非常智能的,比如我們的Worker線程有的時(shí)候可能會(huì)有Sleep()或者WaitForSingleObject()之類(lèi)的情況,這樣同一個(gè)CPU核心上的另一個(gè)線程就可以代替這個(gè)Sleep的線程執(zhí)行了;因?yàn)橥瓿啥丝诘哪繕?biāo)是要使得CPU滿負(fù)荷的工作。
??????? 這里也有人說(shuō)是建立 CPU“核心數(shù)量 * 2 +2”個(gè)線程,我想這個(gè)應(yīng)該沒(méi)有什么太大的區(qū)別,我就是按照我自己的習(xí)慣來(lái)了。
??????? 然后按照這個(gè)數(shù)量,來(lái)啟動(dòng)這么多個(gè)Worker線程就好可以了,接下來(lái)我們開(kāi)始下一個(gè)步驟。
??????? 什么?Worker線程不會(huì)建?
??????? …囧…
?????? Worker線程和普通線程是一樣一樣一樣的啊~~~,代碼大致上如下:
[cpp]?view plaincopy
?????? 其中,_WorkerThread是Worker線程的線程函數(shù),線程函數(shù)的具體內(nèi)容我們后面再講。
???? 【第三步】創(chuàng)建一個(gè)用于監(jiān)聽(tīng)的Socket,綁定到完成端口上,然后開(kāi)始在指定的端口上監(jiān)聽(tīng)連接請(qǐng)求
?????? 最重要的完成端口建立完畢了,我們就可以利用這個(gè)完成端口來(lái)進(jìn)行網(wǎng)絡(luò)通信了。
?????? 首先,我們需要初始化Socket,這里和通常情況下使用Socket初始化的步驟都是一樣的,大約就是如下的這么幾個(gè)過(guò)程(詳情參照我代碼中的LoadSocketLib()和InitializeListenSocket(),這里只是挑出關(guān)鍵部分):
[cpp]?view plaincopy
??????? 需要注意的地方有兩點(diǎn):
??????? (1) 想要使用重疊I/O的話,初始化Socket的時(shí)候一定要使用WSASocket并帶上WSA_FLAG_OVERLAPPED參數(shù)才可以(只有在服務(wù)器端需要這么做,在客戶端是不需要的);
??????? (2) 注意到listen函數(shù)后面用的那個(gè)常量SOMAXCONN了嗎?這個(gè)是在微軟在WinSock2.h中定義的,并且還附贈(zèng)了一條注釋,Maximum queue length specifiable by listen.,所以說(shuō),不用白不用咯^_^
??????? 接下來(lái)有一個(gè)非常重要的動(dòng)作:既然我們要使用完成端口來(lái)幫我們進(jìn)行監(jiān)聽(tīng)工作,那么我們一定要把這個(gè)監(jiān)聽(tīng)Socket和完成端口綁定才可以的吧:
??????? 如何綁定呢?同樣很簡(jiǎn)單,用?CreateIoCompletionPort()函數(shù)。
??????? 等等!大家沒(méi)覺(jué)得這個(gè)函數(shù)很眼熟么?是的,這個(gè)和前面那個(gè)創(chuàng)建完成端口用的居然是同一個(gè)API!但是這里這個(gè)API可不是用來(lái)建立完成端口的,而是用于將Socket和以前創(chuàng)建的那個(gè)完成端口綁定的,大家可要看準(zhǔn)了,不要被迷惑了,因?yàn)樗麄兊膮?shù)是明顯不一樣的,前面那個(gè)的參數(shù)是一個(gè)-1,三個(gè)0,太好記了…
??????? 說(shuō)實(shí)話,我感覺(jué)微軟應(yīng)該把這兩個(gè)函數(shù)分開(kāi),弄個(gè) CreateNewCompletionPort() 多好呢?
??????? 這里在詳細(xì)講解一下CreateIoCompletionPort()的幾個(gè)參數(shù):
[cpp]?view plaincopy
??????? ?這些參數(shù)也沒(méi)什么好講的吧,用處一目了然了。而對(duì)于其中的那個(gè)CompletionKey,我們后面會(huì)詳細(xì)提到。
??????? ?到此才算是Socket全部初始化完畢了。
??????? 初始化Socket完畢之后,就可以在這個(gè)Socket上投遞AcceptEx請(qǐng)求了。
????? 【第四步】在這個(gè)監(jiān)聽(tīng)Socket上投遞AcceptEx請(qǐng)求
?????? ?這里的處理比較復(fù)雜。
?????? ?這個(gè)AcceptEx比較特別,而且這個(gè)是微軟專(zhuān)門(mén)在Windows操作系統(tǒng)里面提供的擴(kuò)展函數(shù),也就是說(shuō)這個(gè)不是Winsock2標(biāo)準(zhǔn)里面提供的,是微軟為了方便咱們使用重疊I/O機(jī)制,額外提供的一些函數(shù),所以在使用之前也還是需要進(jìn)行些準(zhǔn)備工作。
??????? 微軟的實(shí)現(xiàn)是通過(guò)mswsock.dll中提供的,所以我們可以通過(guò)靜態(tài)鏈接mswsock.lib來(lái)使用AcceptEx。但是這是一個(gè)不推薦的方式,我們應(yīng)該用WSAIoctl 配合SIO_GET_EXTENSION_FUNCTION_POINTER參數(shù)來(lái)獲取函數(shù)的指針,然后再調(diào)用AcceptEx。
??????? 這是為什么呢?因?yàn)槲覀冊(cè)谖慈〉煤瘮?shù)指針的情況下就調(diào)用AcceptEx的開(kāi)銷(xiāo)是很大的,因?yàn)锳cceptEx 實(shí)際上是存在于Winsock2結(jié)構(gòu)體系之外的(因?yàn)槭俏④浟硗馓峁┑?,所以如果我們直接調(diào)用AcceptEx的話,首先我們的代碼就只能在微軟的平臺(tái)上用了,沒(méi)有辦法在其他平臺(tái)上調(diào)用到該平臺(tái)提供的AcceptEx的版本(如果有的話), 而且更糟糕的是,我們每次調(diào)用AcceptEx時(shí),Service Provider都得要通過(guò)WSAIoctl()獲取一次該函數(shù)指針,效率太低了,所以還不如我們自己直接在代碼中直接去這么獲取一下指針好了。
??????? 獲取AcceptEx函數(shù)指針的代碼大致如下:
?
[cpp]?view plaincopy
???
??????? 具體實(shí)現(xiàn)就沒(méi)什么可說(shuō)的了,因?yàn)槎际枪潭ǖ奶茁?#xff0c;那個(gè)GUID是微軟給定義好的,直接拿過(guò)來(lái)用就行了,WSAIoctl()就是通過(guò)這個(gè)找到AcceptEx的地址的,另外需要注意的是,通過(guò)WSAIoctl獲取AcceptEx函數(shù)指針時(shí),只需要隨便傳遞給WSAIoctl()一個(gè)有效的SOCKET即可,該Socket的類(lèi)型不會(huì)影響獲取的AcceptEx函數(shù)指針。
??????? 然后,我們就可以通過(guò)其中的指針m_lpfnAcceptEx調(diào)用AcceptEx函數(shù)了。
?????? AcceptEx函數(shù)的定義如下:
[cpp]?view plaincopy
??????? 乍一看起來(lái)參數(shù)很多,但是實(shí)際用起來(lái)也很簡(jiǎn)單:
- 參數(shù)1--sListenSocket, 這個(gè)就是那個(gè)唯一的用來(lái)監(jiān)聽(tīng)的Socket了,沒(méi)什么說(shuō)的;
- 參數(shù)2--sAcceptSocket, 用于接受連接的socket,這個(gè)就是那個(gè)需要我們事先建好的,等有客戶端連接進(jìn)來(lái)直接把這個(gè)Socket拿給它用的那個(gè),是AcceptEx高性能的關(guān)鍵所在。
- 參數(shù)3--lpOutputBuffer,接收緩沖區(qū),這也是AcceptEx比較有特色的地方,既然AcceptEx不是普通的accpet函數(shù),那么這個(gè)緩沖區(qū)也不是普通的緩沖區(qū),這個(gè)緩沖區(qū)包含了三個(gè)信息:一是客戶端發(fā)來(lái)的第一組數(shù)據(jù),二是server的地址,三是client地址,都是精華啊…但是讀取起來(lái)就會(huì)很麻煩,不過(guò)后面有一個(gè)更好的解決方案。
- 參數(shù)4--dwReceiveDataLength,前面那個(gè)參數(shù)lpOutputBuffer中用于存放數(shù)據(jù)的空間大小。如果此參數(shù)=0,則Accept時(shí)將不會(huì)待數(shù)據(jù)到來(lái),而直接返回,如果此參數(shù)不為0,那么一定得等接收到數(shù)據(jù)了才會(huì)返回……所以通常當(dāng)需要Accept接收數(shù)據(jù)時(shí),就需要將該參數(shù)設(shè)成為:sizeof(lpOutputBuffer) - 2*(sizeof sockaddr_in +16),也就是說(shuō)總長(zhǎng)度減去兩個(gè)地址空間的長(zhǎng)度就是了,看起來(lái)復(fù)雜,其實(shí)想明白了也沒(méi)啥……
- 參數(shù)5--dwLocalAddressLength,存放本地址地址信息的空間大小;
- 參數(shù)6--dwRemoteAddressLength,存放本遠(yuǎn)端地址信息的空間大小;
- 參數(shù)7--lpdwBytesReceived,out參數(shù),對(duì)我們來(lái)說(shuō)沒(méi)用,不用管;
- 參數(shù)8--lpOverlapped,本次重疊I/O所要用到的重疊結(jié)構(gòu)。
?????? ?這里面的參數(shù)倒是沒(méi)什么,看起來(lái)復(fù)雜,但是咱們依舊可以一個(gè)一個(gè)傳進(jìn)去,然后在對(duì)應(yīng)的IO操作完成之后,這些參數(shù)Windows內(nèi)核自然就會(huì)幫咱們填滿了。
??????? 但是非常悲催的是,我們這個(gè)是異步操作,我們是在線程啟動(dòng)的地方投遞的這個(gè)操作, 等我們?cè)俅我?jiàn)到這些個(gè)變量的時(shí)候,就已經(jīng)是在Worker線程內(nèi)部了,因?yàn)閃indows會(huì)直接把操作完成的結(jié)果傳遞到Worker線程里,這樣咱們?cè)趩?dòng)的時(shí)候投遞了那么多的IO請(qǐng)求,這從Worker線程傳回來(lái)的這些結(jié)果,到底是對(duì)應(yīng)著哪個(gè)IO請(qǐng)求的呢?。。。。
??????? 聰明的你肯定想到了,是的,Windows內(nèi)核也幫我們想到了:用一個(gè)標(biāo)志來(lái)綁定每一個(gè)IO操作,這樣到了Worker線程內(nèi)部的時(shí)候,收到網(wǎng)絡(luò)操作完成的通知之后,再通過(guò)這個(gè)標(biāo)志來(lái)找出這組返回的數(shù)據(jù)到底對(duì)應(yīng)的是哪個(gè)Io操作的。
??????? 這里的標(biāo)志就是如下這樣的結(jié)構(gòu)體:
[cpp]?view plaincopy
??????? 這個(gè)結(jié)構(gòu)體的成員當(dāng)然是我們隨便定義的,里面的成員你可以隨意修改(除了OVERLAPPED那個(gè)之外……)。
?????? 但是AcceptEx不是普通的accept,buffer不是普通的buffer,那么這個(gè)結(jié)構(gòu)體當(dāng)然也不能是普通的結(jié)構(gòu)體了……
??????? 在完成端口的世界里,這個(gè)結(jié)構(gòu)體有個(gè)專(zhuān)屬的名字“單IO數(shù)據(jù)”,是什么意思呢?也就是說(shuō)每一個(gè)重疊I/O都要對(duì)應(yīng)的這么一組參數(shù),至于這個(gè)結(jié)構(gòu)體怎么定義無(wú)所謂,而且這個(gè)結(jié)構(gòu)體也不是必須要定義的,但是沒(méi)它……還真是不行,我們可以把它理解為線程參數(shù),就好比你使用線程的時(shí)候,線程參數(shù)也不是必須的,但是不傳還真是不行……
??????? 除此以外,我們也還會(huì)想到,既然每一個(gè)I/O操作都有對(duì)應(yīng)的PER_IO_CONTEXT結(jié)構(gòu)體,而在每一個(gè)Socket上,我們會(huì)投遞多個(gè)I/O請(qǐng)求的,例如我們就可以在監(jiān)聽(tīng)Socket上投遞多個(gè)AcceptEx請(qǐng)求,所以同樣的,我們也還需要一個(gè)“單句柄數(shù)據(jù)”來(lái)管理這個(gè)句柄上所有的I/O請(qǐng)求,這里的“句柄”當(dāng)然就是指的Socket了,我在代碼中是這樣定義的:
[cpp]?view plaincopy
???????? 這也是比較好理解的,也就是說(shuō)我們需要在一個(gè)Socket句柄上,管理在這個(gè)Socket上投遞的每一個(gè)IO請(qǐng)求的_PER_IO_CONTEXT。
???????? 當(dāng)然,同樣的,各位對(duì)于這些也可以按照自己的想法來(lái)隨便定義,只要能起到管理每一個(gè)IO請(qǐng)求上需要傳遞的網(wǎng)絡(luò)參數(shù)的目的就好了,關(guān)鍵就是需要跟蹤這些參數(shù)的狀態(tài),在必要的時(shí)候釋放這些資源,不要造成內(nèi)存泄漏,因?yàn)樽鳛镾erver總是需要長(zhǎng)時(shí)間運(yùn)行的,所以如果有內(nèi)存泄露的情況那是非??膳碌?#xff0c;一定要杜絕一絲一毫的內(nèi)存泄漏。
??????? 至于具體這兩個(gè)結(jié)構(gòu)體參數(shù)是如何在Worker線程里大發(fā)神威的,我們后面再看。
???????? 以上就是我們?nèi)康臏?zhǔn)備工作了,具體的實(shí)現(xiàn)各位可以配合我的流程圖再看一下示例代碼,相信應(yīng)該會(huì)理解得比較快。
??????? 完成端口初始化的工作比起其他的模型來(lái)講是要更復(fù)雜一些,所以說(shuō)對(duì)于主線程來(lái)講,它總覺(jué)得自己付出了很多,總覺(jué)得Worker線程是坐享其成,但是Worker自己的苦只有自己明白,Worker線程的工作一點(diǎn)也不比主線程少,相反還要更復(fù)雜一些,并且具體的通信工作全部都是Worker線程來(lái)完成的,Worker線程反而還覺(jué)得主線程是在旁邊看熱鬧,只知道發(fā)號(hào)施令而已,但是大家終究還是誰(shuí)也離不開(kāi)誰(shuí),這也就和公司里老板和員工的微妙關(guān)系是一樣的吧……
??????? 【第五步】我們?cè)賮?lái)看看Worker線程都做了些什么
??????? _Worker線程的工作都是涉及到具體的通信事務(wù)問(wèn)題,主要完成了如下的幾個(gè)工作,讓我們一步一步的來(lái)看。
??????? (1) 使用 GetQueuedCompletionStatus() 監(jiān)控完成端口
??????? 首先這個(gè)工作所要做的工作大家也能猜到,無(wú)非就是幾個(gè)Worker線程哥幾個(gè)一起排好隊(duì)隊(duì)來(lái)監(jiān)視完成端口的隊(duì)列中是否有完成的網(wǎng)絡(luò)操作就好了,代碼大體如下:
[cpp]?view plaincopy
??????? 各位留意到其中的GetQueuedCompletionStatus()函數(shù)了嗎?這個(gè)就是Worker線程里第一件也是最重要的一件事了,這個(gè)函數(shù)的作用就是我在前面提到的,會(huì)讓W(xué)orker線程進(jìn)入不占用CPU的睡眠狀態(tài),直到完成端口上出現(xiàn)了需要處理的網(wǎng)絡(luò)操作或者超出了等待的時(shí)間限制為止。
????????一旦完成端口上出現(xiàn)了已完成的I/O請(qǐng)求,那么等待的線程會(huì)被立刻喚醒,然后繼續(xù)執(zhí)行后續(xù)的代碼。
?????? 至于這個(gè)神奇的函數(shù),原型是這樣的:
[cpp]?view plaincopy
??????? 所以,如果這個(gè)函數(shù)突然返回了,那就說(shuō)明有需要處理的網(wǎng)絡(luò)操作了 --- 當(dāng)然,在沒(méi)有出現(xiàn)錯(cuò)誤的情況下。
??????? 然后switch()一下,根據(jù)需要處理的操作類(lèi)型,那我們來(lái)進(jìn)行相應(yīng)的處理。
??????? 但是如何知道操作是什么類(lèi)型的呢?這就需要用到從外部傳遞進(jìn)來(lái)的loContext參數(shù),也就是我們封裝的那個(gè)參數(shù)結(jié)構(gòu)體,這個(gè)參數(shù)結(jié)構(gòu)體里面會(huì)帶有我們一開(kāi)始投遞這個(gè)操作的時(shí)候設(shè)置的操作類(lèi)型,然后我們根據(jù)這個(gè)操作再來(lái)進(jìn)行對(duì)應(yīng)的處理。
??????? 但是還有問(wèn)題,這個(gè)參數(shù)究竟是從哪里傳進(jìn)來(lái)的呢?傳進(jìn)來(lái)的時(shí)候內(nèi)容都有些什么?
??????? 這個(gè)問(wèn)題問(wèn)得好!
??????? 首先,我們要知道兩個(gè)關(guān)鍵點(diǎn):
??????? (1) 這個(gè)參數(shù),是在你綁定Socket到一個(gè)完成端口的時(shí)候,用的CreateIoCompletionPort()函數(shù),傳入的那個(gè)CompletionKey參數(shù),要是忘了的話,就翻到文檔的“第三步”看看相關(guān)的內(nèi)容;我們?cè)谶@里傳入的是定義的PER_SOCKET_CONTEXT,也就是說(shuō)“單句柄數(shù)據(jù)”,因?yàn)槲覀兘壎ǖ氖且粋€(gè)Socket,這里自然也就需要傳入Socket相關(guān)的上下文,你是怎么傳過(guò)去的,這里收到的就會(huì)是什么樣子,也就是說(shuō)這個(gè)lpCompletionKey就是我們的PER_SOCKET_CONTEXT,直接把里面的數(shù)據(jù)拿出來(lái)用就可以了。
?????? (2)?另外還有一個(gè)很神奇的地方,里面的那個(gè)lpOverlapped參數(shù),里面就帶有我們的PER_IO_CONTEXT。這個(gè)參數(shù)是從哪里來(lái)的呢?我們?nèi)タ纯辞懊嫱哆fAcceptEx請(qǐng)求的時(shí)候,是不是傳了一個(gè)重疊參數(shù)進(jìn)去?這里就是它了,并且,我們可以使用一個(gè)很神奇的宏,把和它存儲(chǔ)在一起的其他的變量,全部都讀取出來(lái),例如:
???????? 這個(gè)宏的含義,就是去傳入的lpOverlapped變量里,找到和結(jié)構(gòu)體中PER_IO_CONTEXT中m_Overlapped成員相關(guān)的數(shù)據(jù)。
???????? 你仔細(xì)想想,其實(shí)真的很神奇……
???????? 但是要做到這種神奇的效果,應(yīng)該確保我們?cè)诮Y(jié)構(gòu)體PER_IO_CONTEXT定義的時(shí)候,把Overlapped變量,定義為結(jié)構(gòu)體中的第一個(gè)成員。
???????? 只要各位能弄清楚這個(gè)GetQueuedCompletionStatus()中各種奇怪的參數(shù),那我們就離成功不遠(yuǎn)了。
?????? ? 既然我們可以獲得PER_IO_CONTEXT結(jié)構(gòu)體,那么我們就自然可以根據(jù)其中的m_OpType參數(shù),得知這次收到的這個(gè)完成通知,是關(guān)于哪個(gè)Socket上的哪個(gè)I/O操作的,這樣就分別進(jìn)行對(duì)應(yīng)處理就好了。
????? ? 在我的示例代碼里,在有AcceptEx請(qǐng)求完成的時(shí)候,我是執(zhí)行的_DoAccept()函數(shù),在有WSARecv請(qǐng)求完成的時(shí)候,執(zhí)行的是_DoRecv()函數(shù),下面我就分別講解一下這兩個(gè)函數(shù)的執(zhí)行流程。
?????? 【第六步】當(dāng)收到Accept通知時(shí) _DoAccept()
??????? 在用戶收到AcceptEx的完成通知時(shí),需要后續(xù)代碼并不多,但卻是邏輯最為混亂,最容易出錯(cuò)的地方,這也是很多用戶為什么寧愿用效率低下的accept()也不愿意去用AcceptEx的原因吧。
?????? 和普通的Socket通訊方式一樣,在有客戶端連入的時(shí)候,我們需要做三件事情:
?????? (1) 為這個(gè)新連入的連接分配一個(gè)Socket;
?????? (2) 在這個(gè)Socket上投遞第一個(gè)異步的發(fā)送/接收請(qǐng)求;
?????? (3) 繼續(xù)監(jiān)聽(tīng)。
??????? 其實(shí)都是一些很簡(jiǎn)單的事情但是由于“單句柄數(shù)據(jù)”和“單IO數(shù)據(jù)”的加入,事情就變得比較亂。因?yàn)槭沁@樣的,讓我們一起縷一縷啊,最好是配合代碼一起看,否則太抽象了……
?????? ?(1) 首先,_Worker線程通過(guò)GetQueuedCompletionStatus()里會(huì)收到一個(gè)lpCompletionKey,這個(gè)也就是PER_SOCKET_CONTEXT,里面保存了與這個(gè)I/O相關(guān)的Socket和Overlapped還有客戶端發(fā)來(lái)的第一組數(shù)據(jù)等等,對(duì)吧?但是這里得注意,這個(gè)SOCKET的上下文數(shù)據(jù),是關(guān)于監(jiān)聽(tīng)Socket的,而不是新連入的這個(gè)客戶端Socket的,千萬(wàn)別弄混了……
??????? (2) 所以,AcceptEx不是給咱們新連入的這個(gè)Socket早就建好了一個(gè)Socket嗎?所以這里,我們需要再用這個(gè)新Socket重新為新客戶端建立一個(gè)PER_SOCKET_CONTEXT,以及下面一系列的新PER_IO_CONTEXT,千萬(wàn)不要去動(dòng)傳入的這個(gè)Listen Socket上的PER_SOCKET_CONTEXT,也不要用傳入的這個(gè)Overlapped信息,因?yàn)檫@個(gè)是屬于AcceptEx I/O操作的,也不是屬于你投遞的那個(gè)Recv I/O操作的……,要不你下次繼續(xù)監(jiān)聽(tīng)的時(shí)候就悲劇了……
??????? (3) 等到新的Socket準(zhǔn)備完畢了,我們就趕緊還是用傳入的這個(gè)Listen Socket上的PER_SOCKET_CONTEXT和PER_IO_CONTEXT去繼續(xù)投遞下一個(gè)AcceptEx,循環(huán)起來(lái),留在這里太危險(xiǎn)了,早晚得被人給改了……
??????? (4) 而我們新的Socket的上下文數(shù)據(jù)和I/O操作數(shù)據(jù)都準(zhǔn)備好了之后,我們要做兩件事情:一件事情是把這個(gè)新的Socket和我們唯一的那個(gè)完成端口綁定,這個(gè)就不用細(xì)說(shuō)了,和前面綁定監(jiān)聽(tīng)Socket是一樣的;然后就是在這個(gè)Socket上投遞第一個(gè)I/O操作請(qǐng)求,在我的示例代碼里投遞的是WSARecv()。因?yàn)楹罄m(xù)的WSARecv,就不是在這里投遞的了,這里只負(fù)責(zé)第一個(gè)請(qǐng)求。
??????? 但是,至于WSARecv請(qǐng)求如何來(lái)投遞的,我們放到下一節(jié)中去講,這一節(jié),我們還有一個(gè)很重要的事情,我得給大家提一下,就是在客戶端連入的時(shí)候,我們?nèi)绾蝸?lái)獲取客戶端的連入地址信息。
???????? 這里我們還需要引入另外一個(gè)很高端的函數(shù),GetAcceptExSockAddrs(),它和AcceptEx()一樣,都是微軟提供的擴(kuò)展函數(shù),所以同樣需要通過(guò)下面的方式來(lái)導(dǎo)入才可以使用……
[cpp]?view plaincopy
??????? 和導(dǎo)出AcceptEx一樣一樣的,同樣是需要用其GUID來(lái)獲取對(duì)應(yīng)的函數(shù)指針 m_lpfnGetAcceptExSockAddrs 。
??????? 說(shuō)了這么多,這個(gè)函數(shù)究竟是干嘛用的呢?它是名副其實(shí)的“AcceptEx之友”,為什么這么說(shuō)呢?因?yàn)槲仪懊嫣崞疬^(guò)AcceptEx有個(gè)很神奇的功能,就是附帶一個(gè)神奇的緩沖區(qū),這個(gè)緩沖區(qū)厲害了,包括了客戶端發(fā)來(lái)的第一組數(shù)據(jù)、本地的地址信息、客戶端的地址信息,三合一啊,你說(shuō)神奇不神奇?
??????? 這個(gè)函數(shù)從它字面上的意思也基本可以看得出來(lái),就是用來(lái)解碼這個(gè)緩沖區(qū)的,是的,它不提供別的任何功能,就是專(zhuān)門(mén)用來(lái)解析AcceptEx緩沖區(qū)內(nèi)容的。例如如下代碼:
[cpp]?view plaincopy
????????解碼完畢之后,于是,我們就可以從如下的結(jié)構(gòu)體指針中獲得很多有趣的地址信息了:
inet_ntoa(ClientAddr->sin_addr) 是客戶端IP地址
ntohs(ClientAddr->sin_port) 是客戶端連入的端口
inet_ntoa(LocalAddr ->sin_addr) 是本地IP地址
ntohs(LocalAddr ->sin_port) 是本地通訊的端口
pIoContext->m_wsaBuf.buf 是存儲(chǔ)客戶端發(fā)來(lái)第一組數(shù)據(jù)的緩沖區(qū)
?
自從用了“AcceptEx之友”,一切都清凈了….
???????? 【第七步】當(dāng)收到Recv通知時(shí), _DoRecv()
???????? 在講解如何處理Recv請(qǐng)求之前,我們還是先講一下如何投遞WSARecv請(qǐng)求的。
???????? WSARecv大體的代碼如下,其實(shí)就一行,在代碼中我們可以很清楚的看到我們用到了很多新建的PerIoContext的參數(shù),這里再?gòu)?qiáng)調(diào)一下,注意一定要是自己另外新建的啊,一定不能是Worker線程里傳入的那個(gè)PerIoContext,因?yàn)槟莻€(gè)是監(jiān)聽(tīng)Socket的,別給人弄壞了……:
[cpp]?view plaincopy
??????? 這里,我再把WSARev函數(shù)的原型再給各位講一下
[cpp]?view plaincopy
???????? 其實(shí)里面的參數(shù),如果你們熟悉或者看過(guò)我以前的重疊I/O的文章,應(yīng)該都比較熟悉,只需要注意其中的兩個(gè)參數(shù):
- LPWSABUF?lpBuffers;
??????? 這里是需要我們自己new 一個(gè) WSABUF 的結(jié)構(gòu)體傳進(jìn)去的;
??? ??? 如果你們非要追問(wèn) WSABUF 結(jié)構(gòu)體是個(gè)什么東東?我就給各位多說(shuō)兩句,就是在ws2def.h中有定義的,定義如下:
[cpp]?view plaincopy
???????? 而且好心的微軟還附贈(zèng)了注釋,真不容易….
??????? ?看到了嗎?如果對(duì)于里面的一些奇怪符號(hào)你們看不懂的話,也不用管他,只用看到一個(gè)ULONG和一個(gè)CHAR*就可以了,這不就是一個(gè)是緩沖區(qū)長(zhǎng)度,一個(gè)是緩沖區(qū)指針么?至于那個(gè)什么 FAR…..讓他見(jiàn)鬼去吧,現(xiàn)在已經(jīng)是32位和64位時(shí)代了……
????? ? 這里需要注意的,我們的應(yīng)用程序接到數(shù)據(jù)到達(dá)的通知的時(shí)候,其實(shí)數(shù)據(jù)已經(jīng)被咱們的主機(jī)接收下來(lái)了,我們直接通過(guò)這個(gè)WSABUF指針去系統(tǒng)緩沖區(qū)拿數(shù)據(jù)就好了,而不像那些沒(méi)用重疊I/O的模型,接收到有數(shù)據(jù)到達(dá)的通知的時(shí)候還得自己去另外recv,太低端了……這也是為什么重疊I/O比其他的I/O性能要好的原因之一。
- LPWSAOVERLAPPED?lpOverlapped
???????? 這個(gè)參數(shù)就是我們所謂的重疊結(jié)構(gòu)了,就是這樣定義,然后在有Socket連接進(jìn)來(lái)的時(shí)候,生成并初始化一下,然后在投遞第一個(gè)完成請(qǐng)求的時(shí)候,作為參數(shù)傳遞進(jìn)去就可以,
[cpp]?view plaincopy
??????? 在第一個(gè)重疊請(qǐng)求完畢之后,我們的這個(gè)OVERLAPPED 結(jié)構(gòu)體里,就會(huì)被分配有效的系統(tǒng)參數(shù)了,并且我們是需要每一個(gè)Socket上的每一個(gè)I/O操作類(lèi)型,都要有一個(gè)唯一的Overlapped結(jié)構(gòu)去標(biāo)識(shí)。
??????? 這樣,投遞一個(gè)WSARecv就講完了,至于_DoRecv()需要做些什么呢?其實(shí)就是做兩件事:
????? ? (1) 把WSARecv里這個(gè)緩沖區(qū)里收到的數(shù)據(jù)顯示出來(lái);
??????? (2) 發(fā)出下一個(gè)WSARecv();
?????? ?Over……
?????? ?至此,我們終于深深的喘口氣了,完成端口的大部分工作我們也完成了,也非常感謝各位耐心的看我這么枯燥的文字一直看到這里,真是一個(gè)不容易的事情!!
?????? 【第八步】如何關(guān)閉完成端口
??????? 休息完畢,我們繼續(xù)……
??????? 各位看官不要高興得太早,雖然我們已經(jīng)讓我們的完成端口順利運(yùn)作起來(lái)了,但是在退出的時(shí)候如何釋放資源咱們也是要知道的,否則豈不是功虧一簣…..
??????? 從前面的章節(jié)中,我們已經(jīng)了解到,Worker線程一旦進(jìn)入了GetQueuedCompletionStatus()的階段,就會(huì)進(jìn)入睡眠狀態(tài),INFINITE的等待完成端口中,如果完成端口上一直都沒(méi)有已經(jīng)完成的I/O請(qǐng)求,那么這些線程將無(wú)法被喚醒,這也意味著線程沒(méi)法正常退出。
??????? 熟悉或者不熟悉多線程編程的朋友,都應(yīng)該知道,如果在線程睡眠的時(shí)候,簡(jiǎn)單粗暴的就把線程關(guān)閉掉的話,那是會(huì)一個(gè)很可怕的事情,因?yàn)楹芏嗑€程體內(nèi)很多資源都來(lái)不及釋放掉,無(wú)論是這些資源最后是否會(huì)被操作系統(tǒng)回收,我們作為一個(gè)C++程序員來(lái)講,都不應(yīng)該允許這樣的事情出現(xiàn)。
??????? 所以我們必須得有一個(gè)很優(yōu)雅的,讓線程自己退出的辦法。
?????? 這時(shí)會(huì)用到我們這次見(jiàn)到的與完成端口有關(guān)的最后一個(gè)API,叫?PostQueuedCompletionStatus(),從名字上也能看得出來(lái),這個(gè)是和 GetQueuedCompletionStatus() 函數(shù)相對(duì)的,這個(gè)函數(shù)的用途就是可以讓我們手動(dòng)的添加一個(gè)完成端口I/O操作,這樣處于睡眠等待的狀態(tài)的線程就會(huì)有一個(gè)被喚醒,如果為我們每一個(gè)Worker線程都調(diào)用一次PostQueuedCompletionStatus()的話,那么所有的線程也就會(huì)因此而被喚醒了。
?????? PostQueuedCompletionStatus()函數(shù)的原型是這樣定義的:
[cpp]?view plaincopy
??????? 我們可以看到,這個(gè)函數(shù)的參數(shù)幾乎和GetQueuedCompletionStatus()的一模一樣,都是需要把我們建立的完成端口傳進(jìn)去,然后后面的三個(gè)參數(shù)是 傳輸字節(jié)數(shù)、結(jié)構(gòu)體參數(shù)、重疊結(jié)構(gòu)的指針.
?????? 注意,這里也有一個(gè)很神奇的事情,正常情況下,GetQueuedCompletionStatus()獲取回來(lái)的參數(shù)本來(lái)是應(yīng)該是系統(tǒng)幫我們填充的,或者是在綁定完成端口時(shí)就有的,但是我們這里卻可以直接使用PostQueuedCompletionStatus()直接將后面三個(gè)參數(shù)傳遞給GetQueuedCompletionStatus(),這樣就非常方便了。
?????? 例如,我們?yōu)榱四軌驅(qū)崿F(xiàn)通知線程退出的效果,可以自己定義一些約定,比如把這后面三個(gè)參數(shù)設(shè)置一個(gè)特殊的值,然后Worker線程接收到完成通知之后,通過(guò)判斷這3個(gè)參數(shù)中是否出現(xiàn)了特殊的值,來(lái)決定是否是應(yīng)該退出線程了。
?????? 例如我們?cè)谡{(diào)用的時(shí)候,就可以這樣:
[cpp]?view plaincopy
??????? 為每一個(gè)線程都發(fā)送一個(gè)完成端口數(shù)據(jù)包,有幾個(gè)線程就發(fā)送幾遍,把其中的dwCompletionKey參數(shù)設(shè)置為NULL,這樣每一個(gè)Worker線程在接收到這個(gè)完成通知的時(shí)候,再自己判斷一下這個(gè)參數(shù)是否被設(shè)置成了NULL,因?yàn)檎G闆r下,這個(gè)參數(shù)總是會(huì)有一個(gè)非NULL的指針傳入進(jìn)來(lái)的,如果Worker發(fā)現(xiàn)這個(gè)參數(shù)被設(shè)置成了NULL,那么Worker線程就會(huì)知道,這是應(yīng)用程序再向Worker線程發(fā)送的退出指令,這樣Worker線程在內(nèi)部就可以自己很“優(yōu)雅”的退出了……
??????? 學(xué)會(huì)了嗎?
??????? 但是這里有一個(gè)很明顯的問(wèn)題,聰明的朋友一定想到了,而且只有想到了這個(gè)問(wèn)題的人,才算是真正看明白了這個(gè)方法。
??????? 我們只是發(fā)送了m_nThreads次,我們?nèi)绾文艽_保每一個(gè)Worker線程正好就收到一個(gè),然后所有的線程都正好退出呢?是的,我們沒(méi)有辦法保證,所以很有可能一個(gè)Worker線程處理完一個(gè)完成請(qǐng)求之后,發(fā)生了某些事情,結(jié)果又再次去循環(huán)接收下一個(gè)完成請(qǐng)求了,這樣就會(huì)造成有的Worker線程沒(méi)有辦法接收到我們發(fā)出的退出通知。
??????? 所以,我們?cè)谕顺龅臅r(shí)候,一定要確保Worker線程只調(diào)用一次GetQueuedCompletionStatus(),這就需要我們自己想辦法了,各位請(qǐng)參考我在Worker線程中實(shí)現(xiàn)的代碼,我搭配了一個(gè)退出的Event,在退出的時(shí)候SetEvent一下,來(lái)確保Worker線程每次就只會(huì)調(diào)用一輪 GetQueuedCompletionStatus() ,這樣就應(yīng)該比較安全了。
??????? 另外,在Vista/Win7系統(tǒng)中,我們還有一個(gè)更簡(jiǎn)單的方式,我們可以直接CloseHandle關(guān)掉完成端口的句柄,這樣所有在GetQueuedCompletionStatus()的線程都會(huì)被喚醒,并且返回FALSE,這時(shí)調(diào)用GetLastError()獲取錯(cuò)誤碼時(shí),會(huì)返回ERROR_INVALID_HANDLE,這樣每一個(gè)Worker線程就可以通過(guò)這種方式輕松簡(jiǎn)單的知道自己該退出了。當(dāng)然,如果我們不能保證我們的應(yīng)用程序只在Vista/Win7中,那還是老老實(shí)實(shí)的PostQueuedCompletionStatus()吧。
??????? 最后,在系統(tǒng)釋放資源的最后階段,切記,因?yàn)橥瓿啥丝谕瑯右彩且粋€(gè)Handle,所以也得用CloseHandle將這個(gè)句柄關(guān)閉,當(dāng)然還要記得用closesocket關(guān)閉一系列的socket,還有別的各種指針什么的,這都是作為一個(gè)合格的C++程序員的基本功,在這里就不多說(shuō)了,如果還是有不太清楚的朋友,請(qǐng)參考我的示例代碼中的 StopListen() 和DeInitialize() 函數(shù)。
?
六. 完成端口使用中的注意事項(xiàng)
??????? 終于到了文章的結(jié)尾了,不知道各位朋友是基本學(xué)會(huì)了完成端口的使用了呢,還是被完成端口以及我這么多口水的文章折磨得不行了……
??????? 最后再補(bǔ)充一些前面沒(méi)有提到了,實(shí)際應(yīng)用中的一些注意事項(xiàng)吧。
?????? 1. Socket的通信緩沖區(qū)設(shè)置成多大合適?
??????? 在x86的體系中,內(nèi)存頁(yè)面是以4KB為單位來(lái)鎖定的,也就是說(shuō),就算是你投遞WSARecv()的時(shí)候只用了1KB大小的緩沖區(qū),系統(tǒng)還是得給你分4KB的內(nèi)存。為了避免這種浪費(fèi),最好是把發(fā)送和接收數(shù)據(jù)的緩沖區(qū)直接設(shè)置成4KB的倍數(shù)。
?????? 2. ?關(guān)于完成端口通知的次序問(wèn)題
??????? 這個(gè)不用想也能知道,調(diào)用GetQueuedCompletionStatus() 獲取I/O完成端口請(qǐng)求的時(shí)候,肯定是用先入先出的方式來(lái)進(jìn)行的。
??????? 但是,咱們大家可能都想不到的是,喚醒那些調(diào)用了GetQueuedCompletionStatus()的線程是以后入先出的方式來(lái)進(jìn)行的。
??????? 比如有4個(gè)線程在等待,如果出現(xiàn)了一個(gè)已經(jīng)完成的I/O項(xiàng),那么是最后一個(gè)調(diào)用GetQueuedCompletionStatus()的線程會(huì)被喚醒。平常這個(gè)次序倒是不重要,但是在對(duì)數(shù)據(jù)包順序有要求的時(shí)候,比如傳送大塊數(shù)據(jù)的時(shí)候,是需要注意下這個(gè)先后次序的。
??????? -- 微軟之所以這么做,那當(dāng)然是有道理的,這樣如果反復(fù)只有一個(gè)I/O操作而不是多個(gè)操作完成的話,內(nèi)核就只需要喚醒同一個(gè)線程就可以了,而不需要輪著喚醒多個(gè)線程,節(jié)約了資源,而且可以把其他長(zhǎng)時(shí)間睡眠的線程換出內(nèi)存,提到資源利用率。
?????? 3. ?如果各位想要傳輸文件…
??????? 如果各位需要使用完成端口來(lái)傳送文件的話,這里有個(gè)非常需要注意的地方。因?yàn)榘l(fā)送文件的做法,按照正常人的思路來(lái)講,都會(huì)是先打開(kāi)一個(gè)文件,然后不斷的循環(huán)調(diào)用ReadFile()讀取一塊之后,然后再調(diào)用WSASend ()去發(fā)發(fā)送。
??????? 但是我們知道,ReadFile()的時(shí)候,是需要操作系統(tǒng)通過(guò)磁盤(pán)的驅(qū)動(dòng)程序,到實(shí)際的物理硬盤(pán)上去讀取文件的,這就會(huì)使得操作系統(tǒng)從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài)去調(diào)用驅(qū)動(dòng)程序,然后再把讀取的結(jié)果返回至用戶態(tài);同樣的道理,WSARecv()也會(huì)涉及到從用戶態(tài)到內(nèi)核態(tài)切換的問(wèn)題 --- 這樣就使得我們不得不頻繁的在用戶態(tài)到內(nèi)核態(tài)之間轉(zhuǎn)換,效率低下……
??????? 而一個(gè)非常好的解決方案是使用微軟提供的擴(kuò)展函數(shù)TransmitFile()來(lái)傳輸文件,因?yàn)橹恍枰獋鬟f給TransmitFile()一個(gè)文件的句柄和需要傳輸?shù)淖止?jié)數(shù),程序就會(huì)整個(gè)切換至內(nèi)核態(tài),無(wú)論是讀取數(shù)據(jù)還是發(fā)送文件,都是直接在內(nèi)核態(tài)中執(zhí)行的,直到文件傳輸完畢才會(huì)返回至用戶態(tài)給主進(jìn)程發(fā)送通知。這樣效率就高多了。
?????? 4. 關(guān)于重疊結(jié)構(gòu)數(shù)據(jù)釋放的問(wèn)題
??????? 我們既然使用的是異步通訊的方式,就得要習(xí)慣一點(diǎn),就是我們投遞出去的完成請(qǐng)求,不知道什么時(shí)候我們才能收到操作完成的通知,而在這段等待通知的時(shí)間,我們就得要千萬(wàn)注意得保證我們投遞請(qǐng)求的時(shí)候所使用的變量在此期間都得是有效的。
??????? 例如我們發(fā)送WSARecv請(qǐng)求時(shí)候所使用的Overlapped變量,因?yàn)樵诓僮魍瓿傻臅r(shí)候,這個(gè)結(jié)構(gòu)里面會(huì)保存很多很重要的數(shù)據(jù),對(duì)于設(shè)備驅(qū)動(dòng)程序來(lái)講,指示保存著我們這個(gè)Overlapped變量的指針,而在操作完成之后,驅(qū)動(dòng)程序會(huì)將Buffer的指針、已經(jīng)傳輸?shù)淖止?jié)數(shù)、錯(cuò)誤碼等等信息都寫(xiě)入到我們傳遞給它的那個(gè)Overlapped指針中去。如果我們已經(jīng)不小心把Overlapped釋放了,或者是又交給別的操作使用了的話,誰(shuí)知道驅(qū)動(dòng)程序會(huì)把這些東西寫(xiě)到哪里去呢?豈不是很崩潰……
??????? 暫時(shí)我想到的問(wèn)題就是這么多吧,如果各位真的是要正兒八經(jīng)寫(xiě)一個(gè)承受很大訪問(wèn)壓力的Server的話,你慢慢就會(huì)發(fā)現(xiàn),只用我附帶的這個(gè)示例代碼是不夠的,還得需要在很多細(xì)節(jié)之處進(jìn)行改進(jìn),例如用更好的數(shù)據(jù)結(jié)構(gòu)來(lái)管理上下文數(shù)據(jù),并且需要非常完善的異常處理機(jī)制等等,總之,非常期待大家的批評(píng)和指正。
??????? 謝謝大家看到這里!!!
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
總結(jié)
以上是生活随笔為你收集整理的完成端口(Completion Port)详解----- By PiggyXP(小猪)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 手把手教你玩转网络编程模型之完成例程(C
- 下一篇: TCP 和 UDP 绑定同一端口通信的解