第1章 - TCP 三次握手与四次挥手
嗨,朋友!
还记得在基础教程第5章我们学过 TCP 是一个可靠的传输协议吗?今天我们要深入了解 TCP 是如何做到"可靠"的,其中最重要的就是三次握手和四次挥手机制。
这两个概念在面试中经常被问到,而且在实际工作中也很重要。理解了它们,你就能明白为什么有时候网络连接会变慢,为什么会出现 TIME_WAIT 状态,以及如何防御 SYN 攻击。
🤔 为什么需要三次握手?
生活中的类比
想象一下,你要给朋友打电话:
第一次(你拨号):
- 你:喂,能听到我说话吗?📞
第二次(朋友回应):
- 朋友:能听到!你能听到我说话吗?📱
第三次(你确认):
- 你:能听到!那我们开始聊吧!✅
这三次对话确保了:
- 你的话筒和朋友的听筒都正常(你→朋友的通道 ✅)
- 朋友的话筒和你的听筒都正常(朋友→你的通道 ✅)
- 双方都准备好了可以开始通话
TCP 的三次握手也是一样的道理!
正式定义
三次握手的目的
- 确认双方的收发能力都正常
- 协商初始序列号(用于数据包排序和去重)
- 协商其他参数(如窗口大小、最大报文段长度等)
📖 三次握手详细过程
状态转换图
客户端状态 服务器状态
CLOSED LISTEN(监听中)
|
| 发送 SYN
↓
SYN_SENT ─────────────→ SYN_RCVD
| |
| | 发送 SYN + ACK
| ←─────────────────────┘
|
| 发送 ACK
↓ ↓
ESTABLISHED ──────────→ ESTABLISHED
(连接建立) (连接建立)
第一次握手(SYN)
客户端 → 服务器
客户端发送一个 SYN(Synchronize) 包,告诉服务器:
我想和你建立连接!
我的初始序列号是 x
关键信息:
SYN = 1(标志位,表示这是一个同步请求)seq = x(客户端的初始序列号,随机生成)- 客户端进入
SYN_SENT状态
为什么序列号要随机?
- 安全性:防止被恶意猜测和劫持
- 避免混淆:防止旧连接的数据包干扰新连接
第二次握手(SYN + ACK)
服务器 → 客户端
服务器收到 SYN 包后,如果同意建立连接,会发送一个 SYN + ACK 包:
好的,我收到了你的请求!
我的初始序列号是 y
我确认收到了你的序列号 x
关键信息:
SYN = 1(服务器也要同步)ACK = 1(确认标志位)seq = y(服务器的初始序列号)ack = x + 1(确认号,表示期望收到客户端的下一个序列号)- 服务器进入
SYN_RCVD状态
第三次握手(ACK)
客户端 → 服务器
客户端收到服务器的 SYN + ACK 后,再发送一个 ACK 包:
好的,我也收到了!
现在可以开始传输数据了!
关键信息:
ACK = 1seq = x + 1ack = y + 1(确认服务器的序列号)- 双方都进入
ESTABLISHED状态 - 可以开始传输数据了!
第三次握手可以携带数据吗?
可以!因为此时客户端已经确认了服务器的接收能力,所以第三次握手的 ACK 包可以携带应用层数据。但前两次握手不能携带数据,因为还没有完全确认连接。
🎯 为什么是三次,不是两次或四次?
为什么不是两次握手?
问题场景:
假设只有两次握手:
1. 客户端发送 SYN
2. 服务器回复 SYN + ACK,连接建立 ✅
看起来没问题?但考虑这个情况:
- 客户端发送了一个 SYN 包(包A)
- 因为网络延迟,包A 在网络中滞留了很久
- 客户端以为丢包了,又发送了一个 SYN 包(包B)
- 包B 正常到达,连接建立并完成通信,然后断开
- 过了很久,包A 终于到达服务器
- 服务器以为这是一个新的连接请求,发送 SYN + ACK
- 在两次握手的情况下,连接就建立了!
- 但客户端根本不知道这个连接,不会发送数据
- 服务器一直等待,浪费资源!❌
三次握手的解决:
在第7步,服务器发送 SYN + ACK 后,需要等待客户端的第三次握手(ACK)。但客户端不会发送这个 ACK(因为它根本不知道这个连接),所以服务器最终会超时关闭,不会浪费资源。✅
为什么不是四次握手?
没必要!三次已经足够确认双方的收发能力和同步序列号了。四次握手只会增加延迟,没有额外的好处。
🔥 四次挥手详细过程
为什么需要四次挥手?
断开连接为什么需要四次,而建立连接只需要三次呢?
核心原因:TCP 是全双工通信(双向都可以传输数据)。
- 建立连接:双方都只需要表达"我准备好了",可以合并在一起
- 断开连接:每一方都要独立表达"我发完了"和"我同意你发完了",不能合并
状态转换图
客户端状态 服务器状态
ESTABLISHED ESTABLISHED
|
| ① 发送 FIN
↓
FIN_WAIT_1 ─────────────→ CLOSE_WAIT
| |
| ② 收到 ACK | (服务器可以继续发送数据)
↓ |
FIN_WAIT_2 |
| | ③ 发送 FIN
| ←─────────────────────────┘
| ↓
| ④ 发送 ACK LAST_ACK
↓ |
TIME_WAIT | 收到 ACK
| ↓
| (等待 2MSL) CLOSED
↓
CLOSED
第一次挥手(FIN)
客户端 → 服务器
客户端发送一个 FIN(Finish) 包:
我的数据发送完了,准备关闭连接!
关键信息:
FIN = 1seq = u- 客户端进入
FIN_WAIT_1状态 - 注意:客户端还可以接收数据,只是不再发送数据
第二次挥手(ACK)
服务器 → 客户端
服务器收到 FIN 后,先发送一个 ACK:
好的,我知道你要关闭了!
但我可能还有数据要发送,请稍等...
关键信息:
ACK = 1ack = u + 1- 服务器进入
CLOSE_WAIT状态 - 客户端收到后进入
FIN_WAIT_2状态
CLOSE_WAIT 状态
服务器处于 CLOSE_WAIT 状态时,可以继续发送数据给客户端。这就是为什么需要四次挥手,而不是三次。
第三次挥手(FIN)
服务器 → 客户端
服务器发送完剩余数据后,发送 FIN 包:
好了,我的数据也发送完了!
现在可以关闭连接了!
关键信息:
FIN = 1ACK = 1seq = wack = u + 1- 服务器进入
LAST_ACK状态
第四次挥手(ACK)
客户端 → 服务器
客户端收到服务器的 FIN 后,发送最后一个 ACK:
好的,确认收到!连接关闭!
关键信息:
ACK = 1ack = w + 1- 客户端进入
TIME_WAIT状态 - 服务器收到后进入
CLOSED状态 - 客户端等待 2MSL 后进入
CLOSED状态
⏰ TIME_WAIT 状态详解
什么是 TIME_WAIT?
客户端发送最后一个 ACK 后,不会立即关闭连接,而是进入 TIME_WAIT 状态,等待 2MSL(Maximum Segment Lifetime)时间。
MSL:一个报文在网络中存活的最长时间,通常是 30秒、1分钟或2分钟(取决于操作系统)。
2MSL:所以 TIME_WAIT 通常等待 1-4 分钟。
为什么需要 TIME_WAIT?
原因1:确保最后的 ACK 能够到达
场景:客户端发送最后的 ACK 后立即关闭
客户端 ────ACK────X 服务器
(ACK 丢失) |
| 超时重传 FIN
? ←───FIN────┘
(已关闭,无法响应)
结果:服务器一直重传 FIN,无法正常关闭 ❌
有了 TIME_WAIT:
客户端 ────ACK────X 服务器
| (ACK 丢失) |
| | 超时重传 FIN
| ←───FIN────────┘
|
| (还在 TIME_WAIT,可以重新发送 ACK)
|
| ────ACK───────→ ✅
原因2:避免旧连接的数据包干扰新连接
如果立即关闭,新连接可能会使用相同的端口,旧连接的延迟数据包可能会干扰新连接。
等待 2MSL 可以确保网络中所有旧连接的数据包都已经消失。
TIME_WAIT 过多怎么办?
问题场景:
在高并发的服务器上(比如短连接的 HTTP 服务),可能会有大量的 TIME_WAIT 连接,占用端口资源。
查看 TIME_WAIT 数量:
# Linux
netstat -an | grep TIME_WAIT | wc -l
# Windows
netstat -an | findstr TIME_WAIT
解决方案:
- 开启 SO_REUSEADDR(应用层):
// Node.js 示例
const server = net.createServer();
server.listen({
port: 8080,
host: '0.0.0.0',
reuseAddress: true // 允许重用 TIME_WAIT 状态的端口
});
- 调整系统参数(操作系统层):
# Linux - 减少 TIME_WAIT 时间(慎用!)
sudo sysctl -w net.ipv4.tcp_fin_timeout=30
# 开启 TIME_WAIT 重用
sudo sysctl -w net.ipv4.tcp_tw_reuse=1
- 使用连接池(推荐):
// 使用 HTTP Keep-Alive 保持连接
const http = require('http');
const agent = new http.Agent({
keepAlive: true,
maxSockets: 50
});
http.get('http://example.com', { agent }, (res) => {
// ...
});
🔒 常见问题:半连接队列和全连接队列
半连接队列(SYN Queue)
存储内容:收到 SYN 但还没完成三次握手的连接(处于 SYN_RCVD 状态)
队列满了会怎样:
- 新的 SYN 包会被丢弃
- 客户端会超时重传
- 用户感觉连接很慢或连接失败
全连接队列(Accept Queue)
存储内容:完成三次握手,等待应用程序 accept() 的连接(ESTABLISHED 状态)
队列满了会怎样:
- 新的连接请求会被拒绝
- 或者丢弃第三次握手的 ACK(取决于配置)
查看队列状态
# Linux - 查看队列溢出统计
netstat -s | grep -i "listen"
# 查看具体应用的队列状态
ss -lnt
调整队列大小
// Node.js - 设置全连接队列大小
const server = net.createServer();
server.listen(8080, '0.0.0.0', 511); // 第三个参数是 backlog
# Linux - 系统级调整
sudo sysctl -w net.ipv4.tcp_max_syn_backlog=2048 # 半连接队列
sudo sysctl -w net.core.somaxconn=1024 # 全连接队列
⚔️ SYN 攻击与防御
什么是 SYN 攻击?
SYN 洪水攻击(SYN Flood)是一种常见的 DDoS 攻击方式。
攻击原理:
攻击者不断发送 SYN 包,但不回复第三次握手的 ACK
攻击者 ──SYN──→ 服务器
攻击者 ──SYN──→ 服务器
攻击者 ──SYN──→ 服务器
攻击者 ──SYN──→ 服务器
...
服务器的半连接队列很快被占满
正常用户无法连接 ❌
危害:
- 半连接队列被占满
- 服务器资源被耗尽
- 正常用户无法建立连接
防御措施
1. SYN Cookie
原理:不使用半连接队列,而是用算法计算一个 Cookie 值作为序列号。
# Linux - 开启 SYN Cookie
sudo sysctl -w net.ipv4.tcp_syncookies=1
优点:
- ✅ 不占用半连接队列
- ✅ 能防御 SYN 攻击
缺点:
- ❌ 无法保存 TCP 选项(如窗口大小)
- ❌ 有轻微性能损耗
2. 增加半连接队列大小
sudo sysctl -w net.ipv4.tcp_max_syn_backlog=4096
3. 减少 SYN_RCVD 超时时间
# 默认可能是 60 秒,可以减少到 30 秒
sudo sysctl -w net.ipv4.tcp_synack_retries=1
4. 使用防火墙和负载均衡器
# iptables 限制单个 IP 的 SYN 速率
sudo iptables -A INPUT -p tcp --syn -m limit --limit 1/s --limit-burst 3 -j ACCEPT
sudo iptables -A INPUT -p tcp --syn -j DROP
💻 实战:监控 TCP 连接状态
Node.js 服务器示例
const net = require('net');
const server = net.createServer((socket) => {
console.log('客户端已连接');
console.log('本地地址:', socket.localAddress, ':', socket.localPort);
console.log('远程地址:', socket.remoteAddress, ':', socket.remotePort);
// 监听数据
socket.on('data', (data) => {
console.log('收到数据:', data.toString());
socket.write('Echo: ' + data);
});
// 监听连接关闭
socket.on('end', () => {
console.log('客户端断开连接(发送了 FIN)');
});
socket.on('close', () => {
console.log('连接已完全关闭');
});
socket.on('error', (err) => {
console.error('连接错误:', err);
});
});
server.listen(8080, () => {
console.log('TCP 服务器启动在端口 8080');
});
// 监听服务器错误
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error('端口已被占用');
} else {
console.error('服务器错误:', err);
}
});
使用 tcpdump 抓包
# 抓取 TCP 握手包
sudo tcpdump -i any port 8080 -nn
# 只看 SYN 包
sudo tcpdump -i any 'tcp[tcpflags] & tcp-syn != 0' -nn
# 只看 FIN 包
sudo tcpdump -i any 'tcp[tcpflags] & tcp-fin != 0' -nn
输出示例:
# 三次握手
14:23:45.123456 IP 192.168.1.100.54321 > 192.168.1.200.8080: Flags [S], seq 1234567890
14:23:45.123789 IP 192.168.1.200.8080 > 192.168.1.100.54321: Flags [S.], seq 9876543210, ack 1234567891
14:23:45.124012 IP 192.168.1.100.54321 > 192.168.1.200.8080: Flags [.], ack 9876543211
# 四次挥手
14:25:30.567890 IP 192.168.1.100.54321 > 192.168.1.200.8080: Flags [F.], seq 1234567900
14:25:30.568123 IP 192.168.1.200.8080 > 192.168.1.100.54321: Flags [.], ack 1234567901
14:25:30.568456 IP 192.168.1.200.8080 > 192.168.1.100.54321: Flags [F.], seq 9876543220
14:25:30.568789 IP 192.168.1.100.54321 > 192.168.1.200.8080: Flags [.], ack 9876543221
标志说明:
[S]= SYN[.]= ACK[S.]= SYN + ACK[F.]= FIN + ACK
Python 客户端测试
import socket
import time
# 创建 TCP 客户端
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print('准备连接服务器...')
try:
# 三次握手
client.connect(('127.0.0.1', 8080))
print('连接成功!')
# 发送数据
client.send(b'Hello, Server!')
print('数据已发送')
# 接收响应
response = client.recv(1024)
print('收到响应:', response.decode())
time.sleep(1)
# 四次挥手
print('准备关闭连接...')
client.close()
print('连接已关闭')
except Exception as e:
print('错误:', e)
finally:
client.close()
📊 总结对比表
三次握手 vs 四次挥手
| 对比项 | 三次握手 | 四次挥手 |
|---|---|---|
| 次数 | 3次 | 4次 |
| 目的 | 建立连接 | 断开连接 |
| 发起方 | 客户端 | 客户端或服务器 |
| 能否合并 | 第2、3次可以携带数据 | 第2、3次通常不能合并 |
| 关键状态 | SYN_SENT, SYN_RCVD | FIN_WAIT_1/2, CLOSE_WAIT, TIME_WAIT |
| 特殊等待 | 无 | TIME_WAIT(2MSL) |
TCP 连接状态速查表
| 状态 | 说明 | 何时出现 |
|---|---|---|
CLOSED | 关闭状态 | 初始状态 |
LISTEN | 监听状态 | 服务器等待连接 |
SYN_SENT | 已发送 SYN | 客户端发起连接后 |
SYN_RCVD | 已收到 SYN | 服务器收到 SYN 后 |
ESTABLISHED | 连接已建立 | 三次握手完成 |
FIN_WAIT_1 | 等待对方 FIN | 发送 FIN 后 |
FIN_WAIT_2 | 等待对方 FIN | 收到对方 ACK 后 |
CLOSE_WAIT | 等待关闭 | 收到对方 FIN 后 |
CLOSING | 双方同时关闭 | 同时发送 FIN |
LAST_ACK | 等待最后 ACK | 发送 FIN 后 |
TIME_WAIT | 时间等待 | 发送最后 ACK 后 |
🎯 学习建议
1. 理解原理优先 🧠
不要死记硬背"三次握手"、"四次挥手"的步骤,要理解为什么要这样设计:
- 为什么需要三次而不是两次?→ 防止旧连接干扰
- 为什么断开需要四次?→ 全双工通信,双方都要独立关闭
- 为什么需要 TIME_WAIT?→ 确保 ACK 到达和避免数据包混淆
2. 动手实践 💻
- 用
tcpdump或 Wireshark 抓包,看真实的握手和挥手过程 - 用
netstat观察连接状态的变化 - 写代码测试不同场景(正常关闭、异常关闭、TIME_WAIT 等)
3. 关注实际问题 🔍
- 生产环境中 TIME_WAIT 过多怎么办?
- 如何防御 SYN 攻击?
- 连接建立慢可能是什么原因?
📝 练习题
基础题
- 为什么 TCP 需要三次握手?两次握手有什么问题?
点击查看答案
两次握手的问题:
- 无法防止旧连接的 SYN 包建立无效连接
- 客户端无法确认服务器是否收到了自己的 ACK
- 容易造成服务器资源浪费
三次握手可以:
- 确认双方的收发能力
- 防止旧连接干扰
- 同步双方的初始序列号
- TIME_WAIT 状态的作用是什么?等待多长时间?
点击查看答案
作用:
- 确保最后的 ACK 能够到达服务器(如果丢失,服务器会重传 FIN)
- 确保旧连接的数据包在网络中消失,不会干扰新连接
时间:2MSL(Maximum Segment Lifetime)
- MSL 通常是 30秒-2分钟
- 所以 TIME_WAIT 通常等待 1-4 分钟
- 半连接队列和全连接队列有什么区别?
点击查看答案
半连接队列(SYN Queue):
- 存储处于
SYN_RCVD状态的连接 - 已收到 SYN,但还未完成三次握手
- 队列满了会丢弃新的 SYN 包
全连接队列(Accept Queue):
- 存储处于
ESTABLISHED状态的连接 - 已完成三次握手,等待应用程序
accept() - 队列满了会拒绝新连接
进阶题
- 如何检测和防御 SYN 攻击?
点击查看答案
检测方法:
# 查看半连接队列溢出
netstat -s | grep -i "SYNs to LISTEN"
# 查看 SYN_RCVD 状态的连接数
netstat -an | grep SYN_RCVD | wc -l
防御措施:
- 开启 SYN Cookie:
sysctl -w net.ipv4.tcp_syncookies=1 - 增加半连接队列大小:
sysctl -w net.ipv4.tcp_max_syn_backlog=4096 - 减少 SYN-ACK 重传次数:
sysctl -w net.ipv4.tcp_synack_retries=1 - 使用防火墙限制 SYN 速率
- 使用负载均衡器和 CDN 分散流量
- 生产环境中发现大量 TIME_WAIT 连接,应该如何优化?
点击查看答案
优化策略:
应用层优化(推荐):
- 使用 HTTP Keep-Alive 保持长连接
- 使用连接池复用连接
- 让客户端主动关闭连接(TIME_WAIT 在客户端)
系统参数优化(谨慎使用):
# 允许 TIME_WAIT 重用 sysctl -w net.ipv4.tcp_tw_reuse=1 # 减少 FIN_WAIT 超时时间 sysctl -w net.ipv4.tcp_fin_timeout=30架构优化:
- 使用负载均衡器分散连接
- 增加服务器数量
- 使用更多的 IP 地址和端口
注意:不要使用 tcp_tw_recycle,它在 NAT 环境下会导致问题,已在 Linux 4.12 中移除。
- 如果在第三次握手时,客户端发送的 ACK 丢失了会怎样?
点击查看答案
场景分析:
客户端 服务器
SYN_SENT ──SYN──→ LISTEN
←─SYN+ACK─ SYN_RCVD
ESTABLISHED ──ACK──X SYN_RCVD (ACK 丢失)
结果:
客户端:
- 认为连接已建立(
ESTABLISHED) - 可以开始发送数据
- 认为连接已建立(
服务器:
- 仍处于
SYN_RCVD状态 - 超时后会重传 SYN+ACK
- 如果客户端收到重传的 SYN+ACK,会再次发送 ACK
- 如果一直收不到,最终超时关闭连接
- 仍处于
如果客户端发送数据:
- 服务器会收到数据包(带有 ACK 标志)
- 服务器会认为这是第三次握手的 ACK
- 连接进入
ESTABLISHED状态 - 所以实际上不会有问题!
结论:TCP 的设计非常健壮,即使第三次握手的 ACK 丢失,也可以通过后续的数据包恢复连接状态。
🎓 下一步学习
恭喜你!现在你已经深入理解了 TCP 连接的建立和断开过程。
建议继续学习:
- 第2章 - 负载均衡原理 - 了解如何提高系统性能
- 第5章 - 网络安全基础 - 学习更多网络攻击和防御
- 基础教程第5章 - 回顾 TCP 基础知识
推荐阅读:
- 《TCP/IP 详解 卷1:协议》- 经典教材
- RFC 793 - TCP 协议标准文档
关键概念回顾:
- ✅ 三次握手:SYN → SYN+ACK → ACK
- ✅ 四次挥手:FIN → ACK → FIN → ACK
- ✅ TIME_WAIT:等待 2MSL,确保连接正常关闭
- ✅ SYN 攻击:利用半连接队列的 DDoS 攻击
