面试官问"TCP为什么三次握手不是两次"——我用一个生活例子+设计推导,他不再追问了

8 阅读18分钟

计算机网络基础:后端必知 TCP/HTTP/TLS 核心机制

TCP 三次握手的本质不是"传三次包"——是双方各自确认"我能收到你发的"和"你能收到我发的"。两次握手为什么不行?因为服务端无法确认客户端还活着——一个迟到的旧 SYN 就能让它为不存在的人白等。这篇文章从设计者视角推导 TCP/HTTPS/HTTP2 三道面试必考题,每道题给你"面试官追问"的标准答案。

阅读约 14 分钟 | 系列第 12/17 篇


一、TCP 三次握手

客户端                            服务端
   │──── SYN=1, seq=x ────────────→│  ① 客户端:请求建立连接,起始序号为 x
   │←─── SYN=1, ACK=1, seq=y,     │  ② 服务端:同意连接请求,起始序号为 y
   │     ack=x+1 ─────────────────│     (SYN 与 ACK 合并在同一报文段中传输)
   │──── ACK=1, seq=x+1,           │  ③ 客户端:确认收到服务端的 SYN
   │     ack=y+1 ─────────────────→│
   │    ═══ 连接建立,可传输数据 ══════│

为什么是三次,而非两次?

两次握手存在一个关键缺陷:无法识别网络中滞留的过期 SYN 报文。若客户端此前发起过一次连接请求,该请求因网络拥塞延迟到达服务端;服务端两次握手后直接建立连接,但客户端此时已放弃该连接——服务端空等,浪费资源。第三次握手让客户端显式确认"当前确实需要建立此连接",排除了历史连接请求的干扰。

为什么不是四次?

服务端的 SYN 与 ACK 在语义上可以合并——服务端对客户端 SYN 的确认(ACK)与自身起始序号的声明(SYN)是同一时刻发生的事件,在一个报文段中同时传输即能完成两步工作。TCP 四次挥手中无法合并的原因见下文。

"三次握手的本质是确认双方的收发能力对等——第一次确认客户端的发送能力,第二次确认服务端的接收与发送能力,第三次确认客户端的接收能力。经过三次报文交换后,双方均确认自身与对方均可正常收发。"

SYN 洪泛攻击与防御

攻击者发送大量 SYN 报文后故意不完成第三次握手,服务端半连接队列(SYN Queue)迅速占满,正常请求无法建立连接。防御手段:SYN Cookie(半连接状态不分配服务端资源,通过 Cookie 验证第三次握手合法性)、增大半连接队列容量、缩短 SYN 超时重传时间。


二、TCP 四次挥手

客户端                              服务端
   │──── FIN=1, seq=u ──────────────→│  ① 客户端:本端数据已发送完毕,请求关闭
   │←─── ACK=1, ack=u+1 ────────────│  ② 服务端:已收到关闭请求(可能仍有数据待发送)
   │←─── FIN=1, seq=w ──────────────│  ③ 服务端:本端数据也已发送完毕,请求关闭
   │──── ACK=1, ack=w+1 ────────────→│  ④ 客户端:确认关闭
   │  ═══ 等待 2MSL 后完全关闭 ════════│

挥手为何比握手多一次?

TCP 是全双工协议——数据可在两个方向上独立传输。当客户端发送 FIN 关闭自身发送方向后,服务端可能仍有数据正在传输中,故步骤②(对客户端 FIN 的确认)和步骤③(服务端自身 FIN)之间服务端继续发送剩余数据,两者不能合并为一条报文。挥手因此比握手多一次报文交换。

TIME_WAIT:为何必须等待 2MSL?

  1. 确保最后一个 ACK 到达:客户端发送的最终 ACK 可能在网络中丢失,服务端超时后会重传 FIN。客户端保持在 TIME_WAIT 状态,收到重传 FIN 后重新发送 ACK,避免服务端持续重传
  2. 清除网络中残留的旧连接报文:2MSL(Maximum Segment Lifetime × 2)确保当前连接的所有报文段在网络中完全消散,避免被相同端口的新连接误收导致数据错乱

生产影响:主动关闭方为服务端时,大量 TIME_WAIT 连接会耗尽本地端口范围。监控命令:netstat -antp | grep TIME_WAIT | wc -l。调优参数:开启 tcp_tw_reuse(复用 TIME_WAIT 状态的连接)、调整 tcp_max_tw_buckets(TIME_WAIT 连接数上限)。


三、TCP 拥塞控制(Congestion Control)

为什么需要拥塞控制:TCP 的确认重传机制解决了数据可靠交付问题,但未解决"以多快的速度发送"的问题。如果发送方不顾网络承载能力盲目发送数据,会在路由器处引发拥塞——缓冲区溢出、丢包增加、大量重传进一步加剧网络负担,最终导致网络吞吐量崩溃(Congestion Collapse)。拥塞控制的核心目标:探测网络的可用带宽,在不过载的前提下最大化传输速率

3.1 拥塞窗口(cwnd)与发送窗口

TCP 发送方维护一个拥塞窗口(cwnd, Congestion Window),实际发送窗口取 cwnd 和接收方通告窗口(rwnd)的较小值:

实际发送窗口 = min(cwnd, rwnd)

在没有接收方窗口限制的理想情况下,cwnd 决定了 TCP 的发送速率。拥塞控制的四个核心算法分别作用于 cwnd 的调整过程。

3.2 慢启动(Slow Start)

连接建立初期,TCP 对网络容量一无所知,直接以高速发送可能导致灾难性拥塞。慢启动采用"试探性增长"策略:

  • cwnd 初始值通常为 1 MSS(Maximum Segment Size,最大报文段长度)
  • 每收到一个 ACK,cwnd +1 MSS
  • 效果:每个 RTT 内 cwnd 翻倍(指数增长)
RTT 1: 发送 1 个报文段 → 收到 1 个 ACK → cwnd = 2
RTT 2: 发送 2 个报文段 → 收到 2 个 ACK → cwnd = 4
RTT 3: 发送 4 个报文段 → 收到 4 个 ACK → cwnd = 8
RTT 4: 发送 8 个报文段 → 收到 8 个 ACK → cwnd = 16
...

"慢启动"一词实为历史名称——虽然初始速度确实很慢,但指数增长使其迅速到达较高水平,并不"慢"。更准确的描述应为指数增长阶段

ssthresh(慢启动阈值):当 cwnd 达到 ssthresh 时,TCP 判断"接近网络容量上限了",从慢启动切换为拥塞避免。ssthresh 的初始值通常较大(Linux 内核默认实现中初始设为无穷大),首次遇到丢包后才根据拥塞情况动态调整。

3.3 拥塞避免(Congestion Avoidance)

进入拥塞避免阶段后,算法从指数增长切换为线性增长,谨慎探测剩余带宽:

  • 每个 RTT 内,cwnd +1 MSS(而非每个 ACK +1)
  • 等效于:每收到一个 ACK,cwnd += MSS × (MSS / cwnd)
线性增长示意(假设 MSS = 1460 字节):
cwnd = 1617181920 → ...

如果持续未发生丢包,cwnd 线性增长;一旦发生丢包(超时或收到 3 个重复 ACK),拥塞控制介入调整策略。

3.4 快速重传(Fast Retransmit)

TCP 有两种丢包检测机制:

机制触发条件反应
超时重传(RTO)定时器超时判断为严重拥塞,ssthresh = cwnd/2,cwnd = 1 MSS,重新进入慢启动
快速重传收到 3 个重复 ACK判断为轻微丢包(仅个别报文段丢失),立即重传,进入快速恢复

为什么是 3 个重复 ACK? 收到 1-2 个重复 ACK 可能是报文段乱序到达(网络中后发的报文段经不同路由先到达),3 个重复 ACK 则强烈暗示该报文段已丢失。等待 3 个重复 ACK 而非立即重传,在乱序和丢包之间取得平衡。

发送方                        接收方
   │── seq=1000 (100字节) ────→│  收到,期望 seq=1100,ACK=1100
   │── seq=1100 (100字节) ────→│  ❌ 丢失
   │── seq=1200 (100字节) ────→│  收到,仍期望 seq=1100,ACK=1100 (dup ACK #1)
   │── seq=1300 (100字节) ────→│  收到,仍期望 seq=1100,ACK=1100 (dup ACK #2)
   │── seq=1400 (100字节) ────→│  收到,仍期望 seq=1100,ACK=1100 (dup ACK #3)
   │                             │
   │← 3个重复ACK触发快速重传 ─────│
   │── seq=1100 (100字节) ────→│  立即重传丢失报文段

3.5 快速恢复(Fast Recovery)

快速恢复是快速重传的后续阶段,设计理念是:"丢包说明网络拥塞,但既然后续报文段仍能被接收(触发了 dup ACK),说明网络并未完全瘫痪,无需将 cwnd 降回 1 MSS 重新慢启动"。

快速恢复的算法步骤:

  1. ssthresh = cwnd / 2(记录拥塞时的窗口减半)
  2. cwnd = ssthresh + 3 × MSS(额外的 3 MSS 补偿已收到并触发 dup ACK 的 3 个后续报文段)
  3. 重传丢失报文段后,每收到一个重复 ACK,cwnd +1 MSS(说明又有报文段到达接收方,网络在清空)
  4. 收到新的 ACK(确认重传的报文段)后,cwnd = ssthresh,退出快速恢复,进入拥塞避免阶段

3.6 拥塞控制状态机

                    ┌──────────────┐
         连接建立   │   慢启动     │  cwnd 指数增长(每个 ACK +1 MSS)
       ──────────→ │  Slow Start  │──── cwnd ≥ ssthresh ────┐
                   └──────┬───────┘                         │
                          │                                 ↓
                          │ 超时重传              ┌──────────────────┐
                          │ ssthresh=cwnd/2       │   拥塞避免        │
                          │ cwnd=1 MSS            │   Congestion      │
                          │ 重新慢启动             │   Avoidance       │
                          └──────────────────────→│  cwnd 线性增长     │
                                                  │  (每RTT +1 MSS)   │
                                                  └────┬────┬────────┘
                                                       │    │
                                         3个重复ACK ───┘    └─── 超时重传
                                              │                    │
                                              ↓                    ↓
                                     ┌──────────────┐    ssthresh=cwnd/2
                                     │  快速重传     │    cwnd=1 MSS
                                     │  Fast        │    重新慢启动
                                     │  Retransmit  │
                                     └──────┬───────┘
                                            │ 立即重传丢失报文
                                            ↓
                                     ┌──────────────┐
                                     │  快速恢复     │
                                     │  Fast        │──── 收到新ACK ──→ 拥塞避免
                                     │  Recovery    │     cwnd=ssthresh
                                     └──────────────┘

3.7 cwnd 锯齿图

拥塞控制的本质是反复"增加→丢包→减半→再增加"的循环,cwnd 随时间呈现锯齿状波动:

cwnd ↑
    │    /\      /\      /\
    │   /  \    /  \    /  \
    │  /    \  /    \  /    \
    │ /      \/      \/      \
    │/                        \
    └──────────────────────────────→ 时间
         ↑        ↑        ↑
      丢包/减半  丢包/减半  丢包/减半

每次丢包后:cwnd 减半 → 线性增长 → 再次丢包 → 反复循环

一句话:拥塞控制的本质不是消除拥塞,而是在拥塞和空闲之间动态平衡——通过丢包信号(隐式反馈)探测网络瓶颈,持续在"过快"和"过慢"之间自适应调整。

3.8 Linux 常用拥塞控制算法

# 查看当前使用的拥塞控制算法
sysctl net.ipv4.tcp_congestion_control

# 查看可用算法列表
sysctl net.ipv4.tcp_available_congestion_control

# 临时切换为 BBR(Bottleneck Bandwidth and RTT,Google 提出)
echo "bbr" > /proc/sys/net/ipv4/tcp_congestion_control
算法核心思想适用场景
Reno经典 AIMD(Additive Increase Multiplicative Decrease),包含慢启动/拥塞避免/快速重传/快速恢复低带宽、低延迟网络
CUBICLinux 默认算法,使用三次函数代替线性增长,窗口增长与 RTT 无关高带宽、高延迟网络(BDP 大)
BBR不依赖丢包信号,通过主动测量瓶颈带宽和 RTT 建模控制发送速率有一定丢包率的网络(如无线网络、跨国链路)

四、TCP 流量控制(Flow Control)

为什么需要流量控制:拥塞控制解决的是"网络能不能承载"的问题,但即使网络空闲,如果接收方应用层处理速度跟不上,发送方持续高速发送仍会导致接收方缓冲区溢出。流量控制是接收方驱动的端到端速率匹配机制。

4.1 滑动窗口机制

接收方在 TCP 报文头中通告 Window Size 字段(16 位,最大 65535 字节;启用窗口缩放选项后最大约 1GB),告知发送方"我的接收缓冲区还剩多少空间":

发送方                             接收方
  │──── seq=1~1000 ────────────────→│  Window=4096 (缓冲区 4KB 空闲)
  │←─── ACK=1001, Window=3096 ──────│  已消费部分数据,剩余 3KB
  │──── seq=1001~2000 ──────────────→│
  │──── seq=2001~3000 ──────────────→│  ← 发送方根据 Window 控制在途数据量
  │──── seq=3001~4096 ──────────────→│
  │  ═══ 等待 Window 更新 ═══════════│  窗口已满,暂停发送
  │←─── ACK=4097, Window=2048 ──────│  应用层消费数据后窗口打开
  │──── seq=4097~6144 ──────────────→│  继续发送

发送方维护的变量关系:

LastByteSent - LastByteAcked ≤ Window (通告窗口)

即:已发送但未确认的数据量不超过接收方通告的窗口大小。

4.2 零窗口探测(Zero Window Probing)

当接收方应用层处理不过来,通告窗口可能降为 0。此时发送方停止发送数据,并启动坚持定时器(Persist Timer),周期性发送窗口探测报文:

接收方窗口=0 → 发送方暂停 → 坚持定时器到期 → 发送1字节探测报文
                                            → 接收方ACK含新窗口值
                                            → 窗口>0:恢复发送
                                            → 窗口=0:重置定时器继续等待

坚持定时器的设计解决了死锁问题:如果接收方窗口恢复后的 ACK 在网络中丢失,没有坚持定时器的话双方将永远等待对方——发送方等窗口更新,接收方等新数据。

4.3 流量控制 vs 拥塞控制

两者容易混淆但解决的是完全不同层面的问题:

维度流量控制拥塞控制
控制目标防止发送方淹没接收方防止发送方淹没网络
约束来源接收方缓冲区容量(端到端)网络瓶颈带宽和缓冲容量(全局)
反馈信号接收方通告窗口大小(显式)丢包/RTT 变化(隐式)
核心变量rwnd(接收窗口)cwnd(拥塞窗口)
发送窗口min(cwnd, rwnd)min(cwnd, rwnd)
可类比水龙头出水速度 vs 杯子接水速度水管总输水能力 vs 整栋楼用水需求

两者通过 实际发送窗口 = min(cwnd, rwnd) 协同工作:任何一个窗口限制生效,发送方都必须遵守。

# 查看接收/发送缓冲区大小
sysctl net.ipv4.tcp_rmem  # 接收缓冲区 (min, default, max)
sysctl net.ipv4.tcp_wmem  # 发送缓冲区 (min, default, max)

# 开启窗口缩放(支持大于 65535 字节的窗口)
sysctl net.ipv4.tcp_window_scaling

五、TCP 状态机

TCP 连接的完整生命周期由 11 种状态精确描述。理解状态迁移不仅是协议层面的基本功,更是线上问题时通过 netstat -antp 判断连接异常状态的前提——大量 CLOSE_WAIT 意味着应用层未关闭连接,大量 TIME_WAIT 意味着短连接过于频繁。

5.1 状态迁移图

                            ┌─────────────────────────────┐
                            │                             │
   主动打开 (客户端)          │    被动打开 (服务端)           │
                            │                             │
     │  connect()           │           bind()+listen()   │
     ↓                      │           accept()          ↓
  ┌──────┐  SYN=1          →│   ┌──────┐                ┌──────┐
  │CLOSED│───────────────────→  │CLOSED│                │LISTEN│
  └──────┘                     →└──────┘                └──┬───┘
     ↑                         →                          │
     │                         →                 收到 SYN  │
     │                         →      send SYN+ACK        │
     ↓  send SYN               →                          ↓
┌──────────┐                   →                    ┌──────────┐
│SYN_SENT  │───────────────────────────────────────→│SYN_RCVD  │
└────┬─────┘                   →                    └────┬─────┘
     │                         →                        │
     │ 收到 SYN+ACK, send ACK   →      收到 ACK (三次握手完成)
     │                         →                        │
     ▼                         →                        ▼
  ┌──────────────────────────────────────────────────────────┐
  │                      ESTABLISHED                         │
  │                    (数据传输阶段)                          │
  └────┬─────────────────────────────────────────────┬───────┘
       │                                             │
       │ 主动关闭: send FIN                           │ 被动关闭: 收到 FIN
       │                                             │ send ACK
       ▼                                             ▼
  ┌──────────────┐                            ┌──────────────┐
  │ FIN_WAIT_1   │                            │ CLOSE_WAIT   │
  └──────┬───────┘                            └──────┬───────┘
         │                                           │
         │ 收到 ACK                                  │ 应用层 close()
         │                                           │ send FIN
         ▼                                           ▼
  ┌──────────────┐                            ┌──────────────┐
  │ FIN_WAIT_2   │                            │  LAST_ACK    │
  └──────┬───────┘                            └──────┬───────┘
         │                                           │
         │ 收到对端 FIN                               │ 收到 ACK
         │ send ACK                                  │
         ▼                                           ▼
  ┌──────────────────┐                        ┌──────────────┐
  │   TIME_WAIT      │                        │   CLOSED     │
  │  (等待 2MSL)      │                        └──────────────┘
  └────────┬─────────┘
           │
           │ 2MSL 超时
           ▼
      ┌──────────┐
      │  CLOSED  │
      └──────────┘

同时关闭(双方同时发送 FIN)的罕见路径:
  FIN_WAIT_1 → 收到 FIN (非ACK) → CLOSING → 收到 ACK → TIME_WAIT → CLOSED

5.2 11 种状态速查

状态含义触发条件排查线索
CLOSED不存在连接初始状态或已完全关闭正常
LISTEN服务端等待连接请求bind + listen正常监听
SYN_SENT已发 SYN,等待匹配请求的 SYNconnect() 调用大量出现说明连接目标不可达
SYN_RCVD收到 SYN,已回 SYN+ACK,等待 ACK服务端收到 SYN大量出现可能遭受 SYN Flood
ESTABLISHED连接已建立,正常数据传输三次握手完成正常
FIN_WAIT_1主动关闭方已发 FIN主动 close()短暂停留
FIN_WAIT_2主动关闭方收到 FIN 的 ACK对端确认了 FIN对端迟迟不发 FIN 会长期停留
CLOSING双方同时关闭同时发送 FIN罕见
TIME_WAIT等待 2MSL(主动关闭方)收到对端 FIN 并回复 ACK大量出现说明短连接过多
CLOSE_WAIT被动关闭方收到 FIN 并回复 ACK,等待本地关闭对端发来 FIN大量出现 = 应用层忘记 close()
LAST_ACK被动关闭方已发 FIN,等待最后的 ACKclose() 后发送 FIN短暂停留

5.3 TIME_WAIT 的三种作用(补充)

前文第二章已提到 TIME_WAIT 的两个核心作用,此处补充到三个:

  1. 确保最后一个 ACK 可靠送达:丢失时对端重传 FIN,本端可重新 ACK
  2. 清除网络中滞留的旧连接报文:2MSL 确保旧连接的所有报文段消散,避免被同四元组的新连接误收
  3. 确保被动关闭方有足够时间收到 ACK 并进入 CLOSED 状态:如果 TIME_WAIT 过短而直接进入 CLOSED,被动关闭方 LAST_ACK 状态收到新连接 SYN 时会导致协议错误(RST)
# 生产环境快速诊断命令
# 统计各状态连接数
netstat -antp | awk '{print $6}' | sort | uniq -c | sort -rn

# 典型异常模式
# CLOSE_WAIT 过多 → 应用代码未调用 close(),fd 泄漏
# TIME_WAIT 过多 → 短连接过多,考虑连接池或长连接,调整内核参数
# SYN_RCVD 过多 → 可能正在遭受 SYN Flood 攻击

六、DNS 域名解析

为什么后端需要理解 DNS:服务间调用、RPC 框架服务发现、CDN 调度和多机房流量切换均依赖 DNS。一次 curl 从执行到建立连接,DNS 解析是无法跳过的第一步,其延迟直接影响 P99 耗时。理解解析链路和缓存策略,有助于解释"为什么偶尔出现 DNS 解析超时"这类线上问题。

6.1 递归查询 vs 迭代查询

DNS 解析存在两种查询模式:

模式客户端行为DNS 服务器行为典型场景
递归查询发一次请求,等最终结果服务器负责完成全部后续查询,返回最终 IP 或失败客户端 → 本地 DNS
迭代查询多次请求,每次得到"去问谁"的指引服务器仅返回下一级 DNS 地址,不代为查询本地 DNS → 各级权威 DNS
递归查询(客户端→本地DNS,只需发一次请求):
  客户端 ──"example.com 的 IP?"──→ 本地DNS
  客户端 ←────"93.184.216.34"───── 本地DNS  (本地DNS完成了后面的所有查询)

迭代查询(本地DNS→各级权威DNS,逐步降级查询):
  本地DNS ──"example.com 的 IP?"──→ 根DNS
  本地DNS ←───"去问 .com 的 DNS"─── 根DNS
  本地DNS ──"example.com 的 IP?"──→ .com 顶级域DNS
  本地DNS ←──"去问 example.com 的 DNS"── .com 顶级域DNS
  本地DNS ──"example.com 的 IP?"──→ 权威DNS
  本地DNS ←───"93.184.216.34"────── 权威DNS

大多数客户端(浏览器、getaddrinfo())发起的是递归查询给本地 DNS 服务器,本地 DNS 服务器再通过迭代查询逐级解析。

6.2 完整解析链路

一次完整的 DNS 解析经过以下多级缓存和查询:

用户请求 http://www.example.com/api/xxx

① 浏览器 DNS 缓存(chrome://net-internals/#dns)
   └─ 命中 → 直接返回 IP ✅
   └─ 未命中 ↓

② 操作系统 hosts 文件
   Linux: /etc/hosts  │  Windows: C:\Windows\System32\drivers\etc\hosts
   └─ 匹配 → 返回配置的 IP ✅
   └─ 未匹配 ↓

③ 本地 DNS 缓存(nscd/systemd-resolved)
   └─ 命中 → 返回 IP ✅
   └─ 未命中 ↓

④ 本地 DNS 服务器(/etc/resolv.conf 中配置,如 8.8.8.8 或公司内网 DNS)
   └─ 自身缓存命中 → 返回 IP ✅
   └─ 未命中,发起迭代查询 ↓

⑤ 根域名服务器(Root DNS, 全球 13 组)
   └─ "我不知道 www.example.com 的 IP,但知道 .com 的 NS 记录"
   └─ 返回 .com 顶级域 DNS 地址 ↓

⑥ 顶级域名服务器(TLD, Top-Level Domain)
   └─ "我不知道 www.example.com 的 IP,但知道 example.com 的 NS 记录"
   └─ 返回 example.com 权威 DNS 地址 ↓

⑦ 权威 DNS 服务器(Authoritative DNS)
   └─ "www.example.com 的 A 记录是 93.184.216.34"
   └─ 返回最终 IP ✅

⑧ 本地 DNS 缓存结果(根据 TTL),返回给客户端

每级缓存都有 TTL(Time To Live),过期后需重新查询。权威 DNS 的 TTL 设置直接影响切换流量的速度——故障切换时 TTL 过长会导致流量长时间指向故障 IP。

# 模拟完整的迭代解析过程(不使用本地 DNS 递归,手动逐级查询)
dig +trace www.example.com

# 查询 A 记录及查看 TTL
dig www.example.com A

# 指定 DNS 服务器查询(绕过本地 DNS)
dig @8.8.8.8 www.example.com

# 反向解析(IP → 域名)
dig -x 93.184.216.34

6.3 DNS 负载均衡

DNS 最简单的负载均衡方式:为同一个域名配置多条 A 记录,DNS 服务器以轮询(Round-Robin)顺序返回不同的 IP:

;; example.com 的 DNS 区域配置中:
www  IN  A  192.168.1.10
www  IN  A  192.168.1.11
www  IN  A  192.168.1.12

第一次查询 → 返回 [192.168.1.10, 192.168.1.11, 192.168.1.12]
第二次查询 → 返回 [192.168.1.11, 192.168.1.12, 192.168.1.10]  (轮询旋转)
第三次查询 → 返回 [192.168.1.12, 192.168.1.10, 192.168.1.11]

DNS 负载均衡的优势是实现极其简单,缺点是服务端故障后切换迟缓——客户端和中间 DNS 缓存不会立即感知服务端宕机,需等待 TTL 过期。对于需要快速故障剔除的场景,Nginx/LVS 等专用负载均衡器更合适。现代方案常将 DNS 轮询与后端健康检查结合(如 AWS Route 53 的健康检查 + DNS Failover)。

6.4 常见 DNS 记录类型

记录类型用途示例
A域名 → IPv4 地址example.com. A 93.184.216.34
AAAA域名 → IPv6 地址example.com. AAAA 2606:2800:220:1::
CNAME域名别名(Canonical Name)www.example.com. CNAME example.com.
NS域名对应的权威 DNS 服务器example.com. NS ns1.example.com.
MX邮件服务器example.com. MX 10 mail.example.com.
TXT文本记录(常用于域名验证、SPF)example.com. TXT "v=spf1 ..."
SRV服务定位(指定服务的端口和主机)_http._tcp SRV 0 5 80 www

七、HTTPS 与 TLS

7.1 TLS 1.2 握手(2-RTT)

① ClientHello(支持加密套件列表 + 客户端随机数₁)
② ServerHello(选定加密套件 + 服务端随机数₂)+ 数字证书(含服务端公钥)
③ 客户端验证证书合法性 → 生成 Premaster Secret → 公钥加密后发送给服务端
④ 双方使用随机数₁ + 随机数₂ + Premaster Secret → 推导对称密钥 → 切换为加密通信

7.2 TLS 1.3 握手(1-RTT)

① ClientHello(支持的加密套件 + 客户端随机数₁ + 预计算的 DH 密钥参数)
② ServerHello(选定加密套件 + 服务端随机数₂ + 证书 + 服务端 DH 密钥参数)
   → 双方均已具备计算对称密钥所需的全部参数 → 无需额外往返
③ 握手完成,立即开始加密通信

核心改进:TLS 1.3 移除了 RSA 密钥交换,统一采用 ECDHE 协议(具备前向安全性);客户端在首次握手时即发送猜测的 DH 参数,若命中期盼的密钥协商方案,服务端直接在 ServerHello 中返回密钥材料,省去一轮往返。TLS 1.3 同时移除了不再安全的加密套件与压缩方案。

7.3 前向安全性(Forward Secrecy)

为什么需要前向安全性:假设攻击者通过任何方式(漏洞利用、法院传票、内部泄露)获取了服务器的私钥。如果使用 RSA 密钥交换,攻击者可以解密所有历史 HTTPS 会话——因为 Premaster Secret 被服务器私钥永久保护。而 ECDHE 密钥交换中,每个会话的对称密钥独立生成且用过即销毁,服务器私钥仅用于身份认证,与数据加密密钥无关。

ECDHE 密钥交换的核心流程

客户端                                    服务端
  │                                          │
  │ ① 生成临时私钥 a,计算公钥 A = a×G       │
  │   ClientHello (包含公钥 A + 支持的曲线)   │
  │──────────────────────────────────────→│
  │                                          │ ② 生成临时私钥 b,计算公钥 B = b×GServerHello (包含公钥 B + 选定曲线)    │    计算共享密钥: S = b×A = b×a×G
  │←──────────────────────────────────────│
  │                                          │
  │ ③ 计算共享密钥: S = a×B = a×b×G          │
  │   两端得到相同的 S(椭圆曲线上的点)        │   两端得到相同的 S
  │   从 S 派生对称密钥                       │   从 S 派生对称密钥
  • ab临时私钥,会话结束后立即销毁——即使服务器私钥泄露,也无法还原 ab,进而无法计算 S
  • 对比 RSA 密钥交换:客户端用服务器公钥加密 Premaster Secret 发送过去,任何持有服务器私钥的人都可以解密,获取所有历史会话密钥
# 使用 OpenSSL 查看 TLS 1.3 支持的加密套件
openssl ciphers -V -s -tls1_3

# 检查服务端是否支持前向安全性(查看是否包含 ECDHE 套件)
openssl s_client -connect example.com:443 -tls1_3 2>/dev/null | grep "Server Temp Key"

# 输出示例:Server Temp Key: X25519, 253 bits
# X25519 即 ECDHE 使用的椭圆曲线,说明具备前向安全性

7.4 0-RTT 会话恢复

TLS 1.3 引入 0-RTT(Zero Round-Trip Time) 模式,允许客户端在首次握手 1-RTT 之后,后续连接在第一个 RTT 内就携带应用数据:

──── 首次连接(1-RTT 完整握手)────
客户端                             服务端
  │──── ClientHello + DH 参数 ──────→│
  │←─── ServerHello + 证书 + DH ─────│
  │──── 加密应用数据 ────────────────→│  ① 服务端附带 session_ticket (PSK)


──── 后续连接(0-RTT 恢复)────
客户端                             服务端
  │──── ClientHello + PSK + DH 参数 + 0-RTT 应用数据 ────→│
  │←─── ServerHello + 新 session_ticket + 加密应用数据 ──→│
  │     ↑ 服务端立即处理 0-RTT 数据并回复                  │

PSK(Pre-Shared Key)机制:首次握手成功后,服务端向客户端发送一个 session_ticket(包含 PSK 标识和有效期)。客户端缓存该票据。下次连接时,客户端在 ClientHello 中附带 PSK 标识,如果服务端认可该票据,双方跳过证书验证和密钥协商,直接使用 PSK 派生会话密钥。

7.5 0-RTT 的安全风险:重放攻击

0-RTT 面临的核心安全问题是重放攻击(Replay Attack):

攻击者截获客户端的 ClientHello + 0-RTT 数据
         │
         ↓ 重放
         │
发送相同的 ClientHello + 0-RTT 数据 ──→ 服务端

服务端收到第二次相同请求 → 再次处理其中的 0-RTT 应用数据
→ 如果是 POST /transfer 转账请求 → 钱被转两次!

缓解措施

策略原理局限性
仅允许幂等操作GET/HEAD/OPTIONS 等无副作用请求可用 0-RTT,POST/PUT/DELETE 禁用需应用层配合,HTTP 协议层面无强制
服务端记录 PSK 使用次数同一 PSK 只能使用一次,重放时拒绝需要全局共享状态存储,引入额外复杂度
ClientHello 中嵌入时间戳服务端检查时间窗口,过期拒绝时钟偏差可能导致误拒绝
TLS 1.3 的 early_data 扩展协议层面标记 0-RTT 数据,HTTP 服务器据此拒绝非幂等请求需服务端和客户端均支持
# Nginx 配置示例:禁止非幂等请求使用 0-RTT
ssl_early_data on;
proxy_set_header Early-Data $ssl_early_data;

# 在应用层检查
# if ($ssl_early_data = "1") {
#     # 仅允许 GET/HEAD/OPTIONS 请求
# }

实践建议:0-RTT 适用于 CDN 边缘节点回源、静态资源请求等读多写少且幂等的场景。涉及金融交易、用户数据修改的接口不应启用 0-RTT。


八、HTTP/2 与 HTTP/3

版本核心改进解决的核心问题
HTTP/1.1持久连接(Keep-Alive)+ 管道化避免每个请求的 TCP 握手开销,但队头阻塞(HOL Blocking)仍然存在
HTTP/2二进制帧 + 多路复用 + HPACK 头部压缩一个 TCP 连接并发传输多个请求-响应流,消除应用层队头阻塞
HTTP/3QUIC(基于 UDP)+ 连接迁移消除 TCP 层队头阻塞(丢包仅影响单个流)+ 网络切换时连接不中断

"HTTP/2 解决了应用层的队头阻塞——在一个 TCP 连接上通过二进制帧多路复用实现了流级并发。但在传输层,单个 TCP 报文段的丢失仍会阻塞该连接中的全部 HTTP/2 流。HTTP/3 的根本性变革在于更换底层传输协议——抛弃 TCP,采用基于 UDP 的 QUIC 协议,每个流独立传输,一个流的丢包不影响其他流。这不是性能优化,而是架构层的决策——TCP 协议固有的顺序语义使其无法根除传输层队头阻塞。"


核心要点回顾

TCP 三次握手的本质是确认双方的收发能力对等——第一次确认客户端的发送能力、第二次确认服务端的接收与发送能力、第三次确认客户端的接收能力。两次握手存在关键缺陷:无法识别网络中滞留的过期 SYN 报文,服务端可能为已放弃的旧连接空等浪费资源。四次挥手比握手多一次报文交换的根本原因是 TCP 的全双工属性:客户端发送 FIN 仅关闭自身发送方向,服务端可能仍有数据正在传输中,因此对客户端 FIN 的确认(ACK)和服务端自身的 FIN 不能合并——ACK 与 FIN 之间服务端继续发送剩余数据。TIME_WAIT 等待 2MSL 有两个作用:确保最终 ACK 可靠到达和清除网络中残留的旧连接报文。生产环境中大量 TIME_WAIT 需关注——主动关闭方为服务端时可能耗尽本地端口。

TCP 拥塞控制的四个核心算法沿固定的状态机流转:慢启动阶段 cwnd 从 1 MSS 起步每 RTT 翻倍(指数增长),达到 ssthresh 后切换为拥塞避免阶段每 RTT 线性加 1(线性增长),收到 3 个重复 ACK 触发快速重传(立即重传丢失报文跳过超时等待)后进入快速恢复(cwnd 减半而非重置为 1,避免不必要的重新慢启动)。最终实际发送窗口取 cwnd 和接收方通告窗口 rwnd 的较小值——拥塞控制是对网络能力的探测,流量控制是对接收方能力的适配。TCP 状态机的 11 种状态中,CLOSE_WAIT 过多表示应用层未调用 close()、TIME_WAIT 过多表示短连接频繁、SYN_RCVD 过多可能是 SYN Flood 攻击。

TLS 1.3 的根本改进是移除了 RSA 密钥交换,统一采用 ECDHE(椭圆曲线 Diffie-Hellman 临时密钥交换),客户端在第一次握手时预发 DH 参数,将握手从 2-RTT 压缩至 1-RTT,同时 ECDHE 天然提供前向安全性。0-RTT 会话恢复允许客户端在首次请求中直接携带加密数据,代价是缺乏前向安全且存在重放攻击风险。HTTP/3 的最大创新是更换底层传输协议——QUIC 基于 UDP,每个流独立传输,一条流的丢包只阻塞自身不阻塞其他流(消除 TCP 的协议层队头阻塞),同时支持连接迁移(网络切换后连接不中断)。


TCP 三次握手、TLS 1.3 1-RTT、HTTP/2 多路复用——这三块是后端面试网络题的"三座大山"。收藏这篇文章,面试前半小时过一遍设计推导,比死记硬背靠谱十倍。

上一篇:《分布式系统设计》 | 下一篇:《负载均衡与高可用》 系列合集掘金Java合集