【Socket 进阶之路】第 3 章 TCP 三次握手 & 四次挥手深度剖析|连接建立、断开、状态机、TIME_WAIT 核心工程问题

💖 点赞 + 收藏 + 关注,Socket 工业级网络编程全套实战持续更新!

✨ 本专栏 全程手写源码、无阉割、纯工程

0. 前言

上一章我们实现了阻塞 TCP 客户端与服务端 Demo,能够正常收发数据。但绝大多数开发者写完 Demo 之后,完全不理解连接建立和断开在内核中发生了什么。

实际项目中经常遇到这些棘手问题:

  1. 服务端重启后,提示 Address already in use 端口占用,需要等待几十秒才能重新启动;
  2. 客户端异常断电拔网线,服务端长时间无法感知连接断开,形成半连接
  3. 抓包看到大量 TIME_WAIT、CLOSE_WAIT 状态连接,连接数不断堆积,程序最终无法新建连接;
  4. 程序调用 close 关闭连接后,偶尔收到 RST 复位报文,数据丢失;

所有问题根源:不懂 TCP 连接状态流转、三次握手、四次挥手内核行为。 本章不堆砌晦涩课本理论,结合抓包、状态流转、工程解决方案讲解,兼顾面试考点和量产调试。

工具推荐:wireshark 抓包工具,可以直观观察握手、挥手报文。

1. TCP 连接基础:五元组与 TCP 头部核心字段

一条 TCP 连接由五元组唯一标识:源IP、源端口、目的IP、目的端口、协议(TCP) TCP 报文头部关键字段:

  1. SEQ(序列号):本次发送数据的起始编号,保证数据流有序;
  2. ACK(确认号):期望下一次收到的序列号 = 对方 SEQ + 已接收数据长度;
  3. 标志位:SYN(同步建立连接)、FIN(结束断开连接)、ACK(确认)、RST(强制复位)、PSH(推送数据);
  4. Window 滑动窗口:流量控制;
  5. Checksum:校验和。

SYN、FIN 报文本身会占用一个序列号,即使没有数据负载。

2. TCP 三次握手(连接建立)完整流程

通信双方:客户端(主动发起连接)、服务端(监听等待连接) 整体目标:协商双方初始序列号 ISN,确认双方收发能力正常

第一次握手:客户端 → 服务端,SYN

客户端调用 connect(),内核封装 SYN 报文,携带客户端初始序列号 ISN (c) 客户端状态:CLOSEDSYN_SENT

第二次握手:服务端 → 客户端,SYN + ACK

服务端监听套接字收到 SYN 报文,内核回复报文: SYN(服务端自己初始序列号 ISN (s)) + ACK(确认号 = ISN (c)+1) 服务端状态:LISTENSYN_RCVD

第三次握手:客户端 → 服务端,ACK

客户端收到 SYN+ACK,校验通过后回复纯 ACK 报文,确认号 = ISN (s)+1 客户端状态:SYN_SENTESTABLISHED 服务端收到这个 ACK,状态:SYN_RCVDESTABLISHED

✅ 三次握手完成,连接正式建立,用户态 connect() 返回 0,accept() 返回新 connfd,程序可以收发业务数据。

工程问答:为什么不是两次握手? 两次握手只能确认客户端发、服务端收正常,无法确认服务端发、客户端收链路正常,会造成半开连接资源浪费。

3. TCP 四次挥手(连接断开)完整流程

TCP 是全双工通信,两个方向数据流可以独立关闭,因此断开需要四次报文交互。 主动关闭方(调用 close)、被动关闭方。

第一次挥手:主动方 → 被动方,FIN

主动调用 close (fd),内核发送 FIN 报文,表示:我不再发送新数据,但我还可以接收你剩下的数据 主动方状态:ESTABLISHEDFIN_WAIT_1

第二次挥手:被动方 → 主动方,ACK

被动方收到 FIN,内核立即回复 ACK 确认。 被动方状态:ESTABLISHEDCLOSE_WAIT

⚠️ 重点:此时被动方应用层还没调用 close! 内核只是确认收到断开请求,业务代码依旧可以继续发送剩余数据。 主动方收到 ACK:状态切换 FIN_WAIT_1FIN_WAIT_2

第三次挥手:被动方业务处理完毕,调用 close → 发送 FIN

被动方业务数据发送完成,调用 close,内核发出 FIN 报文 被动方状态:CLOSE_WAITLAST_ACK

第四次挥手:主动方收到 FIN,回复 ACK

主动方回复 ACK 确认,状态:FIN_WAIT_2TIME_WAIT 被动方收到 ACK:LAST_ACKCLOSED,连接彻底释放。 主动方 等待 2MSL 时长,才最终切换为 CLOSED,释放五元组资源。

MSL:报文最大生存时间,Linux 默认 30s,2MSL 就是 60s

4. 关键连接状态详解(工程高频)

4.1 TIME_WAIT(主动关闭方产生)

产生场景:主动 close,完成四次挥手最后一步 ACK 之后,停留 2MSL 作用:

  1. 防止第四次挥手 ACK 报文丢失,被动方超时重发 FIN,主动方保留状态可以重传 ACK;
  2. 保证这条连接上所有残留报文在网络中全部消失,避免新复用的五元组收到旧滞留数据包。

工程痛点:大量 TIME_WAIT 占用端口,服务无法快速重启 解决方案:端口复用 setsockopt(),代码示例本章末尾提供。

4.2 CLOSE_WAIT(被动关闭方产生)

产生根源:内核收到 FIN 报文,进入 CLOSE_WAIT,但是业务代码一直没有调用 close (connfd) 后果:文件描述符泄漏,连接永久挂着,fd 耗尽后无法新建连接! 排查思路:代码分支检查,所有 read 返回 0、读写异常场景,必须 close 关闭 fd。

4.3 LISTEN:服务端监听状态,等待客户端 SYN 连接

4.4 ESTABLISHED:正常双向数据通信状态

4.5 SYN_SENT:客户端已发送 SYN,等待服务端 SYN+ACK

4.6 SYN_RCVD:服务端收到 SYN,等待客户端第三次握手 ACK

5. RST 复位报文(强制断开,不走四次挥手)

当出现异常场景,TCP 会发送 RST 报文,直接粗暴释放连接,不做优雅关闭:

  1. 向一个不存在监听端口发送数据;
  2. 连接已经关闭,依旧往 fd 写入数据(SIGPIPE 信号);
  3. 半开连接长时间异常;
  4. 报文校验失败、非法序列号。

工程避坑:程序默认收到 SIGPIPE 信号会直接崩溃!量产程序必须捕获或者忽略 SIGPIPE。

复制代码
// 忽略SIGPIPE,防止write断开的socket导致程序崩溃
signal(SIGPIPE, SIG_IGN);

6. 端口复用 setsockopt 解决 TIME_WAIT 端口占用

默认情况下,服务端主动 close 后,端口处于 TIME_WAIT,2MSL 内无法重新 bind。 添加端口复用选项,允许快速重启服务,工业项目必加!

复制代码
int opt = 1;
// 设置端口复用,放在socket创建之后,bind之前调用
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0)
{
    perror("setsockopt fail");
}

注意:SO_REUSEADDR 主要解决 TIME_WAIT 占用;还有 SO_REUSEPORT 支持多进程绑定同一个端口(Linux3.9+)

7. 查看本机 TCP 连接状态调试命令(必备调试工具)

复制代码
# 查看所有tcp连接状态
netstat -antp
# 或者ss命令(性能更好,推荐)
ss -antp
# 过滤指定端口8888
ss -antp | grep 8888

可以直接观察 TIME_WAIT / CLOSE_WAIT / ESTABLISHED 连接数量,定位连接泄漏问题。

8. 本章工程踩坑总结

  1. close 是单向关闭读写,内核完成四次挥手;shutdown 可以精细控制只关闭读 / 写方向,适合优雅断连;
  2. CLOSE_WAIT 99% 都是业务代码忘记 close 套接字,造成句柄泄漏;
  3. TIME_WAIT 是保护机制,不是 bug,服务端必须添加 SO_REUSEADDR 端口复用;
  4. 写入已断开 socket 会触发 SIGPIPE,进程直接退出,量产程序务必忽略该信号;
  5. 客户端断电、拔网线不会发送 FIN 报文!内核无法立刻感知断开,需要应用层心跳检测(后续章节讲解);
  6. SYN 队列溢出:高并发短连接场景,listen 的 backlog 配合内核参数调整,防止 SYN 攻击。

9. 本章小结

本章从报文交互拆解三次握手建立连接、四次挥手断开连接完整流程,梳理 TCP 核心连接状态流转,重点分析 TIME_WAIT、CLOSE_WAIT 两大线上高频故障成因与解决方案。 我们明白了优雅关闭和 RST 强制复位区别,掌握端口复用、SIGPIPE 处理、连接状态排查命令。

遗留思考:

  1. TCP 流式协议没有报文边界,粘包分包到底如何完整解决?
  2. 如何区分正常断连和网络异常掉线?

下一章:TCP 粘包分包终极解决方案|流式无边界问题、环形缓冲区、协议帧切割工业级实现

相关推荐
彧azz6 小时前
图的存储结构详解:邻接矩阵的原理、实现与应用
开发语言·数据结构·学习·php
平头哥AI7 小时前
Day 22 _ 包装错误别丢链_%w、errors.Is 与 errors.As
android·服务器·学习·golang·go
嵌入式学习菌7 小时前
Modbus‑RTU 数据类型分析
开发语言·单片机·bug
QYRdata7 小时前
权威数据披露:2026至2032年边缘云服务CAGR达15.6%,赛道发展驶入快车道
网络·人工智能·云计算·服务发现·边缘计算
matlab代码8 小时前
基于matlab数字图像处理的指纹识别系统【源码68期】
开发语言·matlab
新时代牛马8 小时前
PCI与PCIe 硬件原理、配置空间/BAR 与 Linux 驱动完整篇:从 LTSSM、TLP 到 ECAM 与probe
java·linux·服务器
matlab代码8 小时前
基于matlab的水果识别系统(苹果香蕉菠萝梨桃子)【源码65期】
开发语言·matlab
wuyk5558 小时前
从零吃透 MQTT 通信|第 8 章 FreeRTOS 多任务架构下 MQTT 工程架构,任务拆分、队列解耦、临界区保护
c语言·开发语言·stm32·学习·架构
weixin199701080169 小时前
[特殊字符]《跨境二手ERP对接Back Market:标准化API + 7~10工作日技术合规审核实录》(附Python源码)
开发语言·python·pandas