第 5 章:拥塞控制——在黑暗中找到最佳车速
从高速公路说起
你在高速公路上开车。路况你看不太清(前面是一片浓雾),但你知道几件事:
- 开太快 → 追尾(网络丢包)
- 开太慢 → 浪费时间(没用满带宽)
- 你能感知到的信号:前车的尾灯(ACK 延迟)、刹车灯(丢包通知)、路上的车流密度(RTT 变化)
拥塞控制就是在这种”看不清前方”的情况下,找到那个最佳车速——既不丢包,又能充分利用带宽。
而且更难的是:路上不只你一辆车。所有的车(所有的连接)都在用同样的策略试探,需要大家”公平”地共享道路容量。
为什么 QUIC 的拥塞控制更难?
你可能会想:TCP 的拥塞控制已经研究了 40 年,QUIC 直接搬过来不就行了?
答案是:没那么简单。
挑战一:看不到中间信息
TCP 的头部是明文的,路由器可以设置 ECN(显式拥塞通知)标志位告诉发送端”我快满了”。QUIC 的头部大部分加密了,中间设备帮不上忙。
TCP:路由器能帮忙
路由器发现队列快满了 → 设置 ECN 标志 → TCP 收到后减速
相当于:路边有电子限速牌告诉你"前方拥堵,请减速"
QUIC:只能自己判断
中间设备看不到 QUIC 的内部 → 无法主动通知
只能靠:丢包、RTT 变化、ACK 延迟来推断
相当于:浓雾中没有限速牌,只能靠自己感觉
注:QUIC 支持 ECN,但需要端到端验证,且很多路径不支持
挑战二:用户态实现的精度问题
TCP 的拥塞控制在内核中实现,有纳秒级的时间戳精度。QUIC 在用户态,受到操作系统调度的影响:
内核态(TCP):
收到 ACK → 立即处理 → 微秒级响应
用户态(QUIC):
收到 ACK → 内核唤醒进程 → 上下文切换 → 处理
延迟可能多 10-100 微秒
对 BBR 这种依赖精确时间测量的算法,这个误差不可忽略
挑战三:多流复用的公平性
一个 QUIC 连接里有多个流。拥塞控制是在连接级别做(所有流共享一个拥塞窗口),还是在流级别做(每个流独立控制)?
连接级拥塞控制(当前标准做法):
所有流共享一个拥塞窗口
优点:一个连接不会抢占过多带宽(对其他TCP连接公平)
缺点:重要的流可能被不重要的流拖慢
流级拥塞控制(理论研究):
每个流独立控制
优点:可以优先保证重要流
缺点:一个连接开100个流,等于抢了100份带宽(对其他连接不公平)
当前所有六大实现都采用连接级拥塞控制
拥塞控制基础概念
在讲具体算法之前,先统一几个核心概念:
拥塞窗口(CWND)
拥塞窗口 = 发送端在收到确认之前最多能发多少字节
直觉:
CWND = 10KB → 最多10KB的数据在路上"飞"着还没被确认
CWND 越大 → 发送速率越高
发送速率 ≈ CWND / RTT
例:CWND=100KB, RTT=100ms
发送速率 ≈ 100KB / 0.1s = 1000KB/s = 1MB/s
带宽延迟积(BDP)
BDP = 带宽 × RTT = 管道里能装多少数据
日常类比:
想象一条水管,水管的直径就是带宽,长度就是RTT
BDP = 水管的容积 = 同时"在路上"的数据量
例:10Mbps 带宽,100ms RTT
BDP = 10Mbps × 0.1s = 1Mb = 125KB
意味着:拥塞窗口设为125KB时,刚好能"装满"这条管道
- CWND < BDP → 管道没装满,浪费带宽
- CWND > BDP → 管道溢出,数据在路由器排队,增加延迟
- CWND >> BDP → 路由器队列满了,开始丢包
慢启动(Slow Start)
所有拥塞控制算法都有一个”探索阶段”:
慢启动过程:
1. 初始 CWND = 10个包(约14KB)
2. 每收到一个 ACK → CWND 翻倍
3. 指数增长:10 → 20 → 40 → 80 → 160...
4. 直到发生丢包或达到某个阈值
名字叫"慢启动"但其实增长很快(指数级)
从0到填满管道大约需要 log2(BDP/initial_cwnd) 个RTT
例:BDP=125KB, initial=14KB
14 → 28 → 56 → 112 → 224(超了!)
大约4个RTT就能填满管道
CUBIC:基于丢包的经典算法
核心思想
CUBIC 是目前 Linux/Windows/macOS 的默认 TCP 拥塞控制算法。它的核心思想是:
1. 持续增大发送速率
2. 直到发生丢包
3. 丢包时记住当前窗口大小(Wmax)
4. 将窗口减小到 Wmax × 0.7
5. 用一个三次方(cubic)函数逐渐恢复到 Wmax
6. 超过 Wmax 后缓慢探索更高窗口
窗口变化曲线(像一个被压扁的S):
CWND
^
| Wmax ←── 丢包点
| / ·····→ 缓慢探索新高
| /
| / ← cubic曲线(先快后慢再快)
| /
| /
|/ ← 丢包后窗口减到 0.7×Wmax
+──────────────────────────────> 时间
CUBIC 的优势和局限
优势:
- 简单、稳定、经过20年验证
- 对大多数网络条件表现不错
- 公平性好(多个CUBIC流能均分带宽)
局限:
- "丢包驱动":必须等丢包才减速,意味着总会触发丢包
- 在缓冲区大的链路上(bufferbloat)会造成高延迟
- 在高带宽高延迟链路上收敛慢(需要很多RTT才能填满管道)
- 不适合频繁变化的移动网络
代码示例:CUBIC 在 TQUIC 中的实现
// TQUIC 中 CUBIC 的核心逻辑(简化)
impl CongestionController for Cubic {
fn on_ack(&mut self, bytes_acked: usize, rtt: Duration) {
if self.in_slow_start() {
// 慢启动:每个ACK增加一个MSS
self.cwnd += bytes_acked; // ← 指数增长
} else {
// 拥塞避免:使用cubic函数
let t = now() - self.epoch_start; // ← 距离上次丢包的时间
let w_cubic = self.c * (t - self.k).powi(3) + self.w_max;
self.cwnd = w_cubic as usize; // ← cubic曲线更新
}
}
fn on_loss(&mut self) {
self.w_max = self.cwnd; // ← 记住丢包时的窗口
self.cwnd = (self.cwnd as f64 * 0.7) as usize; // ← 减到70%
self.epoch_start = now(); // ← 开始新的cubic周期
self.k = ((self.w_max - self.cwnd) as f64 / self.c).cbrt();
}
}
BBR:基于带宽估测的革命性算法
为什么需要 BBR?
CUBIC 有一个根本性的问题:它通过”制造丢包”来探测带宽上限。但在现代网络中,路由器有很大的缓冲区(buffer)。CUBIC 会把这些 buffer 填满,导致所有经过这个路由器的连接延迟都变高——这就是bufferbloat(缓冲区膨胀)。
CUBIC 的问题:bufferbloat
路由器的缓冲区 = 排队等待的数据包
理想状态:缓冲区空的,包立即转发,延迟低
CUBIC 工作时:不断增加速率直到丢包
→ 缓冲区被填满之后才丢包
→ 在缓冲区满之前,延迟已经很高了!
延迟(RTT)
^
| xxxxxxxxxx ← buffer满了,开始丢包
| xxxx ← 延迟越来越高(buffer在填)
| xxxx
| xxxx ← 理想延迟(管道满但buffer空)
| x
+────────────────────────> 发送速率
↑ ↑
管道满但 buffer满
buffer空 开始丢包
(最佳点) (CUBIC在这里才减速)
BBR 的思路完全不同:不等丢包,而是主动测量带宽和最小 RTT,找到那个”管道满但 buffer 空”的最佳工作点。
BBR 的核心模型
BBR 全称 Bottleneck Bandwidth and Round-trip propagation time。它维护两个关键估计值:
BtlBw (Bottleneck Bandwidth):瓶颈带宽
= 最近一段时间观测到的最大交付速率
= "这条路最宽处能通多少车"
RTprop (Round-trip Propagation time):最小传播延迟
= 最近一段时间观测到的最小RTT
= "路上没车时走一趟要多久"
目标发送速率 = BtlBw(填满管道)
目标 inflight = BtlBw × RTprop = BDP(不超过管道容量)
BBR 的四个阶段
BBR 状态机:
┌──────────────────────────────────────────────────┐
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ STARTUP │───>│ DRAIN │───>│PROBE_BW │◄─┐│
│ └──────────┘ └──────────┘ └──────────┘ ││
│ │ ││
│ ▼ ││
│ ┌──────────┐ ││
│ │PROBE_RTT │──┘│
│ └──────────┘ │
└──────────────────────────────────────────────────┘
STARTUP (慢启动):
- 指数增长探测带宽
- 直到交付速率不再增长(管道满了)
DRAIN (排空):
- 把 STARTUP 阶段积累的队列排空
- 降低发送速率到 BDP
PROBE_BW (带宽探测,主要状态):
- 周期性上下浮动发送速率(±25%)
- 探测带宽是否增加了
- 大部分时间在这个状态
PROBE_RTT (延迟探测):
- 每 10 秒左右,短暂降低 inflight 到 4 个包
- 排空路由器队列,测量真实最小 RTT
- 持续约 200ms
BBR vs CUBIC:行为对比
场景:100Mbps链路,100ms RTT,路由器buffer可容纳200ms的数据
CUBIC 行为:
1. 慢启动,速率指数增长
2. buffer 被填满后丢包
3. 此时 RTT 已经从 100ms 涨到了 300ms(100ms传播 + 200ms排队)
4. 减速到 70%,然后 cubic 恢复
5. 周而复始:RTT 在 100-300ms 之间波动
BBR 行为:
1. STARTUP 阶段快速找到 BtlBw = 100Mbps
2. DRAIN 阶段排空多余队列
3. PROBE_BW 阶段保持发送速率在 100Mbps 左右
4. RTT 保持在 100-110ms(几乎不排队)
结果:同样的带宽利用率,BBR 的延迟低得多
BBR2/BBR3:BBR 的进化
BBR 的问题
BBR v1 有几个已知问题:
- 与 CUBIC 共存不公平:BBR 倾向于抢占更多带宽
- 在高丢包率下过于激进:不理会丢包,一直按估测带宽发送
- 对实际RTT的反应不够灵敏
BBR2 的改进
BBR2(也称 BBRv2)增加了:
BBR2 关键改进:
1. 丢包响应:如果丢包率超过阈值,降低发送速率
(BBR1 几乎忽略丢包)
2. ProbeRTT 改进:更温和的 RTT 探测
(BBR1 会突然把 inflight 降到 4 个包,造成吞吐量暴跌)
3. 与 CUBIC 的公平性:
BBR1: 可能占 CUBIC 的 2-3 倍带宽
BBR2: 大致公平
4. 更好的 buffer 适应:
根据 buffer 大小动态调整行为
BBR3:TQUIC 的独特实现
BBR3 是 BBR 系列的最新版本,目前只在 Linux 内核的实验分支中有实现。TQUIC 是唯一在 QUIC 用户态实现了 BBR3 的项目。
BBR3 进一步改进:
1. 更精确的带宽探测周期
2. 改进的 inflight 上限控制
3. 更好的 ECN 支持
4. 对瞬态网络变化更鲁棒
TQUIC 的 BBR3 实现位置:
src/congestion_control/bbr3.rs
这对犀牛鸟参赛者是一个重要的贡献点:
- BBR3 的行为验证和测试
- 与其他算法的公平性测试
- 针对移动网络场景的优化
COPA:延迟敏感的算法
核心思想
COPA(Competitive Online Policy Adjustment)的目标是最小化延迟,同时保持合理的吞吐量。它特别适合实时应用(视频通话、游戏)。
COPA 的核心公式(直觉版):
目标速率 = 1 / (δ × RTT_standing)
其中:
- δ 是一个调节参数(越大越保守)
- RTT_standing 是近期观测到的"稳定RTT"
直觉:
- RTT 增高 → 速率降低(因为队列在积累)
- RTT 降低 → 速率增加(队列在排空)
和 BBR 的区别:
- BBR 试图"填满管道但不填buffer"
- COPA 更保守:积极地把延迟控制在最低
适用场景:
- 视频通话(腾讯会议、微信视频):延迟 > 吞吐量
- 实时游戏:需要稳定的低延迟
- 不适合:大文件下载(会浪费带宽)
TQUIC 实现 COPA 的原因很明确:腾讯的业务场景中有大量实时通信需求(微信视频通话、腾讯会议、QQ 视频等)。
各实现的拥塞控制支持对比
┌───────────┬────────┬─────┬──────┬──────┬──────┬──────────┐
│ 算法 │ quiche │ quinn│ s2n │ TQUIC│ msquic│ ngtcp2 │
├───────────┼────────┼─────┼──────┼──────┼──────┼──────────┤
│ CUBIC │ ✓ │ ✓ │ ✓ │ ✓ │ ✓ │ ✓ │
│ BBR │ ✓* │ ✗ │ ✗ │ ✓ │ ✗ │ ✓ │
│ BBR2 │ ✓ │ ✗ │ ✗ │ ✗ │ ✗ │ ✗ │
│ BBR3 │ ✗ │ ✗ │ ✗ │ ✓ │ ✗ │ ✗ │
│ COPA │ ✗ │ ✗ │ ✗ │ ✓ │ ✗ │ ✗ │
│ NewReno │ ✓ │ ✓ │ ✗ │ ✗ │ ✓ │ ✓ │
│ Hystart++ │ ✓ │ ✓ │ ✓ │ ✓ │ ✓ │ ✓ │
└───────────┴────────┴─────┴──────┴──────┴──────┴──────────┘
* quiche 的 BBR 是从 Chromium 移植的
关键观察:
- TQUIC 在拥塞控制算法多样性上是最丰富的(5种)
- BBR3 和 COPA 是 TQUIC 独有的
- quiche 的特色是有 Chromium 移植的 BBR 实现(经过大规模验证)
丢包检测:拥塞控制的”眼睛”
拥塞控制需要知道”什么时候发生了丢包”。QUIC 的丢包检测比 TCP 更精确:
QUIC 的丢包判定规则(RFC 9002)
规则1:包号阈值(Packet Threshold)
如果一个包之后有 3 个更高编号的包被确认了,
但这个包还没被确认 → 判定丢失
例:发送了包 100-110
收到 ACK 确认了 101, 102, 103
包 100 还没被确认 → 丢了(3个之后的包已确认)
规则2:时间阈值(Time Threshold)
如果一个包发送后超过 max(smoothed_rtt, latest_rtt) × 9/8 的时间
还没被确认 → 判定丢失
例:smoothed_rtt = 100ms
阈值 = 100ms × 9/8 = 112.5ms
包 100 发送 113ms 后还没被确认 → 丢了
为什么比 TCP 好?
TCP 重传歧义:SEQ 可复用,ACK 可能确认的是原始包或重传包
QUIC 无歧义:包号单调递增,每个 ACK 明确对应唯一的包
PTO(Probe Timeout):探测超时
当没有任何 ACK 回来时(可能所有包都丢了):
PTO 计算:
PTO = smoothed_rtt + max(4 × rttvar, 1ms) + max_ack_delay
例:srtt=100ms, rttvar=25ms, max_ack_delay=25ms
PTO = 100 + max(100, 1) + 25 = 225ms
PTO 触发时:
发送 1-2 个探测包(不等于判定丢失!)
如果探测包的 ACK 回来了 → 之前的包可能只是延迟了
如果探测包也超时 → PTO翻倍,继续探测
这比 TCP 的 RTO(重传超时)更灵活:
TCP RTO:超时 → 直接判定丢失 → 窗口暴降
QUIC PTO:超时 → 先探测 → 确认了再判定
quiche 的双引擎设计
quiche 有一个独特的设计:它同时维护了两套拥塞控制实现:
quiche 拥塞控制架构:
┌─────────────────────────────────────────┐
│ quiche 连接层 │
├─────────────────────────────────────────┤
│ CongestionControl trait │
├──────────────────┬──────────────────────┤
│ Native 实现 │ Chromium 移植 │
│ - CUBIC │ - BBR (从Chrome移植) │
│ - NewReno │ - BBR2 │
│ - Hystart++ │ │
│ │ 保持与Chrome一致的 │
│ 纯Rust实现 │ 行为(包括bug!) │
└──────────────────┴──────────────────────┘
为什么两套?
1. Native: Rust 风格,干净的抽象,适合阅读和修改
2. Chromium: 经过亿级用户验证的生产实现
→ Cloudflare 实际使用 Chromium 版本
→ 但维护负担大(要跟踪 Chromium 的更新)
s2n-quic 的 PathPublisher:可观测性设计
s2n-quic 在拥塞控制方面的特色不是算法种类多,而是可观测性:
// s2n-quic 的 PathPublisher 设计(简化)
pub trait PathPublisher {
fn on_congestion(&self, event: CongestionEvent);
fn on_recovery(&self, event: RecoveryEvent);
fn on_rtt_update(&self, event: RttEvent);
fn on_pacing(&self, event: PacingEvent);
}
// 使用示例:
struct MetricsCollector;
impl PathPublisher for MetricsCollector {
fn on_congestion(&self, event: CongestionEvent) {
// 记录拥塞事件到 metrics 系统
// 包括:时间、窗口大小、丢包数、恢复方式
metrics::counter!("quic.congestion.events").increment(1);
}
}
// 好处:生产环境中能实时观察拥塞控制的行为
// AWS 大规模部署时这种可观测性是必须的
TQUIC 的拥塞控制架构
TQUIC 是拥塞控制算法最丰富的实现。它的设计体现了”给每种业务场景最好的算法”:
TQUIC 拥塞控制选择逻辑:
业务场景 推荐算法 原因
────────────────────────────────────────────────────
大文件下载/CDN CUBIC 成熟稳定,公平性好
视频流 BBR/BBR3 高吞吐,低延迟
视频通话 COPA 最低延迟
弱网/移动网络 BBR3 对变化反应快
需要与TCP公平共存 CUBIC 行为最接近TCP
// TQUIC 中创建拥塞控制器(简化)
pub fn new_congestion_controller(
algorithm: CongestionControlAlgorithm,
config: &Config,
) -> Box<dyn CongestionController> {
match algorithm {
CongestionControlAlgorithm::Cubic => {
Box::new(Cubic::new(config))
}
CongestionControlAlgorithm::Bbr => {
Box::new(Bbr::new(config))
}
CongestionControlAlgorithm::Bbr3 => {
Box::new(Bbr3::new(config)) // ← 独有
}
CongestionControlAlgorithm::Copa => {
Box::new(Copa::new(config)) // ← 独有
}
}
}
TQUIC 的每路径拥塞控制
在多路径模式下,TQUIC 为每条路径维护独立的拥塞控制器:
多路径拥塞控制:
Path 0 (WiFi):
RTT: 20ms, BW: 50Mbps
拥塞控制器: BBR3
CWND: 独立计算
Path 1 (4G):
RTT: 80ms, BW: 20Mbps
拥塞控制器: BBR3(独立实例)
CWND: 独立计算
为什么每条路径要独立?
- WiFi 和 4G 的网络特征完全不同
- 用同一个拥塞控制器无法适应两条路径
- 一条路径丢包不应该影响另一条路径的发送速率
这是第6章(多路径QUIC)的前置知识
Pacing:发送节奏控制
除了拥塞窗口(CWND)决定”能发多少”,pacing 决定”什么时候发”:
没有 pacing(突发模式):
CWND = 100 个包
所有100个包在一个瞬间全部发出 → 路由器瞬间收到100个包
可能立即触发队列溢出和丢包
|
| ||||||||||||||||||||||| ← 突发!
| ← 空闲
| ||||||||||||||||||||||| ← 又一个突发
+──────────────────────────> 时间
有 pacing(平滑模式):
同样的 CWND = 100 个包
但均匀分布在一个 RTT 内发送
| | | | | | | | | | | | | | | | | | | | ← 平滑!
+──────────────────────────> 时间
发送间隔 = RTT / CWND(每个包之间等一小段时间)
pacing 对 BBR 尤其重要——BBR 的设计假设发送是平滑的。没有 pacing 的 BBR 会产生严重的测量误差。
各实现的 pacing 支持:
| 实现 | Pacing 方式 |
|---|---|
| quiche | 计算 pacing rate,由应用层负责实际调度 |
| quinn | 集成 tokio 定时器 |
| s2n-quic | Pacing Provider trait |
| TQUIC | 内置 pacing 调度器 |
| msquic | 内核层级的精确调度 |
| ngtcp2 | 提供 pacing 时间戳,由应用负责 |
算法选择指南
你的场景 ──────────── 推荐算法 ──── 原因
│
├── 大文件下载/CDN ── CUBIC ──── 最成熟,与TCP公平
│
├── 视频流/直播 ──── BBR/BBR3 ── 高带宽利用,低延迟
│
├── 视频通话 ──────── COPA ───── 最低延迟
│
├── 弱网环境 ──────── BBR3 ───── 对网络变化反应快
│
├── 高丢包率 ──────── CUBIC ──── BBR在高丢包下过于激进
│
└── 不确定 ────────── CUBIC ──── 安全的默认选择
更精确的决策矩阵:
低延迟需求 中延迟需求 不关心延迟
低丢包 COPA/BBR3 BBR/BBR3 CUBIC
中丢包 COPA BBR3 CUBIC
高丢包 COPA CUBIC CUBIC
拥塞控制的未来趋势
机器学习驱动的拥塞控制
传统方法:手工设计规则(if 丢包 then 减速)
ML方法:让模型学习最优策略
已有的研究:
- Orca (SIGCOMM 2022):用强化学习做拥塞控制
- PCC (NSDI 2018):基于在线学习的速率控制
挑战:
- 训练环境和真实网络的差异
- 公平性难以保证
- 可解释性差(出问题时不知道为什么)
目前还没有任何 QUIC 实现集成了 ML 拥塞控制
→ 这是一个潜在的研究方向
多路径感知的拥塞控制
当前:每条路径独立做拥塞控制
未来:路径之间协同决策
例:WiFi 突然丢包严重
当前做法:WiFi 路径独立减速
协同做法:检测到WiFi不佳 → 主动把更多流量转移到4G
TQUIC 的 MultipathScheduler 已经在做类似的事
→ 调度器根据各路径状态决定数据分配
→ 但和拥塞控制的集成还在探索中
本章小结
| 算法 | 核心信号 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|---|
| CUBIC | 丢包 | 成熟稳定公平 | bufferbloat | 通用/下载 |
| BBR | 带宽+RTT | 低延迟高吞吐 | 与CUBIC不公平 | 视频流 |
| BBR2 | 带宽+RTT+丢包 | 修复BBR问题 | 仍在演进 | 通用 |
| BBR3 | 改进的BBR2 | 更精确更鲁棒 | 新/验证少 | 移动网络 |
| COPA | RTT | 最低延迟 | 吞吐量低 | 实时通信 |
一句话总结:拥塞控制是在”利用带宽”和”避免排队”之间走钢丝。CUBIC 靠丢包才转向,BBR 靠测量提前判断,COPA 为了低延迟放弃部分带宽。TQUIC 给你所有选择。
读完本章你能做什么
- 解释 CUBIC 和 BBR 的核心区别(丢包驱动 vs 带宽测量)
- 说出 BBR 的四个状态及其作用
- 解释为什么 QUIC 的拥塞控制比 TCP 难实现
- 根据应用场景选择合适的拥塞控制算法
- 说出 TQUIC 在拥塞控制方面的独特贡献(BBR3、COPA、每路径控制)
- 解释 pacing 的作用
常见误区
误区一:BBR 总是比 CUBIC 好
正确理解:BBR 在”有路由器大缓冲”的场景下延迟更低,但在”缓冲很小 + 高丢包”的场景下可能表现更差(BBR v1 几乎忽略丢包)。而且 BBR 与 CUBIC 共存时会抢占更多带宽——这对其他用户不公平。没有”总是更好”的拥塞控制算法,只有”更适合当前场景”的算法。
误区二:QUIC 一定比 TCP 有更好的拥塞控制
正确理解:QUIC 的优势不在于算法更好(用的是同样的 CUBIC/BBR),而在于:1. 更精确的 RTT 测量(包号不复用);2. 更灵活的部署(可以随 App 更新);3. 可以针对业务定制。同一个算法在 TCP 和 QUIC 上的表现差异不大——差异来自测量精度和部署灵活性。
误区三:拥塞窗口越大越好
正确理解:拥塞窗口超过 BDP 后,多出来的数据会在路由器缓冲区排队。排队 = 延迟增加。CWND 的最优值是恰好等于 BDP:管道满了但没有排队。超过这个值就是在”用延迟换吞吐量”——对下载可以接受,对实时通信不可接受。
导读目录:README.md 上一章:第 4 章 · 连接建立与握手——从陌生人到老朋友 下一章:第 6 章 · 多路径 QUIC——同时走两条路