上一節,我們講的 UDP,基本上包括了傳輸層所必須的埠欄位,它就像我們小時候一樣簡單,相信“網之初,性本善,不丟包,不亂序”,
后來呢,我們都慢慢長大,了解了社會的殘酷,變得復雜而成熟,就像 TCP 協議一樣,它之所以這么復雜,那是因為它秉承的是“性惡論”,它天然認為網路環境是惡劣的,丟包、亂序、重傳,擁塞都是常有的事情,一言不合就可能送達不了,因而要從演算法層面來保證可靠性,
TCP 包頭格式
我們先來看 TCP 頭的格式,從這個圖上可以看出,它比 UDP 復雜得多,

首先,源埠號和目標埠號是不可少的,這一點和 UDP 是一樣的,如果沒有這兩個埠號,資料就不知道應該發給哪個應用,
接下來是包的序號,為什么要給包編號呢?當然是為了解決亂序的問題,不編好號怎么確認哪個應該先來,哪個應該后到呢,編號是為了解決亂序問題,既然是社會老司機,做事當然要穩重,一件件來,面臨再復雜的情況,也臨危不亂,
還應該有的就是確認序號,發出去的包應該有確認,要不然我怎么知道對方有沒有收到呢?如果沒有收到就應該重新發送,直到送達,這個可以解決不丟包的問題,作為老司機,做事當然要靠譜,答應了就要做到,暫時做不到也要有個回復,
TCP 是靠譜的協議,但是這不能說明它面臨的網路環境好,從 IP 層面來講,如果網路狀況的確那么差,是沒有任何可靠性保證的,而作為 IP 的上一層 TCP 也無能為力,唯一能做的就是更加努力,不斷重傳,通過各種演算法保證,也就是說,對于 TCP 來講,IP 層你丟不丟包,我管不著,但是我在我的層面上,會努力保證可靠性,
這有點像如果你在北京,和客戶約十點見面,那么你應該清楚堵車是常態,你干預不了,也控制不了,你唯一能做的就是早走,打車不行就改乘地鐵,盡力不失約,
接下來有一些狀態位,例如 SYN 是發起一個連接,ACK 是回復,RST 是重新連接,FIN 是結束連接等,TCP 是面向連接的,因而雙方要維護連接的狀態,這些帶狀態位的包的發送,會引起雙方的狀態變更,
不像小時候,隨便一個不認識的小朋友都能玩在一起,人大了,就變得禮貌,優雅而警覺,人與人遇到會互相熱情的寒暄,離開會不舍地道別,但是人與人之間的信任會經過多次互動才能建立,
還有一個重要的就是視窗大小,TCP 要做流量控制,通信雙方各宣告一個視窗,標識自己當前能夠的處理能力,別發送的太快,撐死我,也別發的太慢,餓死我,
作為老司機,做事情要有分寸,待人要把握尺度,既能適當提出自己的要求,又不強人所難,除了做流量控制以外,TCP 還會做擁塞控制,對于真正的通路堵車不堵車,它無能為力,唯一能做的就是控制自己,也即控制發送的速度,不能改變世界,就改變自己嘛,
作為老司機,要會自我控制,知進退,知道什么時候應該堅持,什么時候應該讓步,
通過對 TCP 頭的決議,我們知道要掌握 TCP 協議,重點應該關注以下幾個問題:
- 順序問題 ,穩重不亂;
- 丟包問題,承諾靠譜;
- 連接維護,有始有終;
- 流量控制,把握分寸;
- 擁塞控制,知進知退,
TCP 的三次握手
所有的問題,首先都要先建立一個連接,所以我們先來看連接維護問題,
TCP 的連接建立,我們常常稱為三次握手,
A:您好,我是 A,
B:您好 A,我是 B,
A:您好 B,
我們也常稱為“請求 -> 應答 -> 應答之應答”的三個回合,這個看起來簡單,其實里面還是有很多的學問,很多的細節,
首先,為什么要三次,而不是兩次?按說兩個人打招呼,一來一回就可以了啊?為了可靠,為什么不是四次?
我們還是假設這個通路是非常不可靠的,A 要發起一個連接,當發了第一個請求杳無音信的時候,會有很多的可能性,比如第一個請求包丟了,再如沒有丟,但是繞了彎路,超時了,還有 B 沒有回應,不想和我連接,
A 不能確認結果,于是再發,再發,終于,有一個請求包到了 B,但是請求包到了 B 的這個事情,目前 A 還是不知道的,A 還有可能再發,
B 收到了請求包,就知道了 A 的存在,并且知道 A 要和它建立連接,如果 B 不樂意建立連接,則 A 會重試一陣后放棄,連接建立失敗,沒有問題;如果 B 是樂意建立連接的,則會發送應答包給 A,
當然對于 B 來說,這個應答包也是一入網路深似海,不知道能不能到達 A,這個時候 B 自然不能認為連接是建立好了,因為應答包仍然會丟,會繞彎路,或者 A 已經掛了都有可能,
而且這個時候 B 還能碰到一個詭異的現象就是,A 和 B 原來建立了連接,做了簡單通信后,結束了連接,還記得嗎?A 建立連接的時候,請求包重復發了幾次,有的請求包繞了一大圈又回來了,B 會認為這也是一個正常的的請求的話,因此建立了連接,可以想象,這個連接不會進行下去,也沒有個終結的時候,純屬單相思了,因而兩次握手肯定不行,
B 發送的應答可能會發送多次,但是只要一次到達 A,A 就認為連接已經建立了,因為對于 A 來講,他的訊息有去有回,A 會給 B 發送應答之應答,而 B 也在等這個訊息,才能確認連接的建立,只有等到了這個訊息,對于 B 來講,才算它的訊息有去有回,
當然 A 發給 B 的應答之應答也會丟,也會繞路,甚至 B 掛了,按理來說,還應該有個應答之應答之應答,這樣下去就沒底了,所以四次握手是可以的,四十次都可以,關鍵四百次也不能保證就真的可靠了,只要雙方的訊息都有去有回,就基本可以了,
好在大部分情況下,A 和 B 建立了連接之后,A 會馬上發送資料的,一旦 A 發送資料,則很多問題都得到了解決,例如 A 發給 B 的應答丟了,當 A 后續發送的資料到達的時候,B 可以認為這個連接已經建立,或者 B 壓根就掛了,A 發送的資料,會報錯,說 B 不可達,A 就知道 B 出事情了,
當然你可以說 A 比較壞,就是不發資料,建立連接后空著,我們在程式設計的時候,可以要求開啟 keepalive 機制,即使沒有真實的資料包,也有探活包,
另外,你作為服務端 B 的程式設計者,對于 A 這種長時間不發包的客戶端,可以主動關閉,從而空出資源來給其他客戶端使用,
三次握手除了雙方建立連接外,主要還是為了溝通一件事情,就是 TCP 包的序號的問題,
A 要告訴 B,我這面發起的包的序號起始是從哪個號開始的,B 同樣也要告訴 A,B 發起的包的序號起始是從哪個號開始的,為什么序號不能都從 1 開始呢?因為這樣往往會出現沖突,
例如,A 連上 B 之后,發送了 1、2、3 三個包,但是發送 3 的時候,中間丟了,或者繞路了,于是重新發送,后來 A 掉線了,重新連上 B 后,序號又從 1 開始,然后發送 2,但是壓根沒想發送 3,但是上次繞路的那個 3 又回來了,發給了 B,B 自然認為,這就是下一個包,于是發生了錯誤,
因而,每個連接都要有不同的序號,這個序號的起始序號是隨著時間變化的,可以看成一個 32 位的計數器,每 4 微秒加一,如果計算一下,如果到重復,需要 4 個多小時,那個繞路的包早就死翹翹了,因為我們都知道 IP 包頭里面有個 TTL,也即生存時間,
好了,雙方終于建立了信任,建立了連接,前面也說過,為了維護這個連接,雙方都要維護一個狀態機,在連接建立的程序中,雙方的狀態變化時序圖就像這樣,

一開始,客戶端和服務端都處于 CLOSED 狀態,先是服務端主動監聽某個埠,處于 LISTEN 狀態,然后客戶端主動發起連接 SYN,之后處于 SYN-SENT 狀態,服務端收到發起的連接,回傳 SYN,并且 ACK 客戶端的 SYN,之后處于 SYN-RCVD 狀態,客戶端收到服務端發送的 SYN 和 ACK 之后,發送 ACK 的 ACK,之后處于 ESTABLISHED 狀態,因為它一發一收成功了,服務端收到 ACK 的 ACK 之后,處于 ESTABLISHED 狀態,因為它也一發一收了,
TCP 四次揮手
好了,說完了連接,接下來說一說“拜拜”,好說好散,這常被稱為四次揮手,
A:B 啊,我不想玩了,
B:哦,你不想玩了啊,我知道了,
這個時候,還只是 A 不想玩了,也即 A 不會再發送資料,但是 B 能不能在 ACK 的時候,直接關閉呢?當然不可以了,很有可能 A 是發完了最后的資料就準備不玩了,但是 B 還沒做完自己的事情,還是可以發送資料的,所以稱為半關閉的狀態,
這個時候 A 可以選擇不再接收資料了,也可以選擇最后再接收一段資料,等待 B 也主動關閉,
B:A 啊,好吧,我也不玩了,拜拜,
A:好的,拜拜,
這樣整個連接就關閉了,但是這個程序有沒有例外情況呢?當然有,上面是和平分手的場面,
A 開始說“不玩了”,B 說“知道了”,這個回合,是沒什么問題的,因為在此之前,雙方還處于合作的狀態,如果 A 說“不玩了”,沒有收到回復,則 A 會重新發送“不玩了”,但是這個回合結束之后,就有可能出現例外情況了,因為已經有一方率先撕破臉,
一種情況是,A 說完“不玩了”之后,直接跑路,是會有問題的,因為 B 還沒有發起結束,而如果 A 跑路,B 就算發起結束,也得不到回答,B 就不知道該怎么辦了,另一種情況是,A 說完“不玩了”,B 直接跑路,也是有問題的,因為 A 不知道 B 是還有事情要處理,還是過一會兒會發送結束,
那怎么解決這些問題呢?TCP 協議專門設計了幾個狀態來處理這些問題,我們來看斷開連接的時候的狀態時序圖,

斷開的時候,我們可以看到,當 A 說“不玩了”,就進入 FIN_WAIT_1 的狀態,B 收到“A 不玩”的訊息后,發送知道了,就進入 CLOSE_WAIT 的狀態,
A 收到“B 說知道了”,就進入 FIN_WAIT_2 的狀態,如果這個時候 B 直接跑路,則 A 將永遠在這個狀態,TCP 協議里面并沒有對這個狀態的處理,但是 Linux 有,可以調整 tcp_fin_timeout 這個引數,設定一個超時時間,
如果 B 沒有跑路,發送了“B 也不玩了”的請求到達 A 時,A 發送“知道 B 也不玩了”的 ACK 后,從 FIN_WAIT_2 狀態結束,按說 A 可以跑路了,但是最后的這個 ACK 萬一 B 收不到呢?則 B 會重新發一個“B 不玩了”,這個時候 A 已經跑路了的話,B 就再也收不到 ACK 了,因而 TCP 協議要求 A 最后等待一段時間 TIME_WAIT,這個時間要足夠長,長到如果 B 沒收到 ACK 的話,“B 說不玩了”會重發的,A 會重新發一個 ACK 并且足夠時間到達 B,
A 直接跑路還有一個問題是,A 的埠就直接空出來了,但是 B 不知道,B 原來發過的很多包很可能還在路上,如果 A 的埠被一個新的應用占用了,這個新的應用會收到上個連接中 B 發過來的包,雖然序列號是重新生成的,但是這里要上一個雙保險,防止產生混亂,因而也需要等足夠長的時間,等到原來 B 發送的所有的包都死翹翹,再空出埠來,
等待的時間設為 2MSL,MSL 是 Maximum Segment Lifetime,報文最大生存時間,它是任何報文在網路上存在的最長時間,超過這個時間報文將被丟棄,因為 TCP 報文基于是 IP 協議的,而 IP 頭中有一個 TTL 域,是 IP 資料報可以經過的最大路由數,每經過一個處理他的路由器此值就減 1,當此值為 0 則資料報將被丟棄,同時發送 ICMP 報文通知源主機,協議規定 MSL 為 2 分鐘,實際應用中常用的是 30 秒,1 分鐘和 2 分鐘等,
還有一個例外情況就是,B 超過了 2MSL 的時間,依然沒有收到它發的 FIN 的 ACK,怎么辦呢?按照 TCP 的原理,B 當然還會重發 FIN,這個時候 A 再收到這個包之后,A 就表示,我已經在這里等了這么長時間了,已經仁至義盡了,之后的我就都不認了,于是就直接發送 RST,B 就知道 A 早就跑了,
TCP 狀態機
將連接建立和連接斷開的兩個時序狀態圖綜合起來,就是這個著名的 TCP 的狀態機,學習的時候比較建議將這個狀態機和時序狀態機對照著看,不然容易暈,

在這個圖中,加黑加粗的部分,是上面說到的主要流程,其中阿拉伯數字的序號,是連接程序中的順序,而大寫中文數字的序號,是連接斷開程序中的順序,加粗的實線是客戶端 A 的狀態變遷,加粗的虛線是服務端 B 的狀態變遷,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/172560.html
標籤:其他
上一篇:軟體測驗掃盲【教科書級】
