TCP 三次握手、四次挥手这个事儿,凡是搞网络、搞后端、搞运维的,几乎都躲不开。刚入行的时候,我也以为这就是背下来的八股文,直到后来线上真的出了连接堆积、握手超时这类故障,才意识到当初没把这两个过程彻底弄明白,吃亏的是自己。所以这篇东西我不想给你念 RFC 文档,而是站在一个天天跟 TCP 打交道的从业者角度,把三次握手和四次挥手从头到尾拆开揉碎,讲清楚每一步到底发生了什么、为什么必须是这个次数,以及排障的时候这些知识要怎么用。
这篇文章适合刚接触 TCP/IP 的初学者,也适合已经写了不少代码但从来没认真看过抓包的开发,还有那些被 TIME_WAIT、CLOSE_WAIT 折磨过的运维朋友。看完之后,你不仅能对着面试官把原理讲明白,更重要的是,线上出问题的时候你知道从哪儿下手。
1. 先说人话:三次握手到底在干什么
三次握手说白了就是通信双方在正式发数据之前,先互相确认三件事:你的发送能力没问题、我的发送能力没问题、咱俩都能正常收到对方的消息。这跟两个人第一次见面互相确认身份是一个道理。
1.1 一次真实的握手过程长什么样
我不喜欢空谈理论,咱们直接看抓包结果。在 Linux 上随便找一个 HTTP 服务,用 tcpdump 抓一下建立连接的过程:
sudo tcpdump -i eth0 host 192.168.1.100 and port 80 -n当你用浏览器访问这个服务时,会看到类似下面的输出:
14:50:01.123456 IP 192.168.1.50.55001 > 192.168.1.100.80: Flags [S], seq 1000 14:50:01.123568 IP 192.168.1.100.80 > 192.168.1.50.55001: Flags [S.], seq 2000, ack 1001 14:50:01.123589 IP 192.168.1.50.55001 > 192.168.1.100.80: Flags [.], ack 2001这三行就是传说中的三次握手。第一行是客户端发起的 SYN 包,seq 序列号设为 1000;第二行是服务端回应的 SYN+ACK 包,既确认了客户端的序列号(ack 1001),又带上自己的序列号(seq 2000);第三行是客户端对服务端序列号的确认(ack 2001)。到这里,双方都确认了对方已经收到自己的同步信息,连接建立成功,状态变成 ESTABLISHED。
很多人第一次看抓包会疑惑,为什么 SYN 包的 seq 是从 1000 这种整数开始的,而不是从 0 开始?其实这个序列号是经过计算的初始值,跟真实的数据传输起点有关。你可以把它理解成每个人说话前先给自己的话编个号,方便对方按顺序整理。最初的设计里,这个初始序列号会随时间变化,防止跟之前的连接混淆。
1.2 握手过程中双方的状态是怎么翻牌的
三次握手不光是发三个包那么简单,每一方都在同步更新自己的状态,理解这些状态对排查问题特别关键。
- 最开始,服务端进程监听在某端口上,状态是 LISTEN,客户端这边是 CLOSED。
- 客户端第一次发出 SYN,状态从 CLOSED 变成 SYN_SENT,表示我正在等对方回应。
- 服务端收到 SYN 后,状态变成 SYN_RCVD,同时回一个 SYN+ACK,告诉客户端我收到你的同步请求了。
- 客户端收到 SYN+ACK,状态变成 ESTABLISHED,然后回一个 ACK。
- 服务端收到这个 ACK,状态也变成 ESTABLISHED。
这里有个容易忽略的点:客户端是在发出 ACK 之后立即进入 ESTABLISHED,而服务端是在收到 ACK 之后才进入 ESTABLISHED。也就是说,在第三个 ACK 到达服务端之前,客户端已经认为自己连接建立好了,但服务端还停留在 SYN_RCVD。如果这个 ACK 丢了,服务端会超时重传 SYN+ACK,所以客户端即使在 ESTABLISHED 状态下,也要做好收到重复 SYN+ACK 的心理准备。
1.3 为什么一定是三次,两次难道不行吗
这是面试里最常问的问题,也是理解整个握手设计的关键。咱们反过来想,如果只握手两次,会出什么事?
假设只有两次:客户端发 SYN,服务端回 SYN+ACK,然后双方直接认为连接建立。那么当网络中有一个滞留了很久的旧 SYN 包,突然跑到服务端,服务端以为客户端要建立新连接,就回了 SYN+ACK 并进入 ESTABLISHED,开始等客户端发数据。可客户端压根没打算建立连接,收到这个 SYN+ACK 也不会理会,服务端就一直傻等,浪费资源,甚至可能被这种假连接拖垮。
三次握手就解决了这个问题。客户端收到服务端的 SYN+ACK 之后,还要再回一个 ACK,服务端只有在收到这个 ACK 后才真正建立连接。旧 SYN 包的场景下,客户端根本不会回应第二次 SYN+ACK,服务端就会放弃,不会建立起半吊子连接。
从另一个角度看,三次握手也是让双方都确认“我能发、我能收、你能发、你能收”这四件事。第一次,客户端发 SYN,服务端收到,服务端知道“客户端能发,我能收”;第二次,服务端回 SYN+ACK,客户端收到,客户端知道“客户端能发,我能收,服务端能发,我能收”;第三次,客户端回 ACK,服务端收到,服务端这才知道“我能发,客户端能收”。只有三次握手,才能让双方都确认自己和对方的收发能力都没问题,两次握手无论如何都差一个方向的确认。
提示:三次握手还有一个隐形好处,就是双方可以协商一些连接参数,比如最大报文段长度(MSS)、窗口缩放因子(Window Scale)等。这些参数是在握手阶段互相告诉对方的,如果你强行用两次握手,这些协商就没地方放了。
2. 四次挥手:好聚好散比建立连接更难
建立连接是为了干活,断开连接则要考虑怎么把没说完的话说完。四次挥手之所以比三次握手多一次,根本原因在于 TCP 是双工的,两边都能独立地发送和接收数据,断开的时候,每一侧都需要单独确认对方的关闭意愿。
2.1 挥手过程的四个包
还是用抓包来看,当客户端主动关闭连接时,会看到四个包:
14:50:10.123456 IP 192.168.1.50.55001 > 192.168.1.100.80: Flags [F.], seq 5000 14:50:10.123468 IP 192.168.1.100.80 > 192.168.1.50.55001: Flags [.], ack 5001 14:50:10.123512 IP 192.168.1.100.80 > 192.168.1.50.55001: Flags [F.], seq 7000, ack 5001 14:50:10.123524 IP 192.168.1.50.55001 > 192.168.1.100.80: Flags [.], ack 7001第一步,客户端发送 FIN 包,表示“我的数据发完了,准备关闭发送方向”;第二步,服务端回 ACK,表示“我知道了,但我可能还有数据要发”;第三步,等服务端的数据也发完了,服务端再发 FIN 包,表示“我这边也完事了”;第四步,客户端回 ACK,表示“收到,关闭完成”。
注意第二步和第三步之间是可以有间隔的,服务端确认客户端的 FIN 之后,不一定立刻发自己的 FIN。它可以继续发剩余数据,等全部发完再关闭。这个特点就是 TCP 半关闭:连接的两个方向可以独立关闭,一边关了另一边还能继续传数据。
2.2 挥手双方的状态变换全过程
我把所有状态变化写成一张表,方便你对照抓包看:
| 阶段 | 主动关闭方状态 | 被动关闭方状态 |
|---|---|---|
| 初始 | ESTABLISHED | ESTABLISHED |
| 发送 FIN 后 | FIN_WAIT_1 | CLOSE_WAIT |
| 收到对方 ACK | FIN_WAIT_2 | CLOSE_WAIT |
| 对方发送 FIN | FIN_WAIT_2 | LAST_ACK |
| 发送 ACK 后 | TIME_WAIT | CLOSED |
| 等待 2MSL 后 | CLOSED | (早已关闭) |
被动关闭方收到 FIN 后,会进入 CLOSE_WAIT,同时回 ACK。这个状态非常关键,因为它代表“对方想关了,但我还没关,我还有数据要发”。如果程序代码里忘了关闭 socket,连接就会一直卡在 CLOSE_WAIT,这是后端排查里特别常见的场景,后面我会专门讲。
主动关闭方收到 FIN 后,会进入 TIME_WAIT,等 2MSL(Maximum Segment Lifetime,报文最大生存时间)才彻底关闭。这里的等待不是白等的,主要是为了保证最后的 ACK 能送达对端,万一 ACK 丢了,对方会重发 FIN,TIME_WAIT 期间还能再回一次 ACK。
2.3 为什么必须四次,为什么不能合并成三次
很多人问,三次握手能省一次,挥手为什么不能合并成三次?假如服务端收到 FIN 后,立刻把自己的 FIN 和 ACK 一起发出去,那确实可以只发三个包,但是这样做有一个前提:服务端在收到 FIN 的那一刻,已经没有数据要发了。
可是实际传输中,服务端很可能在收到 FIN 时还有数据没发完。比如客户端发完请求后要关闭连接,但服务端还有响应数据正在传输。如果服务端强行把 FIN 和 ACK 合并,就等于告诉客户端“我不但确认了你的关闭,我自己也不发了”,可它明明还有响应要发,这就会造成数据丢失。
TCP 这样设计,本质上是允许两个方向独立关闭。ACK 只是确认收到对方的 FIN,而 FIN 才是本端主动发起的关闭请求。这两个动作在时间上可能相差很久,所以不能强行合并。看到这里你就明白,挥手是四次而不是三次,不是协议设计得啰嗦,而是 TCP 要保证数据的完整性和双方独立的关闭意愿。
2.4 TIME_WAIT 为什么要等 2MSL
TIME_WAIT 是最容易被忽视、也最影响线上系统的一个状态。主动关闭方发送完最后一个 ACK 之后,会进入 TIME_WAIT,并且等待 2MSL 才关闭。MSL 是报文在网络中存活的最长时间,一般取 30 秒到 2 分钟不等,Linux 上常见的是 60 秒,所以 2MSL 通常是 2 分钟。
为什么要等这么久?两个原因。第一,防止最后一个 ACK 丢失。如果最后一个 ACK 丢了,服务端会重发 FIN,客户端如果没有 TIME_WAIT 状态,直接 CLOSED,那它收到重发的 FIN 后没法回应,服务端就会一直重试,连接永远关不掉。第二,让网络中残留的旧报文自然消失。如果一个连接刚关闭,立刻用相同的 IP 和端口建立新连接,旧连接在网络中滞留的报文可能被新连接误收。等 2MSL 之后,所有旧报文都已经在网络中消亡,新连接就不会被干扰。
注意:TIME_WAIT 时间长是有代价的。在高并发的短连接场景下,主动关闭方会产生大量 TIME_WAIT 连接,占用本地端口和内存。所以线上经常要在服务器参数里调优,比如启用 tcp_tw_reuse,或者在 Java/C 等程序的连接池里复用连接,减少短连接导致的 TIME_WAIT 堆积。
3. 把握手、挥手放进真实场景里看
理解了三步四步的原理,下一步要做的就是把这些概念跟生产环境挂上钩。很多人背得滚瓜烂熟,一到线上看到netstat输出里的各种状态还是懵,就是因为不知道这些状态在真实交互中对应什么行为。
3.1 短连接:一次请求一次握手加挥手
短连接就是每次请求都要建立连接、传输数据、关闭连接。最典型的就是早期 HTTP/1.0 的行为:请求一个网页,浏览器和服务端握手,拿到 HTML 后立刻挥手断开;接着请求 CSS、JS、图片,又要重新建立一次连接。
短连接的缺点是显而易见的:握手一次需要 1 个 RTT(Round-Trip Time,往返时间),挥手又要多个包,在高延迟网络下,光建立和关闭连接就要占不少时间。而且主动关闭方(通常是服务端)会产生一堆 TIME_WAIT,端口和内存都有压力。
我见过一个线上案例,某个老系统用短连接访问后端服务,压测一上来,服务端 TIME_WAIT 直接飙到几万个,ss -s显示 TIME-WAIT 占了大半,本地端口耗尽,新连接都建立不了了。后来把代码改成连接池复用,TIME_WAIT 瞬间降下来,性能提升非常明显。
3.2 长连接:一次握手管多次请求
长连接就是建立连接后,持续复用同一个 TCP 连接处理多个请求,HTTP/1.1 的 Keep-Alive 和 HTTP/2 的多路复用都是基于这个思路。一次握手之后,连接一直是 ESTABLISHED 状态,直到某个方向决定关闭,才走四次挥手。
长连接的好处是省掉了反复握手挥手的开销,尤其在 RTT 较大的跨地域场景下效果明显。但长连接也有自己的问题:连接长时间空闲时,中间的网络设备可能会把空闲连接回收,导致一方还在用,另一方其实已经断了。这就是为什么很多长连接协议要用心跳机制,比如 TCP 的 KeepAlive 参数,或者应用层自己发心跳包。关于 TCP 自带的 KeepAlive,默认是两小时才探测一次,很多场景下不够用,所以应用层心跳更靠谱。
3.3 握手挥手背后的性能账
我算一笔简单的账帮助你理解短连接和长连接的选择。假设网络 RTT 是 30ms,一次 HTTP 请求的数据传输时间是 20ms。
短连接方式,建立连接要 1.5 个 RTT(三次握手理论上是 1.5 RTT,第一次客户端到服务端 0.5 RTT,服务端回客户端 0.5 RTT,客户端再确认 0.5 RTT),再加上传输 20ms,最后挥手还要 1 RTT 左右,总计差不多 45ms + 20ms + 30ms,一请求一开销。
长连接方式,第一个请求多付一次握手成本,后续请求全都省了握手和挥手,每个请求大约就是 20ms 的传输时间加排队时间。在高 QPS 场景下,这个差异会被放大得非常明显。所以很多中间件、数据库客户端,宁可多维护连接状态,也要用连接池保持长连接。
4. 排障实录:从握手挥手的细节定位线上问题
前面讲的都是原理,下面这部分才是实战中最值钱的经验。我在工作里踩过不少跟握手、挥手相关的坑,把典型的几种情况整理一下。
4.1 连接建立不了的排查链路
线上出现“连不上服务”的报错,很多人第一反应去看防火墙规则,但光看规则不够,要结合握手状态来判断问题到底在哪一层。
先用telnet ip port或者nc -vz ip port测一下端口通不通。如果一直卡着不动,那大概率是 SYN 发出去了没收到响应。这时候在客户端和服务端分别抓包,看 SYN 到底丢在哪。常见原因有三种:
- 服务端没监听端口,或者服务进程挂了,表现为服务端网卡上有 SYN 进来但没人回 SYN+ACK。
- 防火墙拦截了 SYN 或 SYN+ACK。比如云安全组、iptables 规则、firewalld 配置,这个用
iptables -L -n或者看安全组规则来排查。 - 服务端的半连接队列满了。内核里有个参数叫
net.ipv4.tcp_max_syn_backlog,当 SYN 请求太多、服务端来不及处理,新的 SYN 会被丢弃,客户端表现为一直 SYN_SENT。
看到这里应该明白,如果 SYN_SENT 一直存在,问题多半在客户端到服务端的路径上;如果抓包看到 SYN+ACK 发出去了但客户端没收到,那问题可能在回程路径上,或者在服务端的连接队列。
4.2 CLOSE_WAIT 堆积是最常见的代码问题
CLOSE_WAIT 堆积是后端开发最容易踩的坑。它出现的原因是服务端收到了客户端的 FIN,内核回了 ACK,但应用程序没有正确关闭 socket,导致连接卡在 CLOSE_WAIT 状态。
我记得有一次排查一个 Java 服务,netstat -anp | grep CLOSE_WAIT | wc -l出来上万个,内存和线程都被拖垮。最后定位到代码里读请求流的时候,异常分支没有走到close(),导致每一个异常请求都泄漏一个连接。修法很简单,用 try-with-resources 或 finally 里关闭资源,但排查的过程确实费劲。
如果你的服务也出现大量 CLOSE_WAIT,优先检查这几处:
- 是否所有分支都关闭了 socket 或包装类。
- 是否读取完请求后没有主动关闭连接,长连接场景下要区分是业务正常 idle 还是泄漏。
- 是否把连接存储到容器里没有清理。
4.3 TIME_WAIT 过多该不该处理
TIME_WAIT 过多不是错误,但到了影响端口分配和内存的时候,就得想办法优化。优化思路有个优先级,我建议按顺序来:
第一,减少不必要的连接建立和关闭,用连接池、长连接替代短连接,这是最根本的解法。第二,开启内核参数net.ipv4.tcp_tw_reuse,允许在 TIME_WAIT 阶段复用连接,注意tcp_tw_recycle在现在的内核里因为 NAT 场景有坑,不建议开启。第三,调整net.ipv4.ip_local_port_range,扩大本地端口范围,给 TIME_WAIT 更多的空间。
注意:如果 TIME_WAIT 都集中在服务端,而且服务端是被动关闭方为主,那说明是客户端先发起的关闭,服务端只需要回 FIN,不会自己产生 TIME_WAIT。大量 TIME_WAIT 在服务端,反而说明是服务端主动关闭了连接,比如设置了不合理的超时时间。
4.4 实用的命令与工具速查
排查网络问题时,我经常用的命令不多,但都很管用:
# 查看系统连接状态汇总 ss -s # 统计各种状态连接数量 netstat -n | awk '/^tcp/ {++state[$NF]} END {for(key in state) print key,"\t",state[key]}' # 查看具体端口的连接状态 ss -tan | grep :8080 | head -20 # 抓包分析握手挥手 sudo tcpdump -i any port 8080 -nn -w /tmp/tcp.pcap用 Wireshark 打开抓包文件,可以非常直观地看到 SYN、ACK、FIN 的交互时序,配合过滤器tcp.flags.syn == 1或者tcp.flags.fin == 1,一眼就能定位问题在哪一步。
5. 从三次握手、四次挥手延伸出的关键知识点
把握手和挥手讲完之后,再补充几个跟它强相关的概念,日常面试和工作中都会碰到。
5.1 状态编码和标志位的实际含义
TCP 报文头里的控制位,其实就对应了握手的 SYN、ACK 和挥手的 FIN。和握手相关的还有 RST、PSH、URG 等。RST 是用来异常断开的,比如连接不存在、端口不可达、收到无意义的报文时,内核会直接发 RST 而不是走四次挥手。所以你在抓包时看到 RST,基本意味着哪里出错了,而不是正常的关闭流程。
PSH 标志位表示接收方收到数据后应立即交给应用层,不要等缓冲区填满。平时不太会被注意,但在抓包分析时如果看到 PSH+ACK,说明这是一次正常的数据交付。
5.2 TCP 与 UDP 的本质差异
TCP 三次握手、四次挥手之所以是讨论的重点,恰恰是因为 UDP 完全没有这些机制。UDP 不建立连接、没有序列号、没有重传机制,直接发数据报,所以面试里常考的“TCP 和 UDP 的区别”,本质上就是在讲“可靠性 vs 效率”的取舍。
TCP 适合对数据完整性要求高的场景,比如网页、文件传输、数据库连接;UDP 适合对实时性要求高、丢一些数据影响不大的场景,比如音视频通话、游戏帧同步、DNS 查询。做架构选型的时候,搞清楚业务能不能接受丢包,就知道该选哪个了。
5.3 从握手挥手看防火墙的会话状态
很多运维知道要在防火墙里放行端口,但不知道防火墙还有状态检测机制。现代防火墙为了安全,会记录 TCP 连接状态,只允许已经建立过握手的连接的数据包通过。
这就产生了一个常见的运维坑:如果防火墙因超时清除了会话记录,但服务端和客户端之间的 TCP 连接还活着,那么后续数据包经过防火墙时,可能会被判定为攻击流量而拦截,业务就“莫名其妙”卡住了。从这个角度说,理解三次握手和四次挥手的包特征,也是配置防火墙、排查网络策略的基础。
我给新人的建议是:别把三次握手、四次挥手当成面试八股,多花点时间自己抓包看一看。找个本机服务,用tcpdump抓一次完整请求,观察每一个包的标志位、序列号和状态变化,比死记硬背一个月都管用。等你哪天线上遇到 CLOSE_WAIT 堆积、SYN 超时、TIME_WAIT 耗尽的时候,就会感谢当年把这几张状态转换图啃下来的自己。