網路與 IP

TCP 三次握手過程詳解:四次揮手與連線狀態怎麼看

TCP 三次握手是建立連線的三個報文:客戶端發 SYN,伺服器回 SYN-ACK,客戶端再回 ACK。走完這三步,雙方都確認了對方的初始序號和兩個方向的收發能力。關閉連線一般要四個報文,先關閉的一方最後停在 TIME_WAIT;伺服器上各狀態的連線有多少,用 ss -tan 就能統計出來。

作者 頂聯產品團隊發布於 7 分鐘閱讀

TCP 三次握手是建立一條 TCP 連線時交換的三個報文:客戶端傳送 SYN 請求建立連線,伺服器回覆 SYN-ACK 表示同意並帶上自己的初始序號,客戶端再回一個 ACK 確認。三個包走完,雙方都拿到了對方的初始序號,也都確認了兩個方向能正常收發,連線進入 ESTABLISHED,之後才開始傳資料。關閉連線用四次揮手:兩個方向各發一個 FIN、各回一個 ACK。握手和揮手過程中,兩端會先後經過 SYN_SENT、SYN_RECV、FIN_WAIT_1、TIME_WAIT 等狀態。維運看連線狀態的意義就在這裡:哪種狀態大量堆積,問題就大致出在握手或揮手的哪一步、哪一方。

TCP 三次握手過程:三個包各帶什麼

TCP 首部裡有 SYN、ACK、FIN、RST 等標誌位,還有 32 位的序號(seq)和確認號(ack)。三次握手就是用這幾個欄位完成的,現行標準是 2022 年釋出的 RFC 9293(取代了 1981 年的 RFC 793):

步驟 方向 標誌位 序號 seq 確認號 ack 發出後傳送方的狀態
1 客戶端 → 伺服器 SYN x,客戶端隨機生成的初始序號 — SYN_SENT
2 伺服器 → 客戶端 SYN + ACK y,伺服器的初始序號 x + 1 SYN_RECV
3 客戶端 → 伺服器 ACK x + 1 y + 1 ESTABLISHED;伺服器收到後也進入 ESTABLISHED
客戶端                                              伺服器
(CLOSED)                                           (LISTEN)
   |------ SYN, seq=x ---------------------------------->|
(SYN_SENT)                                         (SYN_RECV)
   |<----- SYN+ACK, seq=y, ack=x+1 ----------------------|
(ESTABLISHED)
   |------ ACK, seq=x+1, ack=y+1 ----------------------->|
                                                   (ESTABLISHED)

幾個容易忽略的細節:

  • 普通握手的 SYN 不帶資料,但佔用一個序號,所以對方的確認號是 x + 1。後面的 FIN 也一樣佔一個序號。
  • 連線引數只在握手時協商。雙方在 SYN 和 SYN-ACK 的選項裡交換 MSS(單個報文能裝的最大資料量)、視窗擴大因子、是否支援 SACK 和時間戳,連線建立後不能再改。
  • 為什麼不是兩次。只有兩次的話,伺服器發出 SYN-ACK 就認為連線已經建立,卻無法確認客戶端收到了自己的初始序號;網路裡滯留的舊 SYN 遲到時,伺服器還會為一個客戶端早已放棄的連線分配資源。有了第三步,客戶端收到一個對不上號的 SYN-ACK 時可以回 RST 把它作廢。

用 tcpdump 抓一次 curl http://203.0.113.80/,握手是這樣的:

tcpdump -i eth0 -nn 'host 203.0.113.80 and tcp port 80'
14:02:11.401203 IP 198.51.100.77.40112 > 203.0.113.80.80: Flags [S], seq 3205918734, win 64240, options [mss 1460,sackOK,TS val 1771032 ecr 0,nop,wscale 7], length 0
14:02:11.429870 IP 203.0.113.80.80 > 198.51.100.77.40112: Flags [S.], seq 1180237765, ack 3205918735, win 65160, options [mss 1460,sackOK,TS val 905321 ecr 1771032,nop,wscale 7], length 0
14:02:11.429912 IP 198.51.100.77.40112 > 203.0.113.80.80: Flags [.], ack 1, win 502, options [nop,nop,TS val 1771061 ecr 905321], length 0

[S] 是 SYN,[S.] 是 SYN-ACK(點代表 ACK 標誌),[.] 是純 ACK。第二個包的 ack 正好是第一個包的 seq 加 1;第三行起 tcpdump 改用相對序號,ack 1 就是 y + 1。第一個包到第二個包相隔約 28.7 毫秒,這就是客戶端到伺服器的一次往返時間(RTT),也是每建立一條 TCP 連線至少要多等的時間。

握手中丟了一個包會怎樣

握手的三個包都可能丟,丟在哪一步,兩端看到的現象不同:

丟失的包 客戶端看到 伺服器看到 核心怎麼處理
第 1 個 SYN 停在 SYN_SENT 沒有任何記錄 客戶端按逐次加長的間隔重發(經典實現是 1、2、4、8 秒……翻倍),次數由 net.ipv4.tcp_syn_retries 決定,預設 6 次,常見核心上約兩分鐘後報 Connection timed out;具體間隔和總時長隨核心版本不同
第 2 個 SYN-ACK 停在 SYN_SENT,繼續重發 SYN SYN_RECV 伺服器按 tcp_synack_retries(預設 5 次)重發 SYN-ACK,約 63 秒後放棄;客戶端重發的 SYN 到達時,伺服器也會再回一次 SYN-ACK
第 3 個 ACK 已經 ESTABLISHED,開始發請求 SYN_RECV 客戶端的資料包本身帶 ACK 標誌和確認號,伺服器收到後照樣完成建連;客戶端一直不發資料時,伺服器重傳 SYN-ACK,讓客戶端補發 ACK
連接埠上沒有程式監聽 立即報 Connection refused 核心直接回 RST 不重試

所以“連線超時”和“連線被拒絕”是兩種完全不同的結論:前者是包在路上或被防火牆丟棄了,後者是包到了伺服器、但連接埠上沒有服務。測連接埠時給 nc -zv -w 3 203.0.113.80 80 加上超時引數,就不必乾等兩分鐘。

四次揮手:關閉連線為什麼要四個包

TCP 是全雙工的,兩個方向各自獨立關閉。FIN 的意思是“我這邊沒有資料要發了”,但還能繼續接收,所以每個方向都要一個 FIN 和一個 ACK:

主動關閉方(先呼叫 close)                           被動關閉方
(ESTABLISHED)                                      (ESTABLISHED)
   |------ FIN, seq=u ---------------------------------->|
(FIN_WAIT_1)                                       (CLOSE_WAIT) 核心自動回 ACK,等程式呼叫 close
   |<----- ACK, ack=u+1 ---------------------------------|
(FIN_WAIT_2)
   |            (被動方此時還可以把剩下的資料發完)
   |<----- FIN, seq=v -----------------------------------|
(TIME_WAIT)                                        (LAST_ACK)
   |------ ACK, ack=v+1 -------------------------------->|
   | 等待 2MSL,Linux 為 60 秒                          (CLOSED)
(CLOSED)

實際排查時要記住三點:

  1. 誰先呼叫 close,誰就是主動關閉方,與誰是客戶端無關。TIME_WAIT 留在主動關閉方,CLOSE_WAIT 出現在被動關閉方。
  2. 抓包常常只看到三個包。被動方收到 FIN 時如果也沒有資料要發,會把 ACK 和自己的 FIN 合成一個包,揮手就變成 [F.]、[F.]、[.] 三個包。下面是上例連線關閉時的輸出(省略了 options 欄位):客戶端發了 75 位元組請求,伺服器回了 2295 位元組回應,FIN 各佔一個序號,所以最後的確認號是 2297。
14:02:11.512330 IP 198.51.100.77.40112 > 203.0.113.80.80: Flags [F.], seq 76, ack 2296, win 501, length 0
14:02:11.540877 IP 203.0.113.80.80 > 198.51.100.77.40112: Flags [F.], seq 2296, ack 77, win 501, length 0
14:02:11.540921 IP 198.51.100.77.40112 > 203.0.113.80.80: Flags [.], ack 2297, win 501, length 0
  1. RST 是另一種關閉。程式設定了 SO_LINGER 為 0、向已關閉的連線寫資料、中間的防火牆或負載均衡判定連線超時,都會發 RST 直接終止連線,不經過揮手,也不留 TIME_WAIT,應用側報的是 Connection reset by peer。雙方同時發 FIN 時,兩邊都會短暫進入 CLOSING 狀態,很少見。

TCP 連線狀態表:11 種狀態分別代表什麼

狀態(netstat 寫法) ss 寫法 出現在哪一方 含義 正常停留多久 大量堆積說明
LISTEN LISTEN 伺服器 連接埠在監聽,等待連線 一直存在 不會堆積,數量等於監聽的套接字數
SYN_SENT SYN-SENT 發起方 已發 SYN,等 SYN-ACK 一個往返 目標不可達,或 SYN 被對方、中間防火牆、出方向策略丟棄
SYN_RECV SYN-RECV 伺服器 已回 SYN-ACK,等第三個 ACK 一個往返 SYN Flood、掃描,或 SYN-ACK 回不去
ESTABLISHED ESTAB 雙方 連線已建立,正在收發資料 連線存續期間 併發高,或長連線沒有及時釋放
FIN_WAIT1 FIN-WAIT-1 主動關閉方 已發 FIN,等對方確認 一個往返 對方無回應,或對方接收視窗為 0、資料發不完
FIN_WAIT2 FIN-WAIT-2 主動關閉方 FIN 已被確認,等對方的 FIN 取決於對方程式;本機程式已關閉時最多 tcp_fin_timeout,預設 60 秒 對方程式遲遲不關閉連線
CLOSE_WAIT CLOSE-WAIT 被動關閉方 收到 FIN,等本機程式呼叫 close 應該很短 本機程式沒有關閉連線,多為程式碼缺陷
LAST_ACK LAST-ACK 被動關閉方 已發 FIN,等最後一個 ACK 一個往返 對方已經消失,本機在重傳 FIN
CLOSING CLOSING 雙方 雙方同時發了 FIN 一個往返 極少見
TIME_WAIT TIME-WAIT 主動關閉方 關閉已完成,等舊報文過期 Linux 固定 60 秒 本機主動關閉的短連線多
CLOSED — — 沒有連線 — 工具裡不顯示

有一組對應關係很有用:一端的 FIN_WAIT2 對著另一端的 CLOSE_WAIT。本機看到大量 FIN_WAIT2,去對端查 CLOSE_WAIT,問題在對端程式沒關連線。

用 ss 和 netstat 統計連線狀態

# 彙總:estab 是已建立的連線,timewait 單獨列出
ss -s
# 按狀態計數,從多到少排列
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
# 老系統沒有 ss 時
netstat -ant | awk '/^tcp/ {s[$6]++} END {for (k in s) print s[k], k}' | sort -rn
# 只數一種狀態,-H 去掉表頭,結果就是條數
ss -Htan state time-wait | wc -l
ss -Htan state syn-recv '( sport = :443 )' | wc -l
# CLOSE_WAIT 屬於哪個程序
ss -tanp state close-wait

ss -s 的第二行大致如下:

TCP:   38107 (estab 1203, closed 36740, orphaned 12, timewait 36722)

closed 包含了 timewait;orphaned 是程式已經關閉、但核心還在替它走揮手流程的連線。用 ss 按 state 過濾時,輸出裡不再有狀態列,第 3 列是本地位址,第 4 列是對端位址,下文的 awk 命令按這個順序取欄位。Windows 伺服器在 PowerShell 裡執行 Get-NetTCPConnection | Group-Object State | Sort-Object Count -Descending | Select-Object Count, Name,得到的是同樣的分佈。

數字要連續看幾次,或者用 watch -n 5 'ss -s | head -2' 觀察趨勢:穩定在某個量級通常是業務特徵,持續單向增長才是問題。

SYN_RECV 堆積、TIME_WAIT 過多、CLOSE_WAIT 不降:怎麼判斷

SYN_RECV 堆積

每條半連線本該在一個往返之內變成 ESTABLISHED,所以用上面的命令數出來的 SYN_RECV 平時很少。明顯偏多時先看來源:來源散佈在大量網段、nstat -az TcpExtSyncookiesSent 同步上漲,是 SYN Flood,要靠 SYN Cookie、閘道器 SYN 代理和上游清洗;來源只有少數幾個位址,多是掃描或異常客戶端,按位址封禁或限速即可。

還有一種情況常被當成攻擊:SYN_RECV 數量不大,卻一直不消失,反覆出現的是同一批真實客戶,客戶那邊報連線超時。這多半是 SYN-ACK 回不去:伺服器配了多個 IP 或接了多條線路,回包按主路由表從另一個閘道器出去,被上游的源位址校驗丟掉。在伺服器上看 SYN-ACK 實際從哪張網路卡發出,再看從這個位址回包時路由表選了哪個閘道器:

tcpdump -nni any 'tcp[tcpflags] & (tcp-syn|tcp-ack) == (tcp-syn|tcp-ack)' -c 10
ip route get 198.51.100.77 from 203.0.113.80

兩者對不上時,用源位址策略路由讓每個位址的回包走自己的閘道器。假設 203.0.113.80 配在 eth1 上,它的閘道器是 203.0.113.1:

ip route add default via 203.0.113.1 dev eth1 table 100
ip rule add from 203.0.113.80 table 100

TIME_WAIT 過多

先做一個估算:TIME_WAIT 數 ≈ 每秒由本機主動關閉的連線數 × 60。上面 ss -s 裡的 36,722 條,相當於每秒約 612 次主動關閉。數字本身說明不了問題,關鍵是看它們屬於哪一類連線:

# 按本地連接埠統計:集中在 80、443,是伺服器主動關閉的入站連線
ss -Htan state time-wait | awk '{n=split($3,a,":"); print a[n]}' | sort | uniq -c | sort -rn | head
# 按對端位址統計:集中在同一個 IP:連接埠,是本機向外發起的短連線
ss -Htan state time-wait | awk '{print $4}' | sort | uniq -c | sort -rn | head
現象 判斷 怎麼處理
集中在本機 80、443 等監聽連接埠 伺服器主動關閉入站連線留下的,不佔用臨時連接埠,每條只佔很少的記憶體 一般不用處理;數量異常大時,檢查 HTTP keep-alive 是否被關閉
集中在同一個後端位址和連接埠,如 10.0.0.12:3306 本機頻繁新建短連線存取後端 預設臨時連接埠範圍 32768–60999 共 28,232 個,按每條佔 60 秒算,對同一個目標持續超過約每秒 470 次新連線就會耗盡連接埠;改用長連線或連線池,tcp_tw_reuse 和擴大連接埠範圍只作補充
核心日誌出現 TCP: time wait bucket table overflow TIME_WAIT 達到 net.ipv4.tcp_max_tw_buckets 上限,新關閉的連線直接跳過 TIME_WAIT 核心文件明確不建議人為調低這個值,記憶體充足時可以調大

常見誤區是調 net.ipv4.tcp_fin_timeout 來縮短 TIME_WAIT。這個引數管的是 FIN_WAIT2,對 TIME_WAIT 的 60 秒沒有作用。

CLOSE_WAIT 不降

CLOSE_WAIT 沒有核心超時,程式不呼叫 close,它就一直存在,每條佔著一個檔案描述符。數量持續上漲,最終會出現 Too many open files,新連線全部失敗。

ss -Htanp state close-wait | grep -o 'pid=[0-9]*' | sort | uniq -c | sort -rn   # 哪個程序的最多
ls /proc/2231/fd | wc -l                                                     # 這個程序開啟的檔案數
grep 'open files' /proc/2231/limits                                          # 它的上限

對端通常是資料庫或上游介面:對方因空閒超時先關閉了連線(例如 MySQL 的 wait_timeout,預設 28800 秒),本機的連線池沒有察覺,也沒有關閉。重啟程序只能暫時清掉,根治要改程式:異常分支也要關閉連線,連線池在取用前檢測連線是否有效。

在管理系統裡怎麼落地

連線狀態出問題時,SSH 有時也會一起登不上:SSH 本身就是一條 TCP 連線,SYN Flood 把頻寬或連線跟蹤表打滿、系統記憶體或全域性檔案控制代碼耗盡時,它同樣建不起來。這時需要一條不依賴伺服器網路棧的通道。Toplink DCIM 的 IPMI 遠端管理透過 BMC 開啟 VNC 控制檯,走的是帶外管理網,伺服器業務網路卡上的連線再滿也不受影響,登入控制檯後就能執行上面的 ss 命令定位問題。租用伺服器的客戶可以在客戶自助端自己開啟控制檯,並檢視伺服器的流量和被攻擊事件記錄:SYN_RECV 突然暴漲時,先對照同一時段有沒有攻擊記錄,就能區分是被攻擊還是自身的回程問題。

常見問題

三次握手的第三個 ACK 能攜帶資料嗎?

協議允許。第三個包可以和客戶端的第一段請求資料合在一起發出,實際是否合併取決於系統實現和應用發資料的時機。讓第一個 SYN 就攜帶資料則需要 TCP Fast Open,客戶端和伺服器都要支援並開啟。

TIME_WAIT 為什麼要等 2MSL?

MSL 是報文在網路中的最長生存時間。等 2MSL 有兩個目的:最後一個 ACK 丟失時,對方重發的 FIN 還能得到應答;這條連線殘留在網路裡的舊報文全部過期,不會被同一組位址和連接埠的新連線誤收。RFC 793 建議的 MSL 是 2 分鐘,Linux 沒有按它計算,而是直接把 TIME_WAIT 定為 60 秒。

握手時的初始序號為什麼不從 0 開始?

初始序號由系統按 RFC 6528 的方法隨機生成:一是避免與同一組位址和連接埠上舊連線的殘留報文混淆,二是讓攻擊者難以猜中序號、偽造報文插進連線。tcpdump 裡看到的 seq 1、ack 1 是它換算出的相對序號,加 -S 引數才顯示真實值。

想看看在你的機房裡怎麼用?

從一次產品示範開始,梳理你的業務鏈路。

查看價格
服務熱線 400-112-2951

讓我們聊聊你的 IDC 業務

掃碼新增企業微信,預約產品示範或申請試用。

Toplink 企業微信二維碼

長按儲存或使用企業微信 / 微信掃描

也可致電
400-112-2951
Telegram
@TopLink88