TCP 基本認識#
TCP 頭格式有哪些?#
標註顏色的字段表示與本文關聯較大:
- ** 序列號:** 在建立連接時由計算機生成的隨機數作為其初始值,通過 SYN 包傳給接收端主機,每發送一次數據,就「累加」一次該「數據字節數」的大小。用來解決網絡包亂序問題。
- 確認應答號:指下一次「期望」收到的數據的序列號,發送端收到這個確認應答以後可以認為在這個序號以前的數據都已經被正常接收。用來解決丟包的問題。
- 控制位:
- **ACK:** 該位為
1
時,「確認應答」的字段變為有效,TCP 規定除了最初建立連接時的SYN
包之外該位必須設置為1
。 - RST:該位為
1
時,表示 TCP 連接中出現異常必須強制斷開連接。 - SYN:該位為
1
時,表示希望建立連接,並在其「序列號」的字段進行序列號初始值的設定。 - FIN:該位為
1
時,表示今後不會再有數據發送,希望斷開連接。當通信結束希望斷開連接時,通信雙方的主機之間就可以相互交換FIN
位為 1 的 TCP 段。
- **ACK:** 該位為
為什麼需要 TCP 協議? TCP 工作在哪一層?#
如果需要保障網絡數據包的可靠性(交付、按續交付、數據完整性),就需要由傳輸層的 TCP
協議來負責。
因為 TCP 是一個工作在傳輸層的可靠數據傳輸的服務,它能確保接收端接收的網絡包是無損壞、無間隔、非冗餘和按序的。
什麼是 TCP?#
TCP 是面向連接的、可靠的、基於字節流的傳輸層通信協議。
- ** 面向連接:** 一定是「一對一」才能連接,不能像 UDP 協議可以一個主機同時向多個主機發送消息,也就是一對多是無法做到的;
- 可靠的:無論的網絡鏈路中出現了怎樣的鏈路變化,TCP 都可以保證一個報文一定能夠到達接收端;
- 字節流:用戶消息通過 TCP 協議傳輸時,消息可能會被操作系統「分組」成多個的 TCP 報文,如果接收方的程序不知道「消息的邊界」,就無法讀出一個有效的用戶消息。並且 TCP 報文是「有序的」,當「前一個」TCP 報文沒有收到的時候,即使它先收到了後面的 TCP 報文,那麼也不能扔給應用層去處理,同時對 **「重複」的 TCP 報文會自動丟棄 **。
什麼是 TCP 連接?#
RFC 793 是如何定義「連接」的:
Connections: The reliability and flow control mechanisms described above require that TCPs initialize and maintain certain status information for each data stream. The combination of this information, including sockets, sequence numbers, and window sizes, is called a connection.
即用於保證可靠性和流量控制維護的某些狀態信息,這些信息的組合,包括 Socket、序列號和窗口大小稱為連接。
所以,建立一個 TCP 連接需要客戶端與服務器達成上述三個信息的共識:
- Socket:由 IP 地址和端口號組成
- 序列號:解決報文亂序問題
- 窗口大小:用作流量控制
如何唯一確定一個 TCP 連接?#
TCP 四元組可以唯一的確定一個連接:
- 源地址
- 源端口
- 目的地址
- 目的端口
源地址和目的地址的字段(32 位)是在 IP 頭部中,作用是通過 IP 協議發送報文給對方主機。
源端口和目的端口的字段(16 位)是在 TCP 頭部中,作用是告訴 TCP 協議應該把報文發給哪個進程。
UDP 和 TCP 有什麼區別?分別的應用場景是?#
UDP 不提供複雜的控制機制,利用 IP 提供面向「無連接」的通信服務。
UDP 協議非常簡單,頭部只有 8
個字節(64 位),UDP 的頭部格式如下:
- 目標和源端口:主要是告訴 UDP 協議應該把報文發給哪個進程。
- 包長度:該字段保存了 UDP 首部的長度跟數據的長度之和。
- 校驗和:校驗和是為了提供可靠的 UDP 首部和數據而設計,防止收到在網絡傳輸中受損的 UDP 包。
TCP 和 UDP 區別:
- 連接
- TCP 是面向連接的傳輸層協議,傳輸數據前先要建立連接。
- UDP 不需要連接,即刻傳輸數據。
- 服務對象
- TCP 是一對一的兩點服務,即一條連接只有兩個端點。
- UDP 支持一對一、一對多、多對多的交互通信。
- 可靠性
- TCP 是可靠交付數據的,數據可以無差錯、不丟失、不重複、按序到達。
- UDP 是盡最大努力交付,不保證可靠交付數據。但是我們可以基於 UDP 傳輸協議實現一個可靠的傳輸協議,比如 QUIC 協議,具體可以參見這篇文章:如何基於 UDP 協議實現可靠傳輸?
- 擁塞控制、流量控制
- TCP 有擁塞控制和流量控制機制,保證數據傳輸的安全性。
- UDP 則沒有,即使網絡非常擁堵了,也不會影響 UDP 的發送速率。
- 首部開銷
- TCP 首部長度較長,會有一定的開銷,首部在沒有使用「選項」字段時是
20
個字節,如果使用了「選項」字段則會變長。 - UDP 首部只有 8 個字節,並且是固定不變的,開銷較小。
- TCP 首部長度較長,會有一定的開銷,首部在沒有使用「選項」字段時是
- 傳輸方式
- TCP 是流式傳輸,沒有邊界,但保證順序和可靠。
- UDP 是一個包一個包的發送,是有邊界的,但可能會丟包和亂序。
- 分片不同
- TCP 的數據大小如果大於 MSS (Maximum Segment Size) 大小,則會在傳輸層進行分片,目標主機收到後,也同樣在傳輸層組裝 TCP 數據包,如果中途丟失了一個分片,只需要傳輸丟失的這個分片。
- UDP 的數據大小如果大於 MTU (Maximum Transmission Unit) 大小,則會在 IP 層進行分片,目標主機收到後,在 IP 層組裝完數據,接著再傳給傳輸層。
TCP 和 UDP 應用場景:
由於 TCP 是面向連接,能保證數據的可靠性交付,因此經常用於:
- FTP 文件傳輸
- HTTP / HTTPS
由於 UDP 面向無連接,它可以隨時發送數據,再加上 UDP 本身的處理既簡單又高效,因此經常用於:
- 包總量較少的通信,如
DNS
、SNMP
等; - 視頻、音頻等多媒體通信;
- 廣播通信;
TCP 和 UDP 可以使用同一個端口嗎?#
可以。
在數據鏈路層中,通過 MAC 地址來尋找局域網中的主機。
在網絡層中,通過 IP 地址來尋找網絡中互連的主機或路由器。
在傳輸層中,需要通過端口進行尋址,來識別同一計算機中同時通信的不同應用程序。
所以,傳輸層的「端口號」的作用,是為了區分同一主機上不同應用程序的數據包。
傳輸層有兩個傳輸協議分別是 TCP 和 UDP,在內核中是兩個完全獨立的軟件模塊。
當主機收到數據包後,可以在 IP 包頭的「協議號」字段知道該數據包是 TCP/UDP,所以可以根據這個信息確定送給哪個模塊(TCP/UDP)處理,送給 TCP/UDP 模塊的報文根據「端口號」確定送給哪個應用程序處理。
因此,TCP/UDP 各自的端口號也相互獨立,如 TCP 有一個 80 號端口,UDP 也可以有一個 80 號端口,二者並不衝突。
關於端口的知識點,還是挺多可以講的,比如還可以牽扯到這幾個問題:
- 多個 TCP 服務進程可以同時綁定同一個端口嗎?
- 重啟 TCP 服務進程時,為什麼會出現 “Address in use” 的報錯信息?又該怎麼避免?
- 客戶端的端口可以重複使用嗎?
- 客戶端 TCP 連接 TIME_WAIT 狀態過多,會導致端口資源耗尽而無法建立新的連接嗎?
上面這些問題,可以看這篇文章:TCP 和 UDP 可以使用同一個端口嗎?
TCP 連接建立#
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 狀態?#
在 Linux 中可以通過 netstat -napt
查看 TCP 的連接狀態:
為什麼是三次握手?不是兩次、四次?#
常見的片面的回答是:“三次握手後才能保證雙方具有接收和發送的能力”。這沒有說出主要的原因。
前面介紹了什麼是 TCP 連接:用於保證可靠性和流量控制維護的某些狀態信息,這些信息的組合,包括 Socket、序列號和窗口大小稱為連接。
所以,重要的是為什麼三次握手才可以初始化 Socket、序列號和窗口大小並建立 TCP 連接。
接下來,以三個方面分析三次握手的原因:
- 三次握手才可以阻止重複歷史連接的初始化(主要原因)
- 三次握手才可以同步雙方的初始序列號
- 三次握手才可以避免資源浪費
原因一:避免歷史連接#
在 RFC 793 中指出 TCP 連接使用三次握手的主要原因:
The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.
簡單來說,三次握手的首要原因是為了防止舊的重複連接初始化造成混亂。
我們考慮一個場景,客戶端先發送了 SYN(seq = 90)報文,然後客戶端宕機了,而且這個 SYN 報文還被網絡阻塞了,服務端並沒有收到,接著客戶端重啟後,又重新向服務端建立連接,發送了 SYN(seq = 100)報文(注意!不是重傳 SYN,重傳的 SYN 的序列號是樣一樣的)。
看看三次握手是如何阻止歷史連接的:
客戶端連續發送多次 SYN(都是同一個四元組)建立連接的報文,在網絡擁堵情況下:
- 一個「舊 SYN 報文」比「最新的 SYN」 報文早到達了服務端,那麼此時服務端就會回一個
SYN + ACK
報文給客戶端,此報文中的確認號是 91(90+1)。 - 客戶端收到後,發現自己期望收到的確認號應該是 100 + 1,而不是 90 + 1,於是就會回 RST 報文。
- 服務端收到 RST 報文後,就會釋放連接。
- 後續最新的 SYN 抵達了服務端後,客戶端與服務端就可以正常的完成三次握手了。
上述中的「舊 SYN 報文」稱為歷史連接,TCP 使用三次握手建立連接的最主要原因就是防止「歷史連接」初始化了連接。
原因二:同步雙方的初始序列號#
TCP 協議的通信雙方, 都必須維護一個「序列號」, 序列號是可靠傳輸的一個關鍵因素,它的作用:
- 接收方可以去除重複的數據;
- 接收方可以根據數據包的序列號按序接收;
- 可以標識發送出去的數據包中, 哪些是已經被對方收到的(通過 ACK 報文中的序列號知道);
可見,序列號在 TCP 連接中占據著非常重要的作用,所以當客戶端發送攜帶「初始序列號」的 SYN
報文的時候,需要服務端回一個 ACK
應答報文,表示客戶端的 SYN 報文已被服務端成功接收,那當服務端發送「初始序列號」給客戶端的時候,依然也要得到客戶端的應答回應,這樣一來一回,才能確保雙方的初始序列號能被可靠的同步。
四次握手其實也能夠可靠的同步雙方的初始化序號,但由於第二步和第三步可以優化成一步,所以就成了「三次握手」。
而兩次握手只保證了一方的初始序列號能被對方成功接收,沒辦法保證雙方的初始序列號都能被確認接收。
原因三:避免資源浪費#
如果只有「兩次握手」,當客戶端發生的 SYN
報文在網絡中阻塞,客戶端沒有接收到 ACK
報文,就會重新發送 SYN
,由於沒有第三次握手,服務端不清楚客戶端是否收到了自己回复的 ACK
報文,所以服務端每收到一個 SYN
就只能先主動建立一個連接,這會造成什麼情況呢?
如果客戶端發送的 SYN
報文在網絡中阻塞了,重複發送多次 SYN
報文,那麼服務端在收到請求後就會建立多個冗餘的無效鏈接,造成不必要的資源浪費。
即兩次握手會造成消息滯留情況,服務端重複接收無用的連接請求(SYN
報文),而造成重複分配資源。
小結#
TCP 建立連接時,通過三次握手能防止歷史連接的建立,能減少雙方不必要的資源開銷,能幫助雙方同步初始化序列號。序列號能夠保證數據包不重複、不丟棄和按序傳輸。
不使用「兩次握手」和「四次握手」的原因:
- 「兩次握手」:無法防止歷史連接的建立,會造成雙方資源的浪費,也無法可靠的同步雙方序列號;
- 「四次握手」:三次握手已經在理論上滿足最少可靠連接建立的次數,所以不需要使用更多的通信次數。
為什麼每次建立 TCP 連接時,初始化的序列號都要求不一樣呢?#
主要原因有兩個方面:
- 為了防止歷史報文被下個相同四元組的連接接收(主要方面);
- 為了安全性,防止黑客偽造的相同序列號的 TCP 報文被對方接收;
接下來,詳細說說第一點。
假設每次建立連接,客戶端和服務端的初始化序列號都是從 0 開始:
過程如下:
- 客戶端和服務端建立一個 TCP 連接,在客戶端發送數據包被網絡阻塞了,然後超時重傳了這個數據包,而此時服務端設備斷電重啟了,之前與客戶端建立的連接就消失了,於是在收到客戶端的數據包的時候就會發送 RST 報文。
- 緊接著,客戶端又與服務端建立了與上個連接相同四元組的連接;
- 在新連接建立完成後,上個連接中被網絡阻塞的數據包正好抵達了服務端,剛好該數據包的序列號正好是在服務端的接收窗口內,所以該數據包會被服務端正常接收,就會造成數據錯亂。
可以看到,如果每次建立連接,客戶端和服務端的初始化序列號都是一樣的話,很容易出現歷史報文被下個相同四元組的連接接收的問題。
如果每次建立連接客戶端和服務端的初始化序列號都「不一樣」,就有大概率因為歷史報文的序列號「不在」對方接收窗口,從而很大程度上避免了歷史報文,比如下圖:
相反,如果每次建立連接客戶端和服務端的初始化序列號都「一樣」,就有大概率遇到歷史報文的序列號剛「好在」對方的接收窗口內,從而導致歷史報文被新連接成功接收。
所以,每次初始化序列號不一樣很大程度上能夠避免歷史報文被下個相同四元組的連接接收,注意是很大程度上,並不是完全避免了(因為序列號會有回繞的問題,所以需要用時間戳的機制來判斷歷史報文,詳細看這篇:TCP 是如何避免歷史報文的?)。
初始序列號 ISN 是如何隨機產生的?#
起始 ISN
是基於時鐘的,每 4 微秒 + 1,轉一圈要 4.55 個小時。
RFC 793
提到初始化序列號 ISN 隨機生成算法:ISN = M + F(localhost, localport, remotehost, remoteport)
。
M
是一個計時器,這個計時器每隔 4 微秒加 1。F
是一個 Hash 算法,根據源 IP、目的 IP、源端口、目的端口生成一個隨機數值。要保證 Hash 算法不能被外部輕易推算得出,用 MD5 算法是一個比較好的選擇。
可以看到,隨機數是會基於時鐘計時器遞增的,基本不可能會隨機成一樣的初始化序列號。
既然 IP 層會分片,為什麼 TCP 層還需要 MSS (Maximum Segment Size) 呢?#
我們先來認識下 MTU (Maximum Transmission Unit) 和 MSS (Maximum Segment Size):
MTU
:一個網絡包的最大長度,以太網中一般為1500
字節MSS
:除去 IP 和 TCP 頭部之後,一個網絡包所能容納的 TCP 數據的最大長度
第一次握手丟失了,會發生什麼?#
當客戶端想和服務端建立 TCP 連接的時候,首先第一个發的就是 SYN 報文,然後進入到 SYN_SENT
狀態。
在這之後,如果客戶端遲遲收不到服務端的 SYN-ACK 報文(第二次握手),就會觸發「超時重傳」(RTO: Retransmission TimeOut) 機制,重傳 SYN 報文,而且重傳的 SYN 報文的序列號都是一樣的。
不同版本的操作系統可能超时时間不同,有的 1 秒的,也有 3 秒的,這個超时时間是寫死在內核里的,如果想要更改則需要重新編譯內核,比較麻煩。
當客戶端在 1 秒後沒收到服務端的 SYN-ACK 報文後,客戶端就會重發 SYN 報文,那到底重發幾次呢?
在 Linux 裡,客戶端的 SYN 報文最大重傳次數由 tcp_syn_retries
內核參數控制,這個參數是可以自定義的,默認值一般是 5。
cat /proc/sys/net/ipv4/tcp_syn_retries
通常,第一次超時重傳是在 1 秒後,第二次超時重傳是在 2 秒,第三次超時重傳是在 4 秒後,第四次超時重傳是在 8 秒後,第五次是在超時重傳 16 秒後。沒錯,每次超時的時間是上一次的 2 倍。
當第五次超時重傳後,會繼續等待 32 秒,如果服務端仍然沒有回應 ACK,客戶端就不再發送 SYN 包,然後斷開 TCP 連接。
所以,總耗時是 1+2+4+8+16+32=63 秒,大約 1 分鐘左右。
舉個例子,假設 tcp_syn_retries 參數值為 3,那麼當客戶端的 SYN 報文一直在網絡中丟失時,會發生下圖的過程:
具體過程:當客戶端超時重傳 3 次 SYN 報文後,由於 tcp_syn_retries 為 3,已達到最大重傳次數,於是再等待一段時間(時間為上一次超时时間的 2 倍),如果還是沒能收到服務端的第二次握手(SYN-ACK 報文),那麼客戶端就會斷開連接。****
第二次握手丟失了,會發生什麼?#
當服務端收到客戶端的第一次握手後,就會回 SYN-ACK 報文給客戶端,這個就是第二次握手,此時服務端會進入 SYN_RCVD
狀態。
第二次握手的 SYN-ACK
報文其實有兩個目的 :
- 第二次握手裡的 ACK, 是對第一次握手的確認報文
- 第二次握手裡的 SYN,是服務端發起建立 TCP 連接的報文
所以,如果第二次握手丟了,就會發生比較有意思的事情,具體會怎麼樣呢?
- 因為第二次握手的報文裡,包含對客戶端的第一次握手的 ACK 確認報文,所以,如果客戶端遲遲沒有收到第二次握手,那麼客戶端就會覺得可能自己的 SYN 報文(第一次握手)丟失了,於是客戶端就會觸發超時重傳機制,重傳 SYN 報文。
- 然後,因為第二次握手中包含服務端的 SYN 報文,所以當客戶端收到後,需要給服務端發送 ACK 確認報文(第三次握手),服務端才會認為該 SYN 報文被客戶端收到了。如果第二次握手丟失了,服務端就收不到第三次握手,於是服務端會觸發超時重傳機制,重傳 SYN-ACK 報文。
在 Linux 下,SYN-ACK 報文的最大重傳次數由 tcp_synack_retries
內核參數決定,默認值是 5。
cat /proc/sys/net/ipv4/tcp_synack_retries
因此,當第二次握手丟失了,客戶端和服務端都會重傳:
- 客戶端會重傳 SYN 報文,也就是第一次握手,最大重傳次數由
tcp_syn_retries
內核參數決定 - 服務端會重傳 SYN-ACK 報文,也就是第二次握手,最大重傳次數由
tcp_synack_retries
內核參數決定
舉個例子,假設 tcp_syn_retries 參數值為 1,tcp_synack_retries 參數值為 2,那麼當第二次握手一直丟失時,發生的過程如下圖:
具體過程:
- 當客戶端超時重傳 1 次 SYN 報文後,由於 tcp_syn_retries 為 1,已達到最大重傳次數,於是再等待一段時間(時間為上一次超时时間的 2 倍),如果還是沒能收到服務端的第二次握手(SYN-ACK 報文),那麼客戶端就會斷開連接。
- 當服務端超時重傳 2 次 SYN-ACK 報文後,由於 tcp_synack_retries 為 2,已達到最大重傳次數,於是再等待一段時間(時間為上一次超时时間的 2 倍),如果還是沒能收到客戶端的第三次握手(ACK 報文),那麼服務端就會斷開連接。
當第三次握手丟失了,會發生什麼?#
客戶端收到服務端的 SYN-ACK 報文後,就會給服務端回一個 ACK 報文,也就是第三次握手,此時客戶端狀態進入到 ESTABLISH
狀態。
因為這個第三次握手的 ACK 是對第二次握手的 SYN 的確認報文,所以當第三次握手丟失了,服務端遲遲收不到確認報文,就會觸發超時重傳機制,重傳 SYN-ACK 報文,直到收到第三次握手,或達到最大重傳次數。
注意,第三次握手中的 ACK 報文是無法重傳的,當 ACK 丟失了,就由對方(服務器)重傳對應的報文(第二次握手 SYN-ACK)。
舉個例子,假設 tcp_synack_retries 參數值為 2,那麼當第三次握手一直丟失時,發生的過程如下圖:
具體過程:當服務端超時重傳 2 次 SYN-ACK 報文後,由於 tcp_synack_retries 為 2,已達到最大重傳次數,於是再等待一段時間(時間為上一次超时时間的 2 倍),如果還是沒能收到客戶端的第三次握手(ACK 報文),那麼服務端就會斷開連接。
什麼是 SYN 攻擊?如何避免 SYN 攻擊?#
我們都知道 TCP 連接建立是需要三次握手,假設攻擊者短時間偽造不同 IP 地址的 SYN
報文,服務端每接收到一個 SYN
報文,就進入SYN_RCVD
狀態,但服務端發送出去的 ACK + SYN
報文,無法得到未知 IP 主機的 ACK
應答,久而久之就會占滿服務端的半連接隊列,使得服務端不能為正常用戶服務。
我們先來看看 Linux 內核的 SYN
隊列(半連接隊列)與 Accpet
隊列(全連接隊列)是如何工作的?
正常流程:
- 當服務端接收到客戶端的 SYN 報文時,會創建一個半連接的對象,然後將其加入到內核的「 SYN 隊列」;
- 接著服務端發送 SYN + ACK 給客戶端,等待客戶端回應 ACK 報文;
- 服務端接收到 ACK 報文後,從「 SYN 隊列」取出一個半連接對象,然後創建一個新的連接對象放入到「 Accept 隊列」;
- 應用通過調用
accpet()
socket 接口,從「 Accept 隊列」取出連接對象。
不管是半連接隊列還是全連接隊列,都有最大長度限制,超過限制時,默認情況下都會丟棄報文。
SYN 攻擊方式最直接的表現就會把 TCP 半連接隊列打滿,這樣當 TCP 半連接隊列滿了,後續再在收到 SYN 報文就會丟棄,導致客戶端無法和服務端建立連接。
如何避免 SYN 攻擊#
避免 SYN 攻擊方式,可以有以下四種方法:
- 調大 netdev_max_backlog;
- 增大 TCP 半連接隊列;
- 開啟 tcp_syncookies;
- 減少 SYN+ACK 重傳次數
方法一:調大 netdev_max_backlog#
當網卡接收數據包的速度大於內核處理的速度時,會有一個隊列保存這些數據包。控制該隊列的最大值如下參數,默認值是 1000,我們要適當調大該參數的值,比如設置為 10000:
net.core.netdev_max_backlog = 10000
方法二:增大 TCP 半連接隊列#
增大 TCP 半連接隊列,要同時增大下面這三個參數:
- 增大 net.ipv4.tcp_max_syn_backlog
- 增大 listen () 函數中的 backlog
- 增大 net.core.somaxconn
具體為什麼是三個參數決定 TCP 半連接隊列的大小,可以看這篇:可以看這篇:TCP 半連接隊列和全連接隊列滿了會發生什麼?又該如何應對?
方法三:開啟 net.ipv4.tcp_syncookies#
開啟 syncookies 功能就可以在不使用 SYN 半連接隊列的情況下成功建立連接,相當於繞過了 SYN 半連接來建立連接。
具體過程:
- 當 「 SYN 隊列」滿之後,後續服務端收到 SYN 包,不會丟棄,而是根據算法,計算出一個
cookie
值; - 將 cookie 值放到第二次握手報文的「序列號」裡,然後服務端回第二次握手給客戶端;
- 服務端接收到客戶端的應答報文時,服務端會檢查這個 ACK 包的合法性。如果合法,將該連接對象放入到「 Accept 隊列」。
- 最後應用程序通過調用
accpet()
接口,從「 Accept 隊列」取出連接。
可以看到,當開啟了 tcp_syncookies 了,即使受到 SYN 攻擊而導致 SYN 隊列滿時,也能保證正常的連接成功建立。
net.ipv4.tcp_syncookies 參數主要有以下三個值:
- 0 值,表示關閉該功能;
- 1 值,表示僅當 SYN 半連接隊列放不下時,再啟用它;
- 2 值,表示無條件開啟功能;
在應對 SYN 攻擊時,只需要設置為 1 即可。
echo 1 > /proc/sys/net/ipv4/tcp_syncookies
方法四:減少 SYN+ACK 重傳次數#
當服務端受到 SYN 攻擊時,就會有大量處於 SYN_REVC
狀態的 TCP 連接,處於這個狀態的 TCP 會重傳 SYN+ACK ,當重傳超過次數達到上限後,就會斷開連接。
那麼針對 SYN 攻擊的場景,我們可以減少 SYN-ACK 的重傳次數,以加快處於 SYN_REVC
狀態的 TCP 連接斷開。
SYN-ACK 報文的最大重傳次數由 tcp_synack_retries
內核參數決定(默認值是 5 次),比如將 tcp_synack_retries 減少到 2 次:
echo 2 > /proc/sys/net/ipv4/tcp_synack_retries
TCP 連接斷開#
TCP 四次揮手過程是怎樣的?#
天下沒有不散的宴席,對於 TCP 連接也是這樣, TCP 斷開連接是通過四次揮手方式。
雙方都可以主動斷開連接,斷開連接後主機中的「資源」將被釋放,四次揮手的過程如下圖:
流程如下:
- 客戶端打算關閉連接,此時會發送一個 TCP 首部
FIN
標志位被置為1
的報文,也即FIN
報文,之後客戶端進入FIN_WAIT_1
狀態。 - 服務端收到該報文後,就向客戶端發送
ACK
應答報文,接著服務端進入CLOSE_WAIT
狀態。 - 客戶端收到服務端的
ACK
應答報文後,之後進入FIN_WAIT_2
狀態。 - 等待服務端處理完數據後,也向客戶端發送
FIN
報文,之後服務端進入LAST_ACK
狀態。 - 客戶端收到服務端的
FIN
報文後,回一個ACK
應答報文,之後進入TIME_WAIT
狀態 - 服務端收到了
ACK
應答報文後,就進入了CLOSE
狀態,至此服務端已經完成連接的關閉。 - 客戶端在經過
2MSL
一段時間後,自動進入CLOSE
狀態,至此客戶端也完成連接的關閉。
可以看到,每個方向都需要一個 FIN 和一個 ACK,因此通常被稱為四次揮手。
需要注意的是:主動關閉連接的,才有 TIME_WAIT 狀態。
為什麼揮手需要四次?#
再來回顧下四次揮手雙方發 FIN
包的過程,就能理解為什麼需要四次了。
- 關閉連接時,客戶端向服務端發送
FIN
時,僅僅表示客戶端不再發送數據但是還能接收數據。 - 服務端收到客戶端的
FIN
報文時,先回一個ACK
應答報文,而服務端可能還有數據需要處理和發送,等服務端不再發送數據時,才發送FIN
報文給客戶端來表示同意現在關閉連接。
從上面過程可知,服務端通常需要等待完成數據的發送和處理,所以服務端的 ACK
和 FIN
一般都會分開發送,因此是需要四次揮手。
但是在特定情況下,四次揮手是可以變成三次揮手的,具體情況可以看這篇:TCP 四次揮手,可以變成三次嗎?
第一次揮手失敗了,會發生什麼?#
當客戶端(主動關閉方)調用 close 函數後,就會向服務端發送 FIN 報文,試圖與服務端斷開連接,此時客戶端的連接進入到 FIN_WAIT_1
狀態。
正常情況下,如果能及時收到服務端(被動關閉方)的 ACK,則會很快變為 FIN_WAIT2
狀態。
如果第一次揮手丟失了,那麼客戶端遲遲收不到被動方的 ACK 的話,也就會觸發超時重傳機制,重傳 FIN 報文,重發次數由 tcp_orphan_retries
參數控制。
當客戶端重傳 FIN 報文的次數超過 tcp_orphan_retries
後,就不再發送 FIN 報文,則會在等待一段時間(時間為上一次超時的 2 倍),如果還是沒能收到第二次揮手,那麼直接進入到 close
狀態。
舉個例子,假設 tcp_orphan_retries 參數值為 3,當第一次揮手一直丟失時,發生的過程如下圖:
具體過程:當客戶端超時重傳 3 次 FIN 報文後,由於 tcp_orphan_retries 為 3,已達到最大重傳次數,於是再等待一段時間(時間為上一次超時的 2 倍),如果還是沒能收到服務端的第二次揮手(ACK 報文),那麼客戶端就會斷開連接。
第二次揮手丟失了,會發生什麼?#
當服務端收到客戶端的第一次揮手後,就會先回一個 ACK 確認報文,此時服務端的連接進入到 CLOSE_WAIT
狀態。
在前面我們也提了,ACK 報文是無法重傳的,所以如果服務端的第二次揮手丟失了,客戶端就會觸發超時重傳機制,重傳 FIN 報文,直到收到服務端的第二次揮手,或者達到最大的重傳次數。
舉個例子,假設 tcp_orphan_retries 參數值為 2,當第二次揮手一直丟失時,發生的過程如下圖:
具體過程:當客戶端超時重傳 2 次 FIN 報文後,由於 tcp_orphan_retries 為 2,已達到最大重傳次數,於是再等待一段時間(時間為上一次超時的 2 倍),如果還是沒能收到服務端的第二次揮手(ACK 報文),那麼客戶端就會斷開連接。
這裡提一下,當客戶端收到第二次揮手,也就是收到服務端發送的 ACK 報文後,客戶端就會處於 FIN_WAIT2
狀態,在這個狀態需要等服務端發送第三次揮手,也就是服務端的 FIN 報文。
對於調用 close 函數關閉的連接,由於無法再發送和接收數據,所以FIN_WAIT2
狀態不可以持續太久,而 tcp_fin_timeout
控制了這個狀態下連接的持續時長,默認值是 60 秒。
這意味著對於調用 close 關閉的連接,如果在 60 秒後還沒有收到 FIN 報文,客戶端(主動關閉方)的連接就會直接關閉,如下圖:
但注意,如果主動關閉方使用 shutdown() 函數關閉連接,指定了只關閉發送方向,而接收方向並沒有關閉,那麼意味著主動關閉方還是可以接收數據的。
此時,如果主動關閉方一直沒收到第三次揮手,那麼主動關閉方的連接將會一直處於 FIN_WAIT2
狀態(tcp_fin_timeout
無法控制 shutdown 關閉的連接)。如下圖:
第三次揮手丟失了,會發生什麼?#
當 ** 服務端(被動關閉方)** 收到客戶端(主動關閉方)的 FIN 報文後,內核會自動回复 ACK,同時連接處於 CLOSE_WAIT
狀態,顧名思義,它表示等待應用進程調用 close 函數關閉連接。
此時,內核沒有權利替代進程關閉連接,必須由進程主動調用 close 函數來觸發服務端發送 FIN 報文。
服務端處於 CLOSE_WAIT 狀態時,調用了 close 函數,內核就會發出 FIN 報文,同時連接進入 LAST_ACK 狀態,等待客戶端返回 ACK 來確認連接關閉。
如果遲遲收不到這個 ACK,服務端就會重發 FIN 報文,重發次數仍然由 tcp_orphan_retries
參數控制,這與客戶端重發 FIN 報文的重傳次數控制方式是相同的。
舉個例子,假設 tcp_orphan_retries
= 3,當第三次揮手一直丟失時,發生的過程如下圖:
具體過程:
- 當服務端重傳第三次揮手報文的次數達到了 3 次後,由於 tcp_orphan_retries 為 3,達到了重傳最大次數,於是再等待一段時間(時間為上一次超時的 2 倍),如果還是沒能收到客戶端的第四次揮手(ACK 報文),那麼服務端就會斷開連接。
- 客戶端因為是通過 close 函數關閉連接的,處於 FIN_WAIT_2 狀態是有時長限制的,如果 tcp_fin_timeout 時間內還是沒能收到服務端的第三次揮手(FIN 報文),那麼客戶端就會斷開連接。
第四次揮手丟失了,會發生什麼?#
當客戶端收到服務端的第三次揮手的 FIN 報文後,就會回 ACK 報文,也就是第四次揮手,此時客戶端連接進入 TIME_WAIT
狀態。
在 Linux 系統,TIME_WAIT 狀態會持續 2MSL 後才會進入關閉狀態。
然後,** 服務端(被動關閉方)** 沒有收到 ACK 報文前,還是處於 LAST_ACK 狀態。
如果第四次揮手的 ACK 報文沒有到達服務端,服務端就會重發 FIN 報文,重發次數仍然由前面介紹過的 tcp_orphan_retries
參數控制。
舉個例子,假設 tcp_orphan_retries 為 2,當第四次揮手一直丟失時,發生的過程如下:
具體過程:
- 當服務端重傳第三次揮手報文達到 2 時,由於 tcp_orphan_retries 為 2,達到了最大重傳次數,於是再等待一段時間(時間為上一次超時的 2 倍),如果還是沒能收到客戶端的第四次揮手(ACK 報文),那麼服務端就會斷開連接。
- 客戶端在收到第三次揮手後,就會進入 TIME_WAIT 狀態,開啟時長為 2MSL 的定時器,如果途中再次收到第三次揮手(FIN 報文)後,就會重置定時器,當等待 2MSL 時長後,客戶端就會斷開連接。
為什麼 TIME_WAIT 等待的時間是 2MSL?#
MSL
是 Maximum Segment Lifetime,報文最大生存時間,它是任何報文在網絡上存在的最長時間,超過這個時間報文將被丟棄。因為 TCP 報文基於是 IP 協議的,而 IP 頭中有一個 TTL
字段,是 IP 數據報可以經過的最大路由數,每經過一個處理他的路由器此值就減 1,當此值為 0 則數據報將被丟棄,同時發送 ICMP 報文通知源主機。
MSL 與 TTL 的區別: MSL 的單位是時間,而 TTL 是經過路由跳數。所以 MSL 應該要大於等於 TTL 消耗為 0 的時間,以確保報文已被自然消亡。
TTL 的值一般是 64,Linux 將 MSL 設置為 30 秒,意味著 Linux 認為數據報文經過 64 個路由器的時間不會超過 30 秒,如果超過了,就認為報文已經消失在網絡中了。
TIME_WAIT 等待 2 倍的 MSL,比較合理的解釋是: 網絡中可能存在來自發送方的數據包,當這些發送方的數據包被接收方處理後又會向對方發送響應,所以一來一回需要等待 2 倍的時間。
比如,如果被動關閉方沒有收到斷開連接的最後的 ACK 報文,就會觸發超時重發 FIN
報文,另一方接收到 FIN 後,會重發 ACK 給被動關閉方, 一來一去正好 2 個 MSL。
可以看到 2MSL 時長 這其實是相當於至少允許報文丟失一次。比如,若 ACK 在一個 MSL 內丟失,這樣被動方重發的 FIN 會在第 2 個 MSL 內到達,TIME_WAIT 狀態的連接可以應對。
為什麼不是 4 或者 8 MSL 的時長呢?你可以想象一個丟包率達到百分之一的糟糕網絡,連續兩次丟包的概率只有萬分之一,這個概率實在是太小了,忽略它比解決它更具性價比。
2MSL
的時間是從客戶端接收到 FIN 後發送 ACK 開始計時的。如果在 TIME-WAIT 時間內,因為客戶端的 ACK 沒有傳輸到服務端,客戶端又接收到了服務端重發的 FIN 報文,那麼 2MSL 時間將重新計時。
在 Linux 系統裡 2MSL
默認是 60
秒,那麼一個 MSL
也就是 30
秒。Linux 系統停留在 TIME_WAIT 的時間為固定的 60 秒。
其定義在 Linux 內核代碼裡的名稱為 TCP_TIMEWAIT_LEN:
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT state, about 60 seconds */
如果要修改 TIME_WAIT 的時間長度,只能修改 Linux 內核代碼裡 TCP_TIMEWAIT_LEN 的值,並重新編譯 Linux 內核。
為什麼需要 TIME_WAIT 狀態?#
主動發起關閉連接的一方,才會有 TIME-WAIT
狀態。
需要 TIME-WAIT
狀態,主要是兩個原因:
- 防止歷史連接中的數據,被後面相同四元組的連接錯誤的接收
- 保證「被動關閉連接」的一方,能被正確的關閉