圖解 | 原來這就是TCP

閃客sun發表於2021-01-24

你是一臺電腦,你的名字叫 A

圖解 | 原來這就是TCP

經過《圖解 | 原來這就是網路》這篇文章中的一番折騰,只要你知道另一位夥伴 B 的 IP 地址,且你們之間的網路是通的,無論多遠,你都可以將一個資料包傳送給你的夥伴 B

圖解 | 原來這就是TCP

這就是物理層、資料鏈路層、網路層這三層所做的事情。站在第四層的你,就可以不要臉地利用下三層所做的鋪墊,隨心所欲地傳送資料,而不必擔心找不到對方了。

圖解 | 原來這就是TCP

雖然你此時還什麼都沒幹,但你還是給自己這一層起了個響亮的名字,叫做傳輸層。你本以為自己所在的第四層萬事大吉,啥事沒有,但很快問題就接踵而至。

問題來了

前三層協議只能把資料包從一個主機搬到另外一臺主機,但是,到了目的地以後,資料包具體交給哪個程式(程式)呢?

圖解 | 原來這就是TCP

所以,你需要把通訊的程式區分開來,於是就給每個程式分配一個數字編號,你給它起了一個響亮的名字:埠號。

圖解 | 原來這就是TCP

然後你在要傳送的資料包上,增加了傳輸層的頭部,源埠號與目標埠號。
圖解 | 原來這就是TCP

OK,這樣你將原本主機到主機的通訊,升級為了程式和程式之間的通訊。你沒有意識到,你不知不覺實現了 UDP 協議!(當然 UDP 協議中不光有源埠和目標埠,還有資料包長度和校驗值,我們暫且略過)就這樣,你用 UDP 協議無憂無慮地同 B 進行著通訊,一直沒發生什麼問題。

圖解 | 原來這就是TCP

但很快,你發現事情變得非常複雜......

丟包問題

由於網路的不可靠,資料包可能在半路丟失,而 A 和 B 卻無法察覺。

圖解 | 原來這就是TCP

對於丟包問題,只要解決兩個事就好了。第一個,A 怎麼知道包丟了?答案:讓 B 告訴 A第二個,丟了的包怎麼辦?答案:重傳於是你設計瞭如下方案,A 每發一個包,都必須收到來自 B 的確認(ACK),再發下一個,否則在一定時間內沒有收到確認,就重傳這個包。圖解 | 原來這就是TCP

你管它叫停止等待協議。只要按照這個協議來,雖然 A 無法保證 B 一定能收到包,但 A 能夠確認 B 是否收到了包,收不到就重試,盡最大努力讓這個通訊過程變得可靠,於是你們現在的通訊過程又有了一個新的特徵,可靠交付。

效率問題

停止等待雖然能解決問題,但是效率太低了,A 原本可以在發完第一個資料包之後立刻開始發第二個資料包,但由於停止等待協議,A 必須等資料包到達了 B ,且 B 的 ACK 包又回到了 A,才可以繼續發第二個資料包,這效率慢得可不是一點兩點。於是你對這個過程進行了改進,採用流水線的方式,不再傻傻地等。

圖解 | 原來這就是TCP

順序問題

但是網路是複雜的、不可靠的。有的時候 A 發出去的資料包,分別走了不同的路由到達 B,可能無法保證和傳送資料包時一樣的順序。

圖解 | 原來這就是TCP

在流水線中有多個資料包和ACK包在亂序流動,他們之間對應關係就亂掉了。難道還回到停止等待協議?A 每收到一個包的確認(ACK)再發下一個包,那就根本不存在順序問題。應該有更好的辦法!A 在傳送的資料包中增加一個序號(seq),同時 B 要在 ACK 包上增加一個確認號(ack),這樣不但解決了停止等待協議的效率問題,也通過這樣標序號的方式解決了順序問題。

圖解 | 原來這就是TCP

而 B 這個確認號意味深長:比如 B 發了一個確認號為 ack = 3,它不僅僅表示 A 傳送的序號為 2 的包收到了,還表示 2 之前的資料包都收到了。這種方式叫累計確認或累計應答。

圖解 | 原來這就是TCP

注意,實際上 ack 的號是收到的最後一個資料包的序號 seq + 1,也就是告訴對方下一個應該發的序號是多少。但圖中為了便於理解,ack 就表示收到的那個序號,不必糾結。

流量問題

有的時候,A 傳送資料包的速度太快,而 B 的接收能力不夠,但 B 卻沒有告知 A 這個情況。

圖解 | 原來這就是TCP

怎麼解決呢?很簡單,B 告訴 A 自己的接收能力,A 根據 B 的接收能力,相應控制自己的傳送速率,就好了。B 怎麼告訴 A 呢?B 跟 A 說"我很強"這三個字麼?那肯定不行,得有一個嚴謹的規範。於是 B 決定,每次傳送資料包給 A 時,順帶傳過來一個值,叫視窗大小(win),這個值就表示 B 的接收能力。同理,每次 A 給 B 發包時也帶上自己的視窗大小,表示 A 的接收能力。

圖解 | 原來這就是TCP

B 告訴了 A 自己的視窗大小值,A 怎麼利用它去做 A 這邊發包的流量控制呢?很簡單,假如 B 給 A 傳過來的視窗大小 win = 5,那 A 根據這個值,把自己要傳送的資料分成這麼幾類。圖解 | 原來這就是TCP

圖片過於清晰,就不再文字解釋了。當 A 不斷髮送資料包時,已傳送的最後一個序號就往右移動,直到碰到了視窗的上邊界,此時 A 就無法繼續發包,達到了流量控制。圖解 | 原來這就是TCP

但是當 A 不斷髮包的同時,A 也會收到來自 B 的確認包,此時整個視窗會往右移動,因此上邊界也往右移動,A 就能發更多的資料包了。圖解 | 原來這就是TCP

以上都是在視窗大小不變的情況下,而 B 在發給 A 的 ACK 包中,每一個都可以重新設定一個新的視窗大小,如果 A 收到了一個新的視窗大小值,A 會隨之調整。如果 A 收到了比原視窗值更大的視窗大小,比如 win = 6,則 A 會直接將視窗上邊界向右移動 1 個單位。圖解 | 原來這就是TCP

如果 A 收到了比原視窗值小的視窗大小,比如 win = 4,則 A 暫時不會改變視窗大小,更不會將視窗上邊界向左移動,而是等著 ACK 的到來,不斷將左邊界向右移動,直到視窗大小值收縮到新大小為止。圖解 | 原來這就是TCP

OK,終於將流量控制問題解決得差不多了,你看著上面一個個小動圖,給這個視窗起了一個更生動的名字,滑動視窗。

擁塞問題

但有的時候,不是 B 的接受能力不夠,而是網路不太好,造成了網路擁塞。

圖解 | 原來這就是TCP

擁塞控制與流量控制有些像,但流量控制是受 B 的接收能力影響,而擁塞控制是受網路環境的影響。擁塞控制的解決辦法依然是通過設定一定的視窗大小,只不過,流量控制的視窗大小是 B 直接告訴 A 的,而擁塞控制的視窗大小按理說就應該是網路環境主動告訴 A。但網路環境怎麼可能主動告訴 A 呢?只能 A 單方面通過試探,不斷感知網路環境的好壞,進而確定自己的擁塞視窗的大小。

圖解 | 原來這就是TCP

擁塞視窗大小的計算有很多複雜的演算法,就不在本文中展開了,假如擁塞視窗的大小為  cwnd,上一部分流量控制的滑動視窗的大小為 rwnd,那麼視窗的右邊界受這兩個值共同的影響,需要取它倆的最小值。視窗大小 = min(cwnd, rwnd)含義很容易理解,當 B 的接受能力比較差時,即使網路非常通暢,A 也需要根據 B 的接收能力限制自己的傳送視窗。當網路環境比較差時,即使 B 有很強的接收能力,A 也要根據網路的擁塞情況來限制自己的傳送視窗。正所謂受其短板的影響嘛~

連線問題

有的時候,B 主機的相應程式還沒有準備好或是掛掉了,A 就開始傳送資料包,導致了浪費。

圖解 | 原來這就是TCP

這個問題在於,A 在跟 B 通訊之前,沒有事先確認 B 是否已經準備好,就開始發了一連串的資訊。就好比你和另一個人打電話,你還沒有"喂"一下確認對方有沒有在聽,你就巴拉巴拉說了一堆。這個問題該怎麼解決呢?地球人都知道,三次握手嘛!

A:我準備好了(SYN)

B:我知道了(ACK),我也準備好了(SYN)

A:我知道了(ACK)

圖解 | 原來這就是TCP

A 與 B 各自在記憶體中維護著自己的狀態變數,三次握手之後,雙方的狀態都變成了連線已建立(ESTABLISHED)。雖然就只是發了三次資料包,並且在各自的記憶體中維護了狀態變數,但這麼說總覺得太 low,你看這個過程相當於雙方建立連線的過程,於是你靈機一動,就叫它面向連線吧。注意:這個連線是虛擬的,是由 A 和 B 這兩個終端共同維護的,在網路中的裝置根本就不知道連線這回事兒!但凡事有始就有終,有了建立連線的過程,就要考慮釋放連線的過程,又是地球人都知道,四次揮手嘛!

A:再見,我要關閉了(FIN)

B:我知道了(ACK)

     給 B 一段時間把自己的事情處理完...

B:再見,我要關閉了(FIN)

A:我知道了(ACK)

圖解 | 原來這就是TCP

總結

以上講述的,就是 TCP 協議的核心思想,上面過程中需要傳輸的資訊,就體現在 TCP 協議的頭部,這裡放上最常見的 TCP 協議頭解讀的圖。

圖解 | 原來這就是TCP

不知道你現在再看下面這句話,是否能理解:

TCP 是面向連線的、可靠的、基於位元組流的傳輸層通訊協議

面向連線、可靠,這兩個詞通過上面的講述很容易理解,那什麼叫做基於位元組流呢?很簡單,TCP 在建立連線時,需要告訴對方 MSS(最大報文段大小)。也就是說,如果要傳送的資料很大,在 TCP 層是需要按照 MSS 來切割成一個個的 TCP 報文段 的。切割的時候我才不管你原來的資料表示什麼意思,需要在哪裡斷句啥的,我就把它當成一串毫無意義的位元組,在我想要切割的地方咔嚓就來一刀,標上序號,只要接收方再根據這個序號拼成最終想要的完整資料就行了。在我 TCP 傳輸這裡,我就把它當做一個個的位元組,也就是基於位元組流的含義了。

圖解 | 原來這就是TCP

最後留給大家一個作業,模擬 A 與 B 建立一個 TCP 連線。

第一題:A 給 B 傳送 "aaa" ,然後 B 給 A 回覆一個簡單的字串 "success",並將此過程抓包。

第二題:A 給 B 傳送 "aaaaaa ... a" 超過最大報文段大小,然後 B 給 A 回覆一個簡單的字串 "success",並將此過程抓包。

下面是我抓的包(第二題)

三次握手階段

A -> B [SYN] Seq=0 Win=64240 Len=0

                        MSS=1460 WS=256

B - >A [SYN, ACK] Seq=0 Ack=1 Win=29200 Len=0

                        MSS=1424 WS=512

A -> B [ACK] Seq=1 Ack=1 Win=132352 Len=0

資料傳送階段

A -> B [ACK] Seq=1 Ack=1 Win=132352 Len=1424

A -> B [ACK] Seq=1425 Ack=1 Win=132352 Len=1424

A -> B [PSH, ACK] Seq=2849 Ack=1 Win=132352 Len=1247

B -> A [ACK] Seq=1 Ack=1425 Win=32256 Len=0

B -> A [ACK] Seq=1 Ack=2849 Win=35328 Len=0

B -> A [ACK] Seq=1 Ack=4096 Win=37888 Len=0

B -> A [PSH, ACK] Seq=1 Ack=4096 Win=37888 Len=7

四次揮手階段

B -> A [FIN, ACK] Seq=8 Ack=4096 Win=37888 Len=0

A -> B [ACK] Seq=4096 Ack=9 Win=132352 Len=0

A -> B [FIN, ACK] Seq=4096 Ack=9 Win=132352 Len=0

A -> B [ACK]

詳細的抓包資料與分析整理,就不在文章裡展示了,可以在公眾號 低併發程式設計 後臺回覆 TCP 獲取。

 

後記

一提到 TCP,可能很多人都想起被三次握手和四次揮手所支配的恐懼。但其實你跟著文中的思路你就會發現,三次握手與四次揮手只佔 TCP 所解決的核心問題中很小的一部分,只是因為它在面試中很適合作為知識點進行考察,所以在很多人的印象中就好像 TCP 的核心就是握手和揮手似的。
本文希望你能從問題出發,真正理解 TCP 所想要解決的問題,你會發現很多原理就好像生活常識一樣順其自然,並不複雜,希望你有收穫~

相關文章