深入理解 TCP 协议:三次握手与四次挥手的底层原理
一、TCP 协议基础:为什么需要 “握手” 与 “挥手”?
在正式讲解 TCP 连接管理流程前,我们需要先深入理解 TCP 协议的两个核心特性 —— 面向连接(Connection-oriented)和可靠性(Reliability),这也是 "三次握手" 与 "四次挥手" 机制存在的根本原因:
面向连接特性详解:
- 建立连接:TCP 在通信前必须通过三次握手建立逻辑连接,确保通信双方都准备好进行数据传输。这类似于打电话时的拨号接通过程。
- 关闭连接:通信结束后需要通过四次挥手正常关闭连接,释放系统资源(如端口号、缓冲区等)。如果不规范关闭可能导致"僵尸连接"占用资源。
- 连接状态管理:TCP 维护精确的连接状态机(LISTEN、SYN-SENT、ESTABLISHED等),确保连接生命周期的规范管理。
可靠性保障机制:
- 确认机制(ACK):接收方对每个收到的数据包发送确认应答
- 序列号机制(Sequence Number):为每个字节编号,解决数据包乱序问题
- 超时重传:未收到ACK时会重传数据包
- 流量控制:通过滑动窗口机制匹配收发双方的速率
- 拥塞控制:通过慢启动、拥塞避免等算法防止网络过载
三次握手的核心作用:
- 同步双方的初始序列号(ISN),ISN是基于时钟的动态随机值,防止历史连接干扰
- 交换TCP参数(如MSS最大报文段大小、窗口缩放因子等)
- 确认双方的收发能力正常
- 为后续可靠传输建立基础,序列号将用于确认和重传机制
四次挥手的必要性:
- 全双工特性决定需要分别关闭两个方向的数据流
- FIN报文需要得到确认,确保数据完整传输
- TIME_WAIT状态等待2MSL(最大报文生存时间),防止最后ACK丢失
- 彻底释放连接资源,避免出现半关闭状态(Half-close)
示例场景: 当客户端访问web服务器时:
- 三次握手:客户端发送SYN→服务器回复SYN+ACK→客户端回复ACK
- 数据传输:客户端发送HTTP请求,服务器返回网页数据
- 四次挥手:任一方先发送FIN→收到ACK→另一方向发送FIN→最终ACK确认
二、三次握手:TCP 连接的建立过程
1. 三次握手概述
三次握手是 TCP/IP 协议中客户端与服务器之间通过交换 3 个 TCP 报文段来完成连接建立的关键过程。我们以 "客户端发起 HTTP 请求" 这一常见场景为例,详细拆解每一步的操作细节和底层原理。
2. 核心概念铺垫
TCP 报文关键字段
字段 | 含义 | 详细说明 |
---|---|---|
SYN | 同步标志位 | 用于发起连接请求,当取值为 1 时表示"请求同步序列号"。在三次握手中,第一个SYN报文由客户端发出,第二个SYN报文由服务器返回 |
ACK | 确认标志位 | 用于确认收到对方的报文,取值为 1 时表示"确认有效"。在三次握手中,第二个和第三个报文都带有ACK标志 |
Seq | 序列号 | 32位无符号数,用于标识当前发送的报文段中第一个数据字节的编号。每个方向(客户端到服务器或服务器到客户端)都有独立的序列号空间 |
Ack | 确认号 | 32位无符号数,表示期望收到对方下一个报文段的序列号,即"已收到的最大序列号 + 1"。确认号用于确保数据按序到达 |
3. 三次握手具体流程
假设场景:客户端(IP:192.168.1.100,端口:54321)向服务器(IP:10.0.0.1,端口:80)发起 TCP 连接,建立后用于传输 HTTP 请求。
第一步:客户端发送 "连接请求"(SYN 报文)
- 发送方:客户端
- 报文类型:SYN 报文(设置SYN=1,ACK=0)
- 关键字段:
- Seq = x(x 为客户端随机生成的初始序列号,例如 1000,用于标识客户端后续发送数据的起始编号)
- 目的端口:80(HTTP服务标准端口)
- 目的:告诉服务器"我想和你建立连接,请你确认,并同步你的序列号给我"
- 服务器行为:
- 收到 SYN 报文后,确认客户端的连接请求有效
- 在内存中创建传输控制块(TCB),记录客户端的初始序列号 x
- 准备向客户端发送"确认 + 同步"报文
- 将该连接状态置为SYN_RECEIVED(已收到SYN)
第二步:服务器回复"确认 + 同步"(SYN+ACK 报文)
- 发送方:服务器
- 报文类型:SYN+ACK 报文(设置SYN=1,ACK=1)
- 关键字段:
- Seq = y(y 为服务器随机生成的初始序列号,例如 2000,用于标识服务器后续发送数据的起始编号)
- Ack = x + 1(确认客户端的 Seq=x 已收到,期望下次收到客户端的 Seq=x+1)
- 目的:告诉客户端"我已收到你的连接请求(确认),同时我也把我的序列号同步给你(同步),请你确认我的序列号"
- 客户端行为:
- 收到 SYN+ACK 报文后,确认服务器已响应连接请求,且同步了服务器的序列号 y
- 验证 Ack 字段是否为 x+1(若不符,说明报文丢失或错误,会重传 SYN 报文)
- 准备向服务器发送"最终确认"报文
- 将本地连接状态置为ESTABLISHED(已建立)
第三步:客户端发送"最终确认"(ACK 报文)
- 发送方:客户端
- 报文类型:ACK 报文(设置SYN=0,ACK=1)
- 关键字段:
- Seq = x + 1(基于第一步的 Seq=x,后续发送数据将从 x+1 开始)
- Ack = y + 1(确认服务器的 Seq=y 已收到,期望下次收到服务器的 Seq=y+1)
- 目的:告诉服务器"我已收到你的序列号,连接可以正式建立了"
- 服务器行为:
- 收到 ACK 报文后,验证 Ack 字段是否为 y+1(若不符,会重传 SYN+ACK 报文)
- 验证通过后,TCP 连接正式建立(双方进入 ESTABLISHED 状态)
- 此时客户端可以立即发送应用层数据(如 HTTP 请求),服务器则可以开始接收数据
4. 为什么必须是"三次"握手?
这是网络协议面试中最常见的问题之一,核心原因是避免"历史重复的连接请求"导致服务器资源浪费:
问题场景分析
假设采用"两次握手":
- 客户端发送第一个SYN报文(Seq=1000)后,因网络拥塞,该报文长时间滞留
- 客户端超时未收到响应,重新发送SYN报文(Seq=2000),这次成功与服务器建立连接并完成通信后关闭
- 此时滞留在网络中的旧SYN报文(Seq=1000)终于到达服务器
- 服务器误以为是新的连接请求,回复SYN+ACK(Seq=3000, Ack=1001)
- 在两次握手机制下,服务器认为连接已建立,开始等待客户端发送数据
产生的问题
- 客户端已经关闭了该连接,不会回复服务器的SYN+ACK
- 服务器会一直维护这个半开的连接,占用端口、内存等资源
- 如果大量这样的无效连接积累,最终会导致服务器资源耗尽
三次握手如何解决
- 在三次握手中,服务器必须收到客户端的第三次ACK才会真正建立连接
- 对于滞留的旧SYN报文,服务器会回复SYN+ACK
- 但客户端已经关闭该连接上下文,不会发送对应的第三次ACK
- 服务器在超时(通常是30秒到2分钟)后会自动关闭这个半开的连接
- 这样确保了服务器不会长期维护无效连接,避免了资源浪费
其他考虑因素
- 序列号同步:三次握手确保了双方都确认了对方的初始序列号,为可靠传输奠定基础
- 防止DoS攻击:不完全的连接请求不会占用服务器完整资源
- 网络适应性:适应不可靠的网络环境,确保连接建立的可靠性
- 对称性设计:双方都确认了对方的接收能力,建立了双向通信通道
三、四次挥手:TCP 连接的关闭过程
1. 四次挥手的必要性
TCP 连接的关闭需要四次交互(俗称"四次挥手"),这是由 TCP 协议的全双工通信特性决定的。在 HTTP 1.1 中,当浏览器与服务器完成页面数据传输后,就会启动这个关闭流程。让我们通过一个具体的网页访问场景来说明:
假设用户访问 www.example.com,浏览器(客户端)与服务器建立 TCP 连接并完成数据传输后,浏览器会主动发起关闭连接请求。
2. 四次挥手的具体流程
第一步:客户端发送FIN报文(FIN=1)
发送方:客户端(如浏览器)
报文详情:
- 控制位:FIN=1, ACK=1
- 序列号:Seq = m(假设客户端总共发送了100字节数据,序列号从x开始,则m = x + 100)
- 确认号:Ack = n(确认已收到服务器的所有数据)
状态变化:
- 客户端:ESTABLISHED → FIN_WAIT_1
- 服务器:保持ESTABLISHED
实际应用场景:当浏览器完成HTTP请求并收到完整响应后,就会发送这个FIN报文,表示"我的数据都发完了"。
第二步:服务器回复ACK报文
发送方:服务器
报文详情:
- 控制位:ACK=1
- 序列号:Seq = n
- 确认号:Ack = m + 1
状态变化:
- 客户端:FIN_WAIT_1 → FIN_WAIT_2
- 服务器:ESTABLISHED → CLOSE_WAIT
关键点:此时服务器可能还有数据要发送给客户端,比如:
- 未完成的HTTP分块传输
- 服务器推送的数据
- 延迟确认的数据包
第三步:服务器发送FIN报文
发送方:服务器
报文详情:
- 控制位:FIN=1, ACK=1
- 序列号:Seq = p(如果在CLOSE_WAIT期间又发送了50字节数据,则p = n + 50)
- 确认号:Ack = m + 1
状态变化:
- 服务器:CLOSE_WAIT → LAST_ACK
- 客户端:保持FIN_WAIT_2
典型场景:Web服务器完成所有响应数据的发送后,才会发送这个FIN报文。
第四步:客户端发送最终ACK
发送方:客户端
报文详情:
- 控制位:ACK=1
- 序列号:Seq = m + 1
- 确认号:Ack = p + 1
状态变化:
- 客户端:FIN_WAIT_2 → TIME_WAIT(持续2MSL,通常1-4分钟)
- 服务器:收到ACK后立即进入CLOSED状态
3. 关键问题深度解析
3.1 为什么不能是三次挥手?
核心原因:TCP的全双工特性要求两个方向的数据通道需要独立关闭。
示例场景:假设采用三次挥手:
- 客户端FIN
- 服务器FIN+ACK
- 客户端ACK
这样会导致如果服务器在第二步之后还有数据要发送,这些数据就会丢失。四次挥手确保了:
- 先确认关闭客户端→服务器方向
- 再处理服务器→客户端方向的数据
- 最后确认关闭反向通道
3.2 TIME_WAIT状态的重要性
等待2MSL(Maximum Segment Lifetime)的两个关键原因:
可靠终止:
- 如果最后一个ACK丢失,服务器会重传FIN
- 客户端在TIME_WAIT期间能收到这个重传的FIN并再次发送ACK
- 典型值:Linux默认MSL为60秒,所以TIME_WAIT为120秒
防止旧连接数据混淆:
- 确保网络中所有属于这个连接的报文都消失
- 避免新建立的相同四元组(源IP、源端口、目标IP、目标端口)连接收到旧数据
实际影响:
- 在高性能服务器上,大量TIME_WAIT连接会占用端口资源
- 解决方案:
- 启用SO_REUSEADDR套接字选项
- 调整TCP参数(如net.ipv4.tcp_tw_reuse)
3.3 CLOSE_WAIT状态堆积问题
产生原因:
- 服务器应用层没有正确调用close()关闭套接字
- 常见于以下情况:
- 代码存在资源泄漏
- 异常处理路径没有关闭连接
- 使用连接池但管理不当
检测与解决:
# Linux查看CLOSE_WAIT连接
netstat -antp | grep CLOSE_WAIT
解决方案:
- 代码层面:
- 确保所有执行路径都关闭套接字
- 使用try-finally或RAII模式管理资源
- 系统层面:
- 调整TCP keepalive参数
- 设置合理的连接超时
4. 实际应用中的变体
4.1 同时关闭
当双方同时发起关闭时,流程会简化为:
- 客户端FIN → FIN_WAIT_1
- 服务器FIN → FIN_WAIT_1
- 双方收到FIN后都发送ACK → 直接进入CLOSING状态
- 收到ACK后进入TIME_WAIT
4.2 半关闭状态
使用shutdown()函数可以实现半关闭:
- SHUT_WR:关闭写入方向,类似发送FIN
- SHUT_RD:关闭读取方向(较少使用)
- 典型应用:HTTP的流水线传输
5. 性能优化建议
服务器端:
- 适当调整TIME_WAIT超时
- 启用TCP快速回收
echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle
客户端:
- 使用连接池复用连接
- 避免频繁创建短连接
监控指标:
- TIME_WAIT连接数
- CLOSE_WAIT连接数
- 连接建立/关闭频率
通过深入理解TCP四次挥手机制,开发人员可以更好地诊断网络问题,优化应用程序的网络性能,构建更可靠的分布式系统。
四、实战:通过 Wireshark 抓包验证三次握手与四次挥手
1. 实验环境准备
客户端配置
- 操作系统:Windows 10/11 或 macOS 10.15 及以上版本
- 浏览器:Chrome/Firefox/Edge 最新版本
- 推荐配置:8GB 内存,100MB 以上硬盘空间
服务器选择(二选一)
本地服务器方案:
- 安装 Nginx 1.18+ 版本
- 配置监听端口为 80(默认配置)
- 本地访问地址:
http://localhost
或http://127.0.0.1
公网服务器方案:
- 选择稳定网站如
www.baidu.com
- 确保能正常访问 HTTP 协议(非 HTTPS)
必备工具
- Wireshark 3.6+ 版本
- 安装时勾选所有组件(特别是 WinPcap/Npcap)
- 管理员权限运行(Windows 需要)
2. 详细抓包步骤
网卡配置
- 以管理员身份启动 Wireshark
- 在首页选择当前活动的网卡:
- 有线连接:通常显示为 "Ethernet" 或 "本地连接"
- 无线连接:显示为 "Wi-Fi" 或 "Wireless"
- 右键网卡选择 "Start Capture"
过滤规则设置
- 在过滤栏输入精确表达式:
tcp.port == 80
- 可选辅助过滤:
- 本地测试:
ip.addr == 127.0.0.1
- 公网测试:
ip.addr == <目标服务器IP>
- 本地测试:
操作流程
- 清空现有抓包数据(Edit → Clear All)
- 点击绿色鲨鱼图标开始抓包
- 立即打开浏览器访问目标地址
- 等待页面完全加载(包括所有资源)
- 关闭浏览器所有标签页
- 返回 Wireshark 点击红色方块停止抓包
3. 协议分析详解
三次握手分析
- 筛选
tcp.flags.syn==1 and tcp.flags.ack==0
找到初始 SYN 包- 查看 Seq=0(实际为随机值)
- 确认源端口为临时端口(通常 > 32768)
- 筛选
tcp.flags.syn==1 and tcp.flags.ack==1
找到 SYN-ACK 响应- 确认 Ack=Seq+1
- 目标端口应为 80
- 筛选
tcp.flags.ack==1
找到最后的 ACK- 确认 Seq=初始 Ack 值
- Ack=服务端 Seq+1
数据传输观察
- 查找 HTTP GET 请求(筛选
http.request
) - 观察后续 TCP 段的 Seq/Ack 编号变化规律
- 注意窗口大小(Window Size)的动态调整
四次挥手分析
- 首次 FIN 包特征:
- 筛选
tcp.flags.fin==1
- 通常由客户端发起(浏览器关闭时)
- 状态转为 FIN_WAIT_1
- 筛选
- 服务端 ACK 响应:
- 确认 Ack=Seq+1
- 客户端进入 FIN_WAIT_2
- 服务端 FIN 包:
- 可能包含最后的数据(PSH 标志)
- 服务端进入 LAST_ACK
- 最终 ACK:
- 客户端进入 TIME_WAIT(约4分钟)
- 观察序列号最终确认
异常情况排查
- 重传检测:
tcp.analysis.retransmission
- 乱序检查:
tcp.analysis.out_of_order
- 连接重置:
tcp.flags.reset==1
4. 实验报告要点
建议记录以下关键数据:
- 三次握手各阶段的精确时间戳(右键包 → "Set Time Reference")
- 初始序列号(原始值而非相对值)
- 四次挥手各标志位的组合情况
- 计算整个会话的持续时间(Statistics → Conversations → TCP)
- 流量统计(Statistics → HTTP → Packet Counter)