题目
请简述 TCP 三次握手和四次挥手的过程。
📝 标准答案
核心要点
三次握手(建立连接):
- 第一次:客户端发送 SYN
- 第二次:服务器发送 SYN + ACK
- 第三次:客户端发送 ACK
四次挥手(断开连接):
- 第一次:客户端发送 FIN
- 第二次:服务器发送 ACK
- 第三次:服务器发送 FIN
- 第四次:客户端发送 ACK
为什么需要三次握手:
- 确认双方的收发能力
- 防止已失效的连接请求突然又传到服务器
- 同步序列号
为什么需要四次挥手:
- TCP 是全双工通信
- 需要双方都确认关闭
- 服务器可能还有数据要发送
详细说明
三次握手过程
客户端 服务器
| |
| 1. SYN (seq=x) |
|----------------------------------->|
| 客户端:我要建立连接 |
| |
| 2. SYN-ACK (seq=y, ack=x+1) |
|<-----------------------------------|
| 服务器:收到,我也准备好了 |
| |
| 3. ACK (ack=y+1) |
|----------------------------------->|
| 客户端:收到,开始传输数据 |
| |
| 连接建立,开始传输数据 |
|<=================================>|详细过程:
第一次握手(SYN)
- 客户端发送 SYN 包(seq=x)到服务器
- 客户端进入 SYN_SENT 状态
- 表示:客户端请求建立连接
第二次握手(SYN-ACK)
- 服务器收到 SYN 包
- 发送 SYN-ACK 包(seq=y, ack=x+1)
- 服务器进入 SYN_RCVD 状态
- 表示:服务器确认收到,并同意建立连接
第三次握手(ACK)
- 客户端收到 SYN-ACK 包
- 发送 ACK 包(ack=y+1)
- 客户端和服务器都进入 ESTABLISHED 状态
- 表示:连接建立成功,可以开始传输数据
四次挥手过程
客户端 服务器
| |
| 1. FIN (seq=u) |
|----------------------------------->|
| 客户端:我要关闭连接 |
| |
| 2. ACK (ack=u+1) |
|<-----------------------------------|
| 服务器:收到,等我发完数据 |
| |
| 3. FIN (seq=w) |
|<-----------------------------------|
| 服务器:我也要关闭连接了 |
| |
| 4. ACK (ack=w+1) |
|----------------------------------->|
| 客户端:收到,再见 |
| |
| 连接关闭 |详细过程:
第一次挥手(FIN)
- 客户端发送 FIN 包(seq=u)
- 客户端进入 FIN_WAIT_1 状态
- 表示:客户端没有数据要发送了
第二次挥手(ACK)
- 服务器收到 FIN 包
- 发送 ACK 包(ack=u+1)
- 服务器进入 CLOSE_WAIT 状态
- 客户端进入 FIN_WAIT_2 状态
- 表示:服务器确认收到,但可能还有数据要发送
第三次挥手(FIN)
- 服务器发送完数据后,发送 FIN 包(seq=w)
- 服务器进入 LAST_ACK 状态
- 表示:服务器也没有数据要发送了
第四次挥手(ACK)
- 客户端收到 FIN 包
- 发送 ACK 包(ack=w+1)
- 客户端进入 TIME_WAIT 状态(等待 2MSL)
- 服务器收到 ACK 后进入 CLOSED 状态
- 客户端等待 2MSL 后进入 CLOSED 状态
- 表示:连接完全关闭
🧠 深度理解
为什么需要三次握手?
1. 确认双方的收发能力
第一次握手:客户端发送能力 OK,服务器接收能力 OK
第二次握手:服务器发送能力 OK,客户端接收能力 OK
第三次握手:确认双方收发能力都 OK2. 防止已失效的连接请求
场景:
1. 客户端发送 SYN1(因网络延迟,未到达)
2. 客户端超时,重新发送 SYN2
3. SYN2 到达,建立连接,传输数据,关闭连接
4. SYN1 延迟后到达服务器
如果只有两次握手:
- 服务器收到 SYN1,发送 SYN-ACK
- 服务器认为连接建立,等待数据
- 客户端不理会(已关闭),造成资源浪费
有三次握手:
- 服务器发送 SYN-ACK
- 客户端不回复 ACK
- 服务器超时,关闭连接3. 同步序列号
- 序列号用于保证数据包的顺序
- 双方需要交换初始序列号
- 三次握手确保双方都知道对方的序列号为什么需要四次挥手?
TCP 是全双工通信:
客户端 → 服务器:数据通道1
客户端 ← 服务器:数据通道2
关闭连接需要关闭两个通道:
1. 客户端关闭发送通道(FIN)
2. 服务器确认(ACK)
3. 服务器关闭发送通道(FIN)
4. 客户端确认(ACK)为什么不能合并成三次?
- 服务器收到 FIN 后,可能还有数据要发送
- 需要先发送 ACK 确认,然后继续发送数据
- 数据发送完毕后,再发送 FINTIME_WAIT 状态
为什么需要 TIME_WAIT?
1. 确保最后的 ACK 能够到达服务器
- 如果 ACK 丢失,服务器会重发 FIN
- 客户端需要等待,以便重发 ACK
2. 确保旧连接的数据包都已消失
- 防止旧连接的数据包影响新连接
- 等待 2MSL(Maximum Segment Lifetime)TIME_WAIT 的问题:
- 占用端口资源
- 大量 TIME_WAIT 会导致端口耗尽
- 解决:调整系统参数,使用连接池常见问题
1. 如果第三次握手失败?
- 服务器没收到 ACK,会重发 SYN-ACK
- 重发多次后,服务器关闭连接
- 客户端认为连接已建立,发送数据时会收到 RST2. 如果第二次挥手失败?
- 客户端没收到 ACK,会重发 FIN
- 服务器收到重复的 FIN,会重发 ACK3. 半关闭状态
- 客户端发送 FIN 后,不能再发送数据
- 但仍可以接收服务器的数据
- 这就是半关闭状态💡 面试回答技巧
🎯 一句话回答(快速版)
三次握手:SYN → SYN-ACK → ACK,确认双方收发能力。四次挥手:FIN → ACK → FIN → ACK,因为 TCP 全双工需要双向关闭。
📣 口语化回答(推荐)
面试时可以这样回答:
"TCP 建立连接需要三次握手:
第一次,客户端发送 SYN 包,告诉服务器我要建立连接,同时带上一个序列号。
第二次,服务器收到后发送 SYN-ACK 包,表示收到了,同时也带上自己的序列号。
第三次,客户端发送 ACK 包,确认收到服务器的响应。
为什么要三次?因为要确认双方的收发能力都正常。两次的话,服务器不知道客户端能不能收到自己的消息。
断开连接需要四次挥手:
第一次,客户端发送 FIN,表示我不发数据了。
第二次,服务器发送 ACK,表示知道了。
第三次,服务器发送 FIN,表示我也不发了。
第四次,客户端发送 ACK,确认收到。
为什么是四次不是三次?因为 TCP 是全双工的,两个方向的数据传输要分别关闭。服务器收到客户端的 FIN 后,可能还有数据要发,所以 ACK 和 FIN 不能合并。
还有个 TIME_WAIT 状态,客户端发完最后一个 ACK 后要等 2MSL,确保服务器收到了,也让网络中残留的数据包消失。"
推荐回答顺序
先说三次握手:
- "第一次:客户端发送 SYN"
- "第二次:服务器发送 SYN-ACK"
- "第三次:客户端发送 ACK"
再说四次挥手:
- "第一次:客户端发送 FIN"
- "第二次:服务器发送 ACK"
- "第三次:服务器发送 FIN"
- "第四次:客户端发送 ACK"
然后说原因:
- "三次握手:确认双方收发能力,防止失效连接"
- "四次挥手:TCP 全双工,需要双向关闭"
最后说细节:
- TIME_WAIT 状态
- 序列号的作用
- 可能的问题
重点强调
- ✅ 三次握手的必要性
- ✅ 四次挥手不能合并的原因
- ✅ TIME_WAIT 的作用
- ✅ 全双工通信的特点
可能的追问
Q1: 为什么不是两次握手?
A: 两次握手无法确认客户端的接收能力,也无法防止已失效的连接请求。
Q2: 为什么不是四次握手?
A: 三次握手已经足够确认双方的收发能力,四次握手是多余的。
Q3: 什么是 SYN 洪水攻击?
A: 攻击者发送大量 SYN 包,但不回复 ACK,导致服务器维护大量半连接状态,耗尽资源。
防御:
- SYN Cookie
- 增加半连接队列大小
- 减少 SYN-ACK 重试次数
Q4: 如何优化 TCP 连接?
A:
- 使用 HTTP/2 多路复用
- 启用 TCP Fast Open
- 调整 TCP 参数(窗口大小、超时时间)
- 使用连接池
💻 代码示例
模拟 TCP 连接
// 模拟 TCP 三次握手
class TCPConnection {
constructor() {
this.state = 'CLOSED';
this.seq = Math.floor(Math.random() * 1000);
}
// 客户端:发起连接
connect() {
console.log('客户端:发送 SYN,seq=' + this.seq);
this.state = 'SYN_SENT';
// 模拟网络延迟
setTimeout(() => {
this.receiveSynAck();
}, 100);
}
// 客户端:收到 SYN-ACK
receiveSynAck() {
console.log('客户端:收到 SYN-ACK');
console.log('客户端:发送 ACK');
this.state = 'ESTABLISHED';
console.log('连接建立成功!');
}
// 客户端:关闭连接
close() {
console.log('客户端:发送 FIN');
this.state = 'FIN_WAIT_1';
setTimeout(() => {
this.receiveAck();
}, 100);
}
// 客户端:收到 ACK
receiveAck() {
console.log('客户端:收到 ACK');
this.state = 'FIN_WAIT_2';
setTimeout(() => {
this.receiveFin();
}, 100);
}
// 客户端:收到 FIN
receiveFin() {
console.log('客户端:收到 FIN');
console.log('客户端:发送 ACK');
this.state = 'TIME_WAIT';
setTimeout(() => {
this.state = 'CLOSED';
console.log('连接关闭!');
}, 200); // 模拟 2MSL
}
}
// 测试
const conn = new TCPConnection();
conn.connect();
setTimeout(() => {
conn.close();
}, 1000);