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 参数才显示真实值。