從本章開始,我們開始介紹最重要的傳輸層。傳輸層位於 OSI 七層模型的第四層(由下往上)。顧名思義,傳輸層的主要作用是實現應用程式之間的通訊。網路層主要是保證不同資料鏈路下資料的可達性,至於如何傳輸資料則是由傳輸層負責。
傳輸層協議簡介
常見的傳輸層協議主要有 TCP 協議和 UDP 協議。TCP 協議是面向有連線的協議,也就是說在使用 TCP 協議傳輸資料之前一定要在傳送方和接收方之間建立連線。一般情況下建立連線需要三步,關閉連線需要四步。
建立 TCP 連線後,由於有資料重傳、流量控制等功能,TCP 協議能夠正確處理丟包問題,保證接收方能夠收到資料,與此同時還能夠有效利用網路頻寬。然而 TCP 協議中定義了很多複雜的規範,因此效率不如 UDP 協議,不適合實時的視訊和音訊傳輸。
UDP 協議是面向無連線的協議,它只會把資料傳遞給接收端,但是不會關注接收端是否真的收到了資料。但是這種特性反而適合多播,實時的視訊和音訊傳輸。因為個別資料包的丟失並不會影響視訊和音訊的整體效果。
IP 協議中的兩大關鍵要素是源 IP 地址和目標 IP 地址。而剛剛我們說過,傳輸層的主要作用是實現應用程式之間的通訊。因此傳輸層的協議中新增了三個要素:源埠號,目標埠號和協議號。通過這五個資訊,可以唯一識別一個通訊。
不同的埠用於區分同一臺主機上不同的應用程式。假設你開啟了兩個瀏覽器,瀏覽器 A 發出的請求不會被瀏覽器 B 接收,這就是因為 A 和 B 具有不同的埠。
協議號用於區分使用的是 TCP 還是 UDP。因此相同兩臺主機上,相同的兩個程式之間的通訊,在分別使用 TCP 協議和 UDP 協議時也可以被正確的區分開來。
用一句話來概括就是:“源 IP 地址,目標 IP 地址,源埠號,目標埠號和協議號”這五個資訊只要有一個不同,都被認為是不同的通訊。
UDP 首部
UDP 協議最大的特點就是簡單,它的首部如下圖所示:
包長度表示 UDP 首部的長度和 UDP 資料長度之和。
校驗和用來判斷資料在傳輸過程中是否損壞。計算這個校驗和的時候,不僅考慮源埠號和目標埠號,還要考慮 IP 首部中的源 IP 地址,目標 IP 地址和協議號(這些又稱為 UDP 偽首部)。這是因為以上五個要素用於識別通訊時缺一不可,如果校驗和只考慮埠號,那麼另外三個要素收到破壞時,應用就無法得知。這有可能導致不該收到包的應用收到了包,該收到包的應用反而沒有收到。
這個概念同樣適用於即將介紹的 TCP 首部。
TCP 首部
和 UDP 首部相比,TCP 首部要複雜得多。解析這個首部的時間也相應的會增加,這是導致 TCP 連線的效率低於 UDP 的原因之一。
其中某些關鍵欄位解釋如下:
-
序列號:它表示傳送資料的位置,假設當前的序列號為 s,傳送資料長度為 l,則下次傳送資料時的序列號為 s + l。在建立連線時通常由計算機生成一個隨機數作為序列號的初始值。
-
確認應答號:它等於下一次應該接收到的資料的序列號。假設傳送端的序列號為 s,傳送資料的長度為 l,那麼接收端返回的確認應答號也是 s + l。傳送端接收到這個確認應答後,可以認為這個位置以前所有的資料都已被正常接收。
-
資料偏移:TCP 首部的長度,單位為 4 位元組。如果沒有可選欄位,那麼這裡的值就是 5。表示 TCP 首部的長度為 20 位元組。
-
控制位:改欄位長度為 8 位元,分別有 8 個控制標誌。依次是 CWR,ECE,URG,ACK,PSH,RST,SYN 和 FIN。在後續的文章中你會陸續接觸到其中的某些控制位。
-
視窗大小:用於表示從應答號開始能夠接受多少個 8 位位元組。如果視窗大小為 0,可以傳送視窗探測。
-
緊急指標:盡在 URG 控制位為 1 時有效。表示緊急資料的末尾在 TCP 資料部分中的位置。通常在暫時中斷通訊時使用(比如輸入 Ctrl + C)。
TCP 握手
TCP 是面向有連線的協議,連線在每次通訊前被建立,通訊結束後被關閉。瞭解連線建立和關閉的過程通常是考察的重點。連線的建立和關閉過程可以用一張圖來表示:
通常情況下,我們認為客戶端首先發起連線。
三次握手建立連線
這個過程可以用以下三句形象的對話表示:
- (客戶端):我要建立連線了。
- (服務端):我知道你要建立連線了,我這邊沒有問題。
- (客戶端):我知道你知道我要建立連線了,接下來我們就正式開始通訊。
為什麼是三次握手
根據一般的思路,我們可能會覺得只要兩次握手就可以了,第三步確認看似是多餘的。那麼 TCP 協議為什麼還要費力不討好的加上這一次握手呢?
這是因為在網路請求中,我們應該時刻記住:“網路是不可靠的,資料包是可能丟失的”。假設沒有第三次確認,客戶端向服務端傳送了 SYN,請求建立連線。由於延遲,服務端沒有及時收到這個包。於是客戶端重新傳送一個 SYN 包。回憶一下介紹 TCP 首部時提到的序列號,這兩個包的序列號顯然是相同的。
假設服務端接收到了第二個 SYN 包,建立了通訊,一段時間後通訊結束,連線被關閉。這時候最初被髮送的 SYN 包剛剛抵達服務端,服務端又會傳送一次 ACK 確認。由於兩次握手就建立了連線,此時的服務端就會建立一個新的連線,然而客戶端覺得自己並沒有請求建立連線,所以就不會向服務端傳送資料。從而導致服務端建立了一個空的連線,白白浪費資源。
在三次握手的情況下,服務端直到收到客戶端的應答後才會建立連線。因此在上述情況下,客戶端會接受到一個相同的 ACK 包,這時候它會拋棄這個資料包,不會和服務端進行第三次握手,因此避免了服務端建立空的連線。
ACK 確認包丟失怎麼辦
三次握手其實解決了第二步的資料包丟失問題。那麼第三步的 ACK 確認丟失後,TCP 協議是如何處理的呢?
按照 TCP 協議處理丟包的一般方法,服務端會重新向客戶端傳送資料包,直至收到 ACK 確認為止。但實際上這種做法有可能遭到 SYN 泛洪攻擊。所謂的泛洪攻擊,是指傳送方偽造多個 IP 地址,模擬三次握手的過程。當伺服器返回 ACK 後,攻擊方故意不確認,從而使得伺服器不斷重發 ACK。由於伺服器長時間處於半連線狀態,最後消耗過多的 CPU 和記憶體資源導致當機。
正確處理方法是服務端傳送 RST 報文,進入 CLOSE 狀態。這個 RST 資料包的 TCP 首部中,控制位中的 RST 位被設定為 1。這表示連線資訊全部被初始化,原有的 TCP 通訊不能繼續進行。客戶端如果還想重新建立 TCP 連線,就必須重新開始第一次握手。
四次握手關閉連線
這個過程可以用以下四句形象的對話表示:
- (客戶端):我要關閉連線了。
- (服務端):你那邊的連線可以關閉了。
- (服務端):我這邊也要關閉連線了。
- (客戶端):你那邊的連線可以關閉了。
由於連線是雙向的,所以雙方都要主動關閉自己這一側的連線。
關閉連線的最後一個 ACK 丟失怎麼辦
實際上,在第三步中,客戶端收到 FIN 包時,它會設定一個計時器,等待相當長的一段時間。如果客戶端返回的 ACK 丟失,那麼服務端還會重發 FIN 並重置計時器。假設在計時器失效前伺服器重發的 FIN 包沒有到達客戶端,客戶端就會進入 CLOSE 狀態,從而導致服務端永遠無法收到 ACK 確認,也就無法關閉連線。
示意圖如下: