本文系翻譯文章1。上一篇:傳輸層:TCP和UDP
傳輸控制協議(TCP):高級內容
傳輸層是網絡層和應用層之間的橋樑,傳輸控制協議(Transmission Control Protocol,TCP)可以以可靠的方式實現銜接。在之前的文章2中,上述已經解釋了UDP和TCP的區別,現在是時候去理解如何改善TCP的性能以滿足現代應用程序的需求,以及如何以最佳的方式使用基礎網絡。這樣,我們將在主機上學習到管理TCP連接的原理,例如狀態和窗口操作:歡迎閱讀有關高級TCP的文章。
TCP 狀態
TCP狀態理論
爲了運行TCP,任一設備都必須落實一些原則:設備必須知道當沒有收到ACK的時候重傳數據,設備必須知道對收到的數據發送相應的確認號,等等。所有的這些規則都可以用狀態圖或流程圖定義。任意給定時間,設備必須在圖中某一個狀態,且只能在一個狀態。基於設備所處的狀態,它可以執行什麼操作,不能執行什麼操作,並且可以轉移至特定狀態。一眼看去,圖很複雜,但實際上並不複雜。看看下圖以及它的含義吧!
下面把它拆解來看。如您所見,圖中有11種不同的狀態,沒有真正的開始點。這意味着任何一個狀態(有箭頭指向的)都可以從至少一個狀態轉移而來。每一個矩形代表一種狀態,箭頭指向代表當前狀態可以轉移到的狀態。箭頭上的文本指明瞭狀態成功轉移需要的觸發條件。最後,所有狀態分成了不同顏色的類別:同一組的是帶有同樣目的的。
注意:您可能發現不同書中有不同的狀態表示方式:使用下劃線、虛線或沒有連接詞。
儘管這裏沒有一個真正的開始點,可以認爲CLOSED
是開始點:此種狀態下,連接不能是打開狀態。因爲任何連接都是從沒有連接開始;考慮一下,開始沒有連接,三次握手之後協商建立連接。一個期望建立連接的設備,可以依據其角色從CLOSED
狀態轉移至其它兩個狀態。首先考慮服務器(這裏稱之爲響應者)。服務器不向客戶端初始化連接,而是等待客戶端的連接請求。這意味着服務器端必須準備好接收請求,並在應用程序啓動時從CLOSED
狀態轉移至LISTEN
狀態。這個狀態表明服務器端已經做好了建立連接的準備,正等待從客戶端發來的SYN段。
圖中決定主動建立連接的設備稱之爲發起者,哦通常是客戶端。它在向服務器端發送SYN段後狀態從CLOSED
轉移至SYN_SENT
。一旦響應者在LISTEN
狀態收到了SYN段,它會立即回覆SYN+ACK段,並且其狀態轉移至SYN_RECEIVED
。一旦發起者收到了SYN+ACK段,它在向響應者發送ACK段之後狀態轉移至ESTABLISHED
,響應者收到這個段之後的狀態也轉移至ESTABLISHED
。這意味着連接建立了,數據可以交換了。
注意:一旦一個連接初始化了,傳輸控制塊(Transmission Control Block)就定義好了。它是一個在整個連接過程中保存在設備內存中的信息和變量的集合。其中包含了源端口和目的端口。
一旦數據交換完成,雙方之一想要關閉連接。這種情況下,有一個設備(稱之爲Initiator,發起者)初始化關閉,另一個設備對此進行響應,但是這不是必須要求一方是客戶端,另一方是服務器端。連接關閉發起者只是主動發起連接關閉請求的設備,而響應者是響應關閉連接的一方(被動關閉)。發起者會發送一個FIN報文段,狀態從ESTABLISHED
轉移到FIN_WAIT_1
狀態,響應者的狀態也隨着它接收到FIN報文段而轉移到CLOSE_WAIT
,併發送一個ACK報文段。當發起者收到ACK報文段,它的狀態轉移到FIN_WAIT_2
並處在這種狀態不做任何事。響應者還是處於CLOSE_WAIT
狀態,它在等着應用程序完成所有的事情並關閉。一旦應用程序完事了,響應者設備將會給發起者發送一個FIN報文段,告知其也要關閉連接了,此時它的狀態轉移到LAST_ACK
。一旦發起者接收到響應者的FIN報文段,它會回發一個ACK報文段,且其狀態轉移到TIME_WAIT
。響應者已收到ACK報文段,就會狀態轉移至CLOSED
。兩毫秒之後,發起者狀態也會轉移至CLOSED
。這時,連接正式關閉。
TCP的狀態
這看起來有些複雜,但是很快您將會看到如何用到連接上並使之有意義。這是一個帶有設備狀態的典型的TCP連接過程圖。
在解釋之前,請記住狀態對於設備連接非常重要:它們不是連接自身的狀態,所以一個設備可以處於一種狀態,另一個設備可能處於不同的狀態。如您所見,客戶端狀態從CLOSED
開始,但是此時服務器端已經處於LISTEN
狀態了,因爲服務器端的應用程序必須監聽客戶端請求併產生響應,所以需要提前開啓。客戶端發送一個SYN報文段並進入SYS_SENT
狀態。一旦服務器端收到上個SYN報文段,它會回送SYN+ACK報文段並進入SYN_RECEIVED
狀態,之後客戶端回送ACK報文段並進入ESTABLISHED
狀態,而服務器端收到ACK報文段後也進入ESTABLISHED
狀態。
注意:對於每一個收到的連接,服務器端的狀態都會從LISTEN
轉移到SYN_RECEIVED
狀態,因爲服務器端會併發收到很多連接請求。這意味着每次服務器端收到一個SYN
報文段,服務器端都會爲之創建一個狀態圖所示師力,當連接關閉時,這個也會隨着銷燬。這樣總會有一個LISTEN
的狀態實例等待接收新的客戶端的新的連接。
一旦數據交換結束了,本例子中是客戶端要求結束連接。它通過發送一個FIN報文段,之後轉移至FIN_WAIT_1
狀態。當服務器端收到了FIN報文段,它會回送一個ACK報文段並轉移至CLOSE_WAIT
狀態。當客戶端收到了ACK報文段,會轉移至FIN_WAIT_2
狀態,等着服務器端關閉。一旦服務器端傳送完了數據,它會發送FIN報文,並轉移到LAST_ACK
狀態。當客戶端收到了FIN段報文,會轉移到TIME_WAIT
狀態,並在2ms之後,轉移到CLOSED
狀態(其實要建立在沒有收到報文的情況下,否則繼續等着)。當服務器端收到ACK段報文後轉移到CLOSED
狀態。連接結束。
同時關閉
一個有趣的案例:同時關閉一個TCP連接。本例中,沒有主動發起和被動響應關閉的雙方,而是雙方同時準備關閉連接。
如圖所示,關閉流程時完美同步的。客戶端向服務器端發送一個FIN報文段,來關閉連接並轉移到FIN_WAIT_1
狀態。同時,服務器出於同樣目的,發送一個FIN報文段後也轉移到FIN_WAIT_1
狀態。客戶端收到FIN報文段,之後它轉移到CLOSING
狀態併發送一個ACK報文段。服務器端也一樣,服務器端也會收到FIN報文段併發送一個ACK報文段後轉移到CLOSING
狀態。當任何一方收到了ACK報文段,它會轉移到TIME_WAIT
狀態,超時之後,再轉移到CLOSED
狀態。基本上,伴隨同時關閉,FIN標誌實在FIN_WAIT_1
狀態收到的。
也有同時打開連接的情況,類似於同步關閉連接(在SYN_SENT
狀態收到一個SYN報文段),但是在現實世界中很不常見,因爲C/S架構模式,這種模式是客戶端主動連接服務器。
重置TCP連接
衆所周知,設備可以隨時使用RST報文轉移到CLOSED
狀態。如同下圖高亮部分,展示了兩種可能的RST報文及重置方式。
在第一幅圖中,重置被發送並收到了。如您所見,它可以在任意狀態的時候發送並隨之關閉連接。這意味着它不會監聽並接收連接發來的任何信息,所以當其它設備收到了RST報文段,它只能選擇關閉連接。
第二幅圖片中的例子,展示了當RST報文段丟失的場景。作爲最後一個發送的出去的報文段,沒有相應的重傳機制。發送方發送了就立即關閉連接,不會再處理後續了。所以,對端設備沒有收到RST時,再發送數據也不會收到確認回覆,它會在超時後斷掉連接。
TCP 窗口
TCP報頭中有一個16位長的字段,叫做窗口大小。它標識了接收窗口,即入流量的緩衝區。平鋪直敘,不拽名詞,即窗口大小是發送端設備用於存儲的等待應用程序處理的字節數量。這隻用於接收數據。通過發送報文段,設備說:“嗨老兄,你可以給我發送X字節,我保證我可以臨時儲存它們,以使得應用程序後面處理它們。”X就是窗口大小。
串口大小在每個報文段中都有,但是是對端程序來使用它:窗口大小指示了接收窗口的大小,並不指定已經用了多少。爲了跟蹤對端程序用了多少內存,本端蓄婢比較發送了多少數據以及多少數據得到了確認回覆。每一個發送後的報文段都會佔用對端的接收窗口,每一個確認回覆都會從對端的接收窗口移除對應的報文段。請記住,所述操作都沒有考慮報文段的數量,而是每個報文段中字節數量。如下圖所示。
爲了簡化,稱客戶端是”A“,服務器端是”B“。當通過三次握手協商了連接,兩個設備互告之窗口大小:A的接收窗口(RWIN或RWND)大小是32KB,B的接收窗口大小是16KB。每一個設備都會記住對方的接收窗口大小並考慮對方是否還有足夠的緩衝區空間。然而,因爲信息描述“緩衝區佔用率”,設備必須保持跟蹤。好消息是隻有本端發送的消息會佔用對端的緩衝區空間(其實就是一個TCP流一個唄):本端知曉對端緩衝區總大小,也知曉其何時爲空,還知悉本端發送了多少數據以及對端確認了多少數據。本例爲了簡單:客戶端每次發送1K數據,服務器端每次都少了1K可用緩衝。重複了3次,同時服務器端開始處理數據,並回送確認報文,每收到一個確認報文,客戶端都會檢查確認了多少並增加對應字節數量的可用空間。
一會之後,服務器端要向客戶端發送數據。然而它有較多的數據需要發送:它每次發送1K,重複發送20次(圖中省略了),可用空間從32K減小到了11K。客戶端花費了大量時間處理數據,一旦它處理不過來了(如本例),它會通過發送一個“窗口大小現在爲0”的報文告知服務器停止傳送。
注意:窗口大小在每個報文段中都有,其定義了當前發送該報文段的設備中總可用緩衝空間,所以除非該值和三次握手時原始接收值(或最後一個值,如果在三次握手過程中多次更改)不一樣,否則設備不會考慮它。
這是因爲一個設備會盡可能的多傳送數據,但是沒有收到回覆的字節數量不能超過窗口大小。接收窗口經過專門設計,可以避免設備過載,就像它們緩衝區滿了之後,任何無法容納在緩衝區中的接收段都將會被丟棄。現代設備,沒有這麼嚴格了。它可以和擁塞窗口連用,來處理網絡擁塞。
選擇確認
選擇確認,也稱爲Selective ACK或簡稱SACK(RFC 2018),是對TCP性能的另一改善,它允許設備單獨確認報文段。傳統的TCP實現中,一個報文段丟失的時候,不得不重傳,丟失報文段所有後續的報文段也不得不重傳,儘管它們被正確接收了。這是因爲確認號的本質所導致的,確認號告知對端設備它下面想要接收的是哪一個。這種邏輯可以確認連續的字節流,但是中間不能有中斷(例如,“OK,我接受到了0-8以及10-20,但是沒有收到8-10”)。SACK機制改變了這一狀況,它允許設備獨自決定確認哪些報文段,所以只有丟失的報文段會重傳,整體上改善了吞吐量。下圖所示是對比傳統確認機制和選擇重傳機制。
在左邊,帶有警告標誌的報文段收發了兩次。
傳統的確認機制,服務器端向客戶端先後發送了三個報文段,分別包含了1-1460字節,1461-2920字節以及2921-4380字節。然而,第二個包含1461-2920字節的報文段因爲暫時的網絡原因丟失了,所以客戶端只確認了第一個報文段(確認號是1461),但是丟棄了第三個報文段,因爲如果發送了確認號是4381的報文段,將無法表示中間丟失了的報文了。所以,服務器端收到確認號是1461的報文段後,又重新發送了1461-2920(丟失的報文段)以及2921-4380兩個報文段。這樣總共發送了兩次第三個有效報文段,浪費了帶寬和時間。
爲了解決上述問題,選擇重傳機制實現了兩個不同的TCP報頭選項。第一個是Sack-Permitted Option(允許SACK選項),它的標識爲設置爲4。這個選項用在三次握手時驗證連接雙方都支持SACK機制。第一個階段成功後,**Sack Option(SACK選項,類型設置爲5)**解釋選項格式超出了本文範圍,只需知道它是TCP報頭重用於告知對端設備哪一個報文段使用SACK確認了。瞭解更多請查看RFC2018。基本上,一個報文段丟失的時候,後續報文段正確接收,所有丟失報文之前的報文已經按傳統確認機制確認了,丟失報文之後的報文都使用SACK機制確認了,使用的是TCP“Sack Option”報頭擴展中適當的字段。如圖所示,因爲唯一的丟失的報文段是1461-2920,客戶端使用傳統確認機制發送了確認號爲1461,使用SACK機制確認了2920-4380。
報頭壓縮
報頭壓縮是很棒的TCP功能,它使得低速連接,如衛星連接等的帶寬增強了。這一功能非常簡單,但也與衆不同,因爲它不是在TCP主機上實現的。其它的功能都是由TCP雙方實施的增強功能,然而這裏不是這樣子的。實際上,它是在路徑上的路由器上實現的。
想象一下,路由器處理包含了TCP報文段的IP包,如果您想要大量使用連接(例如從服務端下載文件),路由器會處理大量源IP地址和目的IP地址相同的數據包,而且報文段中的源端口和目的端口也一樣。但是這些數據包不得不一層層傳送出去。然而,報頭開銷佔據的帶寬即使在低速連接上也會降低性能。但是所有的這些報頭中帶有的相同的連接信息,難道不能只發送一次嗎?通過報頭壓縮可以做到。
實現了報頭壓縮功能的路由器將報頭中的IP和TCP地址,以及在連接期間不會發生改變的其它字段,通過算法產生唯一的標識符(Hash ID),該標識符要小得多。大多數情況下,我們從40字節報頭壓縮到4字節的標識符。很明顯,所有的路由器收到了一個壓縮報頭,需要知道其擴展後的值,併發送至正確的目的地址。最後,路由器會解壓縮報頭併發送至目的設備(或者下一個不支持報頭壓縮的路由器)。
如圖所示,兩個路由器在衛星鏈接通過Hash ID來交互,每一個Hash都對應唯一一個數據流。圖中只有單向數據流,但是實際上也會有一個迴應數據流的Hash ID。
處理網絡擁塞
快速重傳
快速重傳是一個用於實現主動重傳數據的非常簡單的功能。在傳統TCP實現中,我們應該等待ACK以決定我們應該重傳的數據。有了快速重傳功能,如果在超時前沒有收到ACK,爲了節省時間,設備會自動重傳尚未確認的報文段。這在高延遲低帶寬網絡中不太好,因爲信息可能並不是丟失了,而是還沒有到達對端。
TCP 擁塞控制
構建網絡越來越便宜,現在網絡可以實現10年前無法想象的吞吐量。然而,隨着網絡速度的提升,現代應用程序的要求也在上升,日常的帶寬殺手應用,如VoIP,視頻或大量的網絡遊戲。因此,100%使用網絡性能是必須的,但是如果我們嘗試發送的數據量超過了網絡的處理能力,則會導致性能下降。需要在帶寬利用率和網絡性能之間做出取捨。網絡管理需要一些工具來控制擁塞,而TCP正好有這能力。
TCP兩端是網絡邊緣的兩個主機,它們無法知曉網絡的整體狀況,因此,它們無法知道它們之間有效的吞吐量。但是,它們必須能夠知曉這種情況。TCP使用了擁塞窗口(congestion window, CWND),其中是在發送必須停止並等待確認前,發送端能夠發送的字節數量。擁塞窗口不像接收窗口處於每個報文中,它是設備本地的,不會出現在連接中。無論何時,設備最多可以發送由接收器窗口和擁塞窗口之間的最小值指定的最大字節數,如以下公式所示。
transmittable bytes = min(cwnd, rwnd)
這意味着,如果擁塞窗口小於接收窗口,則設備可以在等待確認之前最多發送由擁塞窗口中定義的字節數。反之,如果接收窗口比擁塞窗口小,那麼設備可以在等待確認之前最多發送由接收窗口定義的字節數。
擁塞窗口根據網絡擁塞程度動態調整。每次一個報文未收到確認,都認爲是網絡擁塞導致的。擁塞窗口的變化是由實現使用的算法決定的。下面是最常見的一種算法,它遵從的規則如下:
- 擁塞窗口從1個分段大小開始(大約是1KB);
- 定義擁塞窗口門限值(ssthresh);
- 如果收到了確認報文,且當前擁塞窗口小於門限值,擁塞窗口翻倍;
- 如果收到了確認報文,但是擁塞窗口大小大於等於門限值,則擁塞窗口只增加其初始值(例:1KB);
- 如果沒有收到確認報文段,出發重傳,擁塞窗口減半,門限值設置爲此值;
- 擁塞窗口不能大於接收窗口。
爲了解釋方便,請看下方一個使用了擁塞控制機制的報文交換示例。如您所見,每個設備都有其自己的擁塞窗口(CWND,綠色的)以及對方的的接收窗口(RWND)。左邊記錄了報文交換的編號(行列),方便後方單行參考。
當協商連接的時候,兩個設備互換接收窗口(本例中各有32KB)。它們的擁塞窗口大小都從1KB開始,但是由於客戶端是本例唯一發送數據的一方,所以它是唯一會使用擁塞窗口的一方。第2行,客戶端收到一個ACK報文並把其CWND翻倍(現在是2k),服務器端也一樣(第3行)。然後,客戶端發送兩個1k數據報文,這兩個報文分別於第6行和第7行得到確認回覆,同時收到確認後擁塞窗口分別再次翻倍(4k再到8k)。然後,客戶端繼續發送了1k報文並立即得到了服務器確認,擁塞窗口也變成了16k(第9行)。第10、11行重複上述過程,客戶端的CWND達到了32k。這時客戶端擁塞窗口達到了最大值,如果再增加,需要服務器端的接收窗口也增大。在第12行,發生了報文丟失,等待超時(沒有體現在圖中),擁塞窗口和ssthresh(門限值)設置爲16k,在13-14行再次發送並接收到確認回覆,但是此時擁塞窗口沒有翻倍,因爲其已經達到了門限值(ssthresh),它只是增加了1k。第16行,服務器端縮減了其接收窗口到8k,所以客戶端的擁塞窗口也設置爲8k。
雖然上例不錯,但是讓我們看一個更加複雜的。這次我們使用的是一個擁塞窗口值隨時間變化的圖表。
如圖所示,擁塞窗口從1k開始,在達到閾值之前,每接收到一個ACK都會翻倍,然後開始線性增長知道丟失了一個報文。然後,閾值縮小爲擁塞窗口最大值的一半,然後擁塞窗口從這個一半開始繼續線性增長。最後,接收窗口縮減了,閾值和擁塞窗口也隨之變化。
UDP 的佔優和 TCP 的飢餓(UDP predominance and TCP starvation)
知道了擁塞控制,下面將探討同一個網絡上的UDP和TCP。TCP實現了一個複雜的回退機制以適應網絡擁塞,而UDP並沒有。所以,如果大量流量使用了UDP,TCP和UDP一起將超出網絡的能力,TCP將會因爲擁塞控制算法而產生回退。UDP則不會,它會繼續佔用其已經使用的帶寬,如果一些UDP流量因爲網絡擁塞而排隊,則由於(TCP擁塞控制使得)網絡不再擁塞,將立即觸發它。如果網絡再次飽和,TCP會再次執行擁塞控制算法直到網絡全部都是UDP流量。如下圖所示:
這在局域網(LAN)中不是問題,因爲局域網帶寬較高,很難達到網絡飽和裝填。但是在廣域網(WAN,連接地理位置相距較遠的站點)中,需要採取措施來避免UDP流量的佔優。如**服務質量(Quality of Service,QoS)**使用一系列規則定義了網絡應該在擁塞時做出何種應對【好像一般是UDP隨機丟包或者全丟】。這些規則設置在路由器上,並定義了網絡擁塞時丟掉哪些包。QoS的規則可以分開UDP和TCP的流量,可使得UDP只能使其可用的網絡飽和。QoS規則還能給一些應用分配百分比帶寬,或給一些應用保留帶寬(也就是說,這部分帶寬只能被一些應用使用,就算它們不用別的也不能用)。
本文,我們覆蓋了所有的TCP高級功能。現在您可以探索TCP如何在CCNP級別上工作了【本文其實是CCNA的免費文章,Cisco認證】。這些知識在談論防火牆的時候也有用【防火牆有一種工作方式就是兩方發送3個RST重置報文】,下一篇文章將探討UDP以及會話層和表示層的內容。