完成端口(CompletionPort)詳解 - 手把手教你玩轉網絡編程系列之三 分類: VC網絡編程基礎 2011-11-01 08:17 26072人閱讀 評論(182) 收藏 舉報 手把手叫你玩轉網絡編程系列之三 完成端口(Completion Port)詳解
----- By PiggyXP(小豬)
前 言
本系列裏完成端口的代碼在兩年前就已經寫好了,但是由於許久沒有寫東西了,不知該如何提筆,所以這篇文檔總是在醞釀之中……醞釀了兩年之後,終於決定開始動筆了,但願還不算晚…..
這篇文檔我非常詳細並且圖文並茂的介紹了關於網絡編程模型中完成端口的方方面面的信息,從API的用法到使用的步驟,從完成端口的實現機理到實際使用的注意事項,都有所涉及,並且爲了讓朋友們更直觀的體會完成端口的用法,本文附帶了有詳盡註釋的使用MFC編寫的圖形界面的示例代碼。
我的初衷是希望寫一份互聯網上能找到的最詳盡的關於完成端口的教學文檔,而且讓對Socket編程略有了解的人都能夠看得懂,都能學會如何來使用完成端口這麼優異的網絡編程模型,但是由於本人水平所限,不知道我的初衷是否實現了,但還是希望各位需要的朋友能夠喜歡。
由於篇幅原因,本文假設你已經熟悉了利用Socket進行TCP/IP編程的基本原理,並且也熟練的掌握了多線程編程技術,太基本的概念我這裏就略過不提了,網上的資料應該遍地都是。
本文檔凝聚着筆者心血,如要轉載,請指明原作者及出處,謝謝!不過代碼沒有版權,可以隨便散播使用,歡迎改進,特別是非常歡迎能夠幫助我發現Bug的朋友,以更好的造福大家。^_^
本文配套的示例源碼下載地址(在我的下載空間裏,已經補充上了客戶端的代碼)
http://piggyxp.download.csdn.net/
(裏面的代碼包括VC 2008/VC 2010編寫的完成端口服務器端和客戶端的代碼,還包括一個對服務器端進行壓力測試的客戶端,都是經過我精心調試過,並且帶有非常詳盡的代碼註釋的。當然,作爲教學代碼,爲了能夠使得代碼結構清晰明瞭,我還是對代碼有所簡化,如果想要用於產品開發,最好還是需要自己再完善一下,另外我的工程是用2010編寫的,附帶的2008工程不知道有沒有問題,但是其中代碼都是一樣的,暫未測試)
忘了囑咐一下了,文章篇幅很長很長,基本涉及到了與完成端口有關的方方面面,一次看不完可以分好幾次,中間注意休息,好身體纔是咱們程序員最大的本錢!
對了,還忘了囑咐一下,因爲本人的水平有限,雖然我反覆修正了數遍,但文章和示例代碼裏肯定還有我沒發現的錯誤和紕漏,希望各位一定要指出來,拍磚、噴我,我都能Hold住,但是一定要指出來,我會及時修正,因爲我不想讓文中的錯誤傳遍互聯網,禍害大家。
OK, Let’s go ! Have fun !
目錄:
1. 完成端口的優點
2. 完成端口程序的運行演示
3. 完成端口的相關概念
4. 完成端口的基本流程
5. 完成端口的使用詳解
6. 實際應用中應該要注意的地方
一. 完成端口的優點
1. 我想只要是寫過或者想要寫C/S模式網絡服務器端的朋友,都應該或多或少的聽過完成端口的大名吧,完成端口會充分利用Windows內核來進行I/O的調度,是用於C/S通信模式中性能最好的網絡通信模型,沒有之一;甚至連和它性能接近的通信模型都沒有。
2. 完成端口和其他網絡通信方式最大的區別在哪裏呢?
(1) 首先,如果使用“同步”的方式來通信的話,這裏說的同步的方式就是說所有的操作都在一個線程內順序執行完成,這麼做缺點是很明顯的:因爲同步的通信操作會阻塞住來自同一個線程的任何其他操作,只有這個操作完成了之後,後續的操作纔可以完成;一個最明顯的例子就是咱們在MFC的界面代碼中,直接使用阻塞Socket調用的代碼,整個界面都會因此而阻塞住沒有響應!所以我們不得不爲每一個通信的Socket都要建立一個線程,多麻煩?這不坑爹呢麼?所以要寫高性能的服務器程序,要求通信一定要是異步的。
(2) 各位讀者肯定知道,可以使用使用“同步通信(阻塞通信) 多線程”的方式來改善(1)的情況,那麼好,想一下,我們好不容易實現了讓服務器端在每一個客戶端連入之後,都要啓動一個新的Thread和客戶端進行通信,有多少個客戶端,就需要啓動多少個線程,對吧;但是由於這些線程都是處於運行狀態,所以系統不得不在所有可運行的線程之間進行上下文的切換,我們自己是沒啥感覺,但是CPU卻痛苦不堪了,因爲線程切換是相當浪費CPU時間的,如果客戶端的連入線程過多,這就會弄得CPU都忙着去切換線程了,根本沒有多少時間去執行線程體了,所以效率是非常低下的,承認坑爹了不?
(3) 而微軟提出完成端口模型的初衷,就是爲了解決這種"one-thread-per-client"的缺點的,它充分利用內核對象的調度,只使用少量的幾個線程來處理和客戶端的所有通信,消除了無謂的線程上下文切換,最大限度的提高了網絡通信的性能,這種神奇的效果具體是如何實現的請看下文。
3. 完成端口被廣泛的應用於各個高性能服務器程序上,例如著名的Apache….如果你想要編寫的服務器端需要同時處理的併發客戶端連接數量有數百上千個的話,那不用糾結了,就是它了。
二. 完成端口程序的運行演示
首先,我們先來看一下完成端口在筆者的PC機上的運行表現,筆者的PC配置如下:
大體就是i7 2600 16GB內存,我以這臺PC作爲服務器,簡單的進行了如下的測試,通過Client生成3萬個併發線程同時連接至Server,然後每個線程每隔3秒鐘發送一次數據,一共發送3次,然後觀察服務器端的CPU和內存的佔用情況。
如圖2所示,是客戶端3萬個併發線程發送共發送9萬條數據的log截圖
圖3是服務器端接收完畢3萬個併發線程和每個線程的3份數據後的log截圖
最關鍵是圖4,圖4是服務器端在接收到28000個併發線程的時候,CPU佔用率的截圖,使用的軟件是大名鼎鼎的Process Explorer,因爲相對來講這個比自帶的任務管理器要準確和精確一些。
我們可以發現一個令人驚訝的結果,採用了完成端口的Server程序(藍色橫線所示)所佔用的CPU才爲 3.82%,整個運行過程中的峯值也沒有超過4%,是相當氣定神閒的……哦,對了,這還是在Debug環境下運行的情況,如果採用Release方式執行,性能肯定還會更高一些,除此以外,在UI上顯示信息也很大成都上影響了性能。
相反採用了多個併發線程的Client程序(紫色橫線所示)居然佔用的CPU高達11.53%,甚至超過了Server程序的數倍……
其實無論是哪種網絡操模型,對於內存佔用都是差不多的,真正的差別就在於CPU的佔用,其他的網絡模型都需要更多的CPU動力來支撐同樣的連接數據。
雖然這遠遠算不上服務器極限壓力測試,但是從中也可以看出來完成端口的實力,而且這種方式比純粹靠多線程的方式實現併發資源佔用率要低得多。
三. 完成端口的相關概念
在開始編碼之前,我們先來討論一下和完成端口相關的一些概念,如果你沒有耐心看完這段大段的文字的話,也可以跳過這一節直接去看下下一節的具體實現部分,但是這一節中涉及到的基本概念你還是有必要了解一下的,而且你也更能知道爲什麼有那麼多的網絡編程模式不用,非得要用這麼又複雜又難以理解的完成端口呢??也會堅定你繼續學習下去的信心^_^
3.1 異步通信機制及其幾種實現方式的比較
我們從前面的文字中瞭解到,高性能服務器程序使用異步通信機制是必須的。
而對於異步的概念,爲了方便後面文字的理解,這裏還是再次簡單的描述一下:
異步通信就是在咱們與外部的I/O設備進行打交道的時候,我們都知道外部設備的I/O和CPU比起來簡直是龜速,比如硬盤讀寫、網絡通信等等,我們沒有必要在咱們自己的線程裏面等待着I/O操作完成再執行後續的代碼,而是將這個請求交給設備的驅動程序自己去處理,我們的線程可以繼續做其他更重要的事情,大體的流程如下圖所示:
我可以從圖中看到一個很明顯的並行操作的過程,而“同步”的通信方式是在進行網絡操作的時候,主線程就掛起了,主線程要等待網絡操作完成之後,才能繼續執行後續的代碼,就是說要末執行主線程,要末執行網絡操作,是沒法這樣並行的;
“異步”方式無疑比 “阻塞模式 多線程”的方式效率要高的多,這也是前者爲什麼叫“異步”,後者爲什麼叫“同步”的原因了,因爲不需要等待網絡操作完成再執行別的操作。
而在Windows中實現異步的機制同樣有好幾種,而這其中的區別,關鍵就在於圖1中的最後一步“通知應用程序處理網絡數據”上了,因爲實現操作系統調用設備驅動程序去接收數據的操作都是一樣的,關鍵就是在於如何去通知應用程序來拿數據。它們之間的具體區別我這裏多講幾點,文字有點多,如果沒興趣深入研究的朋友可以跳過下一面的這一段,不影響的:)
(1) 設備內核對象,使用設備內核對象來協調數據的發送請求和接收數據協調,也就是說通過設置設備內核對象的狀態,在設備接收數據完成後,馬上觸發這個內核對象,然後讓接收數據的線程收到通知,但是這種方式太原始了,接收數據的線程爲了能夠知道內核對象是否被觸發了,還是得不停的掛起等待,這簡直是根本就沒有用嘛,太低級了,有木有?所以在這裏就略過不提了,各位讀者要是沒明白是怎麼回事也不用深究了,總之沒有什麼用。
(2) 事件內核對象,利用事件內核對象來實現I/O操作完成的通知,其實這種方式其實就是我以前寫文章的時候提到的《基於事件通知的重疊I/O模型》,鏈接在這裏,這種機制就先進得多,可以同時等待多個I/O操作的完成,實現真正的異步,但是缺點也是很明顯的,既然用WaitForMultipleObjects()來等待Event的話,就會受到64個Event等待上限的限制,但是這可不是說我們只能處理來自於64個客戶端的Socket,而是這是屬於在一個設備內核對象上等待的64個事件內核對象,也就是說,我們在一個線程內,可以同時監控64個重疊I/O操作的完成狀態,當然我們同樣可以使用多個線程的方式來滿足無限多個重疊I/O的需求,比如如果想要支持3萬個連接,就得需要500多個線程…用起來太麻煩讓人感覺不爽;
(3) 使用APC( Asynchronous Procedure Call,異步過程調用)來完成,這個也就是我以前在文章裏提到的《基於完成例程的重疊I/O模型》,鏈接在這裏,這種方式的好處就是在於擺脫了基於事件通知方式的64個事件上限的限制,但是缺點也是有的,就是發出請求的線程必須得要自己去處理接收請求,哪怕是這個線程發出了很多發送或者接收數據的請求,但是其他的線程都閒着…,這個線程也還是得自己來處理自己發出去的這些請求,沒有人來幫忙…這就有一個負載均衡問題,顯然性能沒有達到最優化。
(4) 完成端口,不用說大家也知道了,最後的壓軸戲就是使用完成端口,對比上面幾種機制,完成端口的做法是這樣的:事先開好幾個線程,你有幾個CPU我就開幾個,首先是避免了線程的上下文切換,因爲線程想要執行的時候,總有CPU資源可用,然後讓這幾個線程等着,等到有用戶請求來到的時候,就把這些請求都加入到一個公共消息隊列中去,然後這幾個開好的線程就排隊逐一去從消息隊列中取出消息並加以處理,這種方式就很優雅的實現了異步通信和負載均衡的問題,因爲它提供了一種機制來使用幾個線程“公平的”處理來自於多個客戶端的輸入/輸出,並且線程如果沒事幹的時候也會被系統掛起,不會佔用CPU週期,挺完美的一個解決方案,不是嗎?哦,對了,這個關鍵的作爲交換的消息隊列,就是完成端口。
比較完畢之後,熟悉網絡編程的朋友可能會問到,爲什麼沒有提到WSAAsyncSelect或者是WSAEventSelect這兩個異步模型呢,對於這兩個模型,我不知道其內部是如何實現的,但是這其中一定沒有用到Overlapped機制,就不能算作是真正的異步,可能是其內部自己在維護一個消息隊列吧,總之這兩個模式雖然實現了異步的接收,但是卻不能進行異步的發送,這就很明顯說明問題了,我想其內部的實現一定和完成端口是迥異的,並且,完成端口非常厚道,因爲它是先把用戶數據接收回來之後再通知用戶直接來取就好了,而WSAAsyncSelect和WSAEventSelect之流只是會接收到數據到達的通知,而只能由應用程序自己再另外去recv數據,性能上的差距就更明顯了。
最後,我的建議是,想要使用 基於事件通知的重疊I/O和基於完成例程的重疊I/O的朋友,如果不是特別必要,就不要去使用了,因爲這兩種方式不僅使用和理解起來也不算簡單,而且還有性能上的明顯瓶頸,何不就再努力一下使用完成端口呢?
3.2 重疊結構(OVERLAPPED)
我們從上一小節中得知,要實現異步通信,必須要用到一個很風騷的I/O數據結構,叫重疊結構“Overlapped”,Windows裏所有的異步通信都是基於它的,完成端口也不例外。
至於爲什麼叫Overlapped?Jeffrey Richter的解釋是因爲“執行I/O請求的時間與線程執行其他任務的時間是重疊(overlapped)的”,從這個名字我們也可能看得出來重疊結構發明的初衷了,對於重疊結構的內部細節我這裏就不過多的解釋了,就把它當成和其他內核對象一樣,不需要深究其實現機制,只要會使用就可以了,想要了解更多重疊結構內部的朋友,請去翻閱Jeffrey Richter的《Windows via C/C 》 5th 的292頁,如果沒有機會的話,也可以隨便翻翻我以前寫的Overlapped的東西,不過寫得比較淺顯……
這裏我想要解釋的是,這個重疊結構是異步通信機制實現的一個核心數據結構,因爲你看到後面的代碼你會發現,幾乎所有的網絡操作例如發送/接收之類的,都會用WSASend()和WSARecv()代替,參數裏面都會附帶一個重疊結構,這是爲什麼呢?因爲重疊結構我們就可以理解成爲是一個網絡操作的ID號,也就是說我們要利用重疊I/O提供的異步機制的話,每一個網絡操作都要有一個唯一的ID號,因爲進了系統內核,裏面黑燈瞎火的,也不瞭解上面出了什麼狀況,一看到有重疊I/O的調用進來了,就會使用其異步機制,並且操作系統就只能靠這個重疊結構帶有的ID號來區分是哪一個網絡操作了,然後內核裏面處理完畢之後,根據這個ID號,把對應的數據傳上去。
你要是實在不理解這是個什麼玩意,那就直接看後面的代碼吧,慢慢就明白了……
3.3 完成端口(CompletionPort)
對於完成端口這個概念,我一直不知道爲什麼它的名字是叫“完成端口”,我個人的感覺應該叫它“完成隊列”似乎更合適一些,總之這個“端口”和我們平常所說的用於網絡通信的“端口”完全不是一個東西,我們不要混淆了。
首先,它之所以叫“完成”端口,就是說系統會在網絡I/O操作“完成”之後纔會通知我們,也就是說,我們在接到系統的通知的時候,其實網絡操作已經完成了,就是比如說在系統通知我們的時候,並非是有數據從網絡上到來,而是來自於網絡上的數據已經接收完畢了;或者是客戶端的連入請求已經被系統接入完畢了等等,我們只需要處理後面的事情就好了。
各位朋友可能會很開心,什麼?已經處理完畢了才通知我們,那豈不是很爽?其實也沒什麼爽的,那是因爲我們在之前給系統分派工作的時候,都囑咐好了,我們會通過代碼告訴系統“你給我做這個做那個,等待做完了再通知我”,只是這些工作是做在之前還是之後的區別而已。
其次,我們需要知道,所謂的完成端口,其實和HANDLE一樣,也是一個內核對象,雖然Jeff Richter嚇唬我們說:“完成端口可能是最爲複雜的內核對象了”,但是我們也不用去管他,因爲它具體的內部如何實現的和我們無關,只要我們能夠學會用它相關的API把這個完成端口的框架搭建起來就可以了。我們暫時只用把它大體理解爲一個容納網絡通信操作的隊列就好了,它會把網絡操作完成的通知,都放在這個隊列裏面,咱們只用從這個隊列裏面取就行了,取走一個就少一個…。
關於完成端口內核對象的具體更多內部細節我會在後面的“完成端口的基本原理”一節更詳細的和朋友們一起來研究,當然,要是你們在文章中沒有看到這一節的話,就是說明我又犯懶了沒寫…在後續的文章裏我會補上。這裏就暫時說這麼多了,到時候我們也可以看到它的機制也並非有那麼的複雜,可能只是因爲操作系統其他的內核對象相比較而言實現起來太容易了吧^_^
四. 使用完成端口的基本流程
說了這麼多的廢話,大家都等不及了吧,我們終於到了具體編碼的時候了。
使用完成端口,說難也難,但是說簡單,其實也簡單 ---- 又說了一句廢話=。=
大體上來講,使用完成端口只用遵循如下幾個步驟:
(1) 調用 CreateIoCompletionPort() 函數創建一個完成端口,而且在一般情況下,我們需要且只需要建立這一個完成端口,把它的句柄保存好,我們今後會經常用到它……
(2) 根據系統中有多少個處理器,就建立多少個工作者(爲了醒目起見,下面直接說Worker)線程,這幾個線程是專門用來和客戶端進行通信的,目前暫時沒什麼工作;
(3) 下面就是接收連入的Socket連接了,這裏有兩種實現方式:一是和別的編程模型一樣,還需要啓動一個獨立的線程,專門用來accept客戶端的連接請求;二是用性能更高更好的異步AcceptEx()請求,因爲各位對accept用法應該非常熟悉了,而且網上資料也會很多,所以爲了更全面起見,本文采用的是性能更好的AcceptEx,至於兩者代碼編寫上的區別,我接下來會詳細的講。
(4) 每當有客戶端連入的時候,我們就還是得調用CreateIoCompletionPort()函數,這裏卻不是新建立完成端口了,而是把新連入的Socket(也就是前面所謂的設備句柄),與目前的完成端口綁定在一起。
至此,我們其實就已經完成了完成端口的相關部署工作了,嗯,是的,完事了,後面的代碼裏我們就可以充分享受完成端口帶給我們的巨大優勢,坐享其成了,是不是很簡單呢?
(5) 例如,客戶端連入之後,我們可以在這個Socket上提交一個網絡請求,例如WSARecv(),然後系統就會幫咱們乖乖的去執行接收數據的操作,我們大可以放心的去幹別的事情了;
(6) 而此時,我們預先準備的那幾個Worker線程就不能閒着了, 我們在前面建立的幾個Worker就要忙活起來了,都需要分別調用GetQueuedCompletionStatus() 函數在掃描完成端口的隊列裏是否有網絡通信的請求存在(例如讀取數據,發送數據等),一旦有的話,就將這個請求從完成端口的隊列中取回來,繼續執行本線程中後面的處理代碼,處理完畢之後,我們再繼續投遞下一個網絡通信的請求就OK了,如此循環。
關於完成端口的使用步驟,用文字來表述就是這麼多了,很簡單吧?如果你還是不理解,我再配合一個流程圖來表示一下:
當然,我這裏假設你已經對網絡編程的基本套路有了解了,所以略去了很多基本的細節,並且爲了配合朋友們更好的理解我的代碼,在流程圖我標出了一些函數的名字,並且畫得非常詳細。
另外需要注意的是由於對於客戶端的連入有兩種方式,一種是普通阻塞的accept,另外一種是性能更好的AcceptEx,爲了能夠方面朋友們從別的網絡編程的方式中過渡,我這裏畫了兩種方式的流程圖,方便朋友們對比學習,圖a是使用accept的方式,當然配套的源代碼我默認就不提供了,如果需要的話,我倒是也可以發上來;圖b是使用AcceptEx的,並配有配套的源碼。
採用accept方式的流程示意圖如下:
採用AcceptEx方式的流程示意圖如下:
兩個圖中最大的相同點是什麼?是的,最大的相同點就是主線程無所事事,閒得蛋疼……
爲什麼呢?因爲我們使用了異步的通信機制,這些瑣碎重複的事情完全沒有必要交給主線程自己來做了,只用在初始化的時候和Worker線程交待好就可以了,用一句話來形容就是,主線程永遠也體會不到Worker線程有多忙,而Worker線程也永遠體會不到主線程在初始化建立起這個通信框架的時候操了多少的心……
圖a中是由 _AcceptThread()負責接入連接,並把連入的Socket和完成端口綁定,另外的多個_WorkerThread()就負責監控完成端口上的情況,一旦有情況了,就取出來處理,如果CPU有多核的話,就可以多個線程輪着來處理完成端口上的信息,很明顯效率就提高了。
圖b中最明顯的區別,也就是AcceptEx和傳統的accept之間最大的區別,就是取消了阻塞方式的accept調用,也就是說,AcceptEx也是通過完成端口來異步完成的,所以就取消了專門用於accept連接的線程,用了完成端口來進行異步的AcceptEx調用;然後在檢索完成端口隊列的Worker函數中,根據用戶投遞的完成操作的類型,再來找出其中的投遞的Accept請求,加以對應的處理。
讀者一定會問,這樣做的好處在哪裏?爲什麼還要異步的投遞AcceptEx連接的操作呢?
首先,我可以很明確的告訴各位,如果短時間內客戶端的併發連接請求不是特別多的話,用accept和AcceptEx在性能上來講是沒什麼區別的。
按照我們目前主流的PC來講,如果客戶端只進行連接請求,而什麼都不做的話,我們的Server只能接收大約3萬-4萬個左右的併發連接,然後客戶端其餘的連入請求就只能收到WSAENOBUFS (10055)了,因爲系統來不及爲新連入的客戶端準備資源了。
需要準備什麼資源?當然是準備Socket了……雖然我們創建Socket只用一行SOCKET s= socket(…) 這麼一行的代碼就OK了,但是系統內部建立一個Socket是相當耗費資源的,因爲Winsock2是分層的機構體系,創建一個Socket需要到多個Provider之間進行處理,最終形成一個可用的套接字。總之,系統創建一個Socket的開銷是相當高的,所以用accept的話,系統可能來不及爲更多的併發客戶端現場準備Socket了。
而AcceptEx比Accept又強大在哪裏呢?是有三點:
(1) 這個好處是最關鍵的,是因爲AcceptEx是在客戶端連入之前,就把客戶端的Socket建立好了,也就是說,AcceptEx是先建立的Socket,然後才發出的AcceptEx調用,也就是說,在進行客戶端的通信之前,無論是否有客戶端連入,Socket都是提前建立好了;而不需要像accept是在客戶端連入了之後,再現場去花費時間建立Socket。如果各位不清楚是如何實現的,請看後面的實現部分。
(2) 相比accept只能阻塞方式建立一個連入的入口,對於大量的併發客戶端來講,入口實在是有點擠;而AcceptEx可以同時在完成端口上投遞多個請求,這樣有客戶端連入的時候,就非常優雅而且從容不迫的邊喝茶邊處理連入請求了。
(3) AcceptEx還有一個非常體貼的優點,就是在投遞AcceptEx的時候,我們還可以順便在AcceptEx的同時,收取客戶端發來的第一組數據,這個是同時進行的,也就是說,在我們收到AcceptEx完成的通知的時候,我們就已經把這第一組數據接完畢了;但是這也意味着,如果客戶端只是連入但是不發送數據的話,我們就不會收到這個AcceptEx完成的通知……這個我們在後面的實現部分,也可以詳細看到。
最後,各位要有一個心裏準備,相比accept,異步的AcceptEx使用起來要麻煩得多……
五. 完成端口的實現詳解
又說了一節的廢話,終於到了該動手實現的時候了……
這裏我把完成端口的詳細實現步驟以及會涉及到的函數,按照出現的先後步驟,都和大家詳細的說明解釋一下,當然,文檔中爲了讓大家便於閱讀,這裏去掉了其中的錯誤處理的內容,當然,這些內容在示例代碼中是會有的。
【第一步】創建一個完成端口
首先,我們先把完成端口建好再說。
我們正常情況下,我們需要且只需要建立這一個完成端口,代碼很簡單:
[cpp] view plaincopyprint?- HANDLEm_hIOCompletionPort=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0);
呵呵,看到CreateIoCompletionPort()的參數不要奇怪,參數就是一個INVALID,一個NULL,兩個0…,說白了就是一個-1,三個0……簡直就和什麼都沒傳一樣,但是Windows系統內部卻是好一頓忙活,把完成端口相關的資源和數據結構都已經定義好了(在後面的原理部分我們會看到,完成端口相關的數據結構大部分都是一些用來協調各種網絡I/O的隊列),然後系統會給我們返回一個有意義的HANDLE,只要返回值不是NULL,就說明建立完成端口成功了,就這麼簡單,不是嗎?
有的時候我真的很讚歎Windows API的封裝,把很多其實是很複雜的事整得這麼簡單……
至於裏面各個參數的具體含義,我會放到後面的步驟中去講,反正這裏只要知道創建我們唯一的這個完成端口,就只是需要這麼幾個參數。
但是對於最後一個參數 0,我這裏要簡單的說兩句,這個0可不是一個普通的0,它代表的是NumberOfConcurrentThreads,也就是說,允許應用程序同時執行的線程數量。當然,我們這裏爲了避免上下文切換,最理想的狀態就是每個處理器上只運行一個線程了,所以我們設置爲0,就是說有多少個處理器,就允許同時多少個線程運行。
因爲比如一臺機器只有兩個CPU(或者兩個核心),如果讓系統同時運行的線程多於本機的CPU數量的話,那其實是沒有什麼意義的事情,因爲這樣CPU就不得不在多個線程之間執行上下文切換,這會浪費寶貴的CPU週期,反而降低的效率,我們要牢記這個原則。
【第二步】根據系統中CPU核心的數量建立對應的Worker線程
我們前面已經提到,這個Worker線程很重要,是用來具體處理網絡請求、具體和客戶端通信的線程,而且對於線程數量的設置很有意思,要等於系統中CPU的數量,那麼我們就要首先獲取系統中CPU的數量,這個是基本功,我就不多說了,代碼如下:
[cpp] view plaincopyprint?- SYSTEM_INFOsi;
- GetSystemInfo(si);
- intm_nProcessors=si.dwNumberOfProcessors;
這樣我們根據系統中CPU的核心數量來建立對應的線程就好了,下圖是在我的 i7 2600k CPU上初始化的情況,因爲我的CPU是8核,一共啓動了16個Worker線程,如下圖所示
啊,等等!各位沒發現什麼問題麼?爲什麼我8核的CPU卻啓動了16個線程?這個不是和我們第二步中說的原則自相矛盾了麼?
哈哈,有個小祕密忘了告訴各位了,江湖上都流傳着這麼一個公式,就是:
我們最好是建立CPU核心數量*2那麼多的線程,這樣更可以充分利用CPU資源,因爲完成端口的調度是非常智能的,比如我們的Worker線程有的時候可能會有Sleep()或者WaitForSingleObject()之類的情況,這樣同一個CPU核心上的另一個線程就可以代替這個Sleep的線程執行了;因爲完成端口的目標是要使得CPU滿負荷的工作。
這裏也有人說是建立 CPU“核心數量 * 2 2”個線程,我想這個應該沒有什麼太大的區別,我就是按照我自己的習慣來了。
然後按照這個數量,來啓動這麼多個Worker線程就好可以了,接下來我們開始下一個步驟。
什麼?Worker線程不會建?
…囧…
Worker線程和普通線程是一樣一樣一樣的啊~~~,代碼大致上如下:
[cpp] view plaincopyprint?- //根據CPU數量,建立*2的線程
- m_nThreads=2*m_nProcessors;
- HANDLE*m_phWorkerThreads=newHANDLE[m_nThreads];
- for(inti=0;i
{ - m_phWorkerThreads[i]=::CreateThread(0,0,_WorkerThread,…);
- }
其中,_WorkerThread是Worker線程的線程函數,線程函數的具體內容我們後面再講。
【第三步】創建一個用於監聽的Socket,綁定到完成端口上,然後開始在指定的端口上監聽連接請求
最重要的完成端口建立完畢了,我們就可以利用這個完成端口來進行網絡通信了。
首先,我們需要初始化Socket,這裏和通常情況下使用Socket初始化的步驟都是一樣的,大約就是如下的這麼幾個過程(詳情參照我代碼中的LoadSocketLib()和InitializeListenSocket(),這裏只是挑出關鍵部分):
[cpp] view plaincopyprint?- //初始化Socket庫
- WSADATAwsaData;
- WSAStartup(MAKEWORD(2,2),wsaData);
- //初始化Socket
- structsockaddr_inServerAddress;
- //這裏需要特別注意,如果要使用重疊I/O的話,這裏必須要使用WSASocket來初始化Socket
- //注意裏面有個WSA_FLAG_OVERLAPPED參數
- SOCKETm_sockListen=WSASocket(AF_INET,SOCK_STREAM,0,NULL,0,WSA_FLAG_OVERLAPPED);
- //填充地址結構信息
- ZeroMemory((char*)ServerAddress,sizeof(ServerAddress));
- ServerAddress.sin_family=AF_INET;
- //這裏可以選擇綁定任何一個可用的地址,或者是自己指定的一個IP地址
- //ServerAddress.sin_addr.s_addr=htonl(INADDR_ANY);
- ServerAddress.sin_addr.s_addr=inet_addr(“你的IP”);
- ServerAddress.sin_port=htons(11111);
- //綁定端口
- if(SOCKET_ERROR==bind(m_sockListen,(structsockaddr*)ServerAddress,sizeof(ServerAddress)))
- //開始監聽
- listen(m_sockListen,SOMAXCONN))
需要注意的地方有兩點:
(1) 想要使用重疊I/O的話,初始化Socket的時候一定要使用WSASocket並帶上WSA_FLAG_OVERLAPPED參數纔可以(只有在服務器端需要這麼做,在客戶端是不需要的);
(2) 注意到listen函數後面用的那個常量SOMAXCONN了嗎?這個是在微軟在WinSock2.h中定義的,並且還附贈了一條註釋,Maximum queue length specifiable by listen.,所以說,不用白不用咯^_^
接下來有一個非常重要的動作:既然我們要使用完成端口來幫我們進行監聽工作,那麼我們一定要把這個監聽Socket和完成端口綁定纔可以的吧:
如何綁定呢?同樣很簡單,用 CreateIoCompletionPort()函數。
等等!大家沒覺得這個函數很眼熟麼?是的,這個和前面那個創建完成端口用的居然是同一個API!但是這裏這個API可不是用來建立完成端口的,而是用於將Socket和以前創建的那個完成端口綁定的,大家可要看準了,不要被迷惑了,因爲他們的參數是明顯不一樣的,前面那個的參數是一個-1,三個0,太好記了…
說實話,我感覺微軟應該把這兩個函數分開,弄個 CreateNewCompletionPort() 多好呢?
這裏在詳細講解一下CreateIoCompletionPort()的幾個參數:
[cpp] view plaincopyprint?- HANDLEWINAPICreateIoCompletionPort(
- __inHANDLEFileHandle,//這裏當然是連入的這個套接字句柄了
- __in_optHANDLEExistingCompletionPort,//這個就是前面創建的那個完成端口
- __inULONG_PTRCompletionKey,//這個參數就是類似於線程參數一樣,在
- //綁定的時候把自己定義的結構體指針傳遞
- //這樣到了Worker線程中,也可以使用這個
- //結構體的數據了,相當於參數的傳遞
- __inDWORDNumberOfConcurrentThreads//這裏同樣置0
- );
這些參數也沒什麼好講的吧,用處一目瞭然了。而對於其中的那個CompletionKey,我們後面會詳細提到。
到此纔算是Socket全部初始化完畢了。
初始化Socket完畢之後,就可以在這個Socket上投遞AcceptEx請求了。
【第四步】在這個監聽Socket上投遞AcceptEx請求
這裏的處理比較複雜。
這個AcceptEx比較特別,而且這個是微軟專門在Windows操作系統裏面提供的擴展函數,也就是說這個不是Winsock2標準裏面提供的,是微軟爲了方便咱們使用重疊I/O機制,額外提供的一些函數,所以在使用之前也還是需要進行些準備工作。
微軟的實現是通過mswsock.dll中提供的,所以我們可以通過靜態鏈接mswsock.lib來使用AcceptEx。但是這是一個不推薦的方式,我們應該用WSAIoctl 配合SIO_GET_EXTENSION_FUNCTION_POINTER參數來獲取函數的指針,然後再調用AcceptEx。
這是爲什麼呢?因爲我們在未取得函數指針的情況下就調用AcceptEx的開銷是很大的,因爲AcceptEx 實際上是存在於Winsock2結構體系之外的(因爲是微軟另外提供的),所以如果我們直接調用AcceptEx的話,首先我們的代碼就只能在微軟的平臺上用了,沒有辦法在其他平臺上調用到該平臺提供的AcceptEx的版本(如果有的話), 而且更糟糕的是,我們每次調用AcceptEx時,Service Provider都得要通過WSAIoctl()獲取一次該函數指針,效率太低了,所以還不如我們自己直接在代碼中直接去這麼獲取一下指針好了。
獲取AcceptEx函數指針的代碼大致如下:
[cpp] view plaincopyprint?- LPFN_ACCEPTEXm_lpfnAcceptEx;//AcceptEx函數指針
- GUIDGuidAcceptEx=WSAID_ACCEPTEX;//GUID,這個是識別AcceptEx函數必須的
- DWORDdwBytes=0;
- WSAIoctl(
- m_pListenContext->m_Socket,
- SIO_GET_EXTENSION_FUNCTION_POINTER,
- GuidAcceptEx,
- sizeof(GuidAcceptEx),
- m_lpfnAcceptEx,
- sizeof(m_lpfnAcceptEx),
- dwBytes,
- NULL,
- NULL);
具體實現就沒什麼可說的了,因爲都是固定的套路,那個GUID是微軟給定義好的,直接拿過來用就行了,WSAIoctl()就是通過這個找到AcceptEx的地址的,另外需要注意的是,通過WSAIoctl獲取AcceptEx函數指針時,只需要隨便傳遞給WSAIoctl()一個有效的SOCKET即可,該Socket的類型不會影響獲取的AcceptEx函數指針。
然後,我們就可以通過其中的指針m_lpfnAcceptEx調用AcceptEx函數了。
AcceptEx函數的定義如下:
[cpp] view plaincopyprint?- BOOLAcceptEx(
- SOCKETsListenSocket,
- SOCKETsAcceptSocket,
- PVOIDlpOutputBuffer,
- DWORDdwReceiveDataLength,
- DWORDdwLocalAddressLength,
- DWORDdwRemoteAddressLength,
- LPDWORDlpdwBytesReceived,
- LPOVERLAPPEDlpOverlapped
- );
乍一看起來參數很多,但是實際用起來也很簡單:
參數1--sListenSocket, 這個就是那個唯一的用來監聽的Socket了,沒什麼說的;參數2--sAcceptSocket, 用於接受連接的socket,這個就是那個需要我們事先建好的,等有客戶端連接進來直接把這個Socket拿給它用的那個,是AcceptEx高性能的關鍵所在。參數3--lpOutputBuffer,接收緩衝區,這也是AcceptEx比較有特色的地方,既然AcceptEx不是普通的accpet函數,那麼這個緩衝區也不是普通的緩衝區,這個緩衝區包含了三個信息:一是客戶端發來的第一組數據,二是server的地址,三是client地址,都是精華啊…但是讀取起來就會很麻煩,不過後面有一個更好的解決方案。參數4--dwReceiveDataLength,前面那個參數lpOutputBuffer中用於存放數據的空間大小。如果此參數=0,則Accept時將不會待數據到來,而直接返回,如果此參數不爲0,那麼一定得等接收到數據了纔會返回…… 所以通常當需要Accept接收數據時,就需要將該參數設成爲:sizeof(lpOutputBuffer) - 2*(sizeof sockaddr_in 16),也就是說總長度減去兩個地址空間的長度就是了,看起來複雜,其實想明白了也沒啥……參數5--dwLocalAddressLength,存放本地址地址信息的空間大小;參數6--dwRemoteAddressLength,存放本遠端地址信息的空間大小;參數7--lpdwBytesReceived,out參數,對我們來說沒用,不用管;參數8--lpOverlapped,本次重疊I/O所要用到的重疊結構。這裏面的參數倒是沒什麼,看起來複雜,但是咱們依舊可以一個一個傳進去,然後在對應的IO操作完成之後,這些參數Windows內核自然就會幫咱們填滿了。
但是非常悲催的是,我們這個是異步操作,我們是在線程啓動的地方投遞的這個操作, 等我們再次見到這些個變量的時候,就已經是在Worker線程內部了,因爲Windows會直接把操作完成的結果傳遞到Worker線程裏,這樣咱們在啓動的時候投遞了那麼多的IO請求,這從Worker線程傳回來的這些結果,到底是對應着哪個IO請求的呢?。。。。
聰明的你肯定想到了,是的,Windows內核也幫我們想到了:用一個標誌來綁定每一個IO操作,這樣到了Worker線程內部的時候,收到網絡操作完成的通知之後,再通過這個標誌來找出這組返回的數據到底對應的是哪個Io操作的。
這裏的標誌就是如下這樣的結構體:
[cpp] view plaincopyprint?- typedefstruct_PER_IO_CONTEXT{
- OVERLAPPEDm_Overlapped;//每一個重疊I/O網絡操作都要有一個
- SOCKETm_sockAccept;//這個I/O操作所使用的Socket,每個連接的都是一樣的
- WSABUFm_wsaBuf;//存儲數據的緩衝區,用來給重疊操作傳遞參數的,關於WSABUF後面還會講
- charm_szBuffer[MAX_BUFFER_LEN];//對應WSABUF裏的緩衝區
- OPERATION_TYPEm_OpType;//標誌這個重疊I/O操作是做什麼的,例如Accept/Recv等
- }PER_IO_CONTEXT,*PPER_IO_CONTEXT;
這個結構體的成員當然是我們隨便定義的,裏面的成員你可以隨意修改(除了OVERLAPPED那個之外……)。
但是AcceptEx不是普通的accept,buffer不是普通的buffer,那麼這個結構體當然也不能是普通的結構體了……
在完成端口的世界裏,這個結構體有個專屬的名字“單IO數據”,是什麼意思呢?也就是說每一個重疊I/O都要對應的這麼一組參數,至於這個結構體怎麼定義無所謂,而且這個結構體也不是必須要定義的,但是沒它……還真是不行,我們可以把它理解爲線程參數,就好比你使用線程的時候,線程參數也不是必須的,但是不傳還真是不行……
除此以外,我們也還會想到,既然每一個I/O操作都有對應的PER_IO_CONTEXT結構體,而在每一個Socket上,我們會投遞多個I/O請求的,例如我們就可以在監聽Socket上投遞多個AcceptEx請求,所以同樣的,我們也還需要一個“單句柄數據”來管理這個句柄上所有的I/O請求,這裏的“句柄”當然就是指的Socket了,我在代碼中是這樣定義的:
[cpp] view plaincopyprint?- typedefstruct_PER_SOCKET_CONTEXT
- {
- SOCKETm_Socket;//每一個客戶端連接的Socket
- SOCKADDR_INm_ClientAddr;//這個客戶端的地址
- CArray<_per_io_context>m_arrayIoContext;//數組,所有客戶端IO操作的參數,
- //也就是說對於每一個客戶端Socket
- //是可以在上面同時投遞多個IO請求的
- }PER_SOCKET_CONTEXT,*PPER_SOCKET_CONTEXT;
這也是比較好理解的,也就是說我們需要在一個Socket句柄上,管理在這個Socket上投遞的每一個IO請求的_PER_IO_CONTEXT。
當然,同樣的,各位對於這些也可以按照自己的想法來隨便定義,只要能起到管理每一個IO請求上需要傳遞的網絡參數的目的就好了,關鍵就是需要跟蹤這些參數的狀態,在必要的時候釋放這些資源,不要造成內存泄漏,因爲作爲Server總是需要長時間運行的,所以如果有內存泄露的情況那是非常可怕的,一定要杜絕一絲一毫的內存泄漏。
至於具體這兩個結構體參數是如何在Worker線程裏大發神威的,我們後面再看。
以上就是我們全部的準備工作了,具體的實現各位可以配合我的流程圖再看一下示例代碼,相信應該會理解得比較快。
完成端口初始化的工作比起其他的模型來講是要更復雜一些,所以說對於主線程來講,它總覺得自己付出了很多,總覺得Worker線程是坐享其成,但是Worker自己的苦只有自己明白,Worker線程的工作一點也不比主線程少,相反還要更復雜一些,並且具體的通信工作全部都是Worker線程來完成的,Worker線程反而還覺得主線程是在旁邊看熱鬧,只知道發號施令而已,但是大家終究還是誰也離不開誰,這也就和公司里老板和員工的微妙關係是一樣的吧……
【第五步】我們再來看看Worker線程都做了些什麼
_Worker線程的工作都是涉及到具體的通信事務問題,主要完成了如下的幾個工作,讓我們一步一步的來看。
(1) 使用 GetQueuedCompletionStatus() 監控完成端口
首先這個工作所要做的工作大家也能猜到,無非就是幾個Worker線程哥幾個一起排好隊隊來監視完成端口的隊列中是否有完成的網絡操作就好了,代碼大體如下:
[cpp] view plaincopyprint?- void*lpContext=NULL;
- OVERLAPPED*pOverlapped=NULL;
- DWORDdwBytesTransfered=0;
- BOOLbReturn=GetQueuedCompletionStatus(
- pIOCPModel->m_hIOCompletionPort,
- dwBytesTransfered,
- (LPDWORD)lpContext,
- pOverlapped,
- INFINITE);
各位留意到其中的GetQueuedCompletionStatus()函數了嗎?這個就是Worker線程裏第一件也是最重要的一件事了,這個函數的作用就是我在前面提到的,會讓Worker線程進入不佔用CPU的睡眠狀態,直到完成端口上出現了需要處理的網絡操作或者超出了等待的時間限制爲止。
一旦完成端口上出現了已完成的I/O請求,那麼等待的線程會被立刻喚醒,然後繼續執行後續的代碼。
至於這個神奇的函數,原型是這樣的:
[cpp] view plaincopyprint?- BOOLWINAPIGetQueuedCompletionStatus(
- __inHANDLECompletionPort,//這個就是我們建立的那個唯一的完成端口
- __outLPDWORDlpNumberOfBytes,//這個是操作完成後返回的字節數
- __outPULONG_PTRlpCompletionKey,//這個是我們建立完成端口的時候綁定的那個自定義結構體參數
- __outLPOVERLAPPED*lpOverlapped,//這個是我們在連入Socket的時候一起建立的那個重疊結構
- __inDWORDdwMilliseconds//等待完成端口的超時時間,如果線程不需要做其他的事情,那就INFINITE就行了
- );
所以,如果這個函數突然返回了,那就說明有需要處理的網絡操作了 --- 當然,在沒有出現錯誤的情況下。
然後switch()一下,根據需要處理的操作類型,那我們來進行相應的處理。
但是如何知道操作是什麼類型的呢?這就需要用到從外部傳遞進來的loContext參數,也就是我們封裝的那個參數結構體,這個參數結構體裏面會帶有我們一開始投遞這個操作的時候設置的操作類型,然後我們根據這個操作再來進行對應的處理。
但是還有問題,這個參數究竟是從哪裏傳進來的呢?傳進來的時候內容都有些什麼?
這個問題問得好!
首先,我們要知道兩個關鍵點:
(1) 這個參數,是在你綁定Socket到一個完成端口的時候,用的CreateIoCompletionPort()函數,傳入的那個CompletionKey參數,要是忘了的話,就翻到文檔的“第三步”看看相關的內容;我們在這裏傳入的是定義的PER_SOCKET_CONTEXT,也就是說“單句柄數據”,因爲我們綁定的是一個Socket,這裏自然也就需要傳入Socket相關的上下文,你是怎麼傳過去的,這裏收到的就會是什麼樣子,也就是說這個lpCompletionKey就是我們的PER_SOCKET_CONTEXT,直接把裏面的數據拿出來用就可以了。
(2) 另外還有一個很神奇的地方,裏面的那個lpOverlapped參數,裏面就帶有我們的PER_IO_CONTEXT。這個參數是從哪裏來的呢?我們去看看前面投遞AcceptEx請求的時候,是不是傳了一個重疊參數進去?這裏就是它了,並且,我們可以使用一個很神奇的宏,把和它存儲在一起的其他的變量,全部都讀取出來,例如:
- PER_IO_CONTEXT*pIoContext=CONTAINING_RECORD(lpOverlapped,PER_IO_CONTEXT,m_Overlapped);
這個宏的含義,就是去傳入的lpOverlapped變量裏,找到和結構體中PER_IO_CONTEXT中m_Overlapped成員相關的數據。
你仔細想想,其實真的很神奇……
但是要做到這種神奇的效果,應該確保我們在結構體PER_IO_CONTEXT定義的時候,把Overlapped變量,定義爲結構體中的第一個成員。
只要各位能弄清楚這個GetQueuedCompletionStatus()中各種奇怪的參數,那我們就離成功不遠了。
既然我們可以獲得PER_IO_CONTEXT結構體,那麼我們就自然可以根據其中的m_OpType參數,得知這次收到的這個完成通知,是關於哪個Socket上的哪個I/O操作的,這樣就分別進行對應處理就好了。
在我的示例代碼裏,在有AcceptEx請求完成的時候,我是執行的_DoAccept()函數,在有WSARecv請求完成的時候,執行的是_DoRecv()函數,下面我就分別講解一下這兩個函數的執行流程。
【第六步】當收到Accept通知時 _DoAccept()
在用戶收到AcceptEx的完成通知時,需要後續代碼並不多,但卻是邏輯最爲混亂,最容易出錯的地方,這也是很多用戶爲什麼寧願用效率低下的accept()也不願意去用AcceptEx的原因吧。
和普通的Socket通訊方式一樣,在有客戶端連入的時候,我們需要做三件事情:
(1) 爲這個新連入的連接分配一個Socket;
(2) 在這個Socket上投遞第一個異步的發送/接收請求;
(3) 繼續監聽。
其實都是一些很簡單的事情但是由於“單句柄數據”和“單IO數據”的加入,事情就變得比較亂。因爲是這樣的,讓我們一起縷一縷啊,最好是配合代碼一起看,否則太抽象了……
(1) 首先,_Worker線程通過GetQueuedCompletionStatus()裏會收到一個lpCompletionKey,這個也就是PER_SOCKET_CONTEXT,裏面保存了與這個I/O相關的Socket和Overlapped還有客戶端發來的第一組數據等等,對吧?但是這裏得注意,這個SOCKET的上下文數據,是關於監聽Socket的,而不是新連入的這個客戶端Socket的,千萬別弄混了……
(2) 所以,AcceptEx不是給咱們新連入的這個Socket早就建好了一個Socket嗎?所以這裏,我們需要再用這個新Socket重新爲新客戶端建立一個PER_SOCKET_CONTEXT,以及下面一系列的新PER_IO_CONTEXT,千萬不要去動傳入的這個Listen Socket上的PER_SOCKET_CONTEXT,也不要用傳入的這個Overlapped信息,因爲這個是屬於AcceptEx I/O操作的,也不是屬於你投遞的那個Recv I/O操作的……,要不你下次繼續監聽的時候就悲劇了……
(3) 等到新的Socket準備完畢了,我們就趕緊還是用傳入的這個Listen Socket上的PER_SOCKET_CONTEXT和PER_IO_CONTEXT去繼續投遞下一個AcceptEx,循環起來,留在這裏太危險了,早晚得被人給改了……
(4) 而我們新的Socket的上下文數據和I/O操作數據都準備好了之後,我們要做兩件事情:一件事情是把這個新的Socket和我們唯一的那個完成端口綁定,這個就不用細說了,和前面綁定監聽Socket是一樣的;然後就是在這個Socket上投遞第一個I/O操作請求,在我的示例代碼裏投遞的是WSARecv()。因爲後續的WSARecv,就不是在這裏投遞的了,這裏只負責第一個請求。
但是,至於WSARecv請求如何來投遞的,我們放到下一節中去講,這一節,我們還有一個很重要的事情,我得給大家提一下,就是在客戶端連入的時候,我們如何來獲取客戶端的連入地址信息。
這裏我們還需要引入另外一個很高端的函數,GetAcceptExSockAddrs(),它和AcceptEx()一樣,都是微軟提供的擴展函數,所以同樣需要通過下面的方式來導入纔可以使用……
[cpp] view plaincopyprint?- WSAIoctl(
- m_pListenContext->m_Socket,
- SIO_GET_EXTENSION_FUNCTION_POINTER,
- GuidGetAcceptExSockAddrs,
- sizeof(GuidGetAcceptExSockAddrs),
- m_lpfnGetAcceptExSockAddrs,
- sizeof(m_lpfnGetAcceptExSockAddrs),
- dwBytes,
- NULL,
- NULL);
和導出AcceptEx一樣一樣的,同樣是需要用其GUID來獲取對應的函數指針 m_lpfnGetAcceptExSockAddrs 。
說了這麼多,這個函數究竟是幹嘛用的呢?它是名副其實的“AcceptEx之友”,爲什麼這麼說呢?因爲我前面提起過AcceptEx有個很神奇的功能,就是附帶一個神奇的緩衝區,這個緩衝區厲害了,包括了客戶端發來的第一組數據、本地的地址信息、客戶端的地址信息,三合一啊,你說神奇不神奇?
這個函數從它字面上的意思也基本可以看得出來,就是用來解碼這個緩衝區的,是的,它不提供別的任何功能,就是專門用來解析AcceptEx緩衝區內容的。例如如下代碼:
[cpp] view plaincopyprint?- PER_IO_CONTEXT*pIoContext=本次通信用的I/OContext
- SOCKADDR_IN*ClientAddr=NULL;
- SOCKADDR_IN*LocalAddr=NULL;
- intremoteLen=sizeof(SOCKADDR_IN),localLen=sizeof(SOCKADDR_IN);
- m_lpfnGetAcceptExSockAddrs(pIoContext->m_wsaBuf.buf,pIoContext->m_wsaBuf.len-((sizeof(SOCKADDR_IN) 16)*2),sizeof(SOCKADDR_IN) 16,sizeof(SOCKADDR_IN) 16,(LPSOCKADDR*)LocalAddr,localLen,(LPSOCKADDR*)ClientAddr,remoteLen);
解碼完畢之後,於是,我們就可以從如下的結構體指針中獲得很多有趣的地址信息了:
inet_ntoa(ClientAddr->sin_addr) 是客戶端IP地址
ntohs(ClientAddr->sin_port) 是客戶端連入的端口
inet_ntoa(LocalAddr ->sin_addr) 是本地IP地址
ntohs(LocalAddr ->sin_port) 是本地通訊的端口
pIoContext->m_wsaBuf.buf 是存儲客戶端發來第一組數據的緩衝區
自從用了“AcceptEx之友”,一切都清淨了….
【第七步】當收到Recv通知時, _DoRecv()
在講解如何處理Recv請求之前,我們還是先講一下如何投遞WSARecv請求的。
WSARecv大體的代碼如下,其實就一行,在代碼中我們可以很清楚的看到我們用到了很多新建的PerIoContext的參數,這裏再強調一下,注意一定要是自己另外新建的啊,一定不能是Worker線程裏傳入的那個PerIoContext,因爲那個是監聽Socket的,別給人弄壞了……:
[cpp] view plaincopyprint?- intnBytesRecv=WSARecv(pIoContext->m_Socket,pIoContext->p_wbuf,1,dwBytes,0,pIoContext->p_ol,NULL);
這裏,我再把WSARev函數的原型再給各位講一下
[cpp] view plaincopyprint?- intWSARecv(
- SOCKETs,//當然是投遞這個操作的套接字
- LPWSABUFlpBuffers,//接收緩衝區
- //這裏需要一個由WSABUF結構構成的數組
- DWORDdwBufferCount,//數組中WSABUF結構的數量,設置爲1即可
- LPDWORDlpNumberOfBytesRecvd,//如果接收操作立即完成,這裏會返回函數調用所接收到的字節數
- LPDWORDlpFlags,//說來話長了,我們這裏設置爲0即可
- LPWSAOVERLAPPEDlpOverlapped,//這個Socket對應的重疊結構
- NULL//這個參數只有完成例程模式纔會用到,
- //完成端口中我們設置爲NULL即可
- );
其實裏面的參數,如果你們熟悉或者看過我以前的重疊I/O的文章,應該都比較熟悉,只需要注意其中的兩個參數:
LPWSABUF lpBuffers;這裏是需要我們自己new 一個 WSABUF 的結構體傳進去的;
如果你們非要追問 WSABUF 結構體是個什麼東東?我就給各位多說兩句,就是在ws2def.h中有定義的,定義如下:
[cpp] view plaincopyprint?- typedefstruct_WSABUF{
- ULONGlen;/*thelengthofthebuffer*/
- __field_bcount(len)CHARFAR*buf;/*thepointertothebuffer*/
- }WSABUF,FAR*LPWSABUF;
而且好心的微軟還附贈了註釋,真不容易….
看到了嗎?如果對於裏面的一些奇怪符號你們看不懂的話,也不用管他,只用看到一個ULONG和一個CHAR*就可以了,這不就是一個是緩衝區長度,一個是緩衝區指針麼?至於那個什麼 FAR…..讓他見鬼去吧,現在已經是32位和64位時代了……
這裏需要注意的,我們的應用程序接到數據到達的通知的時候,其實數據已經被咱們的主機接收下來了,我們直接通過這個WSABUF指針去系統緩衝區拿數據就好了,而不像那些沒用重疊I/O的模型,接收到有數據到達的通知的時候還得自己去另外recv,太低端了……這也是爲什麼重疊I/O比其他的I/O性能要好的原因之一。
LPWSAOVERLAPPED lpOverlapped這個參數就是我們所謂的重疊結構了,就是這樣定義,然後在有Socket連接進來的時候,生成並初始化一下,然後在投遞第一個完成請求的時候,作爲參數傳遞進去就可以,
[cpp] view plaincopyprint?- OVERLAPPED*m_pol=newOVERLAPPED;
- eroMemory(m_pol,sizeof(OVERLAPPED));
在第一個重疊請求完畢之後,我們的這個OVERLAPPED 結構體裏,就會被分配有效的系統參數了,並且我們是需要每一個Socket上的每一個I/O操作類型,都要有一個唯一的Overlapped結構去標識。
這樣,投遞一個WSARecv就講完了,至於_DoRecv()需要做些什麼呢?其實就是做兩件事:
(1) 把WSARecv裏這個緩衝區裏收到的數據顯示出來;
(2) 發出下一個WSARecv();
Over……
至此,我們終於深深的喘口氣了,完成端口的大部分工作我們也完成了,也非常感謝各位耐心的看我這麼枯燥的文字一直看到這裏,真是一個不容易的事情!!
【第八步】如何關閉完成端口
休息完畢,我們繼續……
各位看官不要高興得太早,雖然我們已經讓我們的完成端口順利運作起來了,但是在退出的時候如何釋放資源咱們也是要知道的,否則豈不是功虧一簣…..
從前面的章節中,我們已經瞭解到,Worker線程一旦進入了GetQueuedCompletionStatus()的階段,就會進入睡眠狀態,INFINITE的等待完成端口中,如果完成端口上一直都沒有已經完成的I/O請求,那麼這些線程將無法被喚醒,這也意味着線程沒法正常退出。
熟悉或者不熟悉多線程編程的朋友,都應該知道,如果在線程睡眠的時候,簡單粗暴的就把線程關閉掉的話,那是會一個很可怕的事情,因爲很多線程體內很多資源都來不及釋放掉,無論是這些資源最後是否會被操作系統回收,我們作爲一個C 程序員來講,都不應該允許這樣的事情出現。
所以我們必須得有一個很優雅的,讓線程自己退出的辦法。
這時會用到我們這次見到的與完成端口有關的最後一個API,叫 PostQueuedCompletionStatus(),從名字上也能看得出來,這個是和 GetQueuedCompletionStatus() 函數相對的,這個函數的用途就是可以讓我們手動的添加一個完成端口I/O操作,這樣處於睡眠等待的狀態的線程就會有一個被喚醒,如果爲我們每一個Worker線程都調用一次PostQueuedCompletionStatus()的話,那麼所有的線程也就會因此而被喚醒了。
PostQueuedCompletionStatus()函數的原型是這樣定義的:
[cpp] view plaincopyprint?- BOOLWINAPIPostQueuedCompletionStatus(
- __inHANDLECompletionPort,
- __inDWORDdwNumberOfBytesTransferred,
- __inULONG_PTRdwCompletionKey,
- __in_optLPOVERLAPPEDlpOverlapped
- );
我們可以看到,這個函數的參數幾乎和GetQueuedCompletionStatus()的一模一樣,都是需要把我們建立的完成端口傳進去,然後後面的三個參數是 傳輸字節數、結構體參數、重疊結構的指針.
注意,這裏也有一個很神奇的事情,正常情況下,GetQueuedCompletionStatus()獲取回來的參數本來是應該是系統幫我們填充的,或者是在綁定完成端口時就有的,但是我們這裏卻可以直接使用PostQueuedCompletionStatus()直接將後面三個參數傳遞給GetQueuedCompletionStatus(),這樣就非常方便了。
例如,我們爲了能夠實現通知線程退出的效果,可以自己定義一些約定,比如把這後面三個參數設置一個特殊的值,然後Worker線程接收到完成通知之後,通過判斷這3個參數中是否出現了特殊的值,來決定是否是應該退出線程了。
例如我們在調用的時候,就可以這樣:
[cpp] view plaincopyprint?- for(inti=0;i
{ - PostQueuedCompletionStatus(m_hIOCompletionPort,0,(DWORD)NULL,NULL);
- }
爲每一個線程都發送一個完成端口數據包,有幾個線程就發送幾遍,把其中的dwCompletionKey參數設置爲NULL,這樣每一個Worker線程在接收到這個完成通知的時候,再自己判斷一下這個參數是否被設置成了NULL,因爲正常情況下,這個參數總是會有一個非NULL的指針傳入進來的,如果Worker發現這個參數被設置成了NULL,那麼Worker線程就會知道,這是應用程序再向Worker線程發送的退出指令,這樣Worker線程在內部就可以自己很“優雅”的退出了……
學會了嗎?
但是這裏有一個很明顯的問題,聰明的朋友一定想到了,而且只有想到了這個問題的人,纔算是真正看明白了這個方法。
我們只是發送了m_nThreads次,我們如何能確保每一個Worker線程正好就收到一個,然後所有的線程都正好退出呢?是的,我們沒有辦法保證,所以很有可能一個Worker線程處理完一個完成請求之後,發生了某些事情,結果又再次去循環接收下一個完成請求了,這樣就會造成有的Worker線程沒有辦法接收到我們發出的退出通知。
所以,我們在退出的時候,一定要確保Worker線程只調用一次GetQueuedCompletionStatus(),這就需要我們自己想辦法了,各位請參考我在Worker線程中實現的代碼,我搭配了一個退出的Event,在退出的時候SetEvent一下,來確保Worker線程每次就只會調用一輪 GetQueuedCompletionStatus() ,這樣就應該比較安全了。
另外,在Vista/Win7系統中,我們還有一個更簡單的方式,我們可以直接CloseHandle關掉完成端口的句柄,這樣所有在GetQueuedCompletionStatus()的線程都會被喚醒,並且返回FALSE,這時調用GetLastError()獲取錯誤碼時,會返回ERROR_INVALID_HANDLE,這樣每一個Worker線程就可以通過這種方式輕鬆簡單的知道自己該退出了。當然,如果我們不能保證我們的應用程序只在Vista/Win7中,那還是老老實實的PostQueuedCompletionStatus()吧。
最後,在系統釋放資源的最後階段,切記,因爲完成端口同樣也是一個Handle,所以也得用CloseHandle將這個句柄關閉,當然還要記得用closesocket關閉一系列的socket,還有別的各種指針什麼的,這都是作爲一個合格的C 程序員的基本功,在這裏就不多說了,如果還是有不太清楚的朋友,請參考我的示例代碼中的 StopListen() 和DeInitialize() 函數。
六. 完成端口使用中的注意事項
終於到了文章的結尾了,不知道各位朋友是基本學會了完成端口的使用了呢,還是被完成端口以及我這麼多口水的文章折磨得不行了……
最後再補充一些前面沒有提到了,實際應用中的一些注意事項吧。
1. Socket的通信緩衝區設置成多大合適?
在x86的體系中,內存頁面是以4KB爲單位來鎖定的,也就是說,就算是你投遞WSARecv()的時候只用了1KB大小的緩衝區,系統還是得給你分4KB的內存。爲了避免這種浪費,最好是把發送和接收數據的緩衝區直接設置成4KB的倍數。
2. 關於完成端口通知的次序問題
這個不用想也能知道,調用GetQueuedCompletionStatus() 獲取I/O完成端口請求的時候,肯定是用先入先出的方式來進行的。
但是,咱們大家可能都想不到的是,喚醒那些調用了GetQueuedCompletionStatus()的線程是以後入先出的方式來進行的。
比如有4個線程在等待,如果出現了一個已經完成的I/O項,那麼是最後一個調用GetQueuedCompletionStatus()的線程會被喚醒。平常這個次序倒是不重要,但是在對數據包順序有要求的時候,比如傳送大塊數據的時候,是需要注意下這個先後次序的。
-- 微軟之所以這麼做,那當然是有道理的,這樣如果反覆只有一個I/O操作而不是多個操作完成的話,內核就只需要喚醒同一個線程就可以了,而不需要輪着喚醒多個線程,節約了資源,而且可以把其他長時間睡眠的線程換出內存,提到資源利用率。
3. 如果各位想要傳輸文件…
如果各位需要使用完成端口來傳送文件的話,這裏有個非常需要注意的地方。因爲發送文件的做法,按照正常人的思路來講,都會是先打開一個文件,然後不斷的循環調用ReadFile()讀取一塊之後,然後再調用WSASend ()去發發送。
但是我們知道,ReadFile()的時候,是需要操作系統通過磁盤的驅動程序,到實際的物理硬盤上去讀取文件的,這就會使得操作系統從用戶態轉換到內核態去調用驅動程序,然後再把讀取的結果返回至用戶態;同樣的道理,WSARecv()也會涉及到從用戶態到內核態切換的問題 --- 這樣就使得我們不得不頻繁的在用戶態到內核態之間轉換,效率低下……
而一個非常好的解決方案是使用微軟提供的擴展函數TransmitFile()來傳輸文件,因爲只需要傳遞給TransmitFile()一個文件的句柄和需要傳輸的字節數,程序就會整個切換至內核態,無論是讀取數據還是發送文件,都是直接在內核態中執行的,直到文件傳輸完畢纔會返回至用戶態給主進程發送通知。這樣效率就高多了。
4. 關於重疊結構數據釋放的問題
我們既然使用的是異步通訊的方式,就得要習慣一點,就是我們投遞出去的完成請求,不知道什麼時候我們才能收到操作完成的通知,而在這段等待通知的時間,我們就得要千萬注意得保證我們投遞請求的時候所使用的變量在此期間都得是有效的。
例如我們發送WSARecv請求時候所使用的Overlapped變量,因爲在操作完成的時候,這個結構裏面會保存很多很重要的數據,對於設備驅動程序來講,指示保存着我們這個Overlapped變量的指針,而在操作完成之後,驅動程序會將Buffer的指針、已經傳輸的字節數、錯誤碼等等信息都寫入到我們傳遞給它的那個Overlapped指針中去。如果我們已經不小心把Overlapped釋放了,或者是又交給別的操作使用了的話,誰知道驅動程序會把這些東西寫到哪裏去呢?豈不是很崩潰……
暫時我想到的問題就是這麼多吧,如果各位真的是要正兒八經寫一個承受很大訪問壓力的Server的話,你慢慢就會發現,只用我附帶的這個示例代碼是不夠的,還得需要在很多細節之處進行改進,例如用更好的數據結構來管理上下文數據,並且需要非常完善的異常處理機制等等,總之,非常期待大家的批評和指正。
謝謝大家看到這裏!!!
------ Finished in DLUT
------ 2011-9-31
分享到:我在代碼里加了DoSend和PostSend函數,
/////////////////////////////////////////////////////////////////
// 投遞發送數據請求
bool CIOCPModel::_PostSend( PER_IO_CONTEXT* pIoContext )
{
// 初始化變量
DWORD dwFlags = 0;
WSABUF *p_wbuf = pIoContext->m_wsaBuf;
OVERLAPPED *p_ol = pIoContext->m_Overlapped;
strcpy(p_wbuf->buf, "123");
p_wbuf->len = 3;
DWORD dwBytes = 3;
//pIoContext->ResetBuffer();
pIoContext->m_OpType = SEND_POSTED;
// 初始化完成後,,投遞WSARecv請求
int nBytesSend = WSASend( pIoContext->m_sockAccept, p_wbuf, 1, dwBytes, dwFlags, p_ol, NULL );
// 如果返回值錯誤,並且錯誤的代碼並非是Pending的話,那就說明這個重疊請求失敗了
if ((SOCKET_ERROR == nBytesSend) (WSA_IO_PENDING != WSAGetLastError()))
{
this->_ShowMessage("投遞第一個WSASend失敗!%d", WSAGetLastError());
return false;
}
}
並在每次DoRecv裏調用,調用的時候不能用DoRecv的參數pIOContext,必須自己用pSocketContext新建一個,請問這是爲什麼呢?Re: wadeshen003 6天前 16:29發表 [回覆] [引用] [舉報]引用“HUXINLINCOLN”的評論:你好 很感謝你的分享。
我在代碼里加了DoSend和PostSend函數,
////////////...
strcpy(p_wbuf->buf, "123");
p_wbuf->len = 3;
注意 這塊 p_wbuf->buf只是一個指針,需要指向一塊有具體內容的內存空間,只要改爲 p_wbuf->buf = new char[4]; 就可以了140樓 suxinpingtao51 2012-08-17 22:04發表 [回覆] [引用] [舉報]很專業啊,謝謝BZ的分享,好好研究下139樓 txtfashion 2012-08-16 12:35發表 [回覆] [引用] [舉報]這個看完了還是模模糊糊的,看來自己的水平還是有限。。。138樓 jonsenkiar 2012-08-10 01:35發表 [回覆] [引用] [舉報]膜拜。。。137樓 zcy828 2012-08-09 14:40發表 [回覆] [引用] [舉報]受益匪淺,膜拜樓主了136樓 huang462028247 2012-08-03 17:33發表 [回覆] [引用] [舉報]撿到寶了!好久沒遇到這麼好的文章了,樓主辛苦!135樓 youngallen 2012-08-02 14:22發表 [回覆] [引用] [舉報]求發送部分代碼或者詳解啊,最近做連續發送時總是遇上內存方面的錯誤134樓 chao742210485 2012-07-24 11:10發表 [回覆] [引用] [舉報]但是要做到這種神奇的效果,應該確保我們在結構體PER_IO_CONTEXT定義的時候,把Overlapped變量,定義爲結構體中的第一個成員。
這裏有問題吧,不一定要定義到第一個成員,這個宏定義會自己便宜的,只要有Overlapped結構就可以了。
感謝樓主!樓主辛苦了。133樓 rwxdfbb 2012-07-18 17:04發表 [回覆] [引用] [舉報]毫不誇張的說,這是我見過的最通俗易懂的關於IOCP的說明了,以前從來沒真正看懂過IOCP,看完這篇,豁然開朗。
不過XP下運行壓力測試的時候,併發數到2500的時候,測壓力軟件崩潰了,服務端倒是正常,看來服務端應該還是很NB的,居然把測試軟件都測死了。
另外要是能提供下send的就更好了。132樓 yao050421103 2012-07-13 10:29發表 [回覆] [引用] [舉報]請教樓主一個問題:在嘗試退出工作者線程的時候,有兩種方式,一種是通過PostQueuedCompletionStatus,還有一種是設置一個狀態標誌位,然後在工作者線程中判斷此標誌位是否爲退出,這兩種方式哪一種比較好?爲什麼?O(∩_∩)O謝謝!131樓 fang437385323 2012-07-12 12:25發表 [回覆] [引用] [舉報]謝謝!樓主辛苦了!130樓 oNaShiHuaKai 2012-07-08 04:46發表 [回覆] [引用] [舉報]標記下 相當容易理解的文章129樓 zuiqiannian123 2012-06-27 16:42發表 [回覆] [引用] [舉報]很好的東西128樓 xichengcn 2012-06-21 10:49發表 [回覆] [引用] [舉報]希望博主儘快把回覆發送的代碼補上啊,不甚感激啊。127樓 yao050421103 2012-06-19 16:23發表 [回覆] [引用] [舉報]非常好的文章,很深入,而且通俗易懂,做到這樣不容易,贊一個 ^_^126樓 xhk456 2012-06-13 19:45發表 [回覆] [引用] [舉報]文章不錯,不過我覺得一點是:
阻塞 不等於 同步
非阻塞 不等於 異步
就如同
老人 不等於 男人
小孩 不等於 女人
阻塞和非阻塞 與 同步和異步 的分類類別是不一樣滴
不能用等號的
可以用阻塞的函數寫出異步的調用
也可以用非阻塞的函數寫出同步的調用
有老男人 也有小女孩
你說呢125樓 liangbch 2012-06-08 18:38發表 [回覆] [引用] [舉報]我有一個疑問?
1.Server端 在一開始會創建 MAX_POST_ACCEPT 個socket
具體調用在 _InitializeListenSocket 函數,_InitializeListenSocket是調用_PostAccept來創建socket的。
2.此後,每收到一個client端的的連接,都會調用 _DoAccpet,而_DoAccpet 又調用_PostAccept來創建socket。
所以,如果客戶端啓動100個連接,則server端總共創建 100 MAX_POST_ACCEPT =110個socket.
這到沒有什麼,我不解的是:
1. _DoAccpet創建新的socket的時候,直接用新的socket句柄覆蓋pIoContext->m_sockAccept.
2. 而停止監聽的時候,僅僅釋放監聽socket,而上面創建的110 socket卻從來沒有被釋放,這樣有問題嗎?
MSDN 說:
An application should always have a matching call to closesocket for each successful call to socket to return any socket resources to the system.124樓 liangbch 2012-06-07 19:03發表 [回覆] [引用] [舉報]寫的不錯,版主辛苦了,正在學習中123樓 hankunchen 2012-06-07 13:18發表 [回覆] [引用] [舉報]剛接觸完成端口,百度到的第一篇博客文章。LZ幸苦了,雖然還是不太懂,收藏起來以後再看。122樓 whhit_436 2012-06-01 14:31發表 [回覆] [引用] [舉報]case OP_READ: // 完成一個接收請求
{
AnalysisMessage(pSocket->s, pPerIO->buf ,dwTrans);
PostRecv(pSocket);//投遞下一個讀請求
}
如果一個套接字端同時傳來幾個數據包時,幾個線程同時用到該套接字的pPerIO,會不會爭用該存儲空間導致後邊AnalysisMessage實際讀到的數據錯誤?121樓 mingwei2004 2012-05-23 16:17發表 [回覆] [引用] [舉報]寫的太好了 有麼有源碼 嘿嘿120樓 dishen10 2012-05-17 02:57發表 [回覆] [引用] [舉報]剛接觸網絡編程和多線程..
有一些疑問:
博主是用PER_SOCKET_CONTEXT類來表示每一個連接的socket,然後_PER_IO_CONTEXT表示每一個socket進行異步操作時的上下文信息。
而PER_SOCKET_CONTEXT有一個_PER_IO_CONTEXT的數組m_arrayIoContext記錄此socket發出的所有異步操作的上下文信息。
但在博主程序中,_PER_IO_CONTEXT中記錄了m_sockAccept,在提交異步accept操作時,這個m_sockAccept是記錄了新的socket的,跟發出accept請求的socket不同,用於在accept請求響應時將新socket添加到服務器CIOCPModel對象的客戶端列表m_arrayClientContext中,以及在處理完accept請求後再次用監聽socket發送新的異步accet請求時使用同一個_PER_IO_CONTEXT(本應是在PER_SOCKET_CONTEXT的m_arrayIoContext數組刪除當前_PER_IO_CONTEXT,再重新申請_PER_IO_CONTEXT的?你在申請_PER_IO_CONTEXT函數中默認將新的IO上下文添加進m_arrayIoContext數組了);而在提交異步rec操作時,這個m_sockAccept記錄的是發出rec操作的socket,也就是跟PER_SOCKET_CONTEXT中的socket是同一個socket,用於異步rec操作響應時在處理rec響應後再次發出異步rec操作即調用_PostRecv函數時使用_PER_IO_CONTEXT的m_sockAccept來發送rec請求。
這裏_PER_IO_CONTEXT中m_sockAccept的意義是不是有點混亂,在異步accept請求響應時,它的m_sockAccept是作爲響應的結果信息的(也就是新的socket連接接入了,相當於發出異步操作時的OUT參數),而在異步rec中,m_sockAccept卻不屬於rec的結果信息,被當做了發出rec操作的socket記錄(相當於發出異步操作時的IN參數)。Re: dishen10 2012-05-17 03:12發表 [回覆] [引用] [舉報]回覆dishen10:其實這樣導致的錯誤應該是在系統stop時,會釋放監聽socket和客戶端socket,會調用PER_SOCKET_CONTEXT析構,析構中會同時清除socket中IO上下文m_arrayIoContext數組的元素,從而導致_PER_IO_CONTEXT析構,而在_PER_IO_CONTEXT的析構中會close掉m_sockAccept,這樣,對於監聽socket的IO上下文m_arrayIoContext數組,它的m_sockAccept是新的socket,是需要close掉的,而對於客戶端socket的IO上下文m_arrayIoContext數組,它的m_sockAccept記錄的是同一個客戶端socket的,這個m_sockAccept不應該close掉(因爲會在釋放客戶端socket時close了),但卻也被close掉了,從而會導致同一個客戶端socket被close了2次。
不知是不是真的存在這個問題??
還有一個疑問是:
是不是在程序stop時,有一些已經完成的異步操作(被添加進了IO完成端口的IO完成隊列中但還未來得及處理,所有woker線程就已經因爲post的EXIT_CODE操作而退出了,也就是在IO完成隊列中最後一個EXIT_CODE操作後還有已經完成的異步操作在等待處理)被忽略掉了?
有沒有更好的辦法使得所有發出的異步操作都完成了或者都被取消了才退出所有的worker線程??Re: dishen10 2012-05-17 12:33發表 [回覆] [引用] [舉報]回覆dishen10:對了,忘了感謝博主出了這麼一篇清晰的文章啊..!119樓 wxz 2012-05-10 11:51發表 [回覆] [引用] [舉報]好文章,是我見過的關於完成端口最通俗易懂的文章118樓 test2002 2012-05-05 20:53發表 [回覆] [引用] [舉報]雖然看了好多次,但依然沒弄透徹,樓主說AcceptEx是預先建立連接socket的,效率很高,但這個AcceptEx實際上測試的時候只投遞10個,不明白怎麼應對10000個併發客戶端,怎麼發送心跳包給10000個客戶端,判斷是否Alive。
按我的理解,是不是AcceptEx的好處就是可以有多個線程來處理,而Accept只是單個入口線程處理?AcceptEx是預先建立socket,不過看樓主的程序是預先建立10個而已
把 #define MAX_POST_ACCEPT 10 改成
#define MAX_POST_ACCEPT 10000
會有什麼效果,預先建立10000個socket?
,我是否可以理解成作者設成10個socket,就是說併發10個socket,其實10000個socket也是共有這10個socket而已??
望作者指點迷津,謝謝!Re: Bestsharp007 2012-05-23 10:36發表 [回覆] [引用] [舉報]這裏的10個AcceptEx只是同時存在的最大AcceptEx請求個數,每次在DoAccept之後,又PostAcceptEx了。就是說在某一瞬間,如果有100個客戶端connect進來,只能處理10個,而其他的則要慢慢來。回覆test2002:117樓 hubo520891 2012-05-04 21:29發表 [回覆] [引用] [舉報]0.0116樓 liuchen1206 2012-05-03 22:33發表 [回覆] [引用] [舉報]寫得很不錯!~~~115樓 sxsy323 2012-05-02 15:36發表 [回覆] [引用] [舉報]不知道其他看官有沒有反應,
建議博主,下次再寫如此長篇的時候,換一下字體顏色,關鍵點標紅不好,整篇字體選擇都太亮,太刺眼了,長時間,眼睛會不舒服Re: PiggyXP 2012-05-04 16:41發表 [回覆] [引用] [舉報]回覆sxsy323:謝謝你的提醒!關於文字的配色上,你有什麼好的建議麼?114樓 qw345 2012-05-01 23:00發表 [回覆] [引用] [舉報]用了很長時間看完樓主的文章,思路清晰,文筆幽默,膜拜膜拜!!113樓 cpponly2008 2012-04-28 13:37發表 [回覆] [引用] [舉報]客戶端併發線程數設爲10000,客戶端運行2000-3000的時候崩潰,測試環境win xp sp3Re: PiggyXP 2012-04-29 10:49發表 [回覆] [引用] [舉報]回覆cpponly2008:這麼強度應該不會崩潰啊,你的硬件環境呢?Re: cpponly2008 2012-05-03 17:37發表 [回覆] [引用] [舉報]回覆PiggyXP:Intel Core i3-2310M 2.1G
1.9G內存 dell筆記本Re: cpponly2008 2012-05-03 17:39發表 [回覆] [引用] [舉報]回覆cpponly2008:4 cpu(s)112樓 robin51201 2012-04-25 11:01發表 [回覆] [引用] [舉報]發送方面的代碼什麼時候能帖出,希望LZ早點給出,呵呵111樓 yangdm0209 2012-04-20 11:04發表 [回覆] [引用] [舉報]看代碼中每次監聽連接時都將建立連接的socket留給WorkerTread讀寫數據,然後新建一個socket,繼續進行監聽。如果WorkerThread發現客戶端斷開再釋放socket。這種操作涉及到客戶端可能會頻繁申請socket和釋放socket,是否可以考慮運用對象池技術,即程序開始時就建立預設的最大連接數個socket,然後使用者從對象池中取socket,socket斷開連接時,將socket初始化後返還對象池,這樣可以避免頻繁申請和釋放socket的操作。。。不知道這樣是否可以……110樓 liu_cheng_ran 2012-04-20 09:10發表 [回覆] [引用] [舉報]贊一個!!109樓 yangdm0209 2012-04-19 14:41發表 [回覆] [引用] [舉報]樓主,強大,膜拜……
話說send部分的代碼什麼時候能給貼出來啊,急用啊,可以先把代碼共享出來,詳解再後期補上啊。!108樓 sergery 2012-04-07 23:09發表 [回覆] [引用] [舉報]謝謝樓主的分享!107樓 g6785654 2012-04-07 08:35發表 [回覆] [引用] [舉報]漂亮,火力全開支持LZ106樓 qu_tao 2012-04-04 23:10發表 [回覆] [引用] [舉報]建議樓主搞個視頻,這樣可以省去打字的麻煩,膜拜105樓 c_s_d_n_xieloumima 2012-04-04 00:54發表 [回覆] [引用] [舉報]那個硬件截圖是魯大師的?話說魯大師是360的東西啊。遠離360珍愛生命吧104樓 tony21st2 2012-03-24 02:08發表 [回覆] [引用] [舉報]引用“tony21st2”的評論:[quote=tony21st2]不錯,不過,感覺其中有一步畫蛇添足了,退出的時候,就用ShutDo...
如果 GetQueuedCompletionStatus的等待參數爲0,或者很小的時間如10ms,是不是可以考慮不要用EXIT_CODE,因爲我幫助裏面說If * lpOverlapped is NULL and the function does not dequeue a completion packet from the completion port, the return value is zero. The function does not store information in the variables pointed to by the lpNumberOfBytes and lpCompletionKey parameters.如果是這樣的情況,如果不store information to lpComletionKey的話,它的值 就是初始化的NULL,和EXIT_CODE的NULL是無法區分的。總之我覺得這個EXIT_CODE在這裏有點彆扭,我們可以令GetQueuedCompletionStatus不做等待(或者等待很小時間),這樣,就可以只需要ShutDownEvent變爲信號態而退出工作循環。樓主以爲如何?103樓 tony21st2 2012-03-24 00:35發表 [回覆] [引用] [舉報]引用“tony21st2”的評論:不錯,不過,感覺其中有一步畫蛇添足了,退出的時候,就用ShutDownEvent置爲信號態就ok了,...
抱歉,我的錯誤,GetQueuedCompletionStatus 也是一個同步函數,確實需要有完成操作才能返回。102樓 tony21st2 2012-03-24 00:26發表 [回覆] [引用] [舉報]不錯,不過,感覺其中有一步畫蛇添足了,退出的時候,就用ShutDownEvent置爲信號態就ok了,所有的工作者線程就會退出工作者線程的工作循環,完全不用發什麼EXIT_CODE這個消息了101樓 just_li_ke 2012-03-20 10:45發表 [回覆] [引用] [舉報]謝謝博主介紹的這麼詳細!
但是遺憾的是,代碼部分沒有 發送數據 的功能。
我感到 完成端口 模型中,發送數據部分也有些複雜,希望博主可以貼出來 這部分的代碼。萬分感謝。Re: PiggyXP 2012-03-20 20:29發表 [回覆] [引用] [舉報]回覆just_li_ke:嗯,我也在考慮等有空的時候把發送的代碼加進去100樓 yinor 2012-03-16 10:50發表 [回覆] [引用] [舉報]if((0 == dwBytesTransfered) ( RECV_POSTED==pIoContext->m_OpType || SEND_POSTED==pIoContext->m_OpType))
{
pIOCPModel->_ShowMessage( _T("客戶端 %s:%d 斷開連接."),inet_ntoa(pSocketContext->m_ClientAddr.sin_addr), ntohs(pSocketContext->m_ClientAddr.sin_port) );
工作線程中判斷客戶斷開時,pSocketContext->m_ClientAddr
這個客戶端地址是怎麼得到的99樓 greenheaven 2012-03-08 15:39發表 [回覆] [引用] [舉報]lz很有才,有個問題,就是怎麼樣用Acceptor實現長連接,我在轉發程序中用acceptor監聽端口,但總是只能在client端第三次發數據時才能收到數據,第一次發的時候,什麼也沒有,第二次發的時候就建立連接,第三次發的時候纔有數據顯示出來。98樓 jw5783078 2012-03-08 15:16發表 [回覆] [引用] [舉報]~_PER_IO_CONTEXT()
{
if( m_sockOperate!=INVALID_SOCKET )
{
closesocket(m_sockOperate);
m_sockOperate = INVALID_SOCKET;
}
}
這個 析構的時候
不應該 關閉socket啊Re: PiggyXP 2012-03-08 21:32發表 [回覆] [引用] [舉報]回覆jw5783078:這裏調用closesocket就是爲了讓在裏面阻塞wait的那些socket都斷開連接呀,要不還得等待那些socket超時退出,整個程序就沒法正常退出了,這樣是個既簡便又安全的做法97樓 jinxiuliwen99 2012-03-05 06:40發表 [回覆] [引用] [舉報]太詳細太全面了!!96樓 xzh5508 2012-03-02 11:55發表 [回覆] [引用] [舉報]膜拜高人!95樓 hsc456 2012-03-01 10:58發表 [回覆] [引用] [舉報]再次拜讀, 收穫頗多, 拜謝!!!!94樓 p_wbc 2012-02-09 14:44發表 [回覆] [引用] [舉報]樓主,workerthread分支send部分的代碼,能不能發我一份,正好用到。謝謝,小豬!
[email protected]: p_wbc 2012-02-09 14:53發表 [回覆] [引用] [舉報]回覆p_wbc:以前用c#的,剛剛開始學習vc,謝謝啊93樓 forstef001 2012-02-08 16:01發表 [回覆] [引用] [舉報]幽默,易懂。。。。92樓 zadile 2012-01-25 22:34發表 [回覆] [引用] [舉報]寫的挺詳細的
不過CONTAINING_RECORD 宏是不需要Overlapped變量定義爲結構體中的第一個成員。
#define CONTAINING_RECORD(address, type, field) ((type *)( \
(PCHAR)(address) - \
(ULONG_PTR)(((type *)0)->field)))Re: PiggyXP 2012-03-08 21:33發表 [回覆] [引用] [舉報]回覆zadile:噢,多謝指正!91樓 baiyanshuai444 2012-01-19 22:40發表 [回覆] [引用] [舉報]神作!!!!!90樓 ygd510180 2012-01-08 12:57發表 [回覆] [引用] [舉報]頂了lz,WSASend補充一下更好了就89樓 zhaochunhuiguoerwei 2011-12-28 16:35發表 [回覆] [引用] [舉報]剛好在學習,樓主好厲害!88樓 nickwu1220 2011-12-16 16:51發表 [回覆] [引用] [舉報]感謝樓主的無私!!!87樓 anshanliuhao 2011-12-08 17:44發表 [回覆] [引用] [舉報]我了個去,看得我頭昏眼花啊,皓哥果然牛X啊,要是能給我單獨講兩天我就超神了吧 哈哈86樓 iandy2233 2011-12-07 16:08發表 [回覆] [引用] [舉報]引用“wjb_yd”的評論:爲什麼最後要通過SetEvent通知線程退出呢?
假設你有N個work線程,你post了N個退出通知...
我覺的這個也有問題,不會出現Lz說的那種情況,請LZ個解釋一下,還有一個問題:sizeof(SOCKADDR_IN) 16的 16,那個16是什麼意思,我一直沒查到,謝謝了85樓 Yang_JinBin 2011-12-04 13:36發表 [回覆] [引用] [舉報]學習了,受益了,感謝了……84樓 shunvxiao 2011-12-01 06:36發表 [回覆] [引用] [舉報]噢噢噢噢噢~拉啊啊啊鼓掌~~~~鼓掌~~~~~~~~~
樓主有沒有女朋友啊? 求包養!!!!!!83樓 lironghua2012 2011-11-29 18:05發表 [回覆] [引用] [舉報]樓主,我測了一下,當客戶端併發連接線程是3000的時候,DEBUG版的客戶端和服務端崩潰了。
測試環境:
WINXP SP3
CPU:賽揚430 單核@1.8G
如果想併發線程達到3000,可以做何改進?Re: PiggyXP 2011-12-02 08:11發表 [回覆] [引用] [舉報]回覆lironghua2012:你觀察一下是不是因爲分頁內存不足了?你是多大的內存?82樓 lsxsxs 2011-11-29 09:29發表 [回覆] [引用] [舉報]樓主,我想請教一下,如果我想在一個連接剛連接上的時候就收到它連接的事件應該怎麼辦,例子中的情況必須要客戶端發送了信息纔會和連接信息一起收到。如果連接了不發信息,就等於是一直停在那裏了Re: PiggyXP 2011-12-02 07:49發表 [回覆] [引用] [舉報]回覆lsxsxs:把 AcceptEx 中的 dwReceiveDataLength 參數設置爲0就可以啦81樓 honeybees 2011-11-28 19:07發表 [回覆] [引用] [舉報]相當好的文章,收藏了。
向LZ提個小問題:_DeInitialize()中,RELEASE(m_phWorkerThreads);內存泄露了。應該是delete [] m_phWorkerThreads;
呵呵~~~Re: PiggyXP 2011-12-02 08:10發表 [回覆] [引用] [舉報]回覆honeybees:VC裏對於簡單數據類型,由於對象沒有destructor,用delete 和delete [] 是一樣的呀!可以看下VC安裝目錄下CRT\SRC\DBGDEL.cpp,就是作爲一樣的處理的。80樓 gaotianze1989 2011-11-27 08:21發表 [回覆] [引用] [舉報]我是特地趕來膜拜博主的79樓 PiggyXP 2011-11-24 09:10發表 [回覆] [引用] [舉報]回覆mars0072:嗯,這個問題是我當初理解的有問題,我已經修正了啊,文中還有哪裏沒有修改過來的嗎?78樓 lsxsxs 2011-11-23 17:08發表 [回覆] [引用] [舉報]寫得太好了,就是代碼每次一點下載就說服務器忙,下不了,能不能把代碼發下郵箱啊,謝謝了,大神。
[email protected]樓 hongshunyanyan 2011-11-23 07:54發表 [回覆] [引用] [舉報]配得上“詳解”這個名字,太強了,收藏76樓 haoliull 2011-11-22 08:16發表 [回覆] [引用] [舉報]這是目前爲止能找到的最全面的完成端口的資料了,感激涕零啊!!拜讀了!!!75樓 tianhoo 2011-11-19 01:23發表 [回覆] [引用] [舉報]IOcontext 的析構函數爲何要close Socket74樓 kiven2010 2011-11-14 13:52發表 [回覆] [引用] [舉報]想問下樓主瞭解BindIoCompletionCallback這個方式創建的IOCP嗎?我用這個方法寫了個服務端,用樓主的客戶端進行測試,結果cpu的使用差不多,甚至要低些,我想問下這兩種方式的差別,因爲我也不是太懂IOCP73樓 whypcgames 2011-11-14 10:51發表 [回覆] [引用] [舉報]樓主是來曬配置的。。。72樓 weeksun23 2011-11-14 09:12發表 [回覆] [引用] [舉報]請問有C#版的完成端口嗎?71樓 diyigehaoren 2011-11-10 20:54發表 [回覆] [引用] [舉報]話說我剛準備學網絡,用了一個小時看了lz的文章,感覺很強大,很感謝,入門啓蒙啊……70樓 sky72244 2011-11-10 17:51發表 [回覆] [引用] [舉報]全方位各角度多立場的崇拜樓主!!!
新手級別的葵花寶典啊!69樓 Shawge 2011-11-10 11:37發表 [回覆] [引用] [舉報]感謝樓主,非常好的文章啊,不容易,先前GOOGLE IOCP根本沒什麼有用的中文信息。68樓 wjb_yd 2011-11-09 17:22發表 [回覆] [引用] [舉報]爲什麼最後要通過SetEvent通知線程退出呢?
假設你有N個work線程,你post了N個退出通知(即completionKey,bytesTransffered,overlap都是0)。
那麼每一個線程在GetQueuedCompletionStatus之後,如果發現completionKey,bytesTransffered,overlap都是0之後,就直接return掉了,也不會繼續多餘地GetQueuedCompletionStatus了。
這樣是可以保證每個線程都收到一次退出通知的。Re: jiahui123456 2012-02-17 15:33發表 [回覆] [引用] [舉報]回覆wjb_yd:使用setEvent是爲了保證線程不在調用getQueueCompletionStatus67樓 haiphong 2011-11-08 18:11發表 [回覆] [引用] [舉報]用AcceptEx 有非法連接不發數據,把服務端的初始對象都佔用了, 那正常連接就必須等這些非法連接斷了纔可以連,那怎麼在服務端主動斷掉呢?66樓 fred_fu 2011-11-08 13:45發表 [回覆] [引用] [舉報]好文章,學習~~65樓 slackwater 2011-11-07 14:19發表 [回覆] [引用] [舉報]寫的很好啊,如果能夠在N年前看到這樣的文章就更好了64樓 bobar 2011-11-07 13:49發表 [回覆] [引用] [舉報]很好 很清晰啊 等下去看看源代碼 實踐下 多謝了63樓 hsc456 2011-11-07 09:48發表 [回覆] [引用] [舉報]已經下載了客戶端,正在仔細學習,確實寫得不錯,贊一個,呵呵……收穫頗多,發現原來自己一些沒注意的細節,
樓主的精神可嘉,佩服……以後經常關注樓主博客,向樓主學習,呵呵……62樓 hsc456 2011-11-07 09:41發表 [回覆] [引用] [舉報]早上起來一看,哇,好東西,看見樓主的客戶端代碼了,正在仔細學習,確實不錯,呵呵……比我上次寫得強多了,汗……
樓主精神槓槓滴,謝謝……以後經常關注樓主,多多交流61樓 forever_crying 2011-11-06 22:51發表 [回覆] [引用] [舉報]你的流程圖畫的好漂亮,可以告訴我是什麼軟件畫出來的麼60樓 e513479333 2011-11-06 18:16發表 [回覆] [引用] [舉報]CSDN上已經下不到 《完成端口詳解》配套代碼了,希望樓主能在補上一份,謝謝.....59樓 wxq1987525 2011-11-06 14:41發表 [回覆] [引用] [舉報]我也剛剛寫好了,給網絡遊戲服務器用的。58樓 xiabingliu 2011-11-06 14:02發表 [回覆] [引用] [舉報]樓主 我愛死你了 你太牛了 竟然講的這麼詳細 我(#‵′)KAO 超級佩服啊57樓 abc987200073 2011-11-06 11:15發表 [回覆] [引用] [舉報]win32的代碼看不慣,哈哈。。56樓 mxzy55560593 2011-11-06 10:52發表 [回覆] [引用] [舉報]我們只是發送了m_nThreads次,我們如何能確保每一個Worker線程正好就收到一個,然後所有的線程都正好退出呢?是的,我們沒有辦法保證,所以很有可能一個Worker線程處理完一個完成請求之後,發生了某些事情,結果又再次去循環接收下一個完成請求了,這樣就會造成有的Worker線程沒有辦法接收到我們發出的退出通知。
好像有問題,你投遞m_nThread個退出,一個線程收到一次,就會退出循環,不會再次收到第二次,所以這裏好像沒必要用什麼同步機制
核心編程我也看,書上是說投遞事件但線程不退出循環會出問題,不知道是不是作者理解成這個了...55樓 lzklizhongkai 2011-11-06 10:50發表 [回覆] [引用] [舉報]文章有點長,不過還是值得學習!講的很詳細,也很透徹。感謝樓主分享!頂!54樓 pker911 2011-11-06 00:51發表 [回覆] [引用] [舉報]補充一下:極限測試的目的在於指定時間內,測試服務端高吞吐量的併發壓力。
內存使用高沒關係,因爲硬件可以加;
CPU使用高也沒關係,因爲硬件可以換;
非分頁內存不夠怎麼辦?重新設計IOCP收發機制吧!
網上衆多直接WSASend的echo例子,沒做WSARecv單投遞 WSASend發送緩衝的,基本不符合極限測試的要求。不管客戶端數據來的多頻繁,你直接WSASend合適嗎?可以自己仔細想想。x86 OS的nopage pool不是給你的服務端一個人用的。當然了,x64可以無視。53樓 luo3532861 2011-11-05 23:45發表 [回覆] [引用] [舉報]爲了樓主的毅力和學習精神,頂一個~繼續加油52樓 jAmEs_ 2011-11-05 23:03發表 [回覆] [引用] [舉報]寫的真是詳細。。。51樓 w156445045 2011-11-05 21:34發表 [回覆] [引用] [舉報]我是學習Java的,這些原理應該都是差不多的吧~50樓 fdoyy 2011-11-04 23:45發表 [回覆] [引用] [舉報]樓主好文,讓我這種初學者能夠更容易地開始學習IOCP。49樓 tan625747 2011-11-04 20:47發表 [回覆] [引用] [舉報]// 這裏需要特別注意,如果要使用重疊I/O的話,這裏必須要使用WSASocket來初始化Socket
// 注意裏面有個WSA_FLAG_OVERLAPPED參數
SOCKET m_sockListen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
特此來更正,如果是在客戶端用iocp ,不是必需的Re: PiggyXP 2011-11-05 18:44發表 [回覆] [引用] [舉報]回覆tan625747:OK,修正到文章中了,多謝指正。48樓 emptyness 2011-11-04 15:14發表 [回覆] [引用] [舉報]Nice article! 非常感謝.
API介紹的很詳細.
測試的說明不是很好. 一臺機器再怎麼開線程對於服務器來說也是毫無考驗的,線程開的越多越差. (原因文中也很清楚了,一臺主機自身,CPU調度線程的原因,IO能力上限的原因,) 壓力測試肯定是需要n多臺主機進行測試的.所以我們個人做的測試一般來說是不具有任何說服力的.我們僅能從代碼設計上做到最好.Re: pker911 2011-11-06 00:40發表 [回覆] [引用] [舉報]回覆emptyness:如果一臺測試機器都通不過,還想N臺出測試嗎?
你沒做過壓力測試吧?
1)如果開一臺測試機器做極限測試,服務端IOCP管理不好非分頁內存,尤其是對WSARecv/WSASend沒有很好的限制(隊列緩衝 Qos),都不要說10秒,Server就會Crash,可惜LZ的客戶端編寫方法並不符合極限測試的條件,我指的是在for循環中sleep;
2)至於說LZ服務端的例子,LZ自己也說了“如果各位真的是要正兒八經寫一個承受很大訪問壓力的Server的話,你慢慢就會發現,只用我附帶的這個示例代碼是不夠的,還得需要在很多細節之處進行改進,例如用更好的數據結構來管理上下文數據,並且需要非常完善的異常處理機制等等,總之,非常期待大家的批評和指正。”47樓 wemvyen 2011-11-04 14:13發表 [回覆] [引用] [舉報]樓主厲害, 分4次終於看完了, 不過顏色看的我眼睛不舒服,哈哈哈哈46樓 wjx_0_2001 2011-11-04 12:17發表 [回覆] [引用] [舉報]請教下樓主,如果這個服務在win 2003 server 下 會怎麼樣?
聽說win 2003 對IOCP不是很支持??
那到底win 2003 server 對IOCP支持的怎麼樣?
還有除了win98外,是不是其他的所有的win系統 都對IOCP支持呢??45樓 tpnndhqc 2011-11-04 10:06發表 [回覆] [引用] [舉報]收藏學習~
剛開始學Windows 編程連Socket和多線程都還沒學會的飄過44樓 wzg_j 2011-11-04 03:15發表 [回覆] [引用] [舉報]看完.....我這樣的新手錶示無比膜拜........43樓 hxembed 2011-11-04 00:36發表 [回覆] [引用] [舉報]很好,收藏了,有時間詳細看下,42樓 pker911 2011-11-04 00:11發表 [回覆] [引用] [舉報]測試IOCP服務端併發連接和收發數據,客戶端應該先創建並休眠一批工作線程。等你正在點擊測試的時候,再全部喚醒工作線程,每個工作線程負責建立與IOCP服務端的連接,連接建立後就循環發送數據,直到連接斷開或你手動停止。
這樣才能真正測試你服務端在同一時刻的併發連接與收發壓力,否則按LZ這種壓力測試,任何一個IOCP輕鬆“併發”10W跟玩似的。Re: dfasri 2011-11-05 22:52發表 [回覆] [引用] [舉報]回覆pker911:同感, 強烈要求真正的高壓測試, 不用多, 幾個客戶端不停的發數據, echo回來, 就這樣一個流程就夠了, 以8K的數據, 1K根本不能作爲代表...1K的都是自我欺騙的. 裏面完全把TCP變成UDP用了.41樓 shellching 2011-11-03 17:32發表 [回覆] [引用] [舉報]好文章,收藏了40樓 hatekiky2007 2011-11-03 14:46發表 [回覆] [引用] [舉報]作者寫的:“什麼?超過這個上限? No,no,無論你換成多麼強大的Server,也不可能超過65535的上限的“
這個限制是完全不存在的,建議作者加強底層原理的學習。Re: PiggyXP 2011-11-03 20:20發表 [回覆] [引用] [舉報]回覆hatekiky2007:嗯嗯,我也發現這個問題了,這裏我已經進行了修正,多謝指點!39樓 iiio_oiii 2011-11-03 13:43發表 [回覆] [引用] [舉報]此文只看不頂者,你們都是怎麼想的!
太好的文章了,樓主瞭解的很深啊,學習了38樓 brantyou 2011-11-03 13:20發表 [回覆] [引用] [舉報]非常不錯,支持一個,同時也常來關注37樓 zxj382411196 2011-11-03 10:39發表 [回覆] [引用] [舉報]最大併發連接數,跟端口沒有關係的,10W的併發連接也不是沒有可能, 非要說最大連接數與啥有關,我覺得跟你server端pc機的網卡的存儲芯片的容量有關36樓 jianhuxiaoming 2011-11-03 10:09發表 [回覆] [引用] [舉報]問一個問題:現在的有些CPU不是標稱雙核但卻支持四線程。。是不是他能同時運行四個線程。。
那IOCP的線程數是不是要增加。。35樓 wildwild1 2011-11-03 08:53發表 [回覆] [引用] [舉報]恩,學到了點,完成端口以前看核心編程沒感覺,真要到自己寫c/s模型的時候才知道哪些東西需要考慮34樓 liuchen1206 2011-11-02 23:20發表 [回覆] [引用] [舉報]學習一下!~~~33樓 fengge8ylf 2011-11-02 21:46發表 [回覆] [引用] [舉報]send的時候是怎樣send的 調用完wsasend後內存是否可以立刻釋放掉,還有我必須等上一個wsasend完成後才能再次wsasend嗎?32樓 chenhuilong43 2011-11-02 20:28發表 [回覆] [引用] [舉報]大俠,代碼裏面有了Recv數據,那Send數據怎麼處理呢。菜鳥,求理解,求解釋。31樓 ztsdk 2011-11-02 19:52發表 [回覆] [引用] [舉報]那個65000上限應該是有點問題。
併發連接和 端口數沒關係;
accept返回的socket 端口、地址與監聽套接字一致;
另外,ACCEPTEX之後,不還是要重新創建一個SOCKET,然後再投遞accept請求。沒明白開銷省在哪了,只是將創建開銷從ACCEPTEX移到了別處。Re: PiggyXP 2011-11-02 21:16發表 [回覆] [引用] [舉報]回覆ztsdk:對,就是把創建SOCKET的開銷移到了server啓動的時候,這就減少了很多壓力了,創建一個SOCKET的開銷蠻大的,而且可以利用完成端口異步的來處理accept。那個上限的問題可能真的是我理解錯了,我再修改下,那究竟應該上限是多少呢?Re: zhuyie 2011-11-03 09:41發表 [回覆] [引用] [舉報]回覆PiggyXP:一個TCP連接是用LocalIP:LocalPortRemoteIP:RemotePort四元組表示的,四元組中的任何一部分不同就是兩個不同的連接。就服務器端所能accept的連接數限制來說,假定你在80端口上監聽,所accept出來的新socket的LocalPort也是80而非隨機新選擇一個端口。因此即使你accept了10W個連接,這10W個連接的LocalPort都是80,故你所謂的unsigned short導致65536個限制是不存在的。30樓 fadian258 2011-11-02 19:20發表 [回覆] [引用] [舉報]總而言之,對於普通的PC機來講,我們只有使用AcceptEx,纔可以使我們的併發處理的連接請求達到單主機65000左右的上限。
什麼?超過這個上限? No,no,無論你換成多麼強大的Server,也不可能超過65535的上限的,因爲各位可以去看下錶示地址信息的SOCKADDR_IN www.fadian123.com的定義,對於端口的部分,使用的是unsigned short,這也就意味着端口編號的上限就是65535了,另外系統自己的各種服務還會佔用一些端口,所以65000基本就是極限值了。
這裏應該是個小錯誤, server能接受的連接數, 並不由port來決定的, 是由客戶端的ip和port共同決定的. client連接進來, server並不消耗port.
理論值應該是2**16*2**32=2**48個連接, 但是受限制於socket描述符只是一個int整數, 所以實際最大連接數至多也是2**32.Re: myqq1060151476 昨天 10:48發表 [回覆] [引用] [舉報]29樓 yyunffu 2011-11-02 17:55發表 [回覆] [引用] [舉報]好文,希望客戶端代碼也弄上來。Re: PiggyXP 2011-11-02 21:26發表 [回覆] [引用] [舉報]回覆yyunffu:客戶端代碼我已經放上去了呵呵,但是本來沒準備公開,隨手亂寫的,寫得不好,見諒了...28樓 ycf8788 2011-11-02 15:57發表 [回覆] [引用] [舉報]牛逼。。。謝謝樓主的分享,真不容易27樓 MichealTX 2011-11-02 15:28發表 [回覆] [引用] [舉報]看了將近一天,看得我崩潰了。對我來說,內容前後對應的不夠好,不夠詳細。26樓 wzhj717 2011-11-02 14:38發表 [回覆] [引用] [舉報]好文章,收藏25樓 zhangzhangya 2011-11-02 14:35發表 [回覆] [引用] [舉報]16G24樓 hsc456 2011-11-02 14:18發表 [回覆] [引用] [舉報]以前有寫過一個IOCP,好像還不如樓主這個,慚愧....學習中
希望能看看客戶端代碼, 郵箱:[email protected]
希望樓主完全開源,爲中國軟件事業做貢獻,呵呵......Re: PiggyXP 2011-11-02 21:26發表 [回覆] [引用] [舉報]回覆hsc456:客戶端代碼我已經放上去了呵呵,但是本來沒準備公開,隨手亂寫的,寫得不好,見諒了...Re: MichealTX 2011-11-02 15:30發表 [回覆] [引用] [舉報]回覆hsc456:我也很想看看那個客戶端怎麼寫的。樓主貢獻出來吧。爲什麼我運行那個客戶端說我缺少mfc100d.dll呢?23樓 honeybees 2011-11-02 12:55發表 [回覆] [引用] [舉報]好文章,收藏慢慢體會。22樓 lanzhengpeng2 2011-11-02 11:45發表 [回覆] [引用] [舉報]另外,數據量少,壓力測試也不明顯.
我這面情況是這樣的:
在局域網測試,很穩定,但坑爹的是每次數據量上去了後,路由器掛了.說了N次換路由,無果.
在外網測試,數據量大了後,一開始因爲緩存設置的小,64K每個連接,導致頻繁斷開.後來怒了,改成1M的緩存後,很快OOM,服務器掛了.要求換64位系統,無果
所以,究竟怎樣,不是很清楚.但處理5000~8000個連接很輕鬆Re: PiggyXP 2011-11-02 21:32發表 [回覆] [引用] [舉報]回覆lanzhengpeng2:有條件的話,我也在外網測試下呵呵,主要外網測試的話,受到網絡本身性能的影響就很大了吧21樓 lanzhengpeng2 2011-11-02 11:40發表 [回覆] [引用] [舉報]我對你的電腦配置感興趣.
你這個配置嘛,硬盤是硬傷.
我這面的電腦配置跟你差不多,聲卡用的主板自帶的,顯卡是570.硬盤是日立的,3塊組raid.內存4Gx2,改天再整兩根.其餘的跟你的一樣一樣.Re: PiggyXP 2011-11-02 21:33發表 [回覆] [引用] [舉報]回覆lanzhengpeng2:我裝了一塊Intel的SSD,但是不知道爲什麼檢測結果顯示的是另一塊硬盤=。=....20樓 edward22 2011-11-02 10:48發表 [回覆] [引用] [舉報]引用“edward22”的評論:這我在網上查到的有史以來最完整的IOCP介紹!
請問發送數據如何處理?19樓 edward22 2011-11-02 09:46發表 [回覆] [引用] [舉報]這我在網上查到的有史以來最完整的IOCP介紹!Re: hatekiky2007 2011-11-03 14:51發表 [回覆] [引用] [舉報]回覆edward22:你可以看看這個,其實樓主的iocp還有很多要注意的問題沒提到
http://www.codeproject.com/KB/IP/iocp_server_client.aspx18樓 dfasri 2011-11-02 09:17發表 [回覆] [引用] [舉報]樓主是否有對IOCP進行壓力測試? 假如以16B長度的字符串, 不斷的調用send, 每8K(即512個字符串)爲一個處理單位, IOCP服務器每收到8K滿後, 把這8K的數據傳送給客戶端, 客戶端等待回覆後繼續一下次.
這樣的echo壓力測試, IOCP在你的機器上能跑多少? 只要求單機即可....17樓 hello29 2011-11-02 08:46發表 [回覆] [引用] [舉報]學習了,寫的很詳細。謝謝樓主分享!16樓 wanghq12345 2011-11-02 08:40發表 [回覆] [引用] [舉報]vb15樓 wanghq12345 2011-11-02 08:40發表 [回覆] [引用] [舉報]好14樓 netchick 2011-11-01 22:56發表 [回覆] [引用] [舉報]寫的很好啊,真的很不錯13樓 netchick 2011-11-01 22:53發表 [回覆] [引用] [舉報]寫的很好12樓 ywd_bill 2011-11-01 22:42發表 [回覆] [引用] [舉報]請問樓主,完成端口的工作線程需要同步嗎????11樓 ywd_bill 2011-11-01 22:42發表 [回覆] [引用] [舉報]最近剛好在研究iocp,碰巧看到這篇文章,一看還是今天剛發表的。講的不錯。我也總結了一個文檔,但沒有樓主全面透徹。樓主還有3萬個客戶端的實驗。10樓 lusunshan 2011-11-01 17:34發表 [回覆] [引用] [舉報]來頂····有時間好好看看····嗬嗬·····Re: PiggyXP 2011-11-01 19:16發表 [回覆] [引用] [舉報]回覆lusunshan:我日,小祿,你咋發現的.....9樓 snowolf_538 2011-11-01 15:37發表 [回覆] [引用] [舉報]寫得不錯。
在一般情況下,我們需要且只需要建立這一個完成端口。
那麼,什麼情況需要建立多個完成端口?8樓 Bestsharp007 2011-11-01 15:28發表 [回覆] [引用] [舉報]最近開始要用到iocp,看了很多文章很多代碼都沒明白。
這篇文章,應該就是我要找的了,慢慢看文章看代碼。
非常謝謝樓主,膜拜7樓 fvhdst 2011-11-01 14:17發表 [回覆] [引用] [舉報]很好,不錯,謝謝!6樓 successgl 2011-11-01 11:42發表 [回覆] [引用] [舉報]寫的很好,很全!還有就是好長,看了好久O(∩_∩)O~
謝謝了!
提一個小建議:GetAcceptExSockAddrs函數的SOCKADDR_IN大小參數,需要 16即:sizeof(SOCKADDR_IN) 16,剛看的時候對我這樣的菜鳥確實不太明白,之後查了下API才搞明白。這可以適當補充一下!5樓 maoxing63570 2011-11-01 11:12發表 [回覆] [引用] [舉報]非常感謝分享,如果再附帶講下,爲什麼接收下來後,需要投遞一個WSARecv就更完美些了Re: hatekiky2007 2011-11-03 14:50發表 [回覆] [引用] [舉報]回覆maoxing63570:接收後不投遞recv,怎麼接收後面來的數據呢4樓 wgm001 2011-11-01 10:23發表 [回覆] [引用] [舉報]總而言之,對於普通的PC機來講,我們只有使用AcceptEx,纔可以使我們的併發處理的連接請求達到單主機65000左右的上限。
什麼?超過這個上限? No,no,無論你換成多麼強大的Server,也不可能超過65535的上限的,因爲各位可以去看下錶示地址信息的SOCKADDR_IN的定義,對於端口的部分,使用的是unsigned short,這也就意味着端口編號的上限就是65535了,另外系統自己的各種服務還會佔用一些端口,所以65000基本就是極限值了。
這裏應該是個小錯誤, server能接受的連接數, 並不由port來決定的, 是由客戶端的ip和port共同決定的. client連接進來, server並不消耗port.
理論值應該是2**16*2**32=2**48個連接, 但是受限制於socket描述符只是一個int整數, 所以實際最大連接數至多也是2**32.Re: myqq1060151476 2011-11-01 10:48發表 [回覆] [引用] [舉報]回覆wgm001:很好很強大。
連接數可以達到這麼多。但併發數是達不到的。1秒500左右的併發數據通信(數據量在512byte)。應該已經是極值了。Re: PiggyXP 2011-11-01 10:26發表 [回覆] [引用] [舉報]回覆wgm001:server爲什麼不消耗port呢?server會開一個新的端口和客戶端通信的,不是這樣嗎?Re: wgm001 2011-11-01 10:37發表 [回覆] [引用] [舉報]回覆PiggyXP:你可能是搞反了, client向同一server發起連接時, 會默認在本地開啓一個新的端口, 這以便於server發回數據時正確區分連接.
server和client建立新的連接, 是不需要開啓新的端口, 因爲它本身就可以通過client的ip:port很好區分連接了, server沒有任何理由再開啓一個新的端口.
具體詳細的理論得看
唉, 不說了.Re: successgl 2011-11-01 11:48發表 [回覆] [引用] [舉報]回覆PiggyXP:server確實會開新端口與client進行通信的,但這是臨時端口,是server信息的“出”端口,有可能多次會是相同端口!併發連接數不清楚是否與臨時端口有關係?Re: PiggyXP 2011-11-01 12:52發表 [回覆] [引用] [舉報]回覆successgl:嗯,是的,這個端口是和客戶端建立連接的時候臨時開通的,但是在與客戶端通信期間,這個端口是一直存在的,並且是唯一的吧,只有socket端口之後,這個端口才能被別的連接使用的,是這樣麼?如果是這樣的話,那端口數肯定會決定併發連接的數量呀Re: successgl 2011-11-01 13:11發表 [回覆] [引用] [舉報]回覆PiggyXP:還有一點,不清楚不同的client socket連接,是否可以用相同的臨時端口發送信息,畢竟理論上本地IP,本地端口,遠方IP,遠方端口才唯一確定一條連接。自己試驗這種情況應該不會遇到,但在端口資源缺乏情況下,是否可能出現相同的臨時端口同時發送信息就不清楚了?3樓 luolin326027553 2011-11-01 09:32發表 [回覆] [引用] [舉報]很好啊2樓 akof1314 2011-11-01 09:23發表 [回覆] [引用] [舉報]相當詳解,收藏、感謝1樓 lrj2005 2011-11-01 09:05發表 [回覆] [引用] [舉報]這篇文章樓主足足醞釀了有2年啊,哈哈。。。。Re: ogred3d 2011-11-01 12:33發表 [回覆] [引用] [舉報]回覆lrj2005:確實,從2到3,等了兩年。