1. 程式人生 > >一文詳解TCP

一文詳解TCP

記得以前面試的時候被面試官問起TIME_WAIT有什麼痛點,當時只記得TCP三次握手、四次揮手之類的,至於其中的某個狀態還真是記不起來,之前也沒有過多關注過,還有對於擁塞控制的概念也比較模糊。

TCP報文格式

TCP大家都知道是什麼東西,這個協議的具體報文格式如下:

(圖1)

標誌位

  • URG:指示報文中有緊急資料,應儘快傳送(相當於高優先順序的資料)。
  • PSH:為1表示是帶有push標誌的資料,指示接收方在接收到該報文段以後,應儘快將這個報文段交給應用程式,而不是在緩衝區排隊。
  • RST:TCP連線中出現嚴重差錯(如主機崩潰),必須釋放連線,在重新建立連線。
  • FIN:傳送端已完成資料傳輸,請求釋放連線。
  • SYN:處於TCP連線建立過程。 (Synchronize Sequence Numbers)
  • ACK:確認序號標誌,為1時表示確認號有效,為0表示報文中不含確認資訊,忽略確認號欄位。

視窗
滑動視窗大小,這個欄位是接收端用來告知傳送端自己還有多少緩衝區可以接受資料。於是傳送端可以根據這個接收端的處理能力來發送資料,而不會導致接收端處理不過來。(以此控制傳送端傳送資料的速率,從而達到流量控制。)視窗大小時一個16bit欄位,因而視窗大小最大為65535。

頭部長度(首部長度)
由於TCP首部包含一個長度可變的選項和填充部分,所以需要這麼一個值來指定這個TCP報文段到底有多長。或者可以這麼理解:就是表示TCP報文段中資料部分在整個TCP報文段中的位置。該欄位的單位是32位字,即:4個位元組。TCP的滑動視窗大小實際上就是socket的接收緩衝區大小的位元組數。

選項和填充部分
TCP報文的欄位實現了TCP的功能,標識程序、對位元組流拆分組裝、差錯控制、流量控制、建立和釋放連線等。其最大長度可根據TCP首部長度進行推算。TCP首部長度用4位表示,那麼選項部分最長為:(2^4-1)*(32/8)-20=40位元組。

三次握手

(圖2)

最開始的時候客戶端和伺服器都是處於CLOSED狀態。主動開啟連線的為客戶端,被動開啟連線的是伺服器。

  1. TCP伺服器程序先建立傳輸控制塊TCB,時刻準備接受客戶程序的連線請求,此時伺服器就進入了**LISTEN(監聽)**狀態;
  2. TCP客戶程序也是先建立傳輸控制塊TCB,然後向伺服器發出連線請求報文,這是報文首部中的**同部位SYN=1,同時選擇一個初始序列號 seq=x **,此時,TCP客戶端程序進入了 SYN-SENT(同步已傳送狀態)狀態
    。TCP規定,SYN報文段(SYN=1的報文段)不能攜帶資料,但需要消耗掉一個序號。
  3. TCP伺服器收到請求報文後,如果同意連線,則發出確認報文。確認報文中應該 ACK=1,SYN=1,確認號是ack=x+1,同時也要為自己初始化一個序列號 seq=y,此時,TCP伺服器程序進入了**SYN-RCVD(同步收到)**狀態。這個報文也不能攜帶資料,但是同樣要消耗一個序號。
  4. TCP客戶程序收到確認後,還要向伺服器給出確認。確認報文的ACK=1,ack=y+1,自己的序列號seq=x+1,此時,TCP連線建立,客戶端進入**ESTABLISHED(已建立連線)**狀態。TCP規定,ACK報文段可以攜帶資料,但是如果不攜帶資料則不消耗序號。
  5. 當伺服器收到客戶端的確認後也進入ESTABLISHED狀態,此後雙方就可以開始通訊了。

三次握手主要目的是:資訊對等和防止超時。防止超時導致髒連線。如果使用的是兩次握手建立連線,假設有這樣一種場景,客戶端傳送了第一個請求連線並且沒有丟失,只是因為在網路結點中滯留的時間太長了,由於TCP的客戶端遲遲沒有收到確認報文,以為伺服器沒有收到,此時重新向伺服器傳送這條報文,此後客戶端和伺服器經過兩次握手完成連線,傳輸資料,然後關閉連線。此時此前滯留的那一次請求連線,網路通暢了到達了伺服器,這個報文字該是失效的,但是,兩次握手的機制將會讓客戶端和伺服器再次建立連線,這將導致不必要的錯誤和資源的浪費。如果採用的是三次握手,就算是那一次失效的報文傳送過來了,服務端接受到了那條失效報文並且回覆了確認報文,但是客戶端不會再次發出確認。由於伺服器收不到確認,就知道客戶端並沒有請求連線。

四次揮手

(圖3)

資料傳輸完畢後,雙方都可釋放連線。最開始的時候,客戶端和伺服器都是處於ESTABLISHED狀態,然後客戶端主動關閉,伺服器被動關閉。

  1. 客戶端程序發出連線釋放報文,並且停止傳送資料。釋放資料報文首部,FIN=1,其序列號為seq=u(等於前面已經傳送過來的資料的最後一個位元組的序號加1),此時,客戶端進入FIN-WAIT-1(終止等待1)狀態。 TCP規定,FIN報文段即使不攜帶資料,也要消耗一個序號。
  2. 伺服器收到連線釋放報文,發出確認報文,ACK=1,ack=u+1,並且帶上自己的序列號seq=v,此時,服務端就進入了CLOSE-WAIT(關閉等待)狀態。TCP伺服器通知高層的應用程序,客戶端向伺服器的方向就釋放了,這時候處於半關閉狀態,即客戶端已經沒有資料要傳送了,但是伺服器若傳送資料,客戶端依然要接受。這個狀態還要持續一段時間,也就是整個CLOSE-WAIT狀態持續的時間。
  3. 客戶端收到伺服器的確認請求後,此時,客戶端就進入**FIN-WAIT-2(終止等待2)**狀態,等待伺服器傳送連線釋放報文(在這之前還需要接受伺服器傳送的最後的資料)。
  4. 伺服器將最後的資料傳送完畢後,就向客戶端傳送連線釋放報文,FIN=1,ack=u+1,由於在半關閉狀態,伺服器很可能又傳送了一些資料,假定此時的序列號為seq=w,此時,伺服器就進入了**LAST-ACK(最後確認)**狀態,等待客戶端的確認。
  5. 客戶端收到伺服器的連線釋放報文後,必須發出確認,ACK=1,ack=w+1,而自己的序列號是seq=u+1,此時,客戶端就進入了TIME-WAIT(時間等待)狀態。注意此時TCP連線還沒有釋放,必須經過2MSL(最長報文段壽命)的時間後,當客戶端撤銷相應的TCB後,才進入CLOSED狀態。
  6. 伺服器只要收到了客戶端發出的確認,立即進入CLOSED狀態。同樣,撤銷TCB後,就結束了這次的TCP連線。可以看到,伺服器結束TCP連線的時間要比客戶端早一些。

TIME_WAIT:主動要求關閉的機器表示收到了對方的FIN報文,併發送出了ACK報文,進入TIME_WAIT狀態,等2MSL後即可進入到CLOSED狀態。如果FIN_WAIT_1狀態下,同時收到待FIN標識和ACK標識的報文時,可以直接進入TIME_WAIT狀態,而無需經過FIN_WAIT_2狀態。

CLOSE_WAIT:被動關閉的機器收到對方請求關閉連線的FIN報文,在第一次ACK應答後,馬上進入CLOSE_WAIT狀態。這種狀態其實標識在等待關閉,並且通知應用傳送剩餘資料,處理現場資訊,關閉相關資源。

為什麼客戶端最後還要等待2MSL?
MSL(Maximum Segment Lifetime),TCP允許不同的實現可以設定不同的MSL值。**第一,保證客戶端傳送的最後一個ACK報文能夠到達伺服器,因為這個ACK報文可能丟失。**站在伺服器的角度看來,我已經發送了FIN+ACK報文請求斷開了,客戶端還沒有給我回應,應該是我傳送的請求斷開報文它沒有收到,於是伺服器又會重新發送一次,而客戶端就能在這個2MSL時間段內收到這個重傳的報文,接著給出迴應報文,並且會重啟2MSL計時器。如果客戶端收到服務端的FIN+ACK報文後,傳送一個ACK給服務端之後就“自私”地立馬進入CLOSED狀態,可能會導致服務端無法確認收到最後的ACK指令,也就無法進入CLOSED狀態,這是客戶端不負責任的表現。**第二,防止失效請求。**防止類似與“三次握手”中提到了的“已經失效的連線請求報文段”出現在本連線中。客戶端傳送完最後一個確認報文後,在這個2MSL時間中,就可以使本連線持續的時間內所產生的所有報文段都從網路中消失。這樣新的連線中不會出現舊連線的請求報文。

在TIME_WAIT狀態無法真正釋放控制代碼資源,在此期間,Socket中使用的本地埠在預設情況下不能再被使用。該限制對於客戶端機器來說是無所謂的,但對於高併發伺服器來說,會極大地限制有效連線的建立數量,稱為效能瓶頸。所以建議將高併發伺服器TIME_WAIT超時時間調小。RFC793中規定MSL為2分鐘。但是在當前的高速網路中,2分鐘的等待時間會造成資源的極大浪費,在高併發伺服器上通常會使用更小的值。
在伺服器上通過變更/etc/sysctl.conf檔案來修改該預設值net.ipv4.tcp_fin_timout=30(建議小30s)。修改完之後執行 /sbin/sysctl -p 讓引數生效。
通過如下命令檢視各連線狀態的技術情況:

[[email protected] ~]# netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
TIME_WAIT 63
ESTABLISHED 13

為什麼建立連線是三次握手,關閉連線確是四次揮手呢?
建立連線的時候, 伺服器在LISTEN狀態下,收到建立連線請求的SYN報文後,把ACK和SYN放在一個報文裡傳送給客戶端。 而關閉連線時,伺服器收到對方的FIN報文時,僅僅表示對方不再發送資料了但是還能接收資料,而自己也未必全部資料都發送給對方了,所以己方可以立即關閉,也可以傳送一些資料給對方後,再發送FIN報文給對方來表示同意現在關閉連線,因此,己方ACK和FIN一般都會分開發送,從而導致多了一次。

滑動視窗

TCP滑動視窗技術通過動態改變視窗大小來調節兩臺主機間資料傳輸。每個TCP/IP主機支援全雙工資料傳輸,因此TCP有兩個滑動視窗:一個用於接收資料,另一個用於傳送資料。TCP使用肯定確認技術,其確認號指的是下一個所期待的位元組。 假定傳送方裝置以每一次三個資料包的方式傳送資料,也就是說,視窗大小為3。傳送方傳送序列號為1、2、3的三個資料包,接收方裝置成功接收資料包,用序列號4確認。傳送方裝置收到確認,繼續以視窗大小3傳送資料。當接收方裝置要求降低或者增大網路流量時,可以對視窗大小進行減小或者增加,本例降低視窗大小為2,每一次傳送兩個資料包。當接收方裝置要求視窗大小為0,表明接收方已經接收了全部資料,或者接收方應用程式沒有時間讀取資料,要求暫停傳送。傳送方接收到攜帶視窗號為0的確認,停止這一方向的資料傳輸。當鏈路變好了或者變差了這個視窗還會發生變話,並不是第一次協商好了以後就永遠不變了。

滑動視窗協議,是TCP使用的一種流量控制方法。該協議允許傳送方在停止並等待確認前可以連續傳送多個分組。由於傳送方不必每發一個分組就停下來等待確認,因此該協議可以加速資料的傳輸。 只有在接收視窗向前滑動時(與此同時也傳送了確認),傳送窗口才有可能向前滑動。收發兩端的視窗按照以上規律不斷地向前滑動,因此這種協議又稱為滑動視窗協議。

流量控制:端到端,接收端的應用層處理速度決定和網速無關,由接收端返回的rwnd控制

cwnd:傳送端視窗( congestion window )
rwnd:接收端視窗(receiver window)

擁塞控制

擁塞控制: 傳送端主動控制cwnd,有慢啟動(從cwnd初始為1開始啟動,指數啟動),擁塞避免(到達ssthresh後,為了避免擁塞開始嘗試線性增長),快重傳(接收方每收到一個報文段都要回復一個當前最大連續位置的確認,傳送方只要一連收到三個重複確認就知道接收方丟包了,快速重傳丟包的報文,並TCP馬上把擁塞視窗 cwnd 減小到1),快恢復(直接從ssthresh線性增長)。

如果網路上的延時突然增加,那麼TCP對這個事作出的應對只有重傳資料,但是重傳會導致網路的負擔更重,於是會導致更大的延遲以及更多的丟包,於是這個情況就會進入惡性迴圈被不斷地放大。試想一下,如果一個網路內有成千上萬的TCP連線都這麼行事,那麼馬上就會形成“網路風暴”,TCP這個協議就會拖垮整個網路。所以TCP不能忽略網路上發生的事情,而無腦地一個勁地重發資料,對網路造成更大的傷害。對此TCP的設計理念是:TCP不是一個自私的協議,當擁塞發生的時候,要做自我犧牲。就像交通阻塞一樣,每個車都應該把路讓出來,而不要再去搶路了。

慢啟動
只有在TCP連線建立和網路出現超時時才使用。每經過一個傳輸輪次,擁塞視窗 cwnd 就加倍。一個傳輸輪次所經歷的時間其實就是往返時間RTT。不過“傳輸輪次”更加強調:把擁塞視窗cwnd所允許傳送的報文段都連續傳送出去,並收到了對已傳送的最後一個位元組的確認。另外,慢開始的“慢”並不是指cwnd的增長速率慢,而是指在TCP開始傳送報文段時先設定cwnd=1,使得傳送方在開始時只發送一個報文段(目的是試探一下網路的擁塞情況),然後再逐漸增大cwnd。

為了防止擁塞視窗cwnd增長過大引起網路擁塞,還需要設定一個慢開始門限ssthresh狀態變數(如何設定ssthresh)。慢開始門限ssthresh的用法如下:

  • 當 cwnd < ssthresh 時,使用上述的慢開始演算法。
  • 當 cwnd > ssthresh 時,停止使用慢開始演算法而改用擁塞避免演算法。
  • 當 cwnd = ssthresh 時,既可使用慢開始演算法,也可使用擁塞控制避免演算法。

擁塞避免演算法:讓擁塞視窗cwnd緩慢地增大,即每經過一個往返時間RTT就把傳送方的擁塞視窗cwnd加1,而不是加倍。這樣擁塞視窗cwnd按線性規律緩慢增長,比慢開始演算法的擁塞視窗增長速率緩慢得多。

(圖4)

無論在慢開始階段還是在擁塞避免階段,只要傳送方判斷網路出現擁塞(其根據就是沒有收到確認),就要把慢開始門限ssthresh設定為出現擁塞時的傳送方視窗值的一半(但不能小於2)。然後把擁塞視窗cwnd重新設定為1,執行慢開始演算法。這樣做的目的就是要迅速減少主機發送到網路中的分組數,使得發生擁塞的路由器有足夠時間把佇列中積壓的分組處理完畢。

  1. 當TCP連線進行初始化時,把擁塞視窗cwnd置為1。前面已說過,為了便於理解,圖中的視窗單位不使用位元組而使用報文段的個數。慢開始門限的初始值設定為16個報文段,即 cwnd = 16 。
  2. 在執行慢開始演算法時,擁塞視窗 cwnd 的初始值為1。以後傳送方每收到一個對新報文段的確認ACK,就把擁塞視窗值另1,然後開始下一輪的傳輸(圖中橫座標為傳輸輪次)。因此擁塞視窗cwnd隨著傳輸輪次按指數規律增長。當擁塞視窗cwnd增長到慢開始門限值ssthresh時(即當cwnd=16時),就改為執行擁塞控制演算法,擁塞視窗按線性規律增長。
  3. 假定擁塞視窗的數值增長到24時,網路出現超時(這很可能就是網路發生擁塞了)。更新後的ssthresh值變為12(即變為出現超時時的擁塞視窗數值24的一半),擁塞視窗再重新設定為1,並執行慢開始演算法。當cwnd=ssthresh=12時改為執行擁塞避免演算法,擁塞視窗按線性規律增長,每經過一個往返時間增加一個MSS的大小。

強調:“擁塞避免”並非指完全能夠避免了擁塞。利用以上的措施要完全避免網路擁塞還是不可能的。“擁塞避免”是說在擁塞避免階段將擁塞視窗控制為按線性規律增長,使網路比較不容易出現擁塞。

如果傳送方設定的超時計時器時限已到但還沒有收到確認,那麼很可能是網路出現了擁塞,致使報文段在網路中的某處被丟棄。這時,TCP馬上把擁塞視窗 cwnd 減小到1,並執行慢開始演算法,同時把慢開始門限值ssthresh減半。這是不使用快重傳的情況。快重傳演算法首先要求接收方每收到一個失序的報文段後就立即發出重複確認(為的是使傳送方及早知道有報文段沒有到達對方)而不要等到自己傳送資料時才進行捎帶確認。

(圖5)

接收方收到了M1和M2後都分別發出了確認。現在假定接收方沒有收到M3但接著收到了M4。顯然,接收方不能確認M4,因為M4是收到的失序報文段。根據可靠傳輸原理,接收方可以什麼都不做,也可以在適當時機發送一次對M2的確認。但按照快重傳演算法的規定,接收方應及時傳送對M2的重複確認,這樣做可以讓傳送方及早知道報文段M3沒有到達接收方。傳送方接著傳送了M5和M6。接收方收到這兩個報文後,也還要再次發出對M2的重複確認。這樣,傳送方共收到了接收方的四個對M2的確認,其中後三個都是重複確認。快重傳演算法還規定,傳送方只要一連收到三個重複確認就應當立即重傳對方尚未收到的報文段M3,而不必繼續等待M3設定的重傳計時器到期。由於傳送方儘早重傳未被確認的報文段,因此採用快重傳後可以使整個網路吞吐量提高約20%。

(圖6)

與快重傳配合使用的還有快恢復演算法,其過程有以下兩個要點:

  1. 當傳送方連續收到三個重複確認,就執行“乘法減小”演算法,把慢啟動門限ssthresh減半。這是為了預防網路發生擁塞。請注意:接下去不執行慢開始演算法。
  2. 由於傳送方現在認為網路很可能沒有發生擁塞,因此與慢開始不同之處是現在不執行慢開始演算法(即擁塞視窗cwnd現在不設定為1),而是把cwnd值設定為慢開始門限ssthresh減半後的數值,然後開始執行擁塞避免演算法(“加法增大”),使擁塞視窗緩慢地線性增大。

上圖給出了快重傳和快恢復的示意圖,並標明瞭“TCP Reno版本”。區別:新的 TCP Reno 版本在快重傳之後採用快恢復演算法而不是採用慢開始演算法。

傳送方視窗的上限值 = Min [ rwnd, cwnd ]

  • 當rwnd < cwnd 時,是接收方的接收能力限制傳送方視窗的最大值。
  • 當cwnd < rwnd 時,則是網路的擁塞限制傳送方視窗的最大值。

差錯控制

TCP使用差錯控制來提供可靠性。差錯控制包括以下的一些機制:檢測和重傳受到損傷的報文段、重傳丟失的報文段、儲存失序到達的報文段直至缺失的報文到期,以及檢測和丟棄重複的報文段。TCP通過三個簡單的工具來完成其差錯控制:檢驗和確認以及超時

參考資料:

  1. https://blog.csdn.net/ligupeng7929/article/details/79597423
  2. https://blog.csdn.net/qzcsu/article/details/72861891
  3. https://www.cnblogs.com/zlingh/p/6161088.html

歡迎支援《RabbitMQ實戰指南》以及關注微信公眾號:朱小廝的部落格。