说说TCP为什么需要三次握手和四次挥手
问题解析
TCP的三次握手和四次挥手是网络协议中最经典的设计,面试官通过这个问题考察候选人对TCP连接管理机制的理解,以及能否解释设计背后的原理。
核心概念
TCP连接管理
TCP是面向连接的协议,通信双方需要建立连接后才能传输数据,通信结束后需要释放连接。
连接建立: 三次握手(Three-way Handshake) 连接释放: 四次挥手(Four-way Handshake)
关键字段说明
TCP头部中与连接管理相关的字段:
Sequence Number(32位):序列号,标识本报文段数据的第一个字节的序号
Acknowledgment Number(32位):确认号,期望收到对方下一个报文段的第一个字节的序号
标志位(Flags):
- SYN(Synchronize):同步序号,用于建立连接
- ACK(Acknowledgment):确认号有效
- FIN(Finish):释放连接
- RST(Reset):重置连接
- PSH(Push):立即交付应用层
- URG(Urgent):紧急指针有效
详细解答
三次握手建立连接
握手过程
客户端 服务器
| |
|------------------ SYN, seq=x ------------------->|
| 请求建立连接,初始序号x |
| |
|<---------------- SYN+ACK, seq=y, ack=x+1 --------|
| 同意建立连接,初始序号y,期望收到x+1 |
| |
|------------------ ACK, ack=y+1 ----------------->|
| 确认收到,期望收到y+1 |
| |
|<================= 连接建立成功 =================>|
| |
状态变化:
客户端:CLOSED → SYN_SENT → ESTABLISHED
服务器:CLOSED → LISTEN → SYN_RCVD → ESTABLISHED
详细步骤
第一次握手(SYN):
客户端发送SYN包
- SYN = 1
- seq = x(客户端选择的初始序列号ISN)
- 不携带数据,但消耗一个序号
客户端状态:SYN_SENT
第二次握手(SYN+ACK):
服务器发送SYN+ACK包
- SYN = 1
- ACK = 1
- seq = y(服务器选择的初始序列号ISN)
- ack = x + 1(确认收到客户端的SYN)
- 不携带数据,但消耗一个序号
服务器状态:SYN_RCVD
第三次握手(ACK):
客户端发送ACK包
- ACK = 1
- seq = x + 1
- ack = y + 1(确认收到服务器的SYN)
- 可以携带数据
双方状态:ESTABLISHED
为什么需要三次握手?
原因1:确认双方收发能力正常
三次握手验证的内容:
第一次握手(SYN):
- 客户端发送 → 服务器接收
- 证明:客户端发送能力正常,服务器接收能力正常
第二次握手(SYN+ACK):
- 服务器发送 → 客户端接收
- 证明:服务器发送能力正常,客户端接收能力正常
- 同时确认服务器收到了客户端的SYN
第三次握手(ACK):
- 客户端发送 → 服务器接收
- 确认客户端收到了服务器的SYN+ACK
结论:三次握手后,双方确认彼此的收发能力都正常
原因2:防止历史重复连接初始化
场景:网络中存在延迟的SYN包
假设只有两次握手:
客户端 服务器
|-------- SYN(旧) -------->|
|(这是之前连接延迟到达的) |
|<------- SYN+ACK ---------|
| 服务器建立连接 |
| |
|(客户端已放弃旧连接, |
| 不会发送ACK) |
| |
| 服务器资源浪费! |
三次握手避免这个问题:
- 客户端收到不期望的SYN+ACK,会发送RST重置
- 或者客户端不响应,服务器不会建立半连接
原因3:同步双方初始序列号
TCP使用序列号实现可靠传输:
- 序列号用于数据排序
- 序列号用于去重
- 序列号用于确认应答
三次握手确保双方都收到了对方的初始序列号:
- 客户端:收到服务器的seq=y(第二次握手)
- 服务器:收到客户端的seq=x(第一次握手)
- 双方都确认对方知道了自己的序列号(第三次握手)
为什么不能是两次握手?
两次握手的问题:
客户端 服务器
|-------- SYN -------->|
|<------- SYN+ACK ------|
| 服务器认为连接建立 |
| |
|(客户端没收到SYN+ACK, |
| 不会发送ACK) |
| |
| 服务器单方面建立连接, |
| 浪费资源! |
为什么不能是四次握手?
四次握手可以但没必要:
客户端 服务器
|-------- SYN -------->|
|<------- ACK ----------|
|<------- SYN ----------|
|-------- ACK --------->|
实际上,服务器的SYN和ACK可以合并发送
三次握手已经足够保证可靠性
四次挥手释放连接
挥手过程
客户端 服务器
| |
|<================= 数据传输中 ===================>|
| |
|------------------ FIN, seq=u ------------------>|
| 客户端请求断开连接 |
| |
|<----------------- ACK, ack=u+1 -----------------|
| 服务器确认断开请求 |
| |
| (服务器可能还有数据发送) |
| |
|<----------------- FIN, seq=w ------------------|
| 服务器请求断开连接 |
| |
|------------------ ACK, ack=w+1 ---------------->|
| 客户端确认断开请求 |
| |
| (等待2MSL后关闭) |
| |
| 连接完全关闭 |
状态变化:
主动方:ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED
被动方:ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED
详细步骤
第一次挥手(FIN):
主动关闭方发送FIN包
- FIN = 1
- seq = u(已发送数据的最后一个字节序号+1)
- 表示主动关闭方不再发送数据
主动方状态:FIN_WAIT_1
第二次挥手(ACK):
被动关闭方发送ACK包
- ACK = 1
- ack = u + 1
- 确认收到FIN
- 此时被动方可能还有数据要发送
被动方状态:CLOSE_WAIT
主动方状态:FIN_WAIT_2
第三次挥手(FIN):
被动关闭方发送FIN包
- FIN = 1
- seq = w(已发送数据的最后一个字节序号+1)
- 表示被动关闭方也不再发送数据
被动方状态:LAST_ACK
第四次挥手(ACK):
主动关闭方发送ACK包
- ACK = 1
- ack = w + 1
- 确认收到FIN
主动方状态:TIME_WAIT(等待2MSL后关闭)
被动方状态:CLOSED
为什么需要四次挥手?
原因:TCP全双工通信,需要分别关闭两个方向
TCP连接是全双工的:
- 客户端 → 服务器:一个方向的数据流
- 服务器 → 客户端:另一个方向的数据流
关闭过程:
1. 客户端发送FIN:关闭客户端→服务器方向
2. 服务器发送ACK:确认收到客户端的FIN
(此时服务器→客户端方向仍可发送数据)
3. 服务器发送FIN:关闭服务器→客户端方向
4. 客户端发送ACK:确认收到服务器的FIN
为什么不能合并第二次和第三次?
- 服务器可能还有数据要发送
- 必须等待数据发送完毕才能发送FIN
为什么不能是三次挥手?
假设合并第二次和第三次:
客户端 服务器
|-------- FIN -------->|
|<------- FIN+ACK ------|
|(此时服务器还有数据未发完)|
| 数据丢失! |
|-------- ACK --------->|
三次挥手的问题:
- 服务器收到FIN后立即回复FIN+ACK
- 但服务器可能还有数据要发送给客户端
- 客户端收到FIN+ACK后不再接收数据
- 导致服务器未发送的数据丢失
TIME_WAIT状态
什么是TIME_WAIT
TIME_WAIT是主动关闭方在发送最后一个ACK后进入的状态
持续时间:2MSL(Maximum Segment Lifetime,最大报文段生存时间)
MSL通常是30秒到2分钟,2MSL约为1-4分钟
为什么需要TIME_WAIT
原因1:确保最后一个ACK能到达对方
场景:最后一个ACK丢失
客户端 服务器
|-------- ACK -------->|
|(ACK丢失) |
| |
|<-------- FIN ----------|
|(服务器重传FIN) |
| |
|(如果客户端已关闭, |
| 会回复RST,导致服务器 |
| 异常关闭) |
TIME_WAIT期间:
- 可以重发丢失的ACK
- 确保连接正常关闭
原因2:防止已失效的连接请求报文出现在本连接中
场景:网络中存在延迟的数据包
连接1:客户端A:8080 ←→ 服务器B:80
连接2:客户端A:8080 ←→ 服务器B:80(新连接,使用相同四元组)
如果没有TIME_WAIT:
- 连接1的数据包延迟到达
- 被连接2误认为是自己的数据
- 导致数据混乱
TIME_WAIT期间:
- 延迟的数据包在网络中消失
- 新连接不会收到旧连接的数据
TIME_WAIT的问题与优化
问题:大量TIME_WAIT连接占用资源
在高并发短连接场景下:
- 客户端产生大量TIME_WAIT连接
- 可能耗尽端口资源
- 系统性能下降
优化方案:
# Linux系统优化
# 1. 允许TIME_WAIT socket重用
sysctl -w net.ipv4.tcp_tw_reuse=1
# 仅对客户端有效,用于新建连接时复用端口
# 2. 快速回收TIME_WAIT(已废弃,不建议使用)
# sysctl -w net.ipv4.tcp_tw_recycle=1 # Linux 4.12+已移除
# 3. 调整TIME_WAIT超时时间
sysctl -w net.ipv4.tcp_fin_timeout=30
# 4. 增加可用端口范围
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
# 5. 使用连接池
# 避免频繁创建和关闭连接
# 应用层优化:使用连接池
import urllib3
# 使用连接池复用TCP连接
http = urllib3.PoolManager(
num_pools=10,
maxsize=100,
block=True
)
# 多次请求复用同一连接
response1 = http.request('GET', 'http://api.example.com/data1')
response2 = http.request('GET', 'http://api.example.com/data2')
状态转换图
+--------+
主动打开 | CLOSED | 被动打开
+--------->| |<-----------+
| +--------+ |
| | |
| 关闭/超时 | |
| +-----------+ |
| | |
| v v
+---------+ +-----------+
| SYN | | LISTEN |
| SENT |<-------------------| |
+---------+ 收到SYN +-----------+
| |
| 收到SYN+ACK | 收到SYN
v v
+---------+ +-----------+
| SYN | | SYN_RCVD |
| RECEIVED|------------------->| |
+---------+ 同时打开 +-----------+
| |
| 发送ACK | 发送SYN+ACK
v v
+---------+ +-----------+
| ESTA |<-------------------| |
|BLISHED |<------------------->| ESTABLISHED
+---------+ 数据传输 +-----------+
| |
主动关闭 | | 被动关闭
| | | |
v | | v
+---------+ | | +---------+
|FIN_WAIT_1| | | |CLOSE_WAIT|
+---------+ | | +---------+
| | | |
| 收到ACK | | 发送FIN |
v | | v
+---------+ | | +---------+
|FIN_WAIT_2| | | |LAST_ACK |
+---------+ | | +---------+
| | | |
| 收到FIN | | 收到ACK |
v | | v
+---------+ | | (CLOSED)
| TIME_WAIT| | |
+---------+ | |
| | |
| 2MSL后 | |
v | |
(CLOSED) | |
| |
深入理解
序列号与确认号机制
序列号(Sequence Number):
- 标识本报文段数据的第一个字节的序号
- 初始序列号(ISN)是随机生成的
- SYN和FIN各占一个序列号
确认号(Acknowledgment Number):
- 期望收到对方下一个报文段的第一个字节的序号
- 表示该序号之前的所有数据都已正确接收
示例:
客户端发送:SYN, seq=100
服务器响应:SYN+ACK, seq=200, ack=101
客户端确认:ACK, seq=101, ack=201
数据传输:
客户端发送:PSH+ACK, seq=101, data="Hello"(5字节)
服务器响应:ACK, ack=106(期望收到序号106)
半连接队列与全连接队列
服务器处理连接请求的过程:
客户端 服务器
|-------- SYN -------->|
| | 放入SYN队列(半连接队列)
|<------- SYN+ACK ------|
| |
|-------- ACK -------->|
| | 从SYN队列移除
| | 放入Accept队列(全连接队列)
| | 应用accept()取出
队列溢出处理:
- SYN队列满:丢弃SYN包(客户端超时重传)
- Accept队列满:丢弃ACK包(客户端认为连接未建立)
Linux参数调优:
net.ipv4.tcp_max_syn_backlog # SYN队列大小
net.core.somaxconn # Accept队列大小
TCP保活机制
TCP Keepalive机制:
客户端 服务器
|<===================>|
| 连接空闲 |
| |
|-------- Keepalive --->|(每2小时发送)
|<------- ACK ----------|
| |
|(如果未收到ACK, |
| 重试10次后断开) |
Linux参数:
net.ipv4.tcp_keepalive_time=7200 # 空闲7200秒开始探测
net.ipv4.tcp_keepalive_intvl=75 # 探测间隔75秒
net.ipv4.tcp_keepalive_probes=9 # 探测次数
应用层心跳更常用:
- 频率更高(秒级)
- 更及时发现问题
- 可携带业务状态
连接异常处理
RST(Reset)包的使用场景:
1. 端口未监听
客户端连接未开放的端口,服务器回复RST
2. 连接已关闭
向已关闭的连接发送数据,收到RST
3. 异常终止
应用程序崩溃,OS发送RST关闭连接
4. 超时重传失败
多次重传无响应,发送RST重置连接
示例:
客户端 服务器
|-------- SYN -------->|
|<------- RST ----------|
|(端口未开放) |
最佳实践
服务端优化
# 1. 使用SO_REUSEADDR
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 2. 调整TCP参数
# 禁用Nagle算法(低延迟场景)
server_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
# 启用TCP_QUICKACK(快速确认)
server_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK, 1)
# 3. 优雅关闭连接
def graceful_close(sock):
try:
# 关闭写方向,通知对方数据发送完毕
sock.shutdown(socket.SHUT_WR)
# 等待对方关闭
while True:
data = sock.recv(1024)
if not data:
break
finally:
sock.close()
客户端优化
# 1. 使用连接池
from urllib3 import PoolManager
pool = PoolManager(
num_pools=10,
maxsize=100,
block=True,
timeout=30
)
# 2. 长连接复用
headers = {'Connection': 'keep-alive'}
# 3. 合理设置超时
sock.settimeout(30) # 30秒超时
系统参数调优
# /etc/sysctl.conf
# 连接队列优化
net.ipv4.tcp_max_syn_backlog = 65536
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65536
# TIME_WAIT优化
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30
# 保活优化
net.ipv4.tcp_keepalive_time = 300
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 3
# 内存优化
net.ipv4.tcp_mem = 786432 1048576 1572864
net.ipv4.tcp_rmem = 4096 87380 4194304
net.ipv4.tcp_wmem = 4096 65536 4194304
面试要点
- 三次握手目的:确认双方收发能力、同步序列号、防止历史连接
- 四次挥手原因:全双工通信,需要分别关闭两个方向
- TIME_WAIT作用:确保ACK到达、防止旧数据干扰
- 状态转换:熟记主要状态(LISTEN、SYN_SENT、ESTABLISHED、TIME_WAIT等)
- 序列号机制:初始序列号随机、SYN/FIN占序号
常见面试追问:
- 为什么不是两次握手?
- 为什么挥手需要四次?
- TIME_WAIT为什么要等待2MSL?
- 大量TIME_WAIT怎么解决?
- 什么是SYN Flood攻击?如何防范?