网络与 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