TCP 概述

這是我的暱稱。。發表於2020-10-26

TCP(Transmission Control Protocol) 是面向連線的通訊協議, 通過三次握手建立連線,
然後才能開始資料的讀寫, 通訊完成時要拆除連線, 由於 TCP 是面向連線的所以只能用於端
到端的通訊。
TCP 提供的是一種可靠的資料流服務, 資料有可能被拆分後傳送, 那麼採用超時重傳機
制是和應答確認機制是組成 TCP 可靠傳輸的關鍵設計。
而超時重傳機制中最最重要的就是重傳超時(RTO, Retransmission TimeOut) 的時間選
擇, 很明顯, 在工程上和現實中網路環境是十分複雜多變的, 有時候可能突然的抽風, 有時
候可能突然的又很順暢。 在資料傳送的過程中, 如果用一個固定的值一直作為超時計時器的
時長是非常不經濟也非常不準確的方法, 這樣的話, 超時的時長就需要根據網路情況動態調
整, 就需要取樣統計一個資料包從傳送端傳送出去到接收到這個包的回覆這段時長來動態設
置重傳超時值, 這個時長就是為 RTT, 學名 round-trip time, 然後再根據這個 RTT 通過各種
演算法和公式平滑 RTT 值後, 最終確定重傳超時值。
而 IP 層進行資料傳輸時, 是不能保證資料包按照傳送的順序達到目的機器。 當 IP 將把
它們向‘上’傳送到 TCP 層後, TCP 將包排序並進行錯誤檢查。 TCP 資料包中包括序號和確認,
所以未按照順序收到的包可以被排序, 而損壞的包可以被重傳。
TCP 還採用一種稱為“滑動視窗”的方式進行流量控制, 所謂視窗實際表示接收能力, 用
以限制傳送方的傳送速度。
同時 TCP 還允許在一個 TCP 連線上, 通訊的雙方可以同時傳輸資料, 也就是所謂的全
雙工。
面向連線的服務(例如 Telnet、 FTP、 rlogin、 X Windows 和 SMTP) 需要高度的可靠性,
所以它們使用了 TCP。 DNS 在某些情況下使用 TCP(傳送和接收域名資料庫) , 但使用 UDP
傳送有關單個主機的資訊。
TCP 三次握手

TCP 提供面向有連線的通訊傳輸。面向有連線是指在資料通訊開始之前先做好兩端之間
的準備工作。
所謂三次握手是指建立一個 TCP 連線時需要客戶端和伺服器端總共傳送三個包以確認
連線的建立。 在 socket 程式設計中, 這一過程由客戶端執行 connect 來觸發。
第一次握手: 客戶端將標誌位 SYN 置為 1, 隨機產生一個值 seq=J, 並將該資料包傳送
給伺服器端, 客戶端進入 SYN_SENT 狀態, 等待伺服器端確認。
第二次握手: 伺服器端收到資料包後由標誌位 SYN=1 知道客戶端請求建立連線, 服務
器端將標誌位 SYN 和 ACK 都置為 1, ack=J+1, 隨機產生一個值 seq=K, 並將該資料包傳送給
客戶端以確認連線請求, 伺服器端進入 SYN_RCVD 狀態。
第三次握手: 客戶端收到確認後, 檢查 ack 是否為 J+1, ACK 是否為 1, 如果正確則將
標誌位 ACK 置為 1, ack=K+1, 並將該資料包傳送給伺服器端, 伺服器端檢查 ack 是否為 K+1,
ACK 是否為 1, 如果正確則連線建立成功, 客戶端和伺服器端進入 ESTABLISHED 狀態, 完成
三次握手, 隨後客戶端與伺服器端之間可以開始傳輸資料了。
為什麼 TCP 握手需要三次?
TCP 是可靠的傳輸控制協議, 而三次握手是保證資料可靠傳輸又能提高傳輸效率的最小
次數。 為什麼? RFC793, 也就是 TCP 的協議 RFC 中就談到了原因, 這是因為:
為了實現可靠資料傳輸, TCP 協議的通訊雙方, 都必須維護一個序列號, 以標識傳送
出去的資料包中, 哪些是已經被對方收到的。
舉例說明: 傳送方在傳送資料包(假設大小為 10 byte) 時, 同時送上一個序號( 假設
為 500), 那麼接收方收到這個資料包以後, 就可以回覆一個確認號(510 = 500 + 10) 告
訴傳送方 “我已經收到了你的資料包, 你可以傳送下一個資料包, 序號從 511 開始” 。
三次握手的過程即是通訊雙方相互告知序列號起始值, 並確認對方已經收到了序列號
起始值的必經步驟。
如果只是兩次握手, 至多隻有連線發起方的起始序列號能被確認, 另一方選擇的序列
號則得不到確認。
至於為什麼不是四次, 很明顯, 三次握手後, 通訊的雙方都已經知道了對方序列號起始
值, 也確認了對方知道自己序列號起始值, 第四次握手已經毫無必要了。
TCP 的三次握手的漏洞-SYN 洪泛攻擊
但是在 TCP 三次握手中是有一個缺陷的, 就是如果我們利用三次握手的缺陷進行攻擊。
這個攻擊就是 SYN 洪泛攻擊。 三次握手中有一個第二次握手, 服務端向客戶端應答請求,
應答請求是需要客戶端 IP 的, 攻擊者就偽造這個 IP, 往伺服器端狂傳送第一次握手的內容,
當然第一次握手中的客戶端 IP 地址是偽造的, 從而服務端忙於進行第二次握手但是第二次
握手當然沒有結果, 所以導致伺服器端被拖累, 當機。
當然我們的生活中也有可能有這種例子, 一個家境一般的 IT 男去表白他的女神被拒絕
了, 理由是他家裡沒礦, IT 男為了報復, 採用了洪泛攻擊, 他請了很多人偽裝成有錢人去表
白那位追求礦的女神, 讓女生每次想交往時發現表白的人不見了同時還聯絡不上了。
面對這種攻擊, 有以下的解決方案, 最好的方案是防火牆。
無效連線監視釋放
這種方法不停監視所有的連線, 包括三次握手的, 還有握手一次的, 反正是
所有的, 當達到一定(與)閾值時拆除這些連線, 從而釋放系統資源。 這種方法對
於所有的連線一視同仁, 不管是正常的還是攻擊的, 所以這種方式不推薦。
延緩 TCB 分配方法
一般的做完第一次握手之後, 伺服器就需要為該請求分配一個 TCB(連線控
制資源) , 通常這個資源需要 200 多個位元組。 延遲 TCB 的分配, 當正常連線建
立起來後再分配 TCB 則可以有效地減輕伺服器資源的消耗。
使用防火牆
防火牆在確認了連線的有效性後, 才向內部的伺服器(Listener) 發起 SYN 請求,
TCP 四次揮手(分手)
四次揮手即終止 TCP 連線, 就是指斷開一個 TCP 連線時, 需要客戶端和服務端總共發
送 4 個包以確認連線的斷開。 在 socket 程式設計中, 這一過程由客戶端或服務端任一方執行 close
來觸發。
由於 TCP 連線是全雙工的, 因此, 每個方向都必須要單獨進行關閉, 這一原則是當甲方
完成資料傳送任務後, 傳送一個 FIN 給乙方來終止這一方向的連線, 乙方收到一個 FIN 只是
意味著不會再收到甲方資料了, 但是乙方依然可以給甲方傳送資料, 直到這乙方也傳送了
FIN 給甲方。 首先進行關閉的一方將執行主動關閉, 而另一方則執行被動關閉。

由此可見, TCP 建立一個連線需 3 個分節, 終止一個連線則需 4 個分節。
(1)某個應用程式首先呼叫 close, 我們稱該端執行主動關閉(active close)。 該端的 TCP
於是傳送一個 FIN 分節, 表示資料傳送完畢, 應用程式進入 FIN-WAIT-1(終止等待 1) 狀態。
(2) 接收到這個 FIN 的對端執行被動關閉(passive close), 發出確認報文。 這個 FIN 由 TCP
確認。 因為 FIN 的接收意味著接收端應用程式在相應連線上再無額外資料可接收。 接收端進
入了 CLOSE-WAIT(關閉等待) 狀態, 這時候處於半關閉狀態, 即主動關閉端已經沒有資料
要傳送了, 但是被動關閉端若傳送資料, 主動關閉端依然要接受。 這個狀態還要持續一段時
間, 也就是整個 CLOSE-WAIT 狀態持續的時間。 主動關閉端收到確認報文後進入 FIN-WAIT-2
(終止等待 2) 狀態。
(3)一段時間後, 被動關閉的應用程式將呼叫 close 關閉它的套接字。 這導致它的 TCP 也
傳送一個 FIN。
(4)接收這個最終 FIN 的原傳送端 TCP(即執行主動關閉的那一端) 確認這個 FIN 發出一
個確認 ACK 報文, 並進入了 TIME-WAIT(時間等待) 狀態。 注意此時 TCP 連線還沒有釋放,
必須經過 2∗MSL(最長報文段壽命/最長分節生命期 max segement lifetime, MSL 是任何 IP
資料包能夠在因特網中存活的最長時間, 任何 TCP 實現都必須為 MSL 選擇一個值。 RFC
1122[Braden 1989]的建議值是 2 分鐘, 不過源自 Berkelcy 的實現傳統上改用 30 秒這個值。
這意味著 TIME_WAIT 狀態的持續時間在 1 分鐘到 4 分鐘之間) 的時間後, 當主動關閉端撤
銷相應的 TCB 後, 才進入 CLOSED 狀態。
(5) 被動關閉端只要收到了客戶端發出的確認, 立即進入 CLOSED 狀態。 同樣, 撤銷 TCB
後, 就結束了這次的 TCP 連線。 可以看到, 被動關閉端結束 TCP 連線的時間要比主動關閉
端早一些。
既然每個方向都需要一個 FIN 和一個 ACK, 因此通常需要 4 個分節。 我們使用限定詞“通
常” 是因為: 某些情形下步驟 1 的 FIN 隨資料一起傳送;另外, 步驟 2 和步驟 3 傳送的分節
都出自執行被動關閉那-一端, 有可能被合併成一個分節。
為什麼 TCP 的揮手需要四次?
TCP 是全雙工的連線, 必須兩端同時關閉連線, 連線才算真正關閉。
如果一方已經準備關閉寫, 但是它還可以讀另一方傳送的資料。 傳送給 FIN 結束報文給
對方, 對方收到後, 回覆 ACK 報文。 當這方也已經寫完了準備關閉, 傳送 FIN 報文, 對方回
復 ACK。 兩端都關閉, TCP 連線正常關閉。
為什麼需要 TIME-WAIT 狀態?
TIME_WAIT 狀態存在的原因有兩點
1、 可靠的終止 TCP 連線。
2、 保證讓遲來的 TCP 報文有足夠的時間被識別並丟棄。
根據前面的四次握手的描述, 我們知道, 客戶端收到伺服器的連線釋放的 FIN 報文後,
必須發出確認。 如最後這個 ACK 確認報文丟失, 那麼伺服器沒有收到這個 ACK 確認報文,
就要重發 FIN 連線釋放報文, 客戶端要在某個狀態等待這個 FIN 連線釋放報文段然後回覆確
認報文段, 這樣才能可靠的終止 TCP 連線。
在 Linux 系統上, 一個 TCP 埠不能被同時開啟多次, 當一個 TCP 連線處於 TIME_WAIT
狀態時, 我們無法使用該連結的埠來建立一個新連線。反過來思考, 如果不存在 TIME_WAIT
狀態, 則應用程式能過立即建立一個和剛關閉的連線相似的連線(這裡的相似, 是指他們具
有相同的 IP 地址和埠號) 。 這個新的、 和原來相似的連線被稱為原來連線的化身。 新的
化身可能受到屬於原來連線攜帶應用程式資料的 TCP 報文段(遲到的報文段) , 這顯然是不
該發生的。 這是 TIME_WAIT 狀態存在的第二個原因。