第 10 章:深入 TQUIC——腾讯的多路径 QUIC 引擎
从快递公司说起
想象一家”下一代快递公司”(TQUIC),它有几个与众不同的特点:
多线路并发配送:
- 普通快递公司(quinn/quiche/s2n-quic):一个包裹走一条路,路不通就换一条
- TQUIC 快递公司:一个大包裹拆成小件,同时走多条路(WiFi 走一部分、4G 走一部分),哪条到了就拼上
调度员可以换:
- 不满意当前调度策略?换一个调度员(MultipathScheduler trait)
- 想自己研究新策略?实现一个 trait 就行
- 三个现成的调度员:选最快路线的、全部路线都发一份的、轮流走的
每条路有独立管理员:
- 每条路的拥堵情况独立评估(per-path CC)
- 某条路堵了不影响其他路
- 管理员(拥塞控制算法)也可以换
这就是 TQUIC 的核心竞争力:多路径是一等公民,不是后加的补丁。
TQUIC 基本信息
项目:TQUIC
GitHub: Tencent/tquic
Stars: ~1K
语言:Rust (核心) + C FFI
定位:高性能多路径 QUIC,移动端优化
架构:sans-io + 可插拔组件
代码量:~250 文件
TLS 后端:BoringSSL
维护者:腾讯网络平台部
RFC 合规:RFC 9000 + Multipath QUIC draft
上线环境:微信、QQ、腾讯云 CDN、腾讯游戏
TQUIC 的诞生背景
腾讯的网络挑战:
1. 移动端双栈
- 手机同时有 WiFi 和蜂窝网络
- 用户在地铁中从 WiFi 切到 4G,连接不能断
- 最好能同时用两条路提高速度
2. 高密度场景
- 微信红包:除夕夜数十亿条消息
- QQ 音视频:实时性要求极高
- 腾讯游戏:延迟 > 50ms 就影响体验
3. 网络质量差异大
- 中国 WiFi 经常丢包率 > 5%
- 4G 在高铁上抖动严重
- 需要在恶劣条件下保证体验
TQUIC 的目标:
用多路径解决 → 一条路不行时另一条补上
用可插拔设计 → 不同场景用不同策略
用 Rust → 性能接近 C,安全性远超 C
TQUIC 的整体架构
┌──────────────────────────────────────────────────────────────┐
│ 应用层 │
│ C FFI / iOS SDK / Android SDK / Rust native │
├──────────────────────────────────────────────────────────────┤
│ TQUIC API 层 │
│ Connection / Stream / Endpoint │
├──────────────────────────────────────────────────────────────┤
│ 多路径调度层 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ MultipathScheduler trait │ │
│ │ ┌─────────┐ ┌───────────┐ ┌──────────────┐ │ │
│ │ │ MinRtt │ │ Redundant │ │ RoundRobin │ │ │
│ │ │ (最快路)│ │ (全冗余) │ │ (轮询) │ │ │
│ │ └─────────┘ └───────────┘ └──────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────┤
│ 路径管理层 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ PathMap │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │ Path 0 │ │ Path 1 │ │ Path N │ ... │ │
│ │ │ WiFi │ │ 4G │ │ 5G │ │ │
│ │ │ CC: BBR│ │CC:CUBIC│ │CC:BBR3 │ │ │
│ │ │ RTT:20 │ │ RTT:80 │ │ RTT:10 │ │ │
│ │ └────────┘ └────────┘ └────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────┤
│ 协议核心层 │
│ 连接管理 / 流管理 / 帧处理 / 丢包恢复 │
├──────────────────────────────────────────────────────────────┤
│ TLS 层 │
│ BoringSSL (TLS 1.3) │
├──────────────────────────────────────────────────────────────┤
│ 平台 I/O 层 │
│ UDP socket (sans-io: 应用负责实际收发) │
└──────────────────────────────────────────────────────────────┘
MultipathScheduler:TQUIC 的核心创新
trait 定义
// MultipathScheduler trait(简化自 TQUIC 源码)
pub trait MultipathScheduler: Send + Sync {
/// 选择下一个要发送数据的路径
///
/// 参数:
/// - paths: 所有可用路径的信息
/// - frame: 要发送的数据帧信息
///
/// 返回:选中的路径 ID,或 None(所有路径都不可用)
fn on_select_path(
&mut self,
paths: &PathMap,
frame: &FrameInfo,
) -> Option<PathId>;
/// 当路径状态变化时通知调度器
fn on_path_updated(&mut self, path_id: PathId, event: PathEvent);
/// 当数据被确认时通知调度器
fn on_ack(&mut self, path_id: PathId, acked_bytes: usize);
/// 当数据丢失时通知调度器
fn on_loss(&mut self, path_id: PathId, lost_bytes: usize);
}
三种内置调度器
MinRtt 调度器
// MinRtt:选择 RTT 最小的路径
// 适用场景:延迟敏感(游戏、音视频)
pub struct MinRttScheduler;
impl MultipathScheduler for MinRttScheduler {
fn on_select_path(
&mut self,
paths: &PathMap,
_frame: &FrameInfo,
) -> Option<PathId> {
paths.iter()
.filter(|p| p.is_active() && p.can_send())
.min_by_key(|p| p.rtt()) // ← 选 RTT 最小的
.map(|p| p.id())
}
}
// 工作原理:
//
// Path 0 (WiFi): RTT = 20ms, 可用 ✓ ← 选这条!
// Path 1 (4G): RTT = 80ms, 可用 ✓
// Path 2 (5G): RTT = 10ms, 不可用 ✗(窗口满了)
//
// 每次要发数据时:看看谁最快且还能发 → 选它
//
// 优点:延迟最优
// 缺点:可能让快路径过载,慢路径闲置
Redundant 调度器
// Redundant:在所有可用路径上发送同样的数据
// 适用场景:可靠性极高要求(关键控制消息)
pub struct RedundantScheduler;
impl MultipathScheduler for RedundantScheduler {
fn on_select_path(
&mut self,
paths: &PathMap,
_frame: &FrameInfo,
) -> Option<PathId> {
// 返回所有可用路径(实际实现中会发送到每条路径)
// 简化:这里返回第一条,但会标记"需要冗余"
paths.iter()
.filter(|p| p.is_active() && p.can_send())
.next()
.map(|p| p.id())
}
fn should_replicate(&self) -> bool {
true // 标记:数据需要在所有路径上复制
}
}
// 工作原理:
//
// 要发送数据 "Hello"
// Path 0 (WiFi): 发送 "Hello" ←
// Path 1 (4G): 发送 "Hello" ← 同样的数据!
//
// 收端:谁先到用谁,重复的丢弃
//
// 优点:只要有一条路通,数据就能到达
// 缺点:带宽利用率低(每条路都在传相同数据)
// 适合:小数据量但极高可靠性(如游戏的操作指令)
RoundRobin 调度器
// RoundRobin:轮流在各路径上发送
// 适用场景:带宽最大化(大文件下载)
pub struct RoundRobinScheduler {
next_path_index: usize,
}
impl MultipathScheduler for RoundRobinScheduler {
fn on_select_path(
&mut self,
paths: &PathMap,
_frame: &FrameInfo,
) -> Option<PathId> {
let active_paths: Vec<_> = paths.iter()
.filter(|p| p.is_active() && p.can_send())
.collect();
if active_paths.is_empty() {
return None;
}
// 轮流选择
let selected = &active_paths[self.next_path_index % active_paths.len()];
self.next_path_index += 1;
Some(selected.id())
}
}
// 工作原理:
//
// 数据块 1 → Path 0 (WiFi)
// 数据块 2 → Path 1 (4G)
// 数据块 3 → Path 0 (WiFi)
// 数据块 4 → Path 1 (4G)
// ...
//
// 优点:简单,带宽利用率高
// 缺点:如果两条路 RTT 差异大,接收端需要等慢路径的数据
// → 导致"头部阻塞"(快路径的数据到了但要等慢路径的才能交付)
调度器的选择策略
场景分析:
┌───────────────────────────────────────────────────────┐
│ 场景 │ 推荐调度器 │ 原因 │
├───────────────────────────────────────────────────────┤
│ 游戏实时对战 │ MinRtt │ 延迟是第一优先 │
│ 音视频通话 │ MinRtt │ 卡顿比码率更重要 │
│ 关键控制消息 │ Redundant │ 必须到达 │
│ 大文件下载 │ RoundRobin │ 总带宽最大化 │
│ 视频流播放 │ 自定义* │ 需要平衡延迟和带宽 │
│ 弱网环境 │ Redundant │ 保证可用性 │
└───────────────────────────────────────────────────────┘
*自定义调度器示例:加权 RoundRobin
- 根据每条路径的带宽比例分配数据量
- WiFi 带宽 100Mbps, 4G 带宽 30Mbps
- 那 WiFi 分 77% 的数据,4G 分 23%
- 这就是犀牛鸟竞赛可以做的方向!
PathMap:路径管理的核心数据结构
PathMap 的设计
// PathMap 管理所有路径(简化)
pub struct PathMap {
/// 所有路径
paths: Vec<Path>,
/// 当前活跃路径的数量
active_count: usize,
/// 主路径 ID
primary_path: PathId,
}
// 单条路径的完整信息
pub struct Path {
/// 路径唯一标识
id: PathId,
/// 四元组(本地地址:端口 + 远端地址:端口)
local_addr: SocketAddr,
remote_addr: SocketAddr,
/// 路径状态
state: PathState,
/// RTT 估计器
rtt_estimator: RttEstimator,
/// 独立的拥塞控制器
congestion_controller: Box<dyn CongestionController>,
/// 独立的丢包恢复
recovery: Recovery,
/// 路径 MTU
mtu: u16,
/// 路径验证 token
validation: PathValidation,
/// 统计信息
stats: PathStats,
}
#[derive(Clone, Copy)]
pub enum PathState {
/// 新创建,未验证
New,
/// 正在验证(PATH_CHALLENGE 已发)
Validating,
/// 已验证,可用
Active,
/// 暂时不可用(例如检测到严重丢包)
Degraded,
/// 已关闭
Closed,
}
PathMap 的操作
impl PathMap {
/// 添加新路径
pub fn add_path(&mut self, local: SocketAddr, remote: SocketAddr,
cc: Box<dyn CongestionController>) -> PathId {
let id = PathId(self.paths.len());
self.paths.push(Path {
id,
local_addr: local,
remote_addr: remote,
state: PathState::New,
rtt_estimator: RttEstimator::new(),
congestion_controller: cc,
recovery: Recovery::new(),
mtu: 1200, // 初始 MTU
validation: PathValidation::new(),
stats: PathStats::default(),
});
// 自动发起路径验证
self.initiate_validation(id);
id
}
/// 获取所有活跃路径
pub fn active_paths(&self) -> impl Iterator<Item = &Path> {
self.paths.iter().filter(|p| p.state == PathState::Active)
}
/// 路径上报丢包
pub fn on_path_loss(&mut self, path_id: PathId, lost_bytes: usize) {
let path = &mut self.paths[path_id.0];
path.congestion_controller.on_loss(lost_bytes, Instant::now());
path.stats.bytes_lost += lost_bytes as u64;
// 如果丢包率过高,标记路径为降级
if path.stats.loss_rate() > 0.30 {
path.state = PathState::Degraded;
self.active_count -= 1;
}
}
}
Per-Path 拥塞控制
为什么每条路径需要独立的拥塞控制
场景:手机同时连 WiFi 和 4G
如果共享一个拥塞控制器:
WiFi 丢包 → 整体窗口减小 → 4G 也被限制了
→ 4G 明明没堵,却不能全速发送
→ 浪费带宽
如果每路径独立拥塞控制:
WiFi 丢包 → WiFi 窗口减小 → 4G 不受影响
→ 4G 继续全速
→ 总带宽 = WiFi 当前带宽 + 4G 全速带宽
共享 CC Per-path CC
WiFi: ████████░░░░ WiFi: ████████░░░░
4G: ████░░░░░░░░ (被连累) 4G: ████████████ (独立运行)
总计: 12 单位 总计: 20 单位 ← 多出 67%!
TQUIC 的 per-path CC 实现
// 每条路径创建时选择自己的拥塞控制算法
fn create_path_with_cc(
local: SocketAddr,
remote: SocketAddr,
cc_algorithm: CcAlgorithm
) -> Path {
let cc: Box<dyn CongestionController> = match cc_algorithm {
CcAlgorithm::Cubic => Box::new(CubicController::new()),
CcAlgorithm::Bbr => Box::new(BbrController::new()),
CcAlgorithm::Bbr3 => Box::new(Bbr3Controller::new()),
CcAlgorithm::Copa => Box::new(CopaController::new()),
};
Path {
congestion_controller: cc,
// ...
}
}
// 甚至可以每条路径用不同的算法!
// 例如:WiFi 路径用 BBR(带宽探测好),4G 路径用 CUBIC(公平性好)
Per-path 丢包恢复
// 每条路径独立的丢包恢复
pub struct Recovery {
/// 已发送但未确认的包
sent_packets: BTreeMap<PacketNumber, SentPacket>,
/// PTO 超时计算
pto_count: u32,
/// 最大 ACK 的包号
largest_acked: Option<PacketNumber>,
/// 丢包检测阈值
loss_threshold: Duration,
}
impl Recovery {
/// 在特定路径上检测丢包
pub fn detect_lost_packets(&mut self, now: Instant) -> Vec<LostPacket> {
let mut lost = Vec::new();
if let Some(largest_acked) = self.largest_acked {
for (pn, pkt) in self.sent_packets.iter() {
// 时间阈值:超过 loss_threshold 没被 ACK → 丢了
if now - pkt.time_sent > self.loss_threshold {
lost.push(LostPacket { pn: *pn, bytes: pkt.size });
}
// 包号阈值:比最大 ACK 小 3 个以上 → 丢了
if largest_acked - *pn > 3 {
lost.push(LostPacket { pn: *pn, bytes: pkt.size });
}
}
}
lost
}
}
TQUIC 的 sans-io 设计
TQUIC 和 quiche 类似,使用 sans-io 模式:
// TQUIC 的 API 风格(简化)
let mut conn = Connection::new(config)?;
// 应用的 I/O 循环
loop {
// 1. 从 socket 读取数据,喂给 TQUIC
let (len, from) = socket.recv_from(&mut buf)?;
conn.recv(&mut buf[..len], RecvInfo { from })?;
// 2. 处理应用层事件
for event in conn.events() {
match event {
Event::StreamReadable { stream_id } => {
let mut data = vec![0u8; 4096];
let len = conn.stream_recv(stream_id, &mut data)?;
// 处理收到的流数据
}
Event::StreamWritable { stream_id } => {
conn.stream_send(stream_id, b"response", false)?;
}
Event::Connected => {
println!("Connected!");
}
_ => {}
}
}
// 3. 从 TQUIC 获取要发送的数据
loop {
let (len, send_info) = match conn.send(&mut out) {
Ok(v) => v,
Err(Error::Done) => break,
Err(e) => return Err(e),
};
socket.send_to(&out[..len], send_info.to)?;
}
// 4. 处理超时
if let Some(timeout) = conn.timeout() {
// 设置定时器
}
}
TQUIC vs quiche 的 API 对比
相似点:
- 都是 sans-io
- 都用 recv() + send() 驱动
- 都用 BoringSSL 做 TLS
- 应用都需要自己管理 I/O 循环
不同点:
TQUIC 额外有:
- 多路径相关 API(add_path, set_scheduler)
- 更丰富的事件类型(PathAdded, PathRemoved)
- per-path 统计信息
conn.add_path(local_wifi, remote)?; // 添加 WiFi 路径
conn.add_path(local_cellular, remote)?; // 添加蜂窝路径
conn.set_multipath_scheduler(MinRttScheduler::new())?; // 设置调度策略
TQUIC 的 C FFI
TQUIC 提供完整的 C API,用于 iOS/Android 等移动端集成:
// C 语言使用 TQUIC(简化)
// 创建配置
tquic_config_t *config = tquic_config_new();
tquic_config_set_max_idle_timeout(config, 30000);
tquic_config_enable_multipath(config, true);
tquic_config_set_multipath_algorithm(config, TQUIC_MULTIPATH_MINRTT);
// 创建连接
tquic_conn_t *conn = tquic_connect(
"example.com", &scid, scid_len,
local, local_len, peer, peer_len, config
);
// 添加第二条路径
tquic_conn_add_path(conn, local_wifi, local_wifi_len, peer, peer_len);
// I/O 循环
while (!tquic_conn_is_closed(conn)) {
// 收
ssize_t nread = recvfrom(fd, buf, sizeof(buf), 0, ...);
tquic_conn_recv(conn, buf, nread, &recv_info);
// 发
ssize_t nwrite;
tquic_send_info_t send_info;
while ((nwrite = tquic_conn_send(conn, out, sizeof(out), &send_info)) > 0) {
sendto(fd, out, nwrite, 0, send_info.to, send_info.to_len);
}
}
// 释放
tquic_conn_free(conn);
tquic_config_free(config);
移动端集成
iOS 集成:
1. TQUIC 编译为 .framework(通过 cargo-lipo)
2. Objective-C/Swift 通过 C FFI 调用
3. 在 NetworkExtension 中管理 socket
4. 自动检测 WiFi/Cellular 切换 → add_path / remove_path
Android 集成:
1. TQUIC 编译为 .so(通过 cargo-ndk)
2. Java/Kotlin 通过 JNI + C FFI 调用
3. 使用 ConnectivityManager 监听网络变化
4. 自动管理多路径
微信的使用方式(推测):
- 后台服务监听网络变化
- WiFi 连接 → add_path(wifi_addr)
- 4G 恢复 → add_path(cellular_addr)
- WiFi 断开 → 只剩 4G 路径,无缝切换
- 用户无感知!
TQUIC 的拥塞控制算法
TQUIC 支持多种拥塞控制算法:
TQUIC 支持的拥塞控制算法:
1. CUBIC(默认)
- 标准的 TCP 友好算法
- 适合大多数场景
- 与其他 TCP/QUIC 流公平共存
2. BBR
- 基于带宽探测
- 适合长距离高带宽链路
- 可能对 CUBIC 流不公平
3. BBR3
- BBR 的最新版本
- 修复了与 CUBIC 的公平性问题
- 增加了 ECN 支持
4. COPA
- 基于延迟的算法
- 特别适合实时应用
- 目标:维持队列延迟接近零
- 腾讯的研究贡献
可插拔设计:
pub trait CongestionControlAlgorithm: Send + Sync {
fn on_packet_sent(&mut self, bytes: usize, now: Instant);
fn on_ack(&mut self, bytes: usize, rtt: Duration, now: Instant);
fn on_loss(&mut self, bytes: usize, now: Instant);
fn congestion_window(&self) -> usize;
fn pacing_rate(&self) -> Option<u64>;
fn in_slow_start(&self) -> bool;
fn in_recovery(&self) -> bool;
}
TQUIC 的代码结构
tquic/
├── src/
│ ├── lib.rs ← 主入口
│ ├── connection/ ← 连接管理
│ │ ├── mod.rs ← Connection 主逻辑
│ │ ├── stream.rs ← 流管理
│ │ ├── path.rs ← 路径管理 (PathMap)
│ │ ├── space.rs ← 包号空间
│ │ └── ...
│ │
│ ├── multipath/ ← 多路径核心
│ │ ├── mod.rs ← MultipathScheduler trait
│ │ ├── minrtt.rs ← MinRtt 调度器
│ │ ├── redundant.rs ← Redundant 调度器
│ │ ├── roundrobin.rs ← RoundRobin 调度器
│ │ └── ...
│ │
│ ├── congestion_control/ ← 拥塞控制
│ │ ├── mod.rs ← CC trait 定义
│ │ ├── cubic.rs ← CUBIC
│ │ ├── bbr.rs ← BBR
│ │ ├── bbr3.rs ← BBR3
│ │ ├── copa.rs ← COPA
│ │ └── ...
│ │
│ ├── recovery/ ← 丢包恢复
│ │ ├── mod.rs
│ │ └── ...
│ │
│ ├── frame/ ← QUIC 帧
│ │ ├── mod.rs
│ │ └── ...
│ │
│ ├── packet/ ← 包处理
│ │ ├── mod.rs
│ │ └── ...
│ │
│ ├── tls/ ← TLS 集成 (BoringSSL)
│ │ └── ...
│ │
│ └── ffi.rs ← C FFI 接口
│
├── include/
│ └── tquic.h ← C 头文件
│
├── tools/ ← 工具
│ ├── tquic_client/ ← 示例客户端
│ └── tquic_server/ ← 示例服务端
│
└── benches/ ← 性能基准测试
犀牛鸟竞赛的切入点
自定义 MultipathScheduler
竞赛最核心的方向:实现一个新的 MultipathScheduler。
// 示例:带权重的自适应调度器
pub struct AdaptiveScheduler {
/// 每条路径的权重(基于估计带宽)
weights: HashMap<PathId, f64>,
/// 累计未发送字节
pending: usize,
}
impl MultipathScheduler for AdaptiveScheduler {
fn on_select_path(
&mut self,
paths: &PathMap,
frame: &FrameInfo,
) -> Option<PathId> {
// 策略:根据每条路径的可用带宽分配权重
let active: Vec<_> = paths.active_paths().collect();
if active.is_empty() {
return None;
}
// 计算每条路径的"得分"
// 得分 = 估计带宽 / (RTT * (1 + loss_rate))
let scores: Vec<(PathId, f64)> = active.iter()
.map(|p| {
let bw = p.estimated_bandwidth();
let rtt = p.rtt().as_secs_f64();
let loss = p.loss_rate();
let score = bw / (rtt * (1.0 + loss));
(p.id(), score)
})
.collect();
// 选得分最高的路径
scores.into_iter()
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
.map(|(id, _)| id)
}
fn on_path_updated(&mut self, path_id: PathId, event: PathEvent) {
match event {
PathEvent::RttUpdated(new_rtt) => {
// 路径 RTT 变化 → 重新计算权重
}
PathEvent::CongestionEvent => {
// 路径拥塞 → 降低权重
}
_ => {}
}
}
}
竞赛可以探索的方向
方向 1:基于机器学习的调度器
- 收集路径状态特征(RTT, loss, bandwidth, queue_delay)
- 训练一个小模型决定数据分配比例
- 挑战:模型推理不能太慢(< 1ms per decision)
方向 2:基于 ECN 的调度器
- 利用 ECN (Explicit Congestion Notification) 信息
- 某路径收到 ECN CE 标记 → 预判即将拥塞 → 提前减少该路径流量
- 比丢包反馈更早知道拥塞
方向 3:应用感知调度器
- 视频流:I 帧走可靠路径,P 帧走快路径
- 游戏:操作指令走所有路径(冗余),状态同步走快路径
- 需要上层应用提供"数据优先级"信息
方向 4:联合拥塞控制 + 调度
- 不只是"选路径",还动态调整每路径的拥塞窗口
- 目标:总吞吐最大化 + 延迟最小化的帕累托最优
- 这是学术界的前沿课题(Coupled CC + Scheduling)
方向 5:对 Head-of-Line Blocking 的优化
- 多路径的核心难题:快路径数据到了,等慢路径
- 优化方案:预测哪些包在慢路径容易丢,提前冗余发送
- 或者:根据乱序容忍度动态调整调度比例
TQUIC 与其他实现的对比
| 维度 | TQUIC | quinn | quiche | s2n-quic | msquic | ngtcp2 |
|---|---|---|---|---|---|---|
| 多路径 | 一等公民 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 |
| 调度器 | 3 种 + 可插拔 | N/A | N/A | N/A | N/A | N/A |
| Per-path CC | 完整 | 部分 | 部分 | 部分 | 有 | 有 |
| CC 算法 | CUBIC/BBR/BBR3/COPA | CUBIC/NR | CUBIC/BBR/BBR2 | CUBIC/BBR | CUBIC/BBR | NR/CUBIC/BBR/BBR2 |
| C FFI | 有 | 无 | 有 | 无 | 有 | 有 |
| 移动端 | iOS + Android | 无 | 有 | 无 | 有 | 无 |
| sans-io | 是 | 是 | 是 | 是(Provider) | 否 | 是 |
| TLS | BoringSSL | rustls | BoringSSL | s2n-tls | Schannel/OpenSSL | 5种 |
阅读 TQUIC 源码的推荐路径
第 1 步:tools/tquic_client/ 和 tools/tquic_server/
→ 看示例程序如何使用 TQUIC API
→ 理解 I/O 循环的写法
第 2 步:src/multipath/mod.rs
→ MultipathScheduler trait 的定义
→ 这是你要实现的接口!
第 3 步:src/multipath/minrtt.rs
→ 最简单的调度器实现
→ 理解调度器如何被调用
第 4 步:src/connection/path.rs
→ PathMap 和 Path 的定义
→ 理解调度器能访问什么信息
第 5 步:src/congestion_control/mod.rs
→ CC trait 定义
→ 理解拥塞控制如何与路径关联
第 6 步:src/congestion_control/bbr3.rs
→ 最新的 CC 算法实现
→ 理解 BBR3 的状态机
第 7 步:src/connection/mod.rs
→ 连接主逻辑
→ 理解调度器在整体流程中的位置
→ send() 函数如何调用 scheduler.on_select_path()
关键搜索词(grep 这些找到关键逻辑):
"MultipathScheduler" → trait 定义和使用点
"on_select_path" → 调度决策点
"PathMap" → 路径管理
"congestion_controller" → 每路径 CC
"add_path" → 路径添加逻辑
本章小结
| 维度 | TQUIC 的特点 |
|---|---|
| 核心创新 | 多路径是一等公民 |
| 调度器 | MultipathScheduler trait(可插拔) |
| 内置算法 | MinRtt / Redundant / RoundRobin |
| 路径管理 | PathMap + per-path CC + per-path recovery |
| CC 算法 | CUBIC / BBR / BBR3 / COPA |
| 平台 | Linux + iOS + Android |
| 竞赛方向 | 实现新的 MultipathScheduler |
一句话总结:TQUIC 是目前唯一将多路径作为一等公民设计的开源 QUIC 实现。它的 MultipathScheduler trait 让你可以在不改动任何底层代码的情况下,实现和测试自己的调度策略。这正是犀牛鸟 2026 竞赛的核心赛道——你需要理解 PathMap 提供的信息、三种内置调度器的思路,然后设计一个在特定场景下表现更优的新调度器。
读完本章你能做什么
- 画出 TQUIC 的整体架构图(从 API 到路径管理到 CC)
- 实现一个最简单的 MultipathScheduler(哪怕只是 random)
- 解释为什么 per-path CC 比共享 CC 更好
- 对比三种内置调度器的适用场景
- 给出犀牛鸟竞赛中自定义调度器的设计方向
- 描述阅读 TQUIC 源码的推荐路径
常见误区
误区一:多路径 QUIC 就是”哪条路好就用哪条”
正确理解:”哪条路好就用哪条”只是 MinRtt 调度器——多路径的最简单用法。多路径的真正价值是同时使用多条路径提高总带宽(RoundRobin/加权调度),或者同时发送提高可靠性(Redundant)。不同场景需要不同策略,这就是为什么调度器需要可插拔。
误区二:多路径一定比单路径快
正确理解:如果调度不当,多路径反而可能比单路径慢!经典问题:数据块 1 走快路径(20ms),数据块 2 走慢路径(200ms),接收端必须按顺序交付 → 数据块 1 到了但要等数据块 2 → 整体延迟受慢路径制约。这就是 “Head-of-Line Blocking”。好的调度器必须考虑这个问题——要么避免乱序(只走快路径),要么容忍乱序(应用层能处理),要么预测并补偿(冗余发送可能丢的包)。
误区三:实现 MultipathScheduler 很简单,竞赛拿高分轻松
正确理解:实现 trait 确实简单(就几个函数),但设计一个”好”的调度器非常难。你需要理解:网络测量的噪声(RTT 估计可能不准)、拥塞控制的交互(你的调度决策影响 CC 的判断)、公平性(不能让一条路径饿死)、收敛性(策略需要在变化的网络中稳定)。竞赛比的不是代码量,是对问题的理解深度和算法的创新性。
导读目录:README.md 上一章:第 9 章 · 安全与性能:s2n-quic——AWS 的 Rust 工程标杆 下一章:第 11 章 · 六大实现横评——该选谁?