网络(二):TCP 连接管理、可靠传输与拥塞控制
网络(二):TCP 连接管理、可靠传输与拥塞控制
导语:这是网络面试热度最高的一篇(TCP 协议 780、三次握手 654 稳居前二)。本篇从三次握手/四次挥手的每一个状态讲起,彻底拆开 TIME_WAIT 与 CLOSE_WAIT 两个高频运维题,再展开可靠传输的四大机制、流量控制与拥塞控制的分工、Reno → CUBIC → BBR 的算法演进,补齐 TCP 粘包拆包、半连接/全连接队列与 SYN Flood、Keepalive、超时重传、MTU/MSS 这些必被追问的细节,共 13 题。
一、连接建立与释放
1. 三次握手的完整过程?为什么必须是三次?
答: 假设客户端主动发起:
为什么必须是三次(三个层次的理由,缺一不可):
| 理由 | 说明 |
|---|---|
| ① 确认双方的收发能力 | TCP 是全双工。两次握手只能让服务端确认「客户端能发、我能收」,但客户端无法确认"服务端能收"(不知道自己发的 SYN 有没有被收到)。第三次握手让服务端也确认「客户端收到了我的 SYN+ACK 且能继续发」。 |
| ② 防止历史失效连接建立 | 网络中存在滞留的旧 SYN(上一条连接超时重发、绕路很久才到)。若只有两次握手,服务端一收到 SYN 就认为连接建立,会为一个早已废弃的连接分配资源;三次握手让服务端必须等到客户端确认(客户端会认为这是旧连接的 SYN 而回 RST 或忽略)才真正建立。 |
| ③ 协商初始序号(ISN) | 双方需要交换并确认彼此的 ISN(各随机生成),ISN 是后续排序与去重的基础。三次握手恰好完成「两次交换 + 一次确认」。 |
四个高频追问:
- 「第三次握手可以携带数据吗?」 → 可以(前两次不行,因为还没建立连接,防止 SYN Flood 放大)。这也是「为什么 HTTP 请求能"跟着"第三次握手一起发」的原因。
- 「为什么不是四次?」 → 第二次握手时服务端可以把自己的 SYN 与对客户端 SYN 的 ACK 合并在一个报文里,所以 3 次足够;若 ACK 与 SYN 分开就变四次,但那没有必要。
- 「初始序号为什么随机?」 → 防止被预测(安全)与防止旧连接的序号混入新连接(正确性)。
- 「握手失败了会怎样?」 → 客户端有
tcp_syn_retries(默认 5)次重试,服务端 SYN+ACK 有tcp_synack_retries(默认 5)次重试,都超时则连接失败。
2. 四次挥手的过程?为什么是四次?能不能变成三次?
答: 假设客户端主动关闭:
为什么四次:TCP 是全双工,关闭要双向各关一次。服务端收到 FIN 后:
- 只能先回一个 ACK("我知道你要关了");
- 但它可能还有数据没发完,所以不能立刻发 FIN;
- 必须等应用层调用
close()(或数据发完)后才发自己的 FIN。
ACK 与 FIN 因此不能合并 → 共四次。
「能不能三次?」可以,分两种情况:
| 情况 | 说明 |
|---|---|
| 延迟确认与"捎带" | 若服务端收到 FIN 时正好也有数据要发,可以把 ACK 与数据一起发,之后单独发 FIN —— 仍然是 4 个报文,只是"合并了 ACK 与数据" |
| 真正的"三次挥手" | 若服务端收到 FIN 后立即 close()(没有待发数据),内核可能把 ACK 与 FIN 合并成一个报文 → 就变成 3 个报文。这是实现优化,不是协议要求,面试要说「标准是四次,实际可能出现三次合并」 |
两个高频延伸:
close()vsshutdown()(半关闭):close()会关闭整个 socket(读写都关),并减少引用计数,计数为 0 才真正发 FIN;多线程/fork场景下容易"以为关了其实没关";shutdown(fd, SHUT_WR)只关写方向(发 FIN,对端收到 EOF),仍可继续读取对方数据 —— 这就是半关闭,也是为什么会出现FIN_WAIT_2/CLOSE_WAIT这两个"半边关闭"的状态。
- 谁主动关闭谁进 TIME_WAIT:这是判断「TIME_WAIT 堆积在谁身上」的关键(见第 3 题)。
3. TIME_WAIT 的作用是什么?为什么是 2MSL?过多如何解决?
答: TIME_WAIT 是主动关闭方在发出最后一个 ACK 后进入的状态。
两个存在理由:
| 理由 | 说明 |
|---|---|
| ① 保证最后一个 ACK 可靠到达 | 若这个 ACK 丢失,对端会重发 FIN;主动方若已 CLOSED 就会回 RST(对端会把它当成错误而不是正常关闭)。处于 TIME_WAIT 时收到重发的 FIN,能再次回复 ACK,让对端正常关闭。 |
| ② 让旧连接的延迟报文"消逝" | 假设立即用相同四元组建立新连接,网络里滞留在旧连接的报文可能被新连接误收,导致数据错乱。等待 2MSL 足以让旧报文全部过期(一个来回 = 1 MSL 去 + 1 MSL 回)。 |
为什么是 2MSL:
危害(只发生在"主动关闭方"):
- 高并发短连接场景(尤其是反向代理、压测客户端、爬虫)会堆积大量 TIME_WAIT,占用本地端口与内存,最终
Cannot assign requested address(端口耗尽); - 注意:真正的服务端(作为被动关闭方)一般不产生大量 TIME_WAIT。如果看到服务端上 TIME_WAIT 很多,说明是服务端主动关闭了连接(如
keepalive_timeout到期后服务端发 FIN),或它扮演的是"客户端"角色(调下游)。
解决方案(按推荐度,注意其中有一个常见误区):
| 方案 | 说明 |
|---|---|
| ① 用长连接/连接池(根治) | 减少连接建立次数,TIME_WAIT 自然消失。这才是正解。 |
| ② 服务端主动关闭、客户端被动(角色调整) | 让"连接数多但端口充足"的一方做主动关闭方,把 TIME_WAIT 转移到不会端口耗尽的一侧 |
③ net.ipv4.tcp_tw_reuse = 1(谨慎) | 允许作为客户端复用超过 1 秒的 TIME_WAIT 连接(必须开启 tcp_timestamps=1,默认关闭需显式打开)。不影响服务端,用于缓解客户端端口耗尽。 |
| ④ 扩大本地端口范围 | net.ipv4.ip_local_port_range = 10000 65535,增加可用端口 |
⑤ SO_REUSEADDR | 主要解决服务端重启时"Address already in use"(允许 bind 处于 TIME_WAIT 的地址),不能减少 TIME_WAIT 数量 |
❌ tcp_tw_recycle | Linux 4.12+ 已彻底移除,任何场景都不要用。它在 NAT 环境下会因时间戳回退导致合法连接被静默丢包(历史上坑过大量线上系统) |
⚠️ 调小 tcp_max_tw_buckets(不推荐) | 它是 TIME_WAIT 数量的硬上限,超过后内核直接销毁 TIME_WAIT 并打告警日志。调小它等于破坏 TIME_WAIT 的保护作用(可能引发 RST、旧报文误收),只应作为紧急止血,绝不能当优化手段。 |
面试标准答案的顺序:先问"是谁在主动关闭、为什么会有这么多短连接" → 再用长连接/连接池根治 → 最后才考虑
tcp_tw_reuse+ 端口范围。直接答"调参"会被认为没有排查意识。
4. 服务器出现大量 CLOSE_WAIT 是什么问题?怎么排查?
答: CLOSE_WAIT 是"被动关闭方"收到对端 FIN 后、自己还没调用 close() 的状态。
大量 CLOSE_WAIT 的含义非常明确: 对端已经关了,但本服务的代码没有关连接。这是应用代码问题,不是内核参数问题(调参没用)。
四个常见根因:
| 根因 | 说明 |
|---|---|
忘记 close() | 异常路径没走 finally、流/连接没关闭、HttpClient/Jedis 没用 try-with-resources |
| 线程被阻塞住 | 业务线程正在执行长耗时逻辑(或死锁),无法执行到关闭逻辑,连接一直被占 |
| 连接池泄漏 | 借出的连接未归还,池又不会主动关闭被对端关闭的连接 |
| 对端超时断连而本端不自知 | 下游(如网关、LB、DB)超时主动关连接,本服务仍在读写,未感知 FIN(因为没读,FIN 在内核 receive 队列里没被读出) |
排查与解决:
# ① 看数量与归属
ss -antp | awk '{print $1}' | sort | uniq -c # 各状态计数
ss -antp state close-wait # 列出 CLOSE_WAIT 及其进程
lsof -p <pid> | grep -c CLOSE_WAIT # 定位到进程/端口
# ② 判断是"本端应用没关"
netstat -antp | grep CLOSE_WAIT | wc -l
# 结合 jstack(Java)/gdb(C++)看线程栈,通常能发现卡在 IO 或业务逻辑上解法:
- 修代码:所有连接/流用 try-with-resources 或在
finally中关闭;加读超时/写超时(SO_TIMEOUT、readTimeout),让线程不会被永久阻塞; - 连接池配置:设置
maxLifetime(小于下游的空闲超时)、开启空闲连接检测(testOnBorrow/心跳); - 对账:确认下游/LB 的空闲超时时间,确保本端
maxLifetime略短于下游超时,避免"用到一半被对端关掉"。
对比记忆:TIME_WAIT 是"正常状态但太多"(调架构);CLOSE_WAIT 是"异常状态"(改代码)。这是最经典的二选一陷阱题。
二、可靠传输与流量/拥塞控制
5. TCP 是如何保证可靠传输的?
答: 靠「四套机制 + 一个基础」协同:
| 机制 | 作用 |
|---|---|
| ① 序号 + 确认应答(ACK) | 累计确认:ack=n 表示"n 之前的都收到了";接收方对乱序到达的段会重复发同一 ACK(触发快速重传) |
| ② 超时重传 + 快速重传 | 未在 RTO 内收到 ACK → 重传;收到 3 个重复 ACK → 不等超时立即重传(快重传)。RTO 用 Jacobson/Karels 算法动态估算(基于 RTT 的平滑值与偏差),并指数退避 |
| ③ 流量控制 | 用接收窗口 rwnd 约束发送方,防止压垮接收方缓冲 |
| ④ 拥塞控制 | 用拥塞窗口 cwnd 约束发送方,防止压垮中间网络 |
| ⑤ 校验和 | 检测比特差错,错误报文被丢弃(触发重传) |
| ⑥ 有序重组与去重 | 接收方按 seq 重排、丢弃重复段 |
| ⑦ 连接管理 | 三次握手/四次挥手本身也是"可靠建立与释放"的一部分 |
两个常被追问的细节:
- 「累计确认的代价是什么?」 → 收到乱序段时只能重复 ACK 之前的序号,发送方无法知道"后面的段到了哪些",可能把一大段全部重传 → 这正是 SACK(选择确认) 要解决的问题:让接收方告诉发送方「我还缺哪个段」。
- 「RTO 为什么不能设成固定值?」 → 固定值要么太小(大量不必要的重传,加剧拥塞),要么太大(丢包恢复慢)。RTT 是动态变化的,必须自适应估算。
6. 流量控制与拥塞控制有什么区别?
答:
| 维度 | 流量控制(Flow Control) | 拥塞控制(Congestion Control) |
|---|---|---|
| 解决的问题 | 接收方处理不过来 | 网络(路由器/链路)承受不了 |
| 约束来源 | 接收方的接收缓冲区 | 中间网络的带宽与队列 |
| 关键变量 | 接收窗口 rwnd(接收方通告) | 拥塞窗口 cwnd(发送方自己维护,无显式通告) |
| 作用范围 | 端到端(点对点) | 全局(整条路径上的所有流量) |
| 反馈方式 | 接收方在 TCP 头里显式通告 rwnd | 通过丢包/延迟增加间接推断(BBR 除外) |
| 控制手段 | 发送窗口 ≤ rwnd | cwnd 按算法增减 |
核心公式(必背):
实际发送窗口 = min( cwnd , rwnd ) ← 两者取小,谁更紧就听谁的用一个场景讲清二者的分工:
易错点:UDP 没有这两个机制,所以 UDP 应用必须自己实现(QUIC 就在用户态实现了拥塞控制);HTTP/2 的"多路复用"仍受 TCP 拥塞控制影响——一个流丢包会让整个连接的所有流一起降速(TCP 层队头阻塞),这正是 HTTP/3 换 QUIC 的动机之一。
7. 拥塞控制的四个阶段是什么?Reno、CUBIC、BBR 有何区别?
答: 这是近年最容易被追问的进阶点(只答"慢启动/拥塞避免/快重传/快恢复"已经不够)。
经典四阶段(Reno,1988):
丢包的两种判定与反应(高频对比):
| 事件 | 含义 | 反应 |
|---|---|---|
| 超时(RTO) | 严重拥塞(连 ACK 都回不来) | ssthresh = cwnd/2,cwnd = 1,重回慢启动 |
| 3 个重复 ACK | 轻度丢包(网络还能通) | ssthresh = cwnd/2,cwnd = ssthresh,快恢复 |
算法演进(现代必答):
| 算法 | 特点 | 现状 |
|---|---|---|
| Reno / NewReno | 丢包即减半(AIMD),对高带宽长延迟链路恢复慢 | 历史基线 |
| CUBIC(Linux 2.6.19+ 默认) | 用三次函数替代线性窗口增长:靠近上次拥塞点增长变慢(稳定)、远离时增长快(抢带宽);与 RTT 无关,在高 BDP 网络表现远好于 Reno | Linux 长期默认,仍是绝大多数服务器的算法 |
| BBR(Google,2016) | 不看丢包,而是主动测量「瓶颈带宽 + 最小 RTT」,把 cwnd 建模为 BDP = 带宽 × RTT;靠"周期性地加速探测"估计可用带宽 | 需要内核开启(tcp_bbr),在有丢包的链路(跨国、无线)上明显优于 CUBIC,代价是占用更多带宽、对公平性有争议 |
查看与切换(实操加分):
cat /proc/sys/net/ipv4/tcp_congestion_control # 当前算法,通常为 cubic
sysctl net.ipv4.tcp_available_congestion_control # 可用算法列表
sysctl -w net.ipv4.tcp_congestion_control=bbr # 临时切换为 BBR(需内核模块支持)两个常见追问:
- 「为什么慢启动要"慢"?」 → "慢"指的是初始 cwnd 小(相对当时并不知道网络容量而言),但它增长是指数的——名字有历史误导性;
- 「为什么现在初始 cwnd 是 10?」 → 因为 BDP 大的链路(如 100ms RTT、10Gbps)用 cwnd=1 需要几十个 RTT 才能填满管道,严重影响短连接性能;研究表明 10 MSS 在丢包率可接受的前提下显著提升小文件传输速度(RFC 6928)。
8. 零窗口、持续计时器、糊涂窗口综合症、Nagle 算法分别是什么?
答: 这是滑动窗口的四个衍生问题,面试常作为"你懂多深"的分水岭。
① 零窗口与持续计时器(Persist Timer)
② 糊涂窗口综合症(Silly Window Syndrome, SWS)
问题:接收方频繁通告"很小"的窗口、或发送方频繁发"很小"的数据 → 网络里充满大量小包,有效载荷远小于头部开销,带宽利用率极低。
两个方向的解决:
| 方向 | 算法 | 做法 |
|---|---|---|
| 接收方(不要通告太小窗口) | Clark 算法 | 窗口小于 MSS 或缓冲区一半时不通告,等腾出足够空间再一次通告 |
| 发送方(不要发太小数据) | Nagle 算法 | 已发送但未被 ACK 确认的小数据先攒在缓冲,等攒到 MSS 或收到 ACK 再发;只允许有一个未被确认的"小段" |
③ Nagle 与延迟 ACK 的"经典 40ms 问题"(高频实战坑)
易错提醒:
TCP_NODELAY关的是 Nagle,不是"延迟 ACK"(后者在内核侧,应用无法直接控制)。所以「我在客户端设了TCP_NODELAY为什么还是慢」的答案可能是对端在延迟 ACK。
④ Nagle 与 TCP_CORK 的区别(加分点)
| Nagle | TCP_CORK | |
|---|---|---|
| 行为 | 用"未确认"作为发送条件,最多延迟到下一个 ACK | 强制塞住,直到攒满 MSS 或显式 uncork/超时 200ms |
| 适用 | 一般小包场景 | 明确的批量发送(如 Nginx sendfile 前,或拼装 HTTP 响应头+体) |
三、字节流、队列与健壮性
9. TCP 粘包与拆包是怎么产生的?如何解决?
答: 高频实战题,本质是「TCP 是面向字节流的,没有消息边界」。
产生的三层原因:
| 层 | 原因 |
|---|---|
| 协议本身 | TCP 面向字节流、不保留消息边界(这是最根本原因,UDP 面向数据报就不会粘包) |
| 发送侧 | Nagle 算法攒批发送、应用多次 write 被合并成一个 TCP 段(小于 MSS 时) |
| 接收侧 | 应用 read 不及时/缓冲区大,一次读走多个段;MSS 限制导致大消息被拆成多段 |
三种解决方案(面试要按顺序讲,并主动提边界校验):
① 定长消息(最简单,灵活度差)
② 特殊分隔符(如 \n、\r\n)
③ 长度字段(Length-Field,最通用,推荐)
必须主动说出的两个工程细节(区分"背过"与"做过"):
- 必须给 Length 设上限并校验:否则恶意客户端构造
Length = 2GB会让服务端直接 OOM(这是真实攻击面); - 必须处理"半包"的粘性缓冲:接收方需要维护一个可累积的缓冲区(如 Netty 的
ByteToMessageDecoder+LengthFieldBasedFrameDecoder),把暂不完整的字节留在缓冲区等下一次数据到达,而不是丢弃。
一句话总结:粘包/拆包不是"TCP 的 bug",而是"应用把字节流当消息用"的必然结果——解法永远是"在应用层重新定义消息边界"。
10. 半连接队列与全连接队列是什么?队列溢出有什么表现?
答: 服务端内核为每个监听套接字维护两个队列(与三次握手对应):
关键参数:
| 队列 | 上限由谁决定 |
|---|---|
| 半连接队列 | net.ipv4.tcp_max_syn_backlog |
| 全连接队列 | min(net.core.somaxconn, 应用 listen(backlog)) ← 两个都要够大,只调一个没用 |
溢出表现与应对:
| 溢出 | 现象 | 应对 |
|---|---|---|
| 半连接队列溢出 | 大量 SYN_RCVD;多为 SYN Flood 攻击 | ①开启 SYN Cookie(net.ipv4.tcp_syncookies=1,无需增大队列即可扛住伪造源 IP 的 SYN 洪泛);②调大 tcp_max_syn_backlog;③上游限流/清洗 |
| 全连接队列溢出 | accept() 消费太慢;新连接的 ACK 被内核丢弃,客户端表现为连接变慢/偶发超时 | ①调大 somaxconn 和 listen(backlog);②提升 accept 消费速度(多线程 accept、连接池);③SO_REUSEPORT 让多进程分担 |
tcp_abort_on_overflow 的取舍(高频追问):
| 取值 | 行为 | 建议 |
|---|---|---|
0(默认,推荐) | 丢弃该 ACK,客户端仍认为连接已建立,之后重传;队列有空位时可正常建连 | 对突发流量友好(客户端会重试,最终成功) |
1 | 直接回 RST,客户端立刻报错 | 仅当你确认队列会长期溢出、希望客户端快速失败时用 |
排查命令:
ss -lnt # 查看监听队列
# State Recv-Q Send-Q Local Address:Port
# LISTEN 129 128 0.0.0.0:80
# ↑ Recv-Q = 当前全连接队列中等待 accept 的连接数
# ↑ Send-Q = 全连接队列上限(即 backlog)
ss -s # 总体统计(含 SYN_RECV 数量)
netstat -s | grep -i -E "listen|overflow|SYN" # 内核统计里的溢出次数(关键指标)面试加分:
netstat -s里的times the listen queue of a socket overflowed是全连接队列溢出的直接证据;SYNs to LISTEN sockets dropped则通常指向半连接队列。
11. TCP Keepalive 与应用层心跳有什么区别?
答: TCP Keepalive 是内核提供的传输层探测机制:
| 参数 | 默认值 | 含义 |
|---|---|---|
net.ipv4.tcp_keepalive_time | 7200 秒(2 小时) | 连接空闲多久后开始探测 |
net.ipv4.tcp_keepalive_intvl | 75 秒 | 每次探测的间隔 |
net.ipv4.tcp_keepalive_probes | 9 次 | 连续多少次无响应就判定连接死亡 |
对端的三种反应:
为什么生产上更多用"应用层心跳":
| 维度 | TCP Keepalive | 应用层心跳 |
|---|---|---|
| 层次 | 传输层(内核) | 应用层(业务代码) |
| 默认及时性 | 2 小时才开始探测,太迟钝 | 可秒级(如 5~30s) |
| 能否发现"假死" | 不能——进程还在但业务线程卡死/死锁时,内核仍会回 ACK | 能——业务能感知"对端是否还能正确处理请求" |
| 能否探测 NAT/中间设备回收 | 不能——NAT/LB 已把连接状态清了,但两端内核都不知情 | 能——心跳包能"打穿"连接映射(保活 NAT 表项) |
| 穿透代理 | 只能探测相邻一跳 | 可端到端(业务语义可达) |
结论与工程实践:
- 两者解决的不是同一个问题:Keepalive 是"连接还活着吗"(TCP 层可达),心跳是"对端能正常服务吗";
- 网关/长连接服务(WebSocket、IM、Nacos、Redis)通常:开启 TCP Keepalive(缩短 time/intvl) + 应用层心跳(Redis 有
PING、Netty 有IdleStateHandler); - 务必区分:HTTP 的
Connection: keep-alive指的是"TCP 长连接复用",与 TCP Keepalive 完全是两回事——这是经典陷阱题。
12. TCP 的握手与传输阶段的超时重传由哪些参数控制?
答:
| 阶段 | 参数 | 默认值 | 说明 |
|---|---|---|---|
| 第一次握手(SYN) | net.ipv4.tcp_syn_retries | 6(部分发行版为 5) | 客户端 SYN 最大重传次数,重传间隔指数退避(1s、2s、4s…) |
| 第二次握手(SYN+ACK) | net.ipv4.tcp_synack_retries | 5 | 服务端 SYN+ACK 最大重传次数;超时则销毁该半连接 |
| 已建立连接的数据报 | net.ipv4.tcp_retries2 | 15 | 超过后向上层报"发送失败"(注意:默认约 13~15 分钟才放弃,太长了) |
| 同上(早期探测) | net.ipv4.tcp_retries1 | 3 | 超过后让 IP 层做 MTU 探测/刷新路由,但不断开连接 |
两个必须讲清的细节:
- 重传间隔是指数退避(Exponential Backoff)的:RTO 每次翻倍,
1s → 2s → 4s → 8s …,所以tcp_retries2 = 15的总时长可达 ~15 分钟——这解释了很多"连接没断但业务已经超时"的现象。生产上通常缩短应用层超时(如 RPC 3s、DB 5s)来避免等这么久; - 实际停止重传的条件是"次数或时间"双重约束:即使次数未到,若总重传耗时超过内核推算的上限也会停止。
排查相关指标:
netstat -s | grep -iE "retrans|timeout|reset" # 重传/超时/复位的统计
ss -ti # 单连接详情:rto、rtt、重传次数、cwnd、ssthresh
# ss -ti 输出示例:
# cubic wscale:7,7 rto:204 rtt:2/1 ato:40 cwnd:10 retrans:0/1 ...13. MTU 与 MSS 的区别?为什么 TCP 要按 MSS 分段而不依赖 IP 分片?
答:
| 概念 | 层次 | 含义 | 以太网典型值 |
|---|---|---|---|
| MTU(最大传输单元) | 数据链路层 | 一帧能承载的最大 IP 包大小 | 1500 字节 |
| MSS(最大报文段长度) | 传输层 | TCP 数据部分的最大长度 = MTU − IP 头(20) − TCP 头(20) | 1460 字节(IPv4)/ 1440(IPv6,头 40) |
为什么 TCP 要按 MSS 自己分段(核心是"重传粒度"):
MSS 的协商:在三次握手时用 TCP 选项交换(各端告知自己能接收的最大 MSS),取双方较小值。
两条实战经验(面试加分):
- 路径 MTU 发现(PMTUD)与"黑洞"问题:
- UDP 无 MSS 概念:
UDP 没有握手,无法协商 MSS,超过 MTU 就在 IP 层分片。所以「UDP 报文最好控制在 MTU(约 1400 字节)以内」——这也是 QUIC 把单包控制在 1200 字节左右的原因。若实现"可靠 UDP",丢一个分片要重传整个数据报,效率极差。
下一篇:《网络(三)》进入 HTTP/HTTPS 与 Web 协议——无状态与长连接、HTTP 方法与状态码、HTTP/1.1 → 2 → 3 的演进、HTTP 缓存、Cookie/Session/Token/JWT、同源与跨域 CORS、WebSocket/SSE/长轮询选型、HTTPS 握手与证书链、会话复用与优化。
