TCP 三次握手为什么是三次:丢包、重传与初始序列号

我不会起名字322 · 后端 / 算法 / 数据库

📘 技术栈 | 🧩 力扣 Hot100 | 🐹 Go 项目 | 🟥 Redis | 🐬 MySQL

文章目录

TCP 三次握手为什么是三次:丢包、重传与初始序列号

一、"为什么不是两次"才是真问题

大部分人对三次握手的解释是"确认双方的发送和接收能力都正常",这个说法不算错,但没有回答真正的问题:为什么两次不够?四次又太多?

真正的答案藏在**初始序列号(ISN)**上。

TCP 是可靠传输,每个字节都有序号。连接的初始序列号是双方各自随机生成的,而且必须让对方知道。所以:

  • 客户端要告诉服务端"我的 ISN 是 x" → 第 1 次(SYN)
  • 服务端要告诉客户端"我的 ISN 是 y,并且我收到你的 x 了" → 第 2 次(SYN+ACK)
  • 客户端要告诉服务端"我收到你的 y 了" → 第 3 次(ACK)

第 3 次不是可选的,它是服务端确认"自己的 ISN 被对方收到了"的唯一途径。 少了这一步,服务端就不知道客户端到底收没收到自己的 SYN+ACK------如果那个包丢了,服务端会一直重传。

二、两次握手会造成什么后果

假设只有两次握手,服务端发完 SYN+ACK 就认为连接建立了、开始分配资源、准备发数据。

现在考虑这个场景:

text 复制代码
1. 客户端发 SYN(seq=x),但这个包在网络里绕了很久
2. 客户端超时重传了一个 SYN,这次很快到了,连接建立、传完数据、关闭
3. 此时那个绕路的旧 SYN 终于到了服务端
4. 服务端以为是新连接,回 SYN+ACK,并(在两次握手的假设下)直接建立连接
5. 客户端收到后发现这是自己早就放弃的连接,直接忽略
6. 服务端这边却留下了一条永远不会被使用的连接

这条"半开连接"会占着服务端的内存和 fd,直到超时。 如果有人伪造大量源 IP 发 SYN,服务端的半连接队列会被打满------这就是经典的 SYN Flood 攻击的原理。

三次握手把这个风险降低了:服务端在收到第 3 次 ACK 之前不分配完整资源(只放在半连接队列里),收不到 ACK 就会重传 SYN+ACK,重试几次后放弃。

三、每一次握手丢失会发生什么

这是面试和实践里都最有用的部分。

第 1 次 SYN 丢失

客户端发完 SYN,进入 SYN_SENT,等不到 SYN+ACK 就超时重传。

bash 复制代码
# Linux 重试次数
cat /proc/sys/net/ipv4/tcp_syn_retries
# 默认 6

重试间隔是指数退避:1s、2s、4s、8s、16s、32s,总共约 127 秒 才放弃。这就是为什么 connect() 到一个不存在的 IP 有时会卡两分钟才报 Connection timed out。

第 2 次 SYN+ACK 丢失

双方都会重传,但表现不同:

  • 服务端:没收到第 3 次 ACK,重传 SYN+ACK(次数由 tcp_synack_retries 控制,默认 5);
  • 客户端:没收到 SYN+ACK,重传 SYN(按 tcp_syn_retries)。

服务端重传几次后会把这条半连接从队列里删掉。如果此时客户端的 SYN 才到,服务端会重新走一遍握手流程。

第 3 次 ACK 丢失

这是最有意思的一种。此时:

  • 客户端 已经认为连接建立了(它发完 ACK 就进 ESTABLISHED);
  • 服务端 还没收到 ACK,仍在 SYN_RCVD。

如果这时候客户端开始发数据会怎样?

数据包的 ACK 字段会带上服务端的 ISN+1,服务端收到后就确认了连接 ,直接进入 ESTABLISHED。所以第 3 次握手丢失时,只要客户端随后发了数据,连接依然能建立起来。

如果客户端不发数据(比如一个只连不发的探测),服务端会一直重传 SYN+ACK 直到超时。

这就是为什么有些健康检查看起来"连得上"但服务端日志里没有连接记录------握手没走完,服务端没认为连接建立。

四、初始序列号不是从 0 开始

早期实现里 ISN 是随系统启动时间递增的,这带来安全问题:攻击者可以猜出 ISN,伪造 TCP 包做会话劫持。

现代 Linux 用的是基于时钟 + 连接四元组哈希的生成方式:

c 复制代码
// 简化逻辑:ISN = 定时器计数 + 四元组哈希的偏移
seq = secure_tcp_seq(saddr, daddr, sport, dport)

每 64ns 递增一次,对不同四元组有不同的随机偏移。这样 ISN 既不会在短时间内重复(避免旧连接的报文被新连接误收),又不容易被预测。

ISN 之所以要随机,还有一个原因 :避免上一条连接的延迟报文被下一条相同四元组的新连接当作有效数据------这一点和 TIME_WAIT 存在的理由是一致的。

五、握手阶段的两个队列

服务端在三次握手期间维护两个队列,很多连接问题都出在这里:

text 复制代码
收到 SYN  →  半连接队列(SYN queue)
                ↓ 收到第 3 次 ACK
            全连接队列(accept queue)  →  应用 accept() 取走
bash 复制代码
# 全连接队列最大值(受 backlog 和这个参数共同限制)
cat /proc/sys/net/core/somaxconn     # 默认 4096(旧版 128)

# 查看溢出情况
netstat -s | grep -i "listen"
#    12345 times the listen queue of a socket overflowed
#    6789 SYNs to LISTEN sockets dropped

这两个计数持续增长就说明队列在溢出。

  • 半连接队列溢出 → 新 SYN 被丢弃,客户端表现为连接超时(这是 SYN Flood 的症状);
  • 全连接队列溢出 → 握手完成了但连接被丢,客户端可能收到 RST,也可能超时。

常见诱因:应用 accept() 太慢(比如 Go 服务在 Accept 之后做了耗时操作)、backlog 设太小、突发流量。

排查:

bash 复制代码
# 看监听 socket 的当前队列长度
ss -lnt
# Recv-Q 是当前全连接队列已用,Send-Q 是最大值

六、和握手相关的几个参数

参数 默认值 作用
tcp_syn_retries 6 客户端 SYN 重试次数(约 127s)
tcp_synack_retries 5 服务端 SYN+ACK 重试次数
tcp_syncookies 1 SYN 队列溢出时用 syncookie 防御 SYN Flood
net.core.somaxconn 4096 全连接队列上限
tcp_abort_on_overflow 0 队列满时回 RST(1)还是丢包(0)

tcp_syncookies=1 建议保持开启:它在半连接队列满时用一种无状态的方式完成握手,能有效抵御 SYN Flood,正常情况下不会生效,没有副作用。

而 tcp_abort_on_overflow 一般不要开:开了之后队列满时客户端会立刻收到 RST(快速失败),看起来"更干脆",但会让客户端把"服务端繁忙"误判成"服务不可用",反而影响重试策略。

七、带走物

回答"为什么是三次"的完整答案,三层递进:

  1. 表层:确认双方的收发能力;
  2. 真正的原因:双向初始序列号都要被确认------第 3 次 ACK 是服务端知道"自己的 ISN 被收到了"的唯一途径;
  3. 工程后果:两次握手的场景下,历史重复 SYN 会在服务端留下半开连接,是 SYN Flood 的根源。

再补两条实践要点:

  • 握手丢包时谁重传:第 1 次丢 → 客户端重传 SYN;第 2 次丢 → 双方都重传;第 3 次丢 → 服务端重传 SYN+ACK,但如果客户端紧接着发数据,数据里的 ACK 会自动补上握手;
  • 连接超时排查 :connect() 卡约 127 秒是 tcp_syn_retries=6 的表现;看 netstat -s | grep listen 判断是不是队列溢出。
相关推荐
Yang96111 小时前
鼎讯信通DXA-3S光纤熔接机:光缆教学中的技术精粹解析
网络
xbzb2 小时前
IP、掩码、网关、DNS 到底谁管什么?网络基础与七种设备一次讲清
网络·网关·mac·ip·dns·掩码
前进吧-程序员3 小时前
C++20 协程入门实战:从 co_await 到一个 100 行的异步任务调度器
网络·c++·c++20
前进吧-程序员3 小时前
实战:C++20 协程 + io_uring 写一个 400 行的 echo server
网络·c++·c++20
云飞云共享云桌面4 小时前
机械制造研发部门:一台SolidWorks云主机替代 10 台传统图形工作站
运维·服务器·网络·3d·自动化·制造
loulanyue_4 小时前
从碎片文档到工业知识网络:TDengine工业知识图谱架构设计与大模型落地破局——读涛思数据OpenDay实录
网络·知识图谱·tdengine
东方佑5 小时前
去魅:工具调用、Agent 调用、A2A、MCP、Harness 到底都是什么
服务器·网络·数据库
上海云盾-小余5 小时前
七层 CC 攻击检测难点:如何区分真实用户与攻击请求
网络·人工智能·windows·tcp/ip·udp
2401_885885045 小时前
巴林语音通知接口如何对接?系统告警语音API接入教程
android·前端·网络·python·语音识别