计算机网络基础:后端必知 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?
- 确保最后一个 ACK 到达:客户端发送的最终 ACK 可能在网络中丢失,服务端超时后会重传 FIN。客户端保持在 TIME_WAIT 状态,收到重传 FIN 后重新发送 ACK,避免服务端持续重传
- 清除网络中残留的旧连接报文: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 = 16 → 17 → 18 → 19 → 20 → ...
如果持续未发生丢包,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 重新慢启动"。
快速恢复的算法步骤:
- ssthresh = cwnd / 2(记录拥塞时的窗口减半)
- cwnd = ssthresh + 3 × MSS(额外的 3 MSS 补偿已收到并触发 dup ACK 的 3 个后续报文段)
- 重传丢失报文段后,每收到一个重复 ACK,cwnd +1 MSS(说明又有报文段到达接收方,网络在清空)
- 收到新的 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),包含慢启动/拥塞避免/快速重传/快速恢复 | 低带宽、低延迟网络 |
| CUBIC | Linux 默认算法,使用三次函数代替线性增长,窗口增长与 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,等待匹配请求的 SYN | connect() 调用 | 大量出现说明连接目标不可达 |
| 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,等待最后的 ACK | close() 后发送 FIN | 短暂停留 |
5.3 TIME_WAIT 的三种作用(补充)
前文第二章已提到 TIME_WAIT 的两个核心作用,此处补充到三个:
- 确保最后一个 ACK 可靠送达:丢失时对端重传 FIN,本端可重新 ACK
- 清除网络中滞留的旧连接报文:2MSL 确保旧连接的所有报文段消散,避免被同四元组的新连接误收
- 确保被动关闭方有足够时间收到 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×G
│ ServerHello (包含公钥 B + 选定曲线) │ 计算共享密钥: S = b×A = b×a×G
│←──────────────────────────────────────│
│ │
│ ③ 计算共享密钥: S = a×B = a×b×G │
│ 两端得到相同的 S(椭圆曲线上的点) │ 两端得到相同的 S
│ 从 S 派生对称密钥 │ 从 S 派生对称密钥
a和b是临时私钥,会话结束后立即销毁——即使服务器私钥泄露,也无法还原a和b,进而无法计算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/3 | QUIC(基于 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合集