天天看點

WinSock伺服器設計的四個關鍵問題

轉自 http://blog.csdn.net/phunxm/article/details/5086967

6.2.1 接受連接配接的方法

Winsock擴充函數AcceptEx是唯一能夠使用重疊I/O接受客戶連接配接的函數。下面主要深入探讨使用該函數接收連接配接的問題。

前面已經讨論過,當客戶連接配接進來時,伺服器需要建立一個套接字來負責維護與一個用戶端的會話。使用AcceptEx函數之前必須建立一些套接字,并且這些套接字必須是未綁定、未連接配接的,即使它們可能在調用TransmitFile, TransmitPackets, 或DisconnectEx後可以重用。

響應伺服器必須總是具有足夠的AcceptEx在站崗,以便在有客戶連接配接請求時調用。但是,并沒有具體的數量能夠保證伺服器能夠立即響應連接配接。我們知道在調用listen将監聽套接字置于監聽狀态後,TCP/IP堆棧會自動接受到來的連接配接,直到達到listen的backlog參數設定的限制。對于Windows NT伺服器而言,支援的backlog的最大值為200。如果伺服器投遞了15個AcceptEx調用,然後突然有50個客戶請求連接配接伺服器,它們的連接配接請求都不會遭到拒絕。伺服器投遞的AcceptEx I/O會滿足前面的15個連接配接,剩下的35個連接配接都被系統預設連接配接了。檢查一下backlog的值發現,系統還有能力預設接受165個連接配接。之後,如果伺服器投遞AcceptEx調用,它們會立即成功傳回,因為系統會将預設接收的連接配接放入“等待連接配接隊列”中。

伺服器的特性是決定要投遞多少個AcceptEx操作的重要因素。例如,希望處理大量短時間即時連接配接的客戶要比處理少量長時間連接配接的客戶投遞更多的AcceptEx I/O。一個好的政策是允許AcceptEx的調用數量在最小值和最大值之間變化。具體做法是,應用程式跟蹤未決的AcceptEx I/O的數量,當一個或多個I/O完成使這個未決I/O數量變得比最小值還小時,就再投遞額外的AcceptEx I/O。

在Windows 2000和以後的Windows作業系統版本中,Winsock提供了一種機制,用來确定應用程式是否投遞了足夠的AcceptEx調用。建立監聽套接字時,使用WSAEventSelect函數為監聽套接字關聯一個事件對象,注冊FD_ACCEPT事件。如果投遞的AcceptEx操作用完,但是仍有客戶請求接入(系統根據backlog值決定是否接受這些連接配接),事件對象就是受信,說明應該投遞額外的AcceptEx操作了。這實際上還是利用事件對象來使調用線程處于一種“可警告狀态”,當有客戶連接配接請求時,就根據目前AcceptEx操作是否用完來警告(通知)是否需要投遞新的AcceptEx操作來處理新的客戶連接配接。

使用AcceptEx處理連接配接的另外一個功能就是在處理連接配接時還可以接收使用者發來的第一塊資料(前提是為AcceptEx提供了接收緩沖區),這對于那些請求連接配接的同時發送了一些資料過來的客戶來說很适用。但是,此時,除非接收連接配接的同時接收到了客戶發送過來的一些資料,否則AcceptEx是不會傳回的。

為了滿足客戶的需求,伺服器不得不投遞更多的接受I/O,這會占用大量的系統資源。如果客戶僅調用connect函數連接配接伺服器,長時間既不發送資料,也不關閉連接配接,就可能造成AcceptEx投遞的大量重疊I/O操作不能傳回。這就是“惡意連接配接”。為此,伺服器應該記錄每個AcceptEx投遞的未決I/O,定時掃描它們,設定SO_CONNECT_TIME參數調用getsockopt檢查它們連接配接的時間,如果逾時,就将連接配接關閉。如果使用WSAEventSelect模型來通知有連接配接事件,則當事件受信時,是檢查客戶套接字(AcceptSocket)是否真正連接配接了。

每當調用AcceptEx接受用戶端連接配接時,它也在等待接受客戶發送過來的第一個資料塊,這時不允許投遞另外一個AcceptEx。當AcceptEx傳回後,如果事件對象再次受信則表明有新的連接配接到來。需要注意的是,無論何時,千萬不要關閉一個調用AcceptEx還沒有傳回的套接字(AcceptSocket),因為這會導緻記憶體洩露。因為從内部執行邏輯看,當沒有連接配接的套接字句柄被關閉時,調用AcceptEx所涉及到的核心模式的資料結構并不會清除掉,直到有新的連接配接建立或者監聽套接字被關閉。

盡管在一個等待完成通知的工作者線程中,投遞一個AcceptEx操作,看起來既簡單又合情合理,但是應盡量避免這樣做,因為建立套接字還是很耗費資源的。另外,也不要在工作者線程中進行任何複雜的計算,以便處理器可以盡快的在接到完成通知後進行後續處理。建立套接字耗費資源的一個原因在于Winsock 2.0本身的架構很複雜,成功地建立一個套接字可能需要調用很多核心服務。是以,伺服器應該在單獨線程中建立套接字,投遞AcceptEx操作。當調用線程投遞的AcceptEx重疊操作完成時,一個受信的事件将會通知處理線程。

6.2.2 資料傳輸問題

資料傳輸是通信程式執行的核心操作。當一個客戶與伺服器建立連接配接後,它們的主要工作就是傳輸資料,因為資料是資訊的表示。由上一節幾種I/O模型的性能測試分析可知,當連接配接數量很大時,資料吞吐量是一個重要的性能考核名額。

從性能角度考慮,所有的資料傳輸最好都應采用重疊I/O處理。預設情況下,系統為每個socket配置設定一個的接受緩沖區和一個發送緩沖區,用來緩存接收和發送的資料。但在重疊I/O中,這些緩沖區往往不用,可以傳遞參數SO_SNDBUF或SO_RCVBUF調用setsockopt,來将它們設定為0。

讓我們來看看,當發送緩沖區沒有設定為0時,系統是怎麼處理一個典型的send操作的。當一個應用程式調用send函數時,如果有充足的緩沖空間,需要發送的資料将被拷貝到套接字的發送緩沖區,send函數立即成功傳回,并且一個完成通知被抛出。另外一個方面,如果套接字的發送緩沖區已滿,則應用程式提供的發送緩沖區被鎖定,再次對send函數的調用将會傳回WSA_IO_PENDING錯誤。當發送緩沖區中的資料被處理(例如,送出給傳輸層處理)時,Winsock實際上直接處理鎖定在緩沖區中的資料,也即繞過套接字的發送緩沖區,直接從應用程式緩沖區中送出資料給傳輸層。

接收資料的情況恰好相反。當一個重疊的receive請求抛出後,如果資料已經接收成功,它會被緩存在套接字接收緩沖區。資料會拷貝到應用程式緩沖區(直到飽和)。receive調用傳回,并且一個完成通知被抛出。當套接字緩沖區被設定為空時,如果調用重疊的receive操作将傳回WSA_IO_PENDING錯誤。當有資料到達時,它将繞過套接字緩沖區而直接被拷貝到應用程式緩沖區。

設定單套接字緩沖區為0,并不能提高性能,因為隻要一直有大量的重疊接發請求被抛出,就不會有額外的記憶體拷貝。設定套接字發送緩沖區為空比設定套接字接收緩沖區為空對系統的性能影響要小。因為應用程式的發送緩沖區會被經常鎖定直到它被送出給傳輸層處理。然而,若将接收緩沖區設定為0,并且沒有重疊的receive調用,任何傳進來的資料隻能緩存在傳輸層。傳輸層驅動程式隻會緩存滑動視窗尺寸的資料,即17KB—傳輸層可以配置設定的緩沖區大小的上限。實際的緩沖區要比17KB小。傳輸層緩沖區(針對一次連接配接)是在非分頁池之外配置設定的,這意味着,當服務建立了1000個連接配接時,即使沒有抛出receive請求,非分頁池中也會配置設定17MB的記憶體。而非分頁池是很珍貴的資源,除非伺服器可以保證總是有接收請求抛出,否則套接字接收緩沖區應該不需設定。

隻有在一些特殊情況下,對套接字接收緩沖區不予設定将會導緻性能降低。考慮伺服器需要處理成千上萬個客戶連接配接,而每個連接配接上又都沒有投遞receive請求的情況,如果用戶端零星地發送資料過來,傳輸進來的資料将被緩存在套接字接收緩沖區中。當伺服器處理一個receive重疊I/O時,它會做一些不必要的工作。當完成通知到達時,重疊操作會處理一個I/O請求包(IRP)。在這種情形下,伺服器不能保留很多抛出的receive請求。是以,最好使用簡單的非阻塞接收函數。

6.3 記憶體資源管理問題

由于機器硬體條件所限,系統資源是有限的,是以不得不考慮記憶體資源的管理問題。從上一節對不同I/O模型進行的性能測試結果分析可知,維持大規模的通信連接配接,不僅會耗費掉大量記憶體,而且對CPU的占用也是很高的。

對于配置比較高的伺服器而言,處理成千上萬個連接配接并不成問題。但是随着連接配接量的劇增,記憶體資源的限制将逐漸凸現。最有可能遇到的兩個限制因素就是鎖定頁和非分頁池。鎖定頁的限制不是太嚴重,更應該避免的是非分頁池被耗盡。每一次調用重疊的send或receive請求,送出的緩沖區都可能被鎖住。當記憶體被鎖定時,它就不能從實體記憶體換出。作業系統對鎖定記憶體的數量是有限制的,當達到極限時,重疊操作将會傳回WSAENOBUFS錯誤。如果伺服器在每個連接配接上投遞多個重疊接收操作,随着客戶連接配接數量的增多,極限就會達到。如果期望伺服器能夠處理高并發通信,伺服器可以在每個連接配接上投遞一個0位元組的接受操作,這樣就不會有記憶體鎖定。0位元組的接受完成以後,伺服器可以簡單地執行一個非阻塞的接收函數來擷取緩存在套接字接收緩沖區中的所有資料。當非阻塞接收調用傳回WSAEWOULDBLOCK時,就表示不再有未決的資料了。這種方法非常适合用來設計那些希望通過犧牲每個套接字上的吞吐率來擷取更大規模并發連接配接的伺服器。

當然,最好還要了解用戶端與伺服器通信的方式。在上面的例子中,當0位元組的接收完成後,再投遞一個異步接收操作,将接收到所有緩存在套接字接收緩沖區中的資料。如果伺服器知道用戶端将會連續不斷發送資料,那麼當0位元組的接收完成後,假如用戶端将發送大資料塊(超過單套接字緩沖區8KB的容量)過來,伺服器将抛出一個或多個重疊的接收操作。

另外一個需要重點考慮的問題就是系統所需頁的數量。當系統鎖定傳遞給重疊操作的記憶體時,它是在頁邊界上進行的。在x86體系結構上,記憶體頁的大小為4KB。如果一個操作投遞了1KB的緩沖區,系統實際上會為它鎖定4KB大小的記憶體塊。為避免這種浪費,重疊發送和接收緩沖區的大小應該是頁大小的倍數。可以使用GetSystemInfo這個API來獲知目前系統頁的大小。

如果突破非分頁池極限,将會導緻更嚴重的錯誤,并且很難恢複。非分頁池是記憶體的一部分,它常駐記憶體,并且永遠不會被交換出去。核心模式的系統元件,如驅動程式,通常使用非分頁池,其中包括Winsock和協定驅動程式,例如tcpip.sys。每個套接字的建立将消耗一小部分非分頁池,用于維持套接字狀态資訊。當套接字綁定到一個位址後,TCP/IP堆棧将配置設定額外的非分頁池來儲存本地位址的資訊。當一個對等套接字接入後,TCP/IP堆棧也将配置設定部分非分頁池來儲存遠端位址資訊。基本上,一個建立連接配接的套接字占用2KB非分頁池記憶體,而accept或AcceptEx傳回的套接字則占用1.5KB非分頁池記憶體。之是以出現這個差別,是因為伺服器本地位址資訊已經存儲在監聽套接字中,故accept或AcceptEx傳回的套接字隻需儲存遠端主機位址資訊。此外,每個在套接字上投遞的重疊操作都需要給I/O請求包(IRP)配置設定記憶體,一個IRP使用大約500B非分頁池記憶體。

從以上分析可以看出,為每個連接配接配置設定的非分頁池記憶體并不是很大。然而,随着客戶連接配接量逐增,伺服器對非分頁池的使用将是非常大的。考慮運作在隻有1GB實體記憶體的Windows 2000或以後版本Windows系統上的伺服器,将有256MB的記憶體非配給非分頁池。通常,非分頁池大小是機器實體記憶體的1/4,Windows 2000及以後版本的Windows系統上,非分頁池大小為256MB(/1GB),而Windows NT 4.0限制為128MB(1GB)。擁有256MB的非分頁池的伺服器可以支援50,000或更大的連接配接量。但是必須限制重疊的accept數量,以及在已經建立連接配接的重疊收發操作。在這個例子中,如果已經建立連接配接的套接字,按每個1.5KB計算,将耗費75MB的非分頁池記憶體。如果采用了上面提及的投遞0位元組接收的方法,這樣為每個連接配接配置設定的IRP将占用25MB的非分頁池記憶體。

如果系統耗盡了非分頁池,會有兩種可能的後果。在最好的情況下,Winsock調用将傳回WSAENOBUFS錯誤。最糟糕的情況是系統崩潰,這種情況通常是系統沒能正确處理記憶體非配的問題造成的。沒有一種可行的方案能夠恢複非分頁池耗盡的錯誤,并且也沒有可行的方案來監視非分頁池可配置設定的大小,因為非分頁池耗盡導緻系統崩潰。

由以上探讨,可以得出結論,沒有一種方法可以确定伺服器到底支援多大的并發連接配接和重疊操作,并且也不可能準确地獲知非分頁池是否耗盡或者鎖定記憶體頁數超過極限。因為它們都将導緻Winsock調用都傳回相同的錯誤—WSAENOBUFS。因為以上因素,針對伺服器的測試必須測試不同數量的連接配接情況以及重疊操作完成情況,以便在并發通信規模和資料吞吐率這兩個名額之間選擇一種折中的方案。如果在方案中強加限制,以防止伺服器耗盡非分頁池,則傳回WSAENOBUFS錯誤時,我們就知道是因為超過了鎖定頁的限制。并且可以以一種更優化的處理方式編寫程式,如進一步限制一些待決的操作或關閉某些連接配接。

包重新排序問題

這個問題與伸縮性沒有多大關聯,但是卻是實際通信中不得不考慮的一個問題,因為它涉及到能否正确通信的問題。

雖然使用完成端口的I/O操作總是會按照它們被送出的順序完成,但是線程排程問題可能會導緻關聯到完成端口上的工作不能按正常順序完成。例如,有兩個I/O工作線程,應該接收“位元組塊1,位元組塊2,位元組塊3”,但是你可能以錯誤順序接收這3個位元組塊:“位元組塊2,位元組塊1,位元組塊3”。這也意味着在完成端口上投遞發送請求發送資料時,資料實際也會以錯誤順序被發送出去。

當然,如果隻使用一個工作線程,僅送出一個I/O調用,是不存在順序問題的。因為同一時刻,一個工作線程隻能處理一個I/O操作。但是,這樣就沒有發揮出完成端口的真正優點。

一個簡單的解決方法就是為每個封包添加一個協定頭。協定頭主要是一個封包的實際位元組數,如自定義Package包的第一個字段m_nCmdLen就是這個包占用的位元組數。通信的接受方通過分析協定頭分析本次通信有多少資料要接收,然後繼續讀後面的資料,直到一個封包被完整接收完才接收下一個封包。

當伺服器一次僅做一個異步調用時,上述封包協定頭的解決方案是很有效的。但是,如果要充分發揮IOCP伺服器的潛力,肯定有多個未決的異步讀操作等待資料的到來。這意味着,多個異步操作不能按順序完成,未決讀I/O傳回的位元組流不能按順序處理,接收到的位元組流可能組合成正确的封包,也有可能組合成錯誤的封包。是以,要解決這個問題,還必須為送出的讀I/O配置設定序列号。

說明:

本文主要譯自《Network programming for microsoft windows》一書的