1. 程式人生 > >網路協議 8 - TCP協議(上):性惡就要套路深

網路協議 8 - TCP協議(上):性惡就要套路深

系列文章:

  1. 網路協議 1 - 概述
  2. 網路協議 2 - IP 是怎麼來,又是怎麼沒的?
  3. 網路協議 3 - 從物理層到 MAC 層
  4. 網路協議 4 - 交換機與 VLAN:辦公室太複雜,我要回學校
  5. 網路協議 5 - ICMP 與 ping:投石問路的偵察兵
  6. 網路協議 6 - 路由協議:敢問路在何方?
  7. 網路協議 7 - UDP 協議:性善碰到城會玩

    上次說了“性本善”的 UDP 協議,這哥們秉承“網之初,性本善,不丟包,不亂序”的原則,徜徉在網路世界中。

    與之相對應的,TCP 就像是老大哥一樣,瞭解了社會的殘酷,變得複雜而成熟,秉承“性惡論”。它認為網路環境是惡劣的,丟包、亂序、重傳、擁塞都是常有的事兒,一言不合可能就會丟包,送達不了,所以從演算法層面來保證可靠性。

TCP 包頭格式

    老規矩,咱們先來看看 TCP 頭的格式。

    從上面這個圖可以看出,它比 UDP 要複雜的多。而複雜的地方,也正是它為了解決 UDP 存在的問題所必需的欄位。

    首先,源埠號和目標埠號是兩者都有,不可缺少的欄位。

    接下來是包的序號給包編號就是為了解決亂序的問題。老大哥做事,穩重為主,一件件來,面臨再複雜的情況,也臨危不亂。

    除了傳送端需要給包編號外,接收方也會回覆確認序號。做事靠譜,答應了就要做到,暫時做不到也要給個回覆。

    這裡要注意的是,TCP 是個老大哥沒錯,但不能說他一定會保證傳輸準確無誤的完成。從 IP 層面來講,如果網路的確那麼差,是沒有任何可靠性保證的,即使 TCP 老大哥再穩,他也管不了 IP 層丟包,他只能儘可能的保證在他的層面上的可靠性。

然後是一些狀態位。有以下常見狀態位:

  • SYN(Synchronize Sequence Numbers,同步序列編號):發起一個連線
  • ACK(Acknowledgement,確認字元):回覆
  • RST(Connection reset):重新連線
  • FIN:結束連線

    從這些狀態位就可以看出,TCP 基於“性惡論”,警覺性就很高,不像 UDP 和小朋友似的,隨便一個不認識的小朋友都能玩到一起,他與別人的信任要經過多次互動才能建立。

    還有一個視窗大小。這個是 TCP 用來進行流量控制的。通訊雙方各宣告一個視窗,標識自己當前的處理能力,讓傳送端別傳送的太快,要不然撐死接收端。也不能傳送的太慢,要不然就餓死接收端了。

根據上述對 TCP 頭的分析,我們知道對於 TCP 協議要重點關注以下幾個問題:

  • 順序問題,穩重不亂;
  • 丟包問題,承諾靠譜;
  • 連線偉豪,有始有終;
  • 流量控制,把握分寸;
  • 擁塞控制,知進知退。

TCP 的三次握手

    瞭解完 TCP 頭,我們就來看下 TCP 建立連線的過程,這就是著名的“三次握手”。

三次握手,過程是這樣子的:

  • A:你好,我是 A(SYN)。
  • B:你好 A,我是 B(SYN,ACK)。
  • A:你好 B(ACK 的 ACK)。

    著重記憶上述過程,後續很多分析都是基於這個過程來的。

    記得剛接觸三次握手的時候,就一直很納悶,為啥一定要三次?兩次不行嗎?四次不行嗎?然後很多人就解釋,如果是兩次,就怎樣怎樣,四次,又怎樣怎樣?但這其實都是從結果推原因,沒有說明本質。

    我們應該知道,握手是為了建立穩定的連線,這個是最終目的。而要達到這個目的,就要通訊雙方的互動形成一個確認的閉環

    拿上述 A、B 通訊的例子來看,A 給 B 發信息,B 要告訴 A 他收到資訊了。這時候,算是一個確認閉環嗎?明顯不是,因為 B 沒有收到來自 A 的確認資訊。

    所以,要達到我們上述的目標,還要 A 給 B 一個確認資訊,這樣就形成了一個確認閉環

    A 給 B 的確認資訊發出後,遇到網路不好的情況,也會出現丟包的情況。按理來說,還應該有個迴應,但是,我們發現,好像這樣下去就沒玩沒了啦。

    所以,我們說,只要通訊雙方形成一個確認閉環後,就認為連線已建立。一旦連線建立,A 會馬上傳送資料,而 A 傳送資料,後續的很多問題都得到了解決。

    例如 A 發給 B 的確認訊息丟了,當 A 後續傳送的資料到達的時候,B 可以認為這個連線已經建立。如果 B 直接掛了,A 傳送的資料就會報錯,說 B 不可達,這樣,A 也知道 B 出事情了。

    三次握手除了通訊雙方建立連線外,主要還是為了溝通 TCP 包的序號問題

    A 要告訴 B,我發起的包的序號起始是從哪個號開始的,B 也要告訴 A,B 發起的包的序號的起始號。

    TCP 包的序號是會隨時間變化的,可以看成一個 32 位的計數器,每 4ms 加一。計算一下,這樣到出現重複號,需要 4 個多小時。但是,4 個小時後,還沒到達目的地的包早就死翹翹了。這是因為 IP 包頭裡的 TTL(生存時間)。

    為什麼序號不能從 1 開始呢?因為這樣會很容易出現衝突。

    例如,A 連上 B 之後,傳送了 1、2、3 三個包,但是傳送 3 的時候,中間丟了,或者繞路了,於是重新發送,後來 A 掉線了,重新連上 B 後,序號又從 1 開始,然後傳送 2,但是壓根沒想傳送 3,而如果上次繞路的那個 3 剛好又回來了,發給了 B ,B 自然就認為,這就是下一包,於是發生了錯誤。

    就這樣,雙方歷經千辛萬苦,終於建立了連線。前面也說過,為了維護這個連線,雙方都要維護一個狀態機,在連線建立的過程中,雙方的狀態變化時序圖就像下面這樣:

整體過程是:

  1. 客戶端和服務端都處於 CLOSED 狀態;
  2. 服務端主動監聽某個埠,處於 LISTEN 狀態;
  3. 客戶端主動發起連線 SYN,處於 SYN-SENT 狀態。
  4. 服務端收到客戶端發起的連線,返回 SYN,並且 ACK 客戶端的 SYN,處於 SYN-RCVD 狀態;
  5. 客戶端收到服務端傳送的 SYN 和 ACK 之後,傳送 ACK 的 ACK,處於 ESTABLISHED 狀態;
  6. 服務端收到 ACK 的 ACK 之後,處於 ESTABLISHED 狀態。

TCP 的四次揮手

    說完了連線,接下來就來了解下 TCP 的“再見模式”。這也常被稱為四次揮手

還拿 A 和 B 舉例,揮手過程:

  1. A:B 啊,我不想和你玩了。
  2. B:哦,你不想玩了啊,我知道了。這個時候,還只是 A 不想玩了,就是說 A 不會再發送資料,但是 B 此時還沒做完自己的事情,還是可以傳送資料的,所以此時的 B 處於半關閉狀態
  3. B:A啊,好吧,我也不想和你玩了,拜拜。
  4. A:好的,拜拜。

    這樣這個連線就關閉了。看起來過程很順利,是的,這是通訊雙方“和平分手”的場面。

    A 開始說“不玩了”,B 說“知道了”,這個回合,是沒什麼問題的,因為在此之前,雙方還處於合作的狀態。

    如果 A 說“不玩了”,沒有收到回覆,那麼 A 會重新發送“不玩了”。但是這個回合結束之後,就很可能出現異常情況了,因為有一方率先撕破臉。這種撕破臉有兩種情況。

    一種情況是,A 說完“不玩了”之後,A 直接跑路,這是會有問題的,因為 B 還沒有發起結束,而如果 A 直接跑路,B 就算髮起結束,也得不到回答,B 就就不知道該怎麼辦了。

    另一種情況是,A 說完“不玩了”,B 直接跑路。這樣也是有問題的,因為 A 不知道 B 是還有事情要處理,還是過一會發送結束。

    為了解決這些問題,TCP 專門設計了幾個狀態來處理這些問題。接下來,我們就來看看斷開連線時的狀態時序圖

整體過程是:

  1. A 說“不玩了”,就進入 FIN_WAIT_1 狀態;
  2. B 收到 “A 不玩”的訊息後,回覆“知道了”,就進入 CLOSE_WAIT 狀態;
  3. A 收到“B 說知道了”,進入 FIN_WAIT_2 狀態。這時候,如果 B 直接跑路,則 A 將永遠在這個狀態。TCP 協議裡面並沒有對這個狀態的處理,但是 Linux 有,可以調整 tcp_fin_timeout 這個引數,設定一個超時時間;
  4. B 沒有跑路,傳送了“B 也不玩了”的訊息,處於 LAST_ACK 狀態;
  5. A 收到“B 說不玩了”的訊息,回覆“A 知道 B 也不玩了”的訊息後,從 FINE_WAIT_2 狀態結束。

    最後一個步驟裡,如果 A 直接跑路了,也會出現問題。因為 A 的最後一個回覆,B 如果沒有收到的話就會重複第 4 步,但是因為 A 已經跑路了,所以 B 會一直重複第 4 步。

    因此,TCP 協議要求 A 最後要等待一段時間,這個等待時間是 TIME_WAIT,這個時間要足夠長,長到如果 B 沒收到 A 的回覆,B 重發給 A,A 的回覆要有足夠時間到達 B。

    A 直接跑路還有一個問題是,A 的埠就空出來了,但是 B 不知道,B 原來發過的很多包可能還在路上,如果 A 的埠被新的應用佔用了,這個新的應用會受到上個連線中 B 發過來的包,雖然序列號是重新生成的,但是這裡會有一個雙保險,防止產生混亂。因此也需要 A 等待足夠長的時間,等到 B 傳送的所有未到的包都“死翹翹”,再空出埠。

    這個等待的時間設為 2MSL,MSL 是 Maximum Segment Lifetime,即報文最大生存時間。它是任何報文再網路上存在的最長時間,超過這個時間的報文就會被丟棄。

    因為 TCP 報文基於 IP 協議,而 IP 頭中有一個 TTL 域,是 IP 資料報可以經過的最大路有數,每經過一個處理他的路由器,此值就減 1,當此值為 0 時,資料報就被丟棄,同時傳送 ICMP 報文通知源主機。協議規定 MSL 為 2 分鐘,實際應用中常用的是 30 秒、1分鐘和 2 分鐘等。

    還有一種異常情況,B 超過了 2MS 的時間,依然沒有收到它發的 FIN 的 ACK。按照 TCP 的原理,B 當然還會重發 FIN,這個時候 A 再收到這個包之後,就表示,我已經等你這麼久,算是仁至義盡了,再來的資料包我就不認了,於是直接傳送 RST,這樣 B 就知道 A 跑路了。

TCP 狀態機

    將連線建立和連線斷開的兩個時序狀態圖綜合起來,就是著名的 TCP 狀態機。我們可以將這個狀態機和時序狀態機對照看,就會更加明瞭。

圖中加黑加粗部分,是上面說到的主要流程,相關說明:

  • 阿拉伯數字序號:建立連線順序;
  • 大寫中文數字序號:斷開連線順序;
  • 加粗實線:客戶端 A 的狀態變遷;
  • 加粗虛線:服務端 B 的狀態變遷;

總結

  • TCP 包頭很複雜,主要關注 5 個問題。順序問題、丟包問題、連線維護、流量控制、擁塞控制;
  • 建立連線三次握手,斷開連線四次揮手,狀態圖要牢記。

參考:

  1. 百度百科-TCP 詞條;
  2. 劉超-趣談網路協議系列課;