TCP 三次握手過程是怎樣的?
TCP 是面向連接的協議,所以使用 TCP 前必須先建立連接,而建立連接是通過三次握手來進行的。三次握手的過程如下圖:
一開始,客戶端和服務端都處于?CLOSE
?狀態。先是服務端主動監聽某個端口,處于?LISTEN
?狀態
客戶端會隨機初始化序號(client_isn
),將此序號置于 TCP 首部的「序號」字段中,同時把?SYN
標志位置為?1
,表示?SYN
?報文。接著把第一個 SYN 報文發送給服務端,表示向服務端發起連接,該報文不包含應用層數據,之后客戶端處于?SYN-SENT
?狀態。
服務端收到客戶端的?SYN
?報文后,首先服務端也隨機初始化自己的序號(server_isn
),將此序號填入 TCP 首部的「序號」字段中,其次把 TCP 首部的「確認應答號」字段填入?client_isn + 1
,接著把?SYN
?和?ACK
?標志位置為?1
。最后把該報文發給客戶端,該報文也不包含應用層數據,之后服務端處于?SYN-RCVD
?狀態。
客戶端收到服務端報文后,還要向服務端回應最后一個應答報文,首先該應答報文 TCP 首部?ACK
?標志位置為?1
?,其次「確認應答號」字段填入?server_isn + 1
?,最后把報文發送給服務端,這次報文可以攜帶客戶到服務端的數據,之后客戶端處于?ESTABLISHED
?狀態。
服務端收到客戶端的應答報文后,也進入?ESTABLISHED
?狀態。
從上面的過程可以發現第三次握手是可以攜帶數據的,前兩次握手是不可以攜帶數據的,一旦完成三次握手,雙方都處于?ESTABLISHED
?狀態,此時連接就已建立完成,客戶端和服務端就可以相互發送數據了。
如何在 Linux 系統中查看 TCP 狀態?
TCP 的連接狀態查看,在 Linux 可以通過?netstat -napt
?命令查看。
為什么是三次握手?不是兩次、四次?
相信大家比較常回答的是:“因為三次握手才能保證雙方具有接收和發送的能力。”
這回答是沒問題,但這回答是片面的,并沒有說出主要的原因。
在前面我們知道了什么是?TCP 連接:
用于保證可靠性和流量控制的某些狀態信息,這些信息包括Socet,序列號,窗口大小。
所以,重要的是為什么三次握手才可以初始化 Socket、序列號和窗口大小并建立 TCP 連接。
接下來,以三個方面分析三次握手的原因:
- 三次握手才可以阻止重復歷史連接的初始化(主要原因)
- 三次握手才可以同步雙方的初始序列號
- 次握手才可以避免資源浪費
原因一:阻止重復歷史連接的初始化
?RFC 793 指出的 TCP 連接使用三次握手的首要原因:三次握手的首要原因是為了防止舊的重復連接初始化造成混亂。
我們考慮一個場景,客戶端先發送了 SYN(seq = 90)報文,然后客戶端宕機了,而且這個 SYN 報文還被網絡阻塞了,服務端并沒有收到,接著客戶端重啟后,又重新向服務端建立連接,發送了 SYN(seq =100)報文(注意!不是重傳 SYN,重傳的 SYN 的序列號是一樣的)。
客戶端連續發送多次 SYN(都是同一個四元組)建立連接的報文,在網絡擁堵情況下:
一個「舊 SYN 報文」比「最新的 SYN」 報文早到達了服務端,那么此時服務端就會回一個?SYN +ACK
?報文給客戶端,此報文中的確認號是 91(90+1)。
客戶端收到后,發現自己期望收到的確認號應該是 100 + 1,而不是 90 + 1,于是就會回 RST 報文。
服務端收到 RST 報文后,就會釋放連接。
上述中的「舊 SYN 報文」稱為歷史連接,TCP 使用三次握手建立連接的最主要原因就是防止「歷史連接」初始化了連接。
原因二:同步雙方初始序列號
TCP 協議的通信雙方, 都必須維護一個「序列號」, 序列號是可靠傳輸的一個關鍵因素,它的作用:
- 接收方可以去除重復的數據;
- 接收方可以根據數據包的序列號按序接收;
- 可以標識發送出去的數據包中, 哪些是已經被對方收到的(通過 ACK 報文中的序列號知道);
兩次握手只保證了一方的初始序列號能被對方成功接收,沒辦法保證雙方的初始序列號都能被確認接收。
初始序列號 ISN 是如何隨機產生的?
起始?ISN
?是基于時鐘的,每 4 微秒 + 1,轉一圈要 4.55 個小時。
RFC793 提到初始化序列號 ISN 隨機生成算法:ISN = M + F(localhost, localport, remotehost,remoteport)。
M
?是一個計時器,這個計時器每隔 4 微秒加 1。F
?是一個 Hash 算法,根據源 IP、目的 IP、源端口、目的端口生成一個隨機數值。
以看到,隨機數是會基于時鐘計時器遞增的,基本不可能會隨機成一樣的初始化序列號。
既然 IP 層會分片,為什么 TCP 層還需要 MSS 呢?
MTU
:一個網絡包的最大長度,以太網中一般為?1500
?字節;MSS
:除去 IP 和 TCP 頭部之后,一個網絡包所能容納的 TCP 數據的最大長度;
原因
- 性能開銷大:IP 分片需在路由器拆分數據包并添加額外頭部,接收端需緩存所有分片并重組,消耗網絡和主機資源。
- 可靠性低:分片丟失會導致整個原始數據包失效,需重傳全部數據,而非僅丟失的片段。
TCP 通過三次握手協商 MSS,直接限制數據段大小,從源頭避免分片,減少網絡負擔。,如果一個 TCP 分片丟失后,進行重發時也是以 MSS 為單位,而不用重傳所有的分片,大大增加了重傳的效率。
第一次握手丟失了,會發生什么?
當客戶端想和服務端建立 TCP 連接的時候,首先第一個發的就是 SYN 報文,然后進入到?SYN_SENT
?狀態。因為丟失了,所以服務器收不到SYN報文,客戶端就無法得到回應,就會一直發送。重傳的 SYN 報文的序列號都是一樣的。
間隔一段時間,發一次。不同版本的操作系統可能超時時間不同,有的 1 秒的,也有 3 秒的,這個超時時間是寫死在內核里的,一般是5次
第二次握手丟失了,會發生什么?
客戶端發SYN 報文給服務器,服務器回復ACK和SYN報文,但是它丟失了,這就導致客戶端收不到回應,就一直發SYN,服務端也不知道未發出去,得不到客戶端的ACK,服務端也重發ACK和SYN,所以他們就全部重新發送
第三次握手丟失了,會發生什么?
第三握手丟失,服務器得不到客戶端的ACK,因為單獨的ACK不會重傳,
- 重傳 ACK 無意義(數據重傳會自然觸發新的 ACK);
當服務端超時重傳 2 次 SYN-ACK 報文后,由于 tcp_synack_retries 為 2,已達到最大重傳次數,于是再等待一段時間(時間為上一次超時時間的 2 倍),如果還是沒能收到客戶端的第三次握手,那么服務端就會斷開連接。
什么是 SYN 攻擊?如何避免 SYN 攻擊?
我們都知道 TCP 連接建立是需要三次握手,假設攻擊者短時間偽造不同 IP 地址的?SYN
?報文,服務端每接收到一個?SYN
?報文,就進入SYN_RCVD
?狀態,但服務端發送出去的?ACK + SYN
?報文,無法得到未知IP 主機的?ACK
?應答,久而久之就會占滿服務端的半連接隊列,使得服務端不能為正常用戶服務。
在 TCP 三次握手的時候,Linux 內核會維護兩個隊列,分別是:
- 半連接隊列,也稱 SYN 隊列;
- 全連接隊列,也稱 accept 隊列;
正常流程:
1、當服務端接收到客戶端的 SYN 報文時,會創建一個半連接的對象,然后將其加入到內核的「 SYN 隊列」;
2、接著發送 SYN + ACK 給客戶端,等待客戶端回應 ACK 報文;
3、服務端接收到 ACK 報文后,從「 SYN 隊列」取出一個半連接對象,然后創建一個新的連接對象放入到「 Accept 隊列」;
4、應用通過調用?accpet()
?socket 接口,從「 Accept 隊列」取出連接對象。
SYN 攻擊方式最直接的表現就會把 TCP 半連接隊列打滿,這樣當 TCP 半連接隊列滿了,后續再在收到SYN 報文就會丟棄,導致客戶端無法和服務端建立連接。
避免 SYN 攻擊方式
方式一:調大 netdev_max_backlog
當網卡接收數據包的速度大于內核處理的速度時,會有一個隊列保存這些數據包。控制該隊列的最大值如下參數,默認值是 1000,我們要適當調大該參數的值,比如設置為 10000
方式二:增大 TCP 半連接隊列
方式三:加cookie身份驗證
方式四:減少 SYN+ACK 重傳次數