我那天遇到一个特别头疼的问题:自己写的服务监听 127.0.0.1:11434,重启时报错 error: listen tcp 127.0.0.1:11434: bind: only one usage of each socket address。字面意思是端口被占用,可我明明把旧进程都杀干净了。一路排查下来,发现这事牵扯到 TCP 连接状态、TIME_WAIT 堆积、抓包验证,最后还撞上防火墙拦截策略。整个排查过程让我把 TCP、抓包、TIME_WAIT、防火墙这四块知识完整串了一遍,收获非常大,所以整理成这篇学习总结,写给同样被连接问题折磨过的后端开发、运维和网络初学者。
如果你遇到过 Connection reset by peer、connect timeout、服务端口突然起不来、或者抓包抓不到数据这类问题,这篇文章应该能帮你建立一套完整的排查思路。我会按四次握手的连接状态、TIME_WAIT 产生的原因和调优方法、Wireshark 与 tcpdump 的实际抓包姿势、以及防火墙如何影响 TCP 连接这条线展开,最后附上我踩过的坑和排查记录。
1. TCP 连接管理:从三次握手到四次挥手
1.1 三次握手不是“打招呼”,是双向能力探测
很多人把 TCP 三次握手理解成“你好了我也好了”,这个说法不够准确。三次握手的本质是双方确认彼此的收发能力都正常,同时协商初始序列号。
我用一个生活化的例子解释:你给朋友打电话,说“喂,能听到吗?”(SYN),朋友回答“能听到,你能听到我吗?”(SYN+ACK),你再说“能听到”(ACK)。电话接通,开始聊天。这个过程中,任何一次应答丢失,双方都无法确认链路是通的。
实际报文长这样:
code复制客户端 → 服务端: SYN, seq=1000
服务端 → 客户端: SYN+ACK, seq=5000, ack=1001
客户端 → 服务端: ACK, seq=1001, ack=5001
注意看 seq 和 ack 的变化规律:发送方每次发数据,seq 是它自己的序列号;ack 是“我期望收到对方的下一个字节”,所以 ack 等于对方的 seq 加 1(如果没带数据)。这个机制在后面抓包分析时会反复用到。
握手过程中的两个细节值得记一下:
- SYN 包会消耗一个序列号,所以握手完成后客户端下一次发送数据的 seq 是 1001 而不是 1000。
- 握手的超时重传机制:客户端发出 SYN 后,如果 1 秒没收到 SYN+ACK,会重传,间隔翻倍(1s、2s、4s、8s…),达到重传上限后就报
connect timeout。
1.2 四次挥手与连接状态流转
断开连接比建立连接更复杂,因为 TCP 是全双工的,双方都可以主动发送数据,所以每一方的收发通道都要单独关闭。这就形成了四次挥手:
code复制主动关闭方 → 被动关闭方: FIN, seq=x
被动关闭方 → 主动关闭方: ACK, seq=y, ack=x+1
被动关闭方 → 主动关闭方: FIN, seq=y
主动关闭方 → 被动关闭方: ACK, seq=x+1, ack=y+1
为什么不能把中间两个报文合并成一次?因为被动关闭方收到 FIN 后,可能还有数据要发给对方,它必须先发 ACK 表示“我收到你的关闭请求了”,等自己的数据发完了再发 FIN。这中间的时间差就是 CLOSE_WAIT 状态。
连接状态流转上,有个特别容易混淆的点:主动关闭方和被动关闭方经过的状态完全不同。
主动关闭方:ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED
被动关闭方:ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED
实际线上最容易出问题的两个状态就是 CLOSE_WAIT 和 TIME_WAIT。CLOSE_WAIT 大量堆积,通常说明服务端代码没有正确调用 close(比如 IO 流没关);TIME_WAIT 大量堆积,通常说明有大量短连接主动关闭,或者代理层、网关层频繁创建连接。
1.3 TCP 与 UDP 怎么选
很多人纠结 TCP 和 UDP 的选择问题,我用一个表格整理清楚:
| 维度 | TCP | UDP |
|---|---|---|
| 连接状态 | 有连接,维护状态机 | 无连接,不维护状态 |
| 可靠性 | 可靠传输,有重传机制 | 尽力而为,不保证交付 |
| 有序性 | 保证字节流按序到达 | 不保证顺序 |
| 速度 | 相对慢,有握手和确认 | 快,无握手开销 |
| 适用场景 | HTTP、数据库、文件传输 | 音视频通话、游戏、DNS、广播 |
注意“可靠”不代表“绝对可靠”,TCP 只是通过确认和重传提高了可靠性,网络断了照样会断。搞实时音视频的用 UDP 不是因为 UDP 更好,而是因为 TCP 的重传机制在丢包时会引入延迟,画质卡顿比丢几个音频帧更让人难以接受。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. TIME_WAIT 解剖:为什么是 2MSL
2.1 TIME_WAIT 存在的两个理由
TIME_WAIT 是主动关闭连接的一方在收到对端 FIN 后进入的状态,持续时间为 2 倍的 MSL(Maximum Segment Lifetime,报文最大生存时间)。MSL 在 Linux 上默认是 60 秒,所以 TIME_WAIT 通常持续 120 秒,但一般你看到 TIME_WAIT 状态的连接几乎都在这个时间窗口内。
这个状态为什么必须存在?两个核心原因:
第一,保证最后一个 ACK 能到达对端。如果主动关闭方发完最后的 ACK 就立刻释放连接,这个 ACK 在网络中丢失了,被动关闭方在 LAST_ACK 状态一直等不到 AC
