TCP三次握手与四次挥手:原理、抓包验证与高频故障排查

发布时间:2026/10/11 15:39:55
TCP三次握手与四次挥手:原理、抓包验证与高频故障排查 1. 为什么必须是“三次”双向可达与序号同步的双重验证这个问题的答案得从“两次行不行”开始想。很多人背过流程知道“客户端发 SYN、服务端回 SYN-ACK、客户端再回 ACK”但并没有真的想清楚第三包到底解决了什么。如果只追一个问题——让双方都确认“我能发、你能收、你能发、我能收”那两次其实就够表面用了客户端发 SYN服务端收到后回 SYN-ACK服务端确认了自己能收、自己能发客户端收到 SYN-ACK确认了自己能发、自己能收。此时双方都自认为通道没问题为什么还要第三次因为“自认为”不等于“对方也知道”。1.1 两次握手的致命缺陷服务端进入“无意义等待”三次握手里最容易被忽略的是服务端在收到 SYN 之后会立刻分配一块资源来维护这个半开连接。这个资源包括收发缓冲区、TCP 控制块、初始序号等它不会等到三次握手全部完成才分配而是在收到 SYN 的那一刻就建立。如果只做两次握手服务端发出 SYN-ACK 后就会默认客户端已经准备好了于是把连接状态切到 ESTABLISHED并等待客户端的数据。问题在于客户端此时根本不知道自己发出去的 SYN 是否安全到达。如果 SYN 在网络上丢了客户端会继续重传 SYN如果服务端发回的 SYN-ACK 在网络上丢了客户端同样会继续重传 SYN。但服务端已经进入了 ESTABLISHED它会对着一个“其实客户端还蒙在鼓里”的连接反复等待——这就是经典的无意义资源占用。更要命的是有一种情况会让两次握手的缺陷被无限放大某个客户端连续发送多个 SYN但没有一个 SYN-ACK 能成功回到客户端。这时候服务端会积压大量处于 ESTABLISHED 的僵尸连接每一个都在占着内存。这类场景虽然没有今天说的 SYN 泛洪那么极端但本质上已经把服务端资源暴露给了“没完成握手的陌生人”。1.2 第三次 ACK 的核心作用让服务端拿到确认第三次 ACK 派上用场的时机正是服务端最焦虑的时刻。服务端发出 SYN-ACK 之后内心独白是“我已经告诉对端我可以收发但我不确定他是否收到了这条消息。”它只能等等不到就重传 SYN-ACK等到的是第三次 ACK 才把悬着的状态放下来。客户端发这个 ACK等于正式告诉服务端我收到了你的 SYN-ACK双方序号同步已经完成可以开始传数据了。从这个角度看三次握手本质上是在做两件事一是双向可达性的验证二是初始序列号的同步确认。第二步和第一步同样关键因为 TCP 是字节流协议每一个字节都有序号如果双方对“从哪个号开始编号”都没有达成共识后续的可靠传输、去重、乱序重组就全乱套了。1.3 让“三次”变成“四次”的场景同时打开顺手澄清一个常见误区有人以为连接建立一定是客户端先发 SYN。其实 TCP 是支持“同时打开”的——两端同时向对方发 SYN之后各自收到对方的 SYN 并回 SYN-ACK。这时候从包数量上看是四个包SYN、SYN、SYN-ACK、SYN-ACK但从逻辑上来说协议并没有额外增加“第三/第四次”的必要只是双方各自完成了三次握手的部分职责。在现实中这种模式一般只在点对点协议里出现客户端/服务端模型里极少遇到。2. 三次握手逐包拆解SYN、SYN-ACK、ACK 的背后逻辑知道为什么是三次再看每一包里装着什么才是真正把“背流程”变成“看懂了”的分水岭。2.1 为什么握手要从随机序号开始很多教材会告诉你 TCP 用序号来做可靠传输但不会特意强调握手时使用的初始序号 ISNInitial Sequence Number不是从 0 开始的而是一个随机数。如果初始序号恒定比如总从 0 开始那么网络里一旦出现滞留的旧数据包前一次连接中的包新的连接可能会误把它当成当前连接的数据造成数据污染。让每个连接的初始序号随机化可以把新连接和旧连接在“时间空间”上隔离开。Linux 上 ISN 的生成还引入了时间相关因素保证即使在同一台机器上快速建立大量连接序号也很难猜中。所以抓包时看到 SYN 包里的 Seq 是一个很大的数完全正常不要怀疑是不是抓错了。2.2 第一包 SYN客户端的“自我介绍”客户端发送的 SYN 包是连接的起步它的核心信息包括Flags 中的 SYN 置 1表示这是一个建立连接的请求Seq 设置为客户端的初始序号 X窗口大小、MSS最大报文段长度、SACK 允许、时间戳等选项随包一次带过去不携带应用层数据。这里有个细节值得注意SYN 包本身虽然不携带业务数据但它的 Seq 也要占一个序号位。换句话说客户端这包的序号是 X下一包数据的序号就必须从 X1 开始。这个“一字节占位”的概念对理解后续 ACK 的数值非常关键。很多人在抓包时看到第二个包的 Ack X1而不是 X会楞一下。原因就在这里Ack 表示“我期望收到的下一个字节序号”而 SYN 这个控制位虽然不传送数据字节但它在逻辑上占了一个序号位置所以对方确认时要加 1。2.3 第二包 SYN-ACK服务端进入半连接状态服务端收到 SYN 后会做三件事检查自己的接收队列是否还有空间也就是半连接队列能否容纳这个请求如果可接受分配传输控制块进入 SYN_RCVD 状态回复 SYN-ACK其中 Seq 是服务端自己的初始序号 YAck X1。SYN-ACK 是握手过程中最特殊的一包因为它是唯一一个同时设置两种控制位的包。它的含义是服务端对客户端说“我收到了你的 SYN这是我的初始序号请你也确认一下。”服务端发出这包后连接并没有建立服务端自己会停在 SYN_RCVD 状态。这个状态在系统里看得见netstat或ss输出中典型的半连接表现就是服务端大量 SYN_RCVD。如果这个队列很小或者 SYN 包来得太快服务端的半连接队列就会溢出新版 Linux 内核会根据/proc/sys/net/ipv4/tcp_syncookies等参数决定是丢包还是启用 syncookie 机制。2.4 第三包 ACK客户端结束流程服务端完成状态跃迁客户端收到 SYN-ACK 后需要回复一包纯粹的 ACKSeq X1Ack Y1。服务端收到这包 ACK 后把状态从 SYN_RCVD 切换为 ESTABLISHED。而客户端自己在发出这包 ACK 之后也会从 SYN_SENT 切换为 ESTABLISHED。从发出第三包到服务端真正收到中间还有时间差。所以理论上客户端在这段时间里已经可以发数据了。实际的 TCP 实现里客户端发出 ACK 后立刻进入 ESTABLISHED之后马上就能把 HTTP 请求等业务数据连同后续的包一起发出去——这也是现实中经常看到第三个 ACK 之后紧接着就是应用数据包的原因。提示如果第三包 ACK 丢失服务端会一直停留在 SYN_RCVD 状态并周期性重传 SYN-ACK。客户端此时已经觉得自己建立好连接并开始发数据但服务端完全没有准备好。随着 SYN-ACK 重传超时连接最终可能被放弃表现就是“客户端已经发送数据很久了服务端一条响应都没有”。3. 挥手为什么必须“四次”从双向关闭到 TIME_WAIT 的安全设计如果说三次握手是“礼尚往来地确认”四次挥手就是“把话说完再告别”。3.1 数据单向流动假设下的必然性TCP 连接是双工通道同一时刻数据可以双向传输。关闭连接时一方只表示“我没有数据要发给你了”并不代表“我也不想再收你的数据”。所以每一方向都需要单独声明“我要关了”并且都要收到对方的确认。这个过程天然是两轮“请求确认”主动关闭方发 FIN表示“我的数据发完了”被动关闭方回 ACK表示“收到我知道你不用再发了”被动关闭方把剩下的数据发完后发 FIN表示“我也发完了”主动关闭方回 ACK表示“收到大家都结束”四次包正好对应这四件事。它比三次握手多一包不是因为设计者多此一举而是因为被关闭方在收到 FIN 之后可能还有残留数据需要发送——即使没有也得先安排一包 ACK 及时回应然后再走自己的 FIN。这个“中间隔着一包 ACK”的结构让挥手天然需要四步。3.2 主动关闭方与被动关闭方责任从来不对等挥手过程里有个容易被忽略的角色分化主动关闭方最后要多承担一个 TIME_WAIT 状态。主动关闭方发出 FIN进入 FIN_WAIT_1。收到被动方的 ACK 后进入 FIN_WAIT_2。被动方发完 FIN 后主动方回 ACK然后进入 TIME_WAIT等 2MSLMaximum Segment Lifetime最大报文段生存时间后才完全关闭。被动关闭方轻松得多收到 FIN 后从 ESTABLISHED 进入 CLOSE_WAIT等自己数据发完发出 FIN 后进入 LAST_ACK收到主动方的最终 ACK 后直接进入 CLOSED不需要等待。为什么主动关闭方要等 2MSL两个原因第一保证最后的 ACK 如果丢了被动方会重发 FIN而主动方还有机会再补一个 ACK。2MSL 恰好覆盖一个报文在网络中存活的最长时间两倍能保证“最坏情况下对方重发的 FIN 还在我的等待窗口内”。第二让上一次连接的所有报文在网络里彻底消失避免它们污染后续使用相同四元组的新连接。3.3 TIME_WAIT 的时间与代价MSL 在不同系统上有不同取值。Linux 里通常取 30 秒或 60 秒所以 TIME_WAIT 往往是 60 秒或 120 秒左右。实际观察中一台主动关闭连接量很大的服务端会积累大量 TIME_WAIT 连接这是正常的是协议设计的代价不是故障本身。它带来的核心问题是端口占用和内存占用四元组中如果客户端的源端口被 TIME_WAIT 占着短时间内要建立同样四元组的新连接就会受影响。规避思路通常有三类调整短连接行为、复用连接连接池、谨慎开启tcp_tw_reuse。其中tcp_tw_reuse不是万能药它要求新旧连接的时间戳递增部署前要弄清楚约束条件否则会踩坑。3.4 异常分支RST、丢包与半关闭如果双方进度还没走到 FIN就直接收 RST连接会立刻关闭不经过正常挥手。明确的就是异常——比如端口不可达、程序崩溃、连接超时被重置。如果 FIN 丢了主动关闭方会重传 FIN但重传次数有限超时后会放弃如果被动方收到 FIN 后一直不回复 ACK主动方停在 FIN_WAIT_1直到超时如果被动方应用层迟迟不 close socket主动方已经进入 FIN_WAIT_2被动方还留在 ESTABLISHED 或 CLOSE_WAIT于是出现大量 CLOSE_WAIT 堆积。这些异常分支不是纸上谈兵排障时比正常流程常见得多。后面第 6 节会专门展开。4. 一张表串起全流程握手与挥手的状态流转与核心差异系统化理解 TCP 建立与关闭最有用的工具就是状态表。TCP 的状态机一共 11 个状态握手中涉及 4 个挥手中涉及 6 个下面两两对照列出。4.1 握手与挥手的完整状态流转对照阶段主动方状态路径被动方状态路径关键事件握手CLOSED → SYN_SENT → ESTABLISHEDLISTEN → SYN_RCVD → ESTABLISHED收到/确认 SYN-ACK挥手ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSEDESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSEDFIN/ACK 双向配合很多人在ss里看到某个状态时第一反应是“这是什么”而不是“这对端现在卡在哪一步”。状态表的作用是把“状态”还原成“阶段位置”。比如看到服务端有大量 SYN_RCVD意味着握手只完成了第一包看到大量 CLOSE_WAIT说明被动关闭方收到了 FIN但应用层没有 close socket看到大量 FIN_WAIT_2说明被动方一直没有发送 FIN。4.2 握手与挥手的维度对比对比维度三次握手四次挥手目的建立连接、同步初始序号关闭连接、释放双方资源最小包数3 个包4 个包触发方主动连接方客户端谁先 close socket谁就是主动方确认机制ACK 确认 SYNACK 确认 FINFIN 再被 ACK服务端角色被动监听方可能是被动关闭方也可能是主动关闭方特殊状态SYN_SENT、SYN_RCVDFIN_WAIT_1/2、CLOSE_WAIT、LAST_ACK、TIME_WAIT是否残留等待无 TIME_WAIT主动关闭方必须等 2MSL失败后果连接无法建立连接残留、端口占用、资源泄漏这张表可以当成面试或自查的索引拿一个状态出来能说出它属于哪个阶段、由谁进入、下一步怎么走、异常时会怎样TCP 连接的基础就不算虚。4.3 高频追问为什么挥手要“四”而不是“三”面试里常有一道题“挥手为什么不是三次”答案的核心是被动关闭方收到 FIN 后不能立刻也发 FIN因为它可能还有数据要发。收到 FIN 的那一瞬间被动方只能先回 ACK至于它自己的 FIN要在应用层主动 close 之后才能发。这两个动作在时间上天然分开所以多出一个包。但如果被动方在收到 FIN 的那一刻自己也没有任何待发数据理论上可以合并成“回 ACK 同时发 FIN”——抓包时某些 TCP 实现会看到 ACK 与 FIN 合并在同一包里包数变成 3 个。这种合并是可优化项不代表四次的流程变了它只是把第三步和第二步的间隙压缩到了零。5. 动手验证三条命令观察一次真实的握手与挥手纸上谈兵到此结束。下面用我在本地环境里常用的一套验证流程亲手把三次握手和四次挥手抓出来看。5.1 构造一次连接用 nc 做服务端和客户端先在终端 A 启动一个监听nc -l -p 9999再在终端 B 连接它nc 127.0.0.1 9999连接建立后按 CtrlC 断开整个过程中两次握手和一次挥手会全部发生在瞬间。要看清细节得靠 tcpdump 把包存档sudo tcpdump -i lo -nn -S port 9999解释一下-S的作用默认 tcpdump 会把绝对序号显示成相对序号看起来像是从 1 开始的“美化后序号”不利于观察绝对 ISN。加-S显示原始序号能让你直观看到随机初始序号。5.2 从抓包看握手与挥手的关键字段输出大致长这样我调整过时间列方便阅读00:00.000000 IP 127.0.0.1.50001 127.0.0.1.9999: Flags [S], seq 4000000000, win 65495 00:00.000026 IP 127.0.0.1.9999 127.0.0.1.50001: Flags [S.], seq 2000000000, ack 4000000001, win 65483 00:00.000039 IP 127.0.0.1.50001 127.0.0.1.9999: Flags [.], ack 2000000001, win 65495 00:00.000056 IP 127.0.0.1.9999 127.0.0.1.50001: Flags [F.], seq 2000000001, ack 4000000001 00:00.000066 IP 127.0.0.1.50001 127.0.0.1.9999: Flags [.], ack 2000000002 00:00.000070 IP 127.0.0.1.50001 127.0.0.1.9999: Flags [F.], seq 4000000001, ack 2000000002 00:00.000075 IP 127.0.0.1.9999 127.0.0.1.50001: Flags [.], ack 4000000002注意几个点第一包 Seq4000000000是一个很大的随机数第二包 Ack4000000001恰好是“第一包的 Seq 1”说明 SYN 占一个序号位第二包 Seq2000000000是服务端自己的随机数第三包的 Ack2000000001 对应它的 1断开时先看到服务端发 FIN说明是终端 A 的 CtrlC 触发了服务端的 close服务端变成了主动关闭方。这正好印证了一个容易被忽略的事实主动关闭方不一定永远是“客户端”谁先 close谁就是主动方。我用 CtrlC 关掉 nc 服务端时服务端立刻成为主动关闭方整个过程四包全速完成。如果把这段抓包保存为 pcap 文件还能在 Wireshark 里看一次完整的“TCP 三次握手 四次挥手”时间线可视化的效果比看命令行输出更直观。5.3 用 ss 观察状态实时变化抓包能看包但要观察状态跃迁推荐全程开着sswatch -n 0.2 ss -tan state all | grep 9999连续执行可以发现LISTEN → SYN_RCVD → ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED 这个变化过程。有些状态闪得很快普通netstat的刷新频率根本反映不过来watch -n 0.2能抓到一些尾巴。提示在本地环回接口上做这类实验时间戳几乎都是 0.0x 毫秒级很多状态一闪而过。更真实的做法是在两台机器之间测试或者加人为延迟比如用tc给网卡加一点丢包率这样才能观察到 SYN 重传和 FIN 重传这些在本地测试里看不到的现象。6. 高频故障复盘半连接队列打满、TIME_WAIT 堆积、挥手无响应讲完正常流程最后落到排障。这部分是我平时最常被问到也是很多“背过完整 TCP 流程”的人依然会卡住的地方。6.1 现象一连接建立超时SYN_RCVD 堆积现象是客户端 connect 非常慢甚至直接超时。在服务端执行ss -ant | grep SYN_RCVD | head -20发现大量 SYN_RCVD。这个状态说明服务端收到了 SYN也回了 SYN-ACK但一直没等到客户端的第三包 ACK。原因分两类服务端发送的 SYN-ACK 出不去或回不来。常见于网络链路有丢包或者服务端半连接队列溢出。半连接队列的大小与tcp_max_syn_backlog、somaxconn、应用监听的 backlog 参数有关队列满时新 SYN 可能被直接丢弃客户端根本没有回应。常见于客户端程序在等 connect 返回时也出了问题或者客户端 IP 被防火墙拦截SYN-ACK 被丢弃。排查链路我一般是这样走的先看服务端有多少 SYN_RCVDss -ant | grep SYN_RCVD | wc -l查看半连接队列是否溢出netstat -s | grep -i SYNs to LISTEN若丢弃计数持续增长说明半连接队列吃紧看 tcp_syncookies 是否开启1 表示开启它会缓解队列溢出但不代表问题消失如果服务端已经发出 SYN-ACK但没有收到 ACK抓包应能看到 SYN-ACK 在重复发送。重传说明链路第 3 包回不来重点转向中间链路和客户端最后排查 SYN 泛洪如果来源 IP 分散、SYN 速率异常高需要结合访问控制、SYN cookies 和速率限制来处理。6.2 现象二TIME_WAIT 过多端口不足压测或高并发短连接场景下主动关闭方会出现大量 TIME_WAIT。看到上万条 TIME_WAIT 别慌先区分来源这些连接是正常建立过、正常关闭过的不是泄漏。需要关注的是可用源端口是否被耗尽。Linux 出站连接端口范围默认是 32768 到 60999可用源端口约 28000 个。如果短连接速率很高、连接双方停留时间长TIME_WAIT 会把端口池占满后续新连接会报 “Cannot assign requested address”。处理思路按优先级排优先改造应用层使用连接池避免反复创建短连接开启长连接保活对 HTTP 场景启用 keep-alive减少握手和挥手的频率谨慎使用net.ipv4.tcp_tw_reuse它只对出站连接有效而且要配合时间戳启用不能盲目照抄如无必要不要开启tcp_tw_recycle这个参数在新版内核里已经移除旧版本上它会因时间戳回退导致丢包早已不推荐。6.3 现象三CLOSE_WAIT 堆积连接不释放CLOSE_WAIT 是比 TIME_WAIT 更值得警惕的状态。它表示被动关闭方收到了对端的 FIN也回了 ACK但应用层一直没有关闭自己的 socket。这个状态的堆积通常意味着程序里有 socket 没有被 close属于典型的资源泄漏。排查这类问题思路比背命令更重要用ss -ant | grep CLOSE_WAIT找到堆积的 socket看对端地址和本地端口结合lsof -i定位到具体的进程和线程到代码里查这条连接的处理路径确认是否在所有异常分支上都调用了 close如果应用层有线程池可能是某个线程阻塞在读取或等待响应上迟迟没有执行关闭逻辑。CLOSE_WAIT 堆积会逐渐耗尽文件描述符导致新连接无法建立。它不像半连接队列那样受内核参数直接调节唯一的根治方案是修代码——脚本里临时调大ulimit -n只能争取时间不能解决泄漏问题。6.4 现象四FIN_WAIT_2 长时间不消失主动关闭方发出 FIN收到被动方的 ACK 后会进入 FIN_WAIT_2正常应等被动方回 FIN。如果被动方迟迟不发 FINFIN_WAIT_2 就会停在原处。这通常意味着被动方也处于 CLOSE_WAIT 状态应用层没有 close。也有一种特殊情况对端处于半关闭状态只关闭了发送方向但仍保持接收方向。对内核对 FIN_WAIT_2 有超时控制超时后会自动关闭但时间往往较长。排查时优先确认对端的应用层行为而不是反复调整内核参数。我自己排查这类问题的习惯是先看状态堆在哪一端再分析“这一端有没有收到上一包”。把时序和包对比起来看比单看状态猜测要高效得多。如果你想深入掌握 TCP 连接建议找一台测试机开着 tcpdump 反复建连、断连把正常流程、SYN 重传、FIN 重传、RST 重置全部亲手抓一遍。抓过一轮之后再回头看状态流转表和这篇文章里的表格基本就不会再“背完就忘”了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询