天天看點

linux TCP網絡通信過程

原文:http://blog.csdn.net/russell_tao/article/details/9111769 編寫伺服器時,許多程式員習慣于使用高層次的元件、中間件(例如OO(面向對象)層層封裝過的開源元件),相比于伺服器的運作效率而言,他們更關注程式開發的效率,追求更快的完成項目功能點、希望應用代碼完全不關心通訊細節。他們更喜歡在OO世界裡,去實作某個接口、實作這個元件預定義的各種模式、設定元件參數來達到目的。學習複雜的通訊架構、底層細節,在習慣于使用OO語言的程式員眼裡是絕對事倍功半的。以上做法無可厚非,但有一定的局限性,本文講述的網絡程式設計頭前冠以“高性能”,它是指程式員設計編寫的伺服器需要處理很大的吞吐量,這與簡單網絡應用就有了質的不同。因為:1、高吞吐量下,容易觸發到一些設計上的邊界條件;2、偶然性的小機率事件,會在高吞吐量下變成必然性事件。3、IO是慢速的,高吞吐量通常意味着高并發,如同一時刻存在數以萬計、十萬計、百萬計的TCP活動連接配接。是以,做高性能網絡程式設計不能僅僅滿足于學會開源元件、中間件是如何幫我實作期望功能的,對于企業級産品來說,需要了解更多的知識。

掌握高性能網絡程式設計,涉及到對網絡、作業系統協定棧、程序與線程、常見的網絡元件等知識點,需要有豐富的項目開發經驗,能夠權衡伺服器運作效率與項目開發效率。以下圖來談談我個人對高性能網絡程式設計的了解。

linux TCP網絡通信過程

上面這張圖中,由上至下有以下特點: •關注點,逐漸由特定業務向通用技術轉移 •使用場景上,由專業領域向通用領域轉移 •靈活性上要求越來越高 •性能要求越來越高 •對細節、原理的掌握,要求越來越高 •對各種異常情況的處理,要求越來越高 •穩定性越來越高,bug率越來越少 在做應用層的網絡程式設計時,若伺服器吞吐量大,則應該适度了解以上各層的關注點。

如上圖紅色文字所示,我認為編寫高性能伺服器的關注點有3個: 1、如果基于通用元件程式設計,關注點多是在元件如何封裝套接字程式設計細節。為了使應用程式不感覺套接字層,這些元件往往是通過各種回調機制來向應用層代碼提供網絡服務,通常,出于為應用層提供更高的開發效率,元件都大量使用了線程(Nginx等是個例外),當然,使用了線程後往往可以降低代碼複雜度。但多線程引入的并發解決機制還是需要重點關注的,特别是鎖的使用。另外,使用多線程意味着把應用層的代碼複雜度扔給了作業系統,大吞吐量時,需要關注多線程給作業系統核心帶來的性能損耗。 基于通用元件程式設計,為了程式的高性能運作,需要清楚的了解元件的以下特性:怎麼使用IO多路複用或者異步IO的?怎麼實作并發性的? 怎麼組織線程模型的? 怎麼處理高吞吐量引發的異常情況的?

2、通用元件隻是在封裝套接字,作業系統是通過提供套接字來為程序提供網絡通訊能力的。是以,不了解套接字程式設計,往往對元件的性能就沒有原理上的認識。學習套接字層的程式設計是有必要的,或許很少會自己從頭去寫,但作業系統的API提供方式經久不變,一經學會,受用終身,同時在項目的架構設計時,選用何種網絡元件就非常準确了。 學習套接字程式設計,關注點主要在:套接字的程式設計方法有哪些?阻塞套接字的各方法是如何阻塞住目前代碼段的?非阻塞套接字上的方法如何不阻塞目前代碼段的?IO多路複用機制是怎樣與套接字結合的?異步IO是如何實作的?網絡協定的各種異常情況、作業系統的各種異常情況是怎麼通過套接字傳遞給應用性程式的?

3、網絡的複雜性會影響到伺服器的吞吐量,而且,高吞吐量場景下,多種臨界條件會導緻應用程式的不正常,特别是元件中有bug或考慮不周或沒有配置正确時。了解網絡分組可以定位出這些問題,可以正确的配置系統、元件,可以正确的了解系統的瓶頸。 這裡的關注點主要在:TCP、UDP、IP協定的特點?linux等作業系統如何處理這些協定的?使用tcpdump等抓包工具分析各網絡分組。

一般掌握以上3點,就可以揮灑自如的實作高性能網絡伺服器了。

下面具體談談如何做到高性能網絡程式設計。 衆所周知,IO是計算機上最慢的部分,先不看磁盤IO,針對網絡程式設計,自然是針對網絡IO。網絡協定對網絡IO影響很大,當下, TCP/IP協定是毫無疑問的主流協定,本文就主要以TCP協定為例來說明網絡IO。 網絡IO中應用伺服器往往聚焦于以下幾個由網絡IO組成的功能中:A)與用戶端建立起TCP連接配接。B)讀取用戶端的請求流。C)向用戶端發送響應流。D)關閉TCP連接配接。E)向其他伺服器發起TCP連接配接。 要掌握住這5個功能,不僅僅需要熟悉一些API的使用,更要了解底層網絡如何與上層API之間互相發生影響。同時,還需要對不同的場景下,如何權衡開發效率、程序、線程與這些API的組合使用。下面依次來說說這些網絡IO。

1、 與用戶端建立起TCP連接配接 談這個功能前,先來看看網絡、協定、應用伺服器間的關系 :

linux TCP網絡通信過程

上圖中可知: 為簡化不同場景下的程式設計,TCP/IP協定族劃分了應用層、TCP傳輸層、IP網絡層、鍊路層等,每一層隻專注于少量功能。 例如,IP層隻專注于每一個網絡分組如何到達目的主機,而不管目的主機如何處理。 傳輸層最基本的功能是專注于端到端,也就是一台主機上的程序發出的包,如何到達目的主機上的某個程序。當然,TCP層為了可靠性,還額外需要解決3個大問題:丢包(網絡分組在傳輸中存在的丢失)、重複(協定層異常引發的多個相同網絡分組)、延遲(很久後網絡分組才到達目的地)。 鍊路層則隻關心以太網或其他二層網絡内網絡包的傳輸。

回到應用層,往往隻需要調用類似于accept的API就可以建立TCP連接配接。建立連接配接的流程大家都了解--三次握手,它如何與accept互動呢?下面以一個不太精确卻通俗易懂的圖來說明之:

linux TCP網絡通信過程

研究過backlog含義的朋友都很容易了解上圖。這兩個隊列是核心實作的,當伺服器綁定、監聽了某個端口後,這個端口的SYN隊列和ACCEPT隊列就建立好了。用戶端使用connect向伺服器發起TCP連接配接,當圖中1.1步驟用戶端的SYN包到達了伺服器後,核心會把這一資訊放到SYN隊列(即未完成握手隊列)中,同時回一個SYN+ACK包給用戶端。一段時間後,在較中2.1步驟中用戶端再次發來了針對伺服器SYN包的ACK網絡分組時,核心會把連接配接從SYN隊列中取出,再把這個連接配接放到ACCEPT隊列(即已完成握手隊列)中。而伺服器在第3步調用accept時,其實就是直接從ACCEPT隊列中取出已經建立成功的連接配接套接字而已。

現有我們可以來讨論應用層元件:為何有的應用伺服器程序中,會單獨使用1個線程,隻調用accept方法來建立連接配接,例如tomcat;有的應用伺服器程序中,卻用1個線程做所有的事,包括accept擷取新連接配接。

原因在于:首先,SYN隊列和ACCEPT隊列都不是無限長度的,它們的長度限制與調用listen監聽某個位址端口時傳遞的backlog參數有關。既然隊列長度是一個值,那麼,隊列會滿嗎?當然會,如果上圖中第1步執行的速度大于第2步執行的速度,SYN隊列就會不斷增大直到隊列滿;如果第2步執行的速度遠大于第3步執行的速度,ACCEPT隊列同樣會達到上限。第1、2步不是應用程式可控的,但第3步卻是應用程式的行為,假設程序中調用accept擷取新連接配接的代碼段長期得不到執行,例如擷取不到鎖、IO阻塞等。

那麼,這兩個隊列滿了後,新的請求到達了又将發生什麼? 若SYN隊列滿,則會直接丢棄請求,即新的SYN網絡分組會被丢棄;如果ACCEPT隊列滿,則不會導緻放棄連接配接,也不會把連接配接從SYN列隊中移出,這會加劇SYN隊列的增長。是以,對應用伺服器來說,如果ACCEPT隊列中有已經建立好的TCP連接配接,卻沒有及時的把它取出來,這樣,一旦導緻兩個隊列滿了後,就會使用戶端不能再建立新連接配接,引發嚴重問題。 是以,如TOMCAT等伺服器會使用獨立的線程,隻做accept擷取連接配接這一件事,以防止不能及時的去accept擷取連接配接。

那麼,為什麼如Nginx等一些伺服器,在一個線程内做accept的同時,還會做其他IO等操作呢? 這裡就帶出阻塞和非阻塞的概念。應用程式可以把listen時設定的套接字設為非阻塞模式(預設為阻塞模式),這兩種模式會導緻accept方法有不同的行為。對阻塞套接字,accept行為如下圖:

linux TCP網絡通信過程

這幅圖中可以看到,阻塞套接字上使用accept,第一個階段是等待ACCEPT隊列不為空的階段,它耗時不定,由用戶端是否向自己發起了TCP請求而定,可能會耗時很長。 對非阻塞套接字,accept會有兩種傳回,如下圖:

linux TCP網絡通信過程

非阻塞套接字上的accept,不存在等待ACCEPT隊列不為空的階段,它要麼傳回成功并拿到建立好的連接配接,要麼傳回失敗。

是以,企業級的伺服器程序中,若某一線程既使用accept擷取新連接配接,又繼續在這個連接配接上讀、寫字元流,那麼,這個連接配接對應的套接字通常要設為非阻塞。原因如上圖,調用accept時不會長期占用所屬線程的CPU時間片,使得線程能夠及時的做其他工作。