返回首页

说说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

面试要点

  1. 三次握手目的:确认双方收发能力、同步序列号、防止历史连接
  2. 四次挥手原因:全双工通信,需要分别关闭两个方向
  3. TIME_WAIT作用:确保ACK到达、防止旧数据干扰
  4. 状态转换:熟记主要状态(LISTEN、SYN_SENT、ESTABLISHED、TIME_WAIT等)
  5. 序列号机制:初始序列号随机、SYN/FIN占序号

常见面试追问:

  • 为什么不是两次握手?
  • 为什么挥手需要四次?
  • TIME_WAIT为什么要等待2MSL?
  • 大量TIME_WAIT怎么解决?
  • 什么是SYN Flood攻击?如何防范?