TCP 三次握手過程詳解:四次揮手與連線狀態怎麼看
TCP 三次握手是建立連線的三個報文:客戶端發 SYN,伺服器回 SYN-ACK,客戶端再回 ACK。走完這三步,雙方都確認了對方的初始序號和兩個方向的收發能力。關閉連線一般要四個報文,先關閉的一方最後停在 TIME_WAIT;伺服器上各狀態的連線有多少,用 ss -tan 就能統計出來。
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)
實際排查時要記住三點:
- 誰先呼叫 close,誰就是主動關閉方,與誰是客戶端無關。TIME_WAIT 留在主動關閉方,CLOSE_WAIT 出現在被動關閉方。
- 抓包常常只看到三個包。被動方收到 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
- 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 引數才顯示真實值。