Winsock服务器设计的四个关键问题
6.2.1 接受連接的方法
Winsock擴展函數(shù)AcceptEx是唯一能夠使用重疊I/O接受客戶連接的函數(shù)。下面主要深入探討使用該函數(shù)接收連接的問題。
前面已經(jīng)討論過,當(dāng)客戶連接進來時,服務(wù)器需要創(chuàng)建一個套接字來負責(zé)維護與一個客戶端的會話。使用AcceptEx函數(shù)之前必須創(chuàng)建一些套接字,并且這些套接字必須是未綁定、未連接的,即使它們可能在調(diào)用TransmitFile, TransmitPackets, 或DisconnectEx后可以重用。
響應(yīng)服務(wù)器必須總是具有足夠的AcceptEx在站崗,以便在有客戶連接請求時調(diào)用。但是,并沒有具體的數(shù)量能夠保證服務(wù)器能夠立即響應(yīng)連接。我們知道在調(diào)用listen將監(jiān)聽套接字置于監(jiān)聽狀態(tài)后,TCP/IP堆棧會自動接受到來的連接,直到達到listen的backlog參數(shù)設(shè)定的限制。對于Windows NT服務(wù)器而言,支持的backlog的最大值為200。如果服務(wù)器投遞了15個AcceptEx調(diào)用,然后突然有50個客戶請求連接服務(wù)器,它們的連接請求都不會遭到拒絕。服務(wù)器投遞的AcceptEx I/O會滿足前面的15個連接,剩下的35個連接都被系統(tǒng)默認連接了。檢查一下backlog的值發(fā)現(xiàn),系統(tǒng)還有能力默認接受165個連接。之后,如果服務(wù)器投遞AcceptEx調(diào)用,它們會立即成功返回,因為系統(tǒng)會將默認接收的連接放入“等待連接隊列”中。
服務(wù)器的特性是決定要投遞多少個AcceptEx操作的重要因素。例如,希望處理大量短時間即時連接的客戶要比處理少量長時間連接的客戶投遞更多的AcceptEx I/O。一個好的策略是允許AcceptEx的調(diào)用數(shù)量在最小值和最大值之間變化。具體做法是,應(yīng)用程序跟蹤未決的AcceptEx I/O的數(shù)量,當(dāng)一個或多個I/O完成使這個未決I/O數(shù)量變得比最小值還小時,就再投遞額外的AcceptEx I/O。
在Windows 2000和以后的Windows操作系統(tǒng)版本中,Winsock提供了一種機制,用來確定應(yīng)用程序是否投遞了足夠的AcceptEx調(diào)用。創(chuàng)建監(jiān)聽套接字時,使用WSAEventSelect函數(shù)為監(jiān)聽套接字關(guān)聯(lián)一個事件對象,注冊FD_ACCEPT事件。如果投遞的AcceptEx操作用完,但是仍有客戶請求接入(系統(tǒng)根據(jù)backlog值決定是否接受這些連接),事件對象就是受信,說明應(yīng)該投遞額外的AcceptEx操作了。這實際上還是利用事件對象來使調(diào)用線程處于一種“可警告狀態(tài)”,當(dāng)有客戶連接請求時,就根據(jù)當(dāng)前AcceptEx操作是否用完來警告(通知)是否需要投遞新的AcceptEx操作來處理新的客戶連接。
使用AcceptEx處理連接的另外一個功能就是在處理連接時還可以接收用戶發(fā)來的第一塊數(shù)據(jù)(前提是為AcceptEx提供了接收緩沖區(qū)),這對于那些請求連接的同時發(fā)送了一些數(shù)據(jù)過來的客戶來說很適用。但是,此時,除非接收連接的同時接收到了客戶發(fā)送過來的一些數(shù)據(jù),否則AcceptEx是不會返回的。
為了滿足客戶的需求,服務(wù)器不得不投遞更多的接受I/O,這會占用大量的系統(tǒng)資源。如果客戶僅調(diào)用connect函數(shù)連接服務(wù)器,長時間既不發(fā)送數(shù)據(jù),也不關(guān)閉連接,就可能造成AcceptEx投遞的大量重疊I/O操作不能返回。這就是“惡意連接”。為此,服務(wù)器應(yīng)該記錄每個AcceptEx投遞的未決I/O,定時掃描它們,設(shè)置SO_CONNECT_TIME參數(shù)調(diào)用getsockopt檢查它們連接的時間,如果超時,就將連接關(guān)閉。如果使用WSAEventSelect模型來通知有連接事件,則當(dāng)事件受信時,是檢查客戶套接字(AcceptSocket)是否真正連接了。
每當(dāng)調(diào)用AcceptEx接受客戶端連接時,它也在等待接受客戶發(fā)送過來的第一個數(shù)據(jù)塊,這時不允許投遞另外一個AcceptEx。當(dāng)AcceptEx返回后,如果事件對象再次受信則表明有新的連接到來。需要注意的是,無論何時,千萬不要關(guān)閉一個調(diào)用AcceptEx還沒有返回的套接字(AcceptSocket),因為這會導(dǎo)致內(nèi)存泄露。因為從內(nèi)部執(zhí)行邏輯看,當(dāng)沒有連接的套接字句柄被關(guān)閉時,調(diào)用AcceptEx所涉及到的內(nèi)核模式的數(shù)據(jù)結(jié)構(gòu)并不會清除掉,直到有新的連接建立或者監(jiān)聽套接字被關(guān)閉。
盡管在一個等待完成通知的工作者線程中,投遞一個AcceptEx操作,看起來既簡單又合情合理,但是應(yīng)盡量避免這樣做,因為創(chuàng)建套接字還是很耗費資源的。另外,也不要在工作者線程中進行任何復(fù)雜的計算,以便處理器可以盡快的在接到完成通知后進行后續(xù)處理。創(chuàng)建套接字耗費資源的一個原因在于Winsock 2.0本身的架構(gòu)很復(fù)雜,成功地創(chuàng)建一個套接字可能需要調(diào)用很多內(nèi)核服務(wù)。因此,服務(wù)器應(yīng)該在單獨線程中創(chuàng)建套接字,投遞AcceptEx操作。當(dāng)調(diào)用線程投遞的AcceptEx重疊操作完成時,一個受信的事件將會通知處理線程。
6.2.2 數(shù)據(jù)傳輸問題
數(shù)據(jù)傳輸是通信程序執(zhí)行的核心操作。當(dāng)一個客戶與服務(wù)器建立連接后,它們的主要工作就是傳輸數(shù)據(jù),因為數(shù)據(jù)是信息的表示。由上一節(jié)幾種I/O模型的性能測試分析可知,當(dāng)連接數(shù)量很大時,數(shù)據(jù)吞吐量是一個重要的性能考核指標。
從性能角度考慮,所有的數(shù)據(jù)傳輸最好都應(yīng)采用重疊I/O處理。默認情況下,系統(tǒng)為每個socket分配一個的接受緩沖區(qū)和一個發(fā)送緩沖區(qū),用來緩存接收和發(fā)送的數(shù)據(jù)。但在重疊I/O中,這些緩沖區(qū)往往不用,可以傳遞參數(shù)SO_SNDBUF或SO_RCVBUF調(diào)用setsockopt,來將它們設(shè)置為0。
讓我們來看看,當(dāng)發(fā)送緩沖區(qū)沒有設(shè)置為0時,系統(tǒng)是怎么處理一個典型的send操作的。當(dāng)一個應(yīng)用程序調(diào)用send函數(shù)時,如果有充足的緩沖空間,需要發(fā)送的數(shù)據(jù)將被拷貝到套接字的發(fā)送緩沖區(qū),send函數(shù)立即成功返回,并且一個完成通知被拋出。另外一個方面,如果套接字的發(fā)送緩沖區(qū)已滿,則應(yīng)用程序提供的發(fā)送緩沖區(qū)被鎖定,再次對send函數(shù)的調(diào)用將會返回WSA_IO_PENDING錯誤。當(dāng)發(fā)送緩沖區(qū)中的數(shù)據(jù)被處理(例如,提交給傳輸層處理)時,Winsock實際上直接處理鎖定在緩沖區(qū)中的數(shù)據(jù),也即繞過套接字的發(fā)送緩沖區(qū),直接從應(yīng)用程序緩沖區(qū)中提交數(shù)據(jù)給傳輸層。
接收數(shù)據(jù)的情況恰好相反。當(dāng)一個重疊的receive請求拋出后,如果數(shù)據(jù)已經(jīng)接收成功,它會被緩存在套接字接收緩沖區(qū)。數(shù)據(jù)會拷貝到應(yīng)用程序緩沖區(qū)(直到飽和)。receive調(diào)用返回,并且一個完成通知被拋出。當(dāng)套接字緩沖區(qū)被設(shè)置為空時,如果調(diào)用重疊的receive操作將返回WSA_IO_PENDING錯誤。當(dāng)有數(shù)據(jù)到達時,它將繞過套接字緩沖區(qū)而直接被拷貝到應(yīng)用程序緩沖區(qū)。
設(shè)置單套接字緩沖區(qū)為0,并不能提高性能,因為只要一直有大量的重疊接發(fā)請求被拋出,就不會有額外的內(nèi)存拷貝。設(shè)置套接字發(fā)送緩沖區(qū)為空比設(shè)置套接字接收緩沖區(qū)為空對系統(tǒng)的性能影響要小。因為應(yīng)用程序的發(fā)送緩沖區(qū)會被經(jīng)常鎖定直到它被提交給傳輸層處理。然而,若將接收緩沖區(qū)設(shè)置為0,并且沒有重疊的receive調(diào)用,任何傳進來的數(shù)據(jù)只能緩存在傳輸層。傳輸層驅(qū)動程序只會緩存滑動窗口尺寸的數(shù)據(jù),即17KB—傳輸層可以分配的緩沖區(qū)大小的上限。實際的緩沖區(qū)要比17KB小。傳輸層緩沖區(qū)(針對一次連接)是在非分頁池之外分配的,這意味著,當(dāng)服務(wù)建立了1000個連接時,即使沒有拋出receive請求,非分頁池中也會分配17MB的內(nèi)存。而非分頁池是很珍貴的資源,除非服務(wù)器可以保證總是有接收請求拋出,否則套接字接收緩沖區(qū)應(yīng)該不需設(shè)置。
只有在一些特殊情況下,對套接字接收緩沖區(qū)不予設(shè)置將會導(dǎo)致性能降低。考慮服務(wù)器需要處理成千上萬個客戶連接,而每個連接上又都沒有投遞receive請求的情況,如果客戶端零星地發(fā)送數(shù)據(jù)過來,傳輸進來的數(shù)據(jù)將被緩存在套接字接收緩沖區(qū)中。當(dāng)服務(wù)器處理一個receive重疊I/O時,它會做一些不必要的工作。當(dāng)完成通知到達時,重疊操作會處理一個I/O請求包(IRP)。在這種情形下,服務(wù)器不能保留很多拋出的receive請求。因此,最好使用簡單的非阻塞接收函數(shù)。
6.3 內(nèi)存資源管理問題
由于機器硬件條件所限,系統(tǒng)資源是有限的,因此不得不考慮內(nèi)存資源的管理問題。從上一節(jié)對不同I/O模型進行的性能測試結(jié)果分析可知,維持大規(guī)模的通信連接,不僅會耗費掉大量內(nèi)存,而且對CPU的占用也是很高的。
對于配置比較高的服務(wù)器而言,處理成千上萬個連接并不成問題。但是隨著連接量的劇增,內(nèi)存資源的限制將逐漸凸現(xiàn)。最有可能遇到的兩個限制因素就是鎖定頁和非分頁池。鎖定頁的限制不是太嚴重,更應(yīng)該避免的是非分頁池被耗盡。每一次調(diào)用重疊的send或receive請求,提交的緩沖區(qū)都可能被鎖住。當(dāng)內(nèi)存被鎖定時,它就不能從物理內(nèi)存換出。操作系統(tǒng)對鎖定內(nèi)存的數(shù)量是有限制的,當(dāng)達到極限時,重疊操作將會返回WSAENOBUFS錯誤。如果服務(wù)器在每個連接上投遞多個重疊接收操作,隨著客戶連接數(shù)量的增多,極限就會達到。如果期望服務(wù)器能夠處理高并發(fā)通信,服務(wù)器可以在每個連接上投遞一個0字節(jié)的接受操作,這樣就不會有內(nèi)存鎖定。0字節(jié)的接受完成以后,服務(wù)器可以簡單地執(zhí)行一個非阻塞的接收函數(shù)來獲取緩存在套接字接收緩沖區(qū)中的所有數(shù)據(jù)。當(dāng)非阻塞接收調(diào)用返回WSAEWOULDBLOCK時,就表示不再有未決的數(shù)據(jù)了。這種方法非常適合用來設(shè)計那些希望通過犧牲每個套接字上的吞吐率來獲取更大規(guī)模并發(fā)連接的服務(wù)器。
當(dāng)然,最好還要了解客戶端與服務(wù)器通信的方式。在上面的例子中,當(dāng)0字節(jié)的接收完成后,再投遞一個異步接收操作,將接收到所有緩存在套接字接收緩沖區(qū)中的數(shù)據(jù)。如果服務(wù)器知道客戶端將會連續(xù)不斷發(fā)送數(shù)據(jù),那么當(dāng)0字節(jié)的接收完成后,假如客戶端將發(fā)送大數(shù)據(jù)塊(超過單套接字緩沖區(qū)8KB的容量)過來,服務(wù)器將拋出一個或多個重疊的接收操作。
另外一個需要重點考慮的問題就是系統(tǒng)所需頁的數(shù)量。當(dāng)系統(tǒng)鎖定傳遞給重疊操作的內(nèi)存時,它是在頁邊界上進行的。在x86體系結(jié)構(gòu)上,內(nèi)存頁的大小為4KB。如果一個操作投遞了1KB的緩沖區(qū),系統(tǒng)實際上會為它鎖定4KB大小的內(nèi)存塊。為避免這種浪費,重疊發(fā)送和接收緩沖區(qū)的大小應(yīng)該是頁大小的倍數(shù)。可以使用GetSystemInfo這個API來獲知當(dāng)前系統(tǒng)頁的大小。
如果突破非分頁池極限,將會導(dǎo)致更嚴重的錯誤,并且很難恢復(fù)。非分頁池是內(nèi)存的一部分,它常駐內(nèi)存,并且永遠不會被交換出去。內(nèi)核模式的系統(tǒng)組件,如驅(qū)動程序,通常使用非分頁池,其中包括Winsock和協(xié)議驅(qū)動程序,例如tcpip.sys。每個套接字的創(chuàng)建將消耗一小部分非分頁池,用于維持套接字狀態(tài)信息。當(dāng)套接字綁定到一個地址后,TCP/IP堆棧將分配額外的非分頁池來保存本地地址的信息。當(dāng)一個對等套接字接入后,TCP/IP堆棧也將分配部分非分頁池來保存遠程地址信息。基本上,一個建立連接的套接字占用2KB非分頁池內(nèi)存,而accept或AcceptEx返回的套接字則占用1.5KB非分頁池內(nèi)存。之所以出現(xiàn)這個區(qū)別,是因為服務(wù)器本地地址信息已經(jīng)存儲在監(jiān)聽套接字中,故accept或AcceptEx返回的套接字只需保存遠程主機地址信息。此外,每個在套接字上投遞的重疊操作都需要給I/O請求包(IRP)分配內(nèi)存,一個IRP使用大約500B非分頁池內(nèi)存。
從以上分析可以看出,為每個連接分配的非分頁池內(nèi)存并不是很大。然而,隨著客戶連接量逐增,服務(wù)器對非分頁池的使用將是非常大的。考慮運行在只有1GB物理內(nèi)存的Windows 2000或以后版本Windows系統(tǒng)上的服務(wù)器,將有256MB的內(nèi)存非配給非分頁池。通常,非分頁池大小是機器物理內(nèi)存的1/4,Windows 2000及以后版本的Windows系統(tǒng)上,非分頁池大小為256MB(/1GB),而Windows NT 4.0限制為128MB(1GB)。擁有256MB的非分頁池的服務(wù)器可以支持50,000或更大的連接量。但是必須限制重疊的accept數(shù)量,以及在已經(jīng)建立連接的重疊收發(fā)操作。在這個例子中,如果已經(jīng)建立連接的套接字,按每個1.5KB計算,將耗費75MB的非分頁池內(nèi)存。如果采用了上面提及的投遞0字節(jié)接收的方法,這樣為每個連接分配的IRP將占用25MB的非分頁池內(nèi)存。
如果系統(tǒng)耗盡了非分頁池,會有兩種可能的后果。在最好的情況下,Winsock調(diào)用將返回WSAENOBUFS錯誤。最糟糕的情況是系統(tǒng)崩潰,這種情況通常是系統(tǒng)沒能正確處理內(nèi)存非配的問題造成的。沒有一種可行的方案能夠恢復(fù)非分頁池耗盡的錯誤,并且也沒有可行的方案來監(jiān)視非分頁池可分配的大小,因為非分頁池耗盡導(dǎo)致系統(tǒng)崩潰。
由以上探討,可以得出結(jié)論,沒有一種方法可以確定服務(wù)器到底支持多大的并發(fā)連接和重疊操作,并且也不可能準確地獲知非分頁池是否耗盡或者鎖定內(nèi)存頁數(shù)超過極限。因為它們都將導(dǎo)致Winsock調(diào)用都返回相同的錯誤—WSAENOBUFS。因為以上因素,針對服務(wù)器的測試必須測試不同數(shù)量的連接情況以及重疊操作完成情況,以便在并發(fā)通信規(guī)模和數(shù)據(jù)吞吐率這兩個指標之間選擇一種折中的方案。如果在方案中強加限制,以防止服務(wù)器耗盡非分頁池,則返回WSAENOBUFS錯誤時,我們就知道是因為超過了鎖定頁的限制。并且可以以一種更優(yōu)化的處理方式編寫程序,如進一步限制一些待決的操作或關(guān)閉某些連接。
包重新排序問題
這個問題與伸縮性沒有多大關(guān)聯(lián),但是卻是實際通信中不得不考慮的一個問題,因為它涉及到能否正確通信的問題。
雖然使用完成端口的I/O操作總是會按照它們被提交的順序完成,但是線程調(diào)度問題可能會導(dǎo)致關(guān)聯(lián)到完成端口上的工作不能按正常順序完成。例如,有兩個I/O工作線程,應(yīng)該接收“字節(jié)塊1,字節(jié)塊2,字節(jié)塊3”,但是你可能以錯誤順序接收這3個字節(jié)塊:“字節(jié)塊2,字節(jié)塊1,字節(jié)塊3”。這也意味著在完成端口上投遞發(fā)送請求發(fā)送數(shù)據(jù)時,數(shù)據(jù)實際也會以錯誤順序被發(fā)送出去。
當(dāng)然,如果只使用一個工作線程,僅提交一個I/O調(diào)用,是不存在順序問題的。因為同一時刻,一個工作線程只能處理一個I/O操作。但是,這樣就沒有發(fā)揮出完成端口的真正優(yōu)點。
如第3章《自定義應(yīng)用層通信協(xié)議》所述,一個簡單的解決方法就是為每個封包添加一個協(xié)議頭。協(xié)議頭主要是一個封包的實際字節(jié)數(shù),如自定義Package包的第一個字段m_nCmdLen就是這個包占用的字節(jié)數(shù)。通信的接受方通過分析協(xié)議頭分析本次通信有多少數(shù)據(jù)要接收,然后繼續(xù)讀后面的數(shù)據(jù),直到一個封包被完整接收完才接收下一個封包。
當(dāng)服務(wù)器一次僅做一個異步調(diào)用時,上述封包協(xié)議頭的解決方案是很有效的。但是,如果要充分發(fā)揮IOCP服務(wù)器的潛力,肯定有多個未決的異步讀操作等待數(shù)據(jù)的到來。這意味著,多個一步操作不能按順序完成,未決讀I/O返回的字節(jié)流不能按順序處理,接收到的字節(jié)流可能組合成正確的封包,也有可能組合成錯誤的封包。因此,要解決這個問題,還必須為提交的讀I/O分配序列號。
?
說明:
本文主要譯自《Network programming for microsoft windows》一書的6.2節(jié)《可伸縮的服務(wù)器體系結(jié)構(gòu)》和6.3節(jié)《資源管理》。
其中包重新排序問題,參考王艷平著《Windows網(wǎng)絡(luò)與通信程序設(shè)計》4.3.4節(jié)《包重新排序問題》。
轉(zhuǎn)載于:https://www.cnblogs.com/duzouzhe/archive/2009/11/11/1601022.html
總結(jié)
以上是生活随笔為你收集整理的Winsock服务器设计的四个关键问题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 正尝试安装的adobe flash pl
- 下一篇: Windows窗体编程(二)