**导读**如果说HTTP是互联网世界的通用语言那么TCP就是支撑这一切的地下管道。但这条管道不是想通就通的——它有一套严格的礼仪规范也就是我们常说的三次握手和四次挥手。今天我们就来聊聊这场网络世界的相亲与分手大戏。标签TCP协议三次握手四次挥手网络抓包连接管理一、开场白为什么TCP要搞这么多仪式想象一下你给一个陌生人打电话上来就说喂我要跟你说个事对方可能会一脸懵逼“你谁啊信号好不好我要不要拿笔记一下”TCP面临的也是类似的问题。作为面向连接的可靠传输协议它必须解决三个核心问题双方是否都能收发数据——确认通信能力从哪个序号开始传——初始化序列号窗口大小是多少——协商接收能力这三个问题不解决后面的数据传输就是盲传丢包、乱序、重复包等问题会让你怀疑人生。所以TCP设计了一套握手机制在正式传数据之前先把这些家务事掰扯清楚。二、三次握手网络世界的相亲流程 生活化比喻TCP三次握手就像相亲——男方说你好我想认识你女方说好的我也想认识你男方说那我们开始吧。少了任何一步这亲都相不成。2.1 握手流程详解┌─────────────┐ ┌─────────────┐ │ 客户端 │ │ 服务端 │ │ (Client) │ │ (Server) │ └──────┬──────┘ └──────┬──────┘ │ │ │ ① SYN 1, seq x (ISN) │ │ ─────────────SYN────────────── │ │ 我想认识你我的初始序号是x │ │ │ │ ② SYN 1, ACK 1 │ │ seq y (ISN) │ │ ack x 1 │ │ ──────────SYNACK───────────── │ │ 我也想认识你我的初始序号是y │ │ 期待收到你的x1 │ │ │ │ ③ ACK 1, seq x 1 │ │ ack y 1 │ │ ─────────────ACK────────────── │ │ 好的我收到你的y了期待y1 │ │ │ │◄──────────连接建立开始数据传输──────────►│ │ │ 图1TCP三次握手流程图2.2 每一步都在干什么① 第一次握手SYN客户端发起请求客户端发送一个SYN包携带以下关键信息SYN 1表示这是一个同步请求seq x客户端的初始序列号ISN此时客户端进入SYN_SENT状态。这个ISNInitial Sequence Number初始序列号是怎么来的RFC 793规定ISN应该是一个随时间变化的计数器每4微秒加1。现代操作系统通常采用更复杂的随机算法以防止序列号预测攻击。② 第二次握手SYN-ACK服务端回应服务端收到SYN后如果同意建立连接会回复SYN-ACK包SYN 1我也要同步ACK 1确认收到你的SYNseq y服务端的初始序列号ack x 1期待收到客户端的下一个序号此时服务端进入SYN_RCVD状态。注意这里的ack x 1它表示我已经收到了序号x及之前的所有数据下次请从x1开始发。③ 第三次握手ACK客户端确认客户端收到SYN-ACK后发送最终的ACK包ACK 1确认收到seq x 1从x1开始发送数据ack y 1期待收到服务端的y1此时客户端进入ESTABLISHED状态。服务端收到ACK后也进入ESTABLISHED状态。2.3 为什么是三次两次不行吗这是一个经典的面试题。答案是两次握手无法防止历史重复连接初始化造成的混乱。假设只有两次握手客户端发送SYNseq100但由于网络延迟这个包滞留了客户端重发SYNseq200这次成功建立连接传输数据后断开此时滞留的第一个SYNseq100突然到达服务端服务端以为是新的连接请求回复SYN-ACK进入ESTABLISHED状态但客户端早已放弃这个连接不会回复导致服务端空等**⚠️ 关键区别**三次握手时服务端收到旧的SYN后回复SYN-ACK但客户端发现ack号不对期待的ack应该是201但收到的是101会发送RST包重置连接服务端因此不会进入ESTABLISHED状态。另外三次握手还能确保双方都有收发能力第一次服务端知道客户端能发第二次客户端知道服务端能收也能发第三次服务端知道客户端能收三、四次挥手优雅的分手仪式 生活化比喻四次挥手就像分手——一方说我们分手吧另一方说好的我知道了然后说那我也同意分手最后说再见。为什么分手比相亲多一步因为分手时可能还有话没说完。3.1 挥手流程详解┌─────────────┐ ┌─────────────┐ │ 客户端 │ │ 服务端 │ │ (Client) │ │ (Server) │ └──────┬──────┘ └──────┬──────┘ │ │ │ ① FIN 1, seq u │ │ ─────────────FIN────────────── │ │ 我要关闭发送通道了 │ │ │ │ ② ACK 1, seq v │ │ ack u 1 │ │ ────────────ACK────────────── │ │ 知道了等我发完数据 │ │ │ │ 【服务端继续发送未传完的数据】 │ │ │ │ ③ FIN 1, ACK 1 │ │ seq w │ │ ack u 1 │ │ ────────────FIN────────────── │ │ 我也发完了关闭吧 │ │ │ │ ④ ACK 1, seq u 1 │ │ ack w 1 │ │ ─────────────ACK────────────── │ │ 好的再见 │ │ │ │◄──────────连接关闭进入TIME_WAIT────────►│ │ │ 图2TCP四次挥手流程图3.2 为什么挥手要四次因为TCP连接是全双工的——数据可以同时在两个方向上传输。关闭连接时每个方向都需要单独关闭。步骤方向含义第一次FIN客户端 → 服务端客户端没有数据要发了第一次ACK服务端 → 客户端服务端知道客户端要关了第二次FIN服务端 → 客户端服务端也没有数据要发了第二次ACK客户端 → 服务端客户端知道服务端也要关了注意第二步和第三步之间服务端可能还有数据要发给客户端所以不能合并。只有当服务端也发完数据后才会发送自己的FIN。四、TIME_WAIT分手后的冷静期4.1 什么是TIME_WAIT主动关闭连接的一方通常是客户端在发送最后一个ACK后不会立即关闭而是进入TIME_WAIT状态等待2MSLMaximum Segment Lifetime最大报文生存时间后才真正关闭。MSL是什么MSL是IP数据包在网络中的最大生存时间RFC 793建议值为2分钟。Linux系统中MSL通常设置为30秒或1分钟因此2MSL就是1-2分钟。4.2 为什么要等2MSL有两个原因原因一确保最后一个ACK能被对方收到如果最后一个ACK丢失服务端会重发FIN包。客户端在TIME_WAIT状态下仍能收到这个FIN并回复ACK。如果没有TIME_WAIT客户端直接关闭服务端就会一直重发FIN陷入死循环。原因二防止旧的连接数据包干扰新连接假设没有TIME_WAIT客户端立即关闭并用相同端口建立新连接。此时网络中可能还有旧连接的延迟数据包到达会被新连接误收造成数据混乱。场景没有TIME_WAIT会发生什么 时间线 ─────────────────────────────────────────────────────────► T0: 客户端关闭连接端口8080释放 │ │ 网络中还有一个延迟的数据包在游荡 ▼ T1: 客户端立即用8080建立新连接 │ │ 延迟的数据包突然到达 ▼ T2: 新连接收到旧数据包 → 数据混乱 有了TIME_WAIT2MSL等待 ─────────────────────────────────────────────────────────► T0: 客户端进入TIME_WAIT端口8080仍被占用 │ │ 等待2MSL时间... │ 延迟的数据包要么到达被丢弃要么超时消失 ▼ T1: TIME_WAIT结束端口8080释放 │ ▼ T2: 新连接建立安全无干扰 图3TIME_WAIT的作用示意图4.3 TIME_WAIT过多的问题在高并发场景下如果短连接频繁创建和关闭可能导致大量端口处于TIME_WAIT状态耗尽可用端口。# 查看当前TIME_WAIT连接数 $ netstat -an | grep TIME_WAIT | wc -l 2847 # 查看各状态连接统计 $ netstat -n | awk /^tcp/ {S[$NF]} END {for(a in S) print a, S[a]} ESTABLISHED 156 TIME_WAIT 2847 CLOSE_WAIT 12解决方案连接池复用连接减少频繁创建销毁长连接HTTP Keep-Alive、TCP keepalive端口复用SO_REUSEADDR和SO_REUSEPORT调低TIME_WAIT时间谨慎操作修改tcp_tw_reuse和tcp_tw_recycle五、Wireshark实战亲眼见证握手与挥手5.1 环境准备我们需要一个可以观察TCP连接的场景。最简单的方法是访问一个HTTP网站或者自己写一个客户端/服务端程序。# 简单的Python HTTP服务器用于测试 # 服务端server.py import socket def start_server(host0.0.0.0, port8080): s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((host, port)) s.listen(5) print(fServer listening on {host}:{port}) while True: conn, addr s.accept() print(fConnection from {addr}) data conn.recv(1024) print(fReceived: {data.decode()}) conn.send(bHello, Client!) conn.close() print(Connection closed) if __name__ __main__: start_server()# 客户端client.py import socket import time def start_client(host127.0.0.1, port8080): s socket.socket(socket.AF_INET, socket.SOCK_STREAM) print(Creating socket...) # 连接服务器三次握手发生在这里 s.connect((host, port)) print(Connected to server!) # 发送数据 s.send(bHello, Server!) data s.recv(1024) print(fReceived: {data.decode()}) time.sleep(2) # 等待一会儿再关闭 # 关闭连接四次挥手发生在这里 s.close() print(Connection closed) if __name__ __main__: start_client()5.2 Wireshark抓包步骤Step 1启动Wireshark选择要监听的网络接口通常是Wi-Fi或以太网。Step 2设置过滤条件在过滤栏输入tcp.port 8080只显示8080端口的TCP流量。Step 3运行程序先启动服务端再启动客户端观察Wireshark捕获的包。5.3 分析抓包结果你应该能看到类似下面的包序列No. Time Source Destination Protocol Info 1 0.000 127.0.0.1 127.0.0.1 TCP 50000 → 8080 [SYN] Seq0 Win65535 2 0.001 127.0.0.1 127.0.0.1 TCP 8080 → 50000 [SYN, ACK] Seq0 Ack1 3 0.001 127.0.0.1 127.0.0.1 TCP 50000 → 8080 [ACK] Seq1 Ack1 4 0.002 127.0.0.1 127.0.0.1 TCP 50000 → 8080 [PSH, ACK] Seq1 Ack1 Len14 5 0.003 127.0.0.1 127.0.0.1 TCP 8080 → 50000 [PSH, ACK] Seq1 Ack15 Len14 6 2.005 127.0.0.1 127.0.0.1 TCP 50000 → 8080 [FIN, ACK] Seq15 Ack15 7 2.005 127.0.0.1 127.0.0.1 TCP 8080 → 50000 [ACK] Seq15 Ack16 8 2.006 127.0.0.1 127.0.0.1 TCP 8080 → 50000 [FIN, ACK] Seq15 Ack16 9 2.006 127.0.0.1 127.0.0.1 TCP 50000 → 8080 [ACK] Seq16 Ack16解析包1-3三次握手SYN → SYN-ACK → ACK包4-5数据传输PSH表示推送数据包6-9四次挥手FIN → ACK → FIN → ACK5.4 常用Wireshark过滤表达式# 只显示三次握手包 tcp.flags.syn1 tcp.flags.ack0 # SYN tcp.flags.syn1 tcp.flags.ack1 # SYN-ACK # 只显示四次挥手包 tcp.flags.fin1 # 只显示RST包异常断开 tcp.flags.reset1 # 显示特定端口的所有TCP包 tcp.port 8080 # 显示特定连接的包用stream index过滤 tcp.stream 0六、序列号与确认号TCP的记账本6.1 序列号Sequence NumberTCP把发送的每个字节都编上号这就是序列号。它不是给每个包编号而是给每个字节编号。**示例**假设ISN 1000发送Hello5个字节H → seq 1000e → seq 1001l → seq 1002l → seq 1003o → seq 1004确认号ACK应该是1005表示我已经收到1004及之前的所有字节期待1005。6.2 确认号Acknowledgment Number确认号表示期望收到的下一个字节的序号。如果收到ACK1005意味着发送方知道1004及之前的所有数据都已安全到达。6.3 累积确认TCP采用累积确认机制。即使中间某些ACK丢失只要后面的ACK到达就说明前面的数据也收到了。发送方发送 seq1000, len100 发送 seq1100, len100 发送 seq1200, len100 接收方收到 1000-1099 → 可能不立即ACK延迟ACK 收到 1100-1199 → 可能不立即ACK 收到 1200-1299 → 回复 ACK1300 发送方收到 ACK1300 → 知道1000-1299都已收到 图4累积确认机制示意图七、总结与思考7.1 核心要点回顾概念关键点三次握手SYN → SYN-ACK → ACK初始化序列号确认双向通信能力四次挥手FIN → ACK → FIN → ACK全双工连接需要分别关闭两个方向TIME_WAIT等待2MSL确保ACK到达防止旧数据干扰新连接序列号给每个字节编号实现可靠传输和顺序保证确认号期望收到的下一个字节序号采用累积确认7.2 常见面试题为什么三次握手不是两次为什么四次挥手不是三次TIME_WAIT状态的作用是什么如果服务端收到大量SYN包但不回复会发生什么SYN Flood攻击什么是TCP半连接队列和全连接队列 源码获取本文所有示例代码已整理到GitHub仓库包含Python TCP客户端/服务端示例Wireshark抓包分析脚本TIME_WAIT状态监控工具GitHub地址https://github.com/yourname/tcp-handshake-demo 思考题如果你用tcpdump抓包发现三次握手只看到了两个包可能是什么原因在Linux系统中如何查看和修改tcp_fin_timeout参数这个参数和TIME_WAIT有什么关系为什么HTTP/2和HTTP/3都在努力减少TCP连接的数量这与我们今天讲的握手/挥手成本有什么关系尝试用nc命令手动模拟TCP三次握手的过程提示需要用到nc -vz和抓包工具配合。 系列文章预告网络协议系列文章持续更新中01 | HTTP/1.1 vs HTTP/2 vs HTTP/3进化之路02 | HTTPS原理详解从明文到密文的蜕变03 | TCP三次握手与四次挥手——连接管理的仪式感本文04 | TCP拥塞控制网络堵车的解决方案05 | UDP简单但不可靠为什么还有人用06 | WebSocket全双工通信的实现原理关注专栏第一时间获取更新通知如果本文对你有帮助欢迎点赞、收藏、转发有任何问题欢迎在评论区留言讨论。标签TCP协议三次握手四次挥手网络抓包连接管理