犀牛鸟 2026 研究笔记 estelledc.github.io

第 7 章:社区标杆:quinn 与 quiche——极简 vs 全能

从两种餐厅说起

想象两家餐厅:

quinn 餐厅(精品小店):

quiche 餐厅(五星级酒店):

两家都用 Rust 做菜(写代码),都很好吃(质量高),但设计哲学完全不同:一个追求极简可读,一个追求全能生产级。


quinn:最佳学习入口

基本信息

项目:quinn
GitHub: quinn-rs/quinn
Stars: ~5.2K
语言:Rust
定位:最简洁的 QUIC 实现,适合学习和嵌入
架构:sans-io 核心 + async 运行时集成
代码量:~150 文件
TLS 后端:rustls(纯 Rust TLS)
维护者:社区驱动(主要维护者 djc / Ralith)

三层架构:quinn 的核心设计

quinn 把代码分成了三个独立的 crate(Rust 的库单位),每层职责明确:

┌─────────────────────────────────────────┐
│           quinn (顶层)                    │
│  异步 API + tokio 运行时集成              │
│  用户直接使用的接口                        │
│  Endpoint, Connection, SendStream...     │
├─────────────────────────────────────────┤
│         quinn-proto (协议核心)             │
│  纯逻辑层,无 I/O,无异步                 │
│  状态机 + 包处理 + 拥塞控制               │
│  Connection, Endpoint (内部版本)          │
├─────────────────────────────────────────┤
│         quinn-udp (平台层)                │
│  平台相关的 UDP socket 操作              │
│  GSO/GRO 优化、recvmmsg/sendmmsg        │
└─────────────────────────────────────────┘

为什么分三层?

分层的好处:

1. quinn-proto 是纯逻辑——可以用在任何环境
   - 嵌入式系统?用 quinn-proto + 你自己的 I/O
   - 不想用 tokio?用 quinn-proto + 其他 async 运行时
   - 想做测试?直接驱动 quinn-proto,不需要真实网络

2. quinn(顶层)提供开箱即用体验
   - 大多数用户只需要 `use quinn::*`
   - 自动管理 socket、异步调度、超时

3. quinn-udp 封装平台差异
   - Linux 有 sendmmsg(批量发送),macOS 没有
   - GSO/GRO 加速只在特定内核版本可用
   - 应用层代码不需要关心这些

sans-io 设计模式

quinn-proto 使用”sans-io”模式。这是什么意思?

传统设计(带 I/O):
  fn send_data(socket: &UdpSocket, data: &[u8]) {
      let packet = build_packet(data);
      socket.send(&packet);  // ← 直接做 I/O
  }
  
  问题:
  - 无法测试(需要真实网络)
  - 无法嵌入(绑定了特定的 socket API)
  - 无法在不同运行时之间切换

sans-io 设计(无 I/O):
  fn send_data(state: &mut Connection, data: &[u8]) -> Vec<u8> {
      build_packet(state, data)  // ← 只返回字节,不做 I/O
  }
  
  调用者负责实际的 I/O:
  let packet = proto_conn.send_data(data);
  socket.send(&packet);  // ← I/O 在外面做
  
  好处:
  - 可测试(直接比较输出字节)
  - 可嵌入(调用者决定如何发送)
  - 可在任何运行时使用

quinn-proto 的核心接口:

// quinn-proto 的连接接口(简化)
impl Connection {
    /// 处理收到的 UDP 数据报
    pub fn handle(&mut self, now: Instant, remote: SocketAddr, 
                  data: BytesMut) -> Result<(), ConnectionError> {
        // 解析、解密、处理包
        // 不做任何 I/O!只更新内部状态
    }
    
    /// 获取需要发送的数据(调用者负责实际发送)
    pub fn poll_transmit(&mut self, now: Instant) -> Option<Transmit> {
        // 返回要发送的 UDP 数据报
        // Transmit 包含:目标地址 + 数据
    }
    
    /// 获取下一个超时时间(调用者负责设定时器)
    pub fn poll_timeout(&self) -> Option<Instant> {
        // 返回最早的超时时间
        // 调用者需要在这个时间调用 handle_timeout()
    }
    
    /// 处理超时
    pub fn handle_timeout(&mut self, now: Instant) {
        // 检查哪些超时触发了(PTO、idle timeout 等)
    }
}

这种设计让 quinn-proto 变成了一个”纯状态机”——给它输入(收到的包、时间),它产出输出(要发的包、事件),中间不做任何 I/O。

quinn 的代码结构

quinn/
├── quinn/              ← 顶层 crate(异步 API)
│   ├── src/
│   │   ├── connection.rs   ← 异步 Connection(包装 proto)
│   │   ├── endpoint.rs     ← 异步 Endpoint
│   │   ├── send_stream.rs  ← 发送流
│   │   ├── recv_stream.rs  ← 接收流
│   │   └── ...
│
├── quinn-proto/        ← 协议核心(sans-io)
│   ├── src/
│   │   ├── connection/
│   │   │   ├── mod.rs      ← 连接主逻辑
│   │   │   ├── streams.rs  ← 流管理
│   │   │   ├── spaces.rs   ← 包号空间
│   │   │   └── ...
│   │   ├── endpoint.rs     ← Endpoint 逻辑
│   │   ├── congestion/     ← 拥塞控制
│   │   │   ├── cubic.rs
│   │   │   ├── new_reno.rs
│   │   │   └── ...
│   │   ├── crypto/         ← 加密抽象层
│   │   └── ...
│
├── quinn-udp/          ← UDP socket 封装
│   ├── src/
│   │   ├── unix.rs     ← Linux/macOS 实现
│   │   └── windows.rs  ← Windows 实现
│
└── bench/              ← 性能基准测试

为什么 quinn 是学习 QUIC 的最佳入口

学习友好的原因:

1. 代码量少:~150 文件 vs quiche ~300+ 文件
   → 更容易建立全局认知

2. 纯 Rust:没有 C FFI、没有跨语言调用
   → 跳转定义一路到底,不会突然跳到 C 代码

3. 类型系统即文档:
   - Connection 的状态用 enum 表示
   - 非法状态转换编译时报错
   - 看类型签名就知道这个函数能做什么

4. 测试清晰:
   - 由于 sans-io,测试直接驱动状态机
   - 不需要模拟网络
   - 测试代码就是使用示例

5. 社区活跃:
   - GitHub Discussions 有大量讨论
   - Issue 回复及时
   - 代码注释质量高

quinn 的局限性

不适合的场景:

1. 不支持多路径 QUIC
   - 没有 MultipathScheduler 或 PathMap

2. 拥塞控制算法少
   - 只有 CUBIC 和 NewReno
   - 没有 BBR/BBR3/COPA

3. 没有 C/C++ FFI
   - 纯 Rust 生态,无法被 C 应用调用
   
4. 没有在超大规模生产环境验证
   - 有生产用户但规模远不如 Cloudflare (quiche)

quiche:Cloudflare 的生产利器

基本信息

项目:quiche
GitHub: cloudflare/quiche
Stars: ~11.4K
语言:Rust (核心) + C (FFI)
定位:生产级 QUIC + HTTP/3 实现
架构:sans-io 核心 + 多语言 FFI
代码量:~300+ 文件
TLS 后端:BoringSSL(Google 的 OpenSSL 分支)
维护者:Cloudflare 工程团队

quiche 的 sans-io 与 quinn 的 sans-io 有何不同

两者都是 sans-io,但设计哲学不同:

quinn 的 sans-io:
  目标:让用户可以选择不同的异步运行时
  顶层提供 tokio 集成,但 proto 层是运行时无关的
  面向 Rust 生态

quiche 的 sans-io:
  目标:让 QUIC 库可以被任何语言调用
  核心库不假设任何 I/O 模型
  通过 C FFI 暴露给 C/C++/Python/Go/...
  面向多语言生态

区别:
  quinn: sans-io 为了"Rust 运行时灵活性"
  quiche: sans-io 为了"跨语言 + 跨平台"

quiche 的 API 风格:

// quiche 的使用方式(Rust 侧,简化)
let mut conn = quiche::connect(None, &scid, local, peer, &mut config)?;

// 应用负责 I/O 循环
loop {
    // 1. 从 socket 读取数据
    let (len, from) = socket.recv_from(&mut buf)?;
    
    // 2. 喂给 quiche 处理
    conn.recv(&mut buf[..len], quiche::RecvInfo { from })?;  // ← sans-io!
    
    // 3. 从 quiche 取出要发的数据
    loop {
        let (len, send_info) = match conn.send(&mut out) {  // ← sans-io!
            Ok(v) => v,
            Err(quiche::Error::Done) => break,
            Err(e) => return Err(e),
        };
        socket.send_to(&out[..len], send_info.to)?;
    }
    
    // 4. 处理超时
    conn.on_timeout();
}

注意:quiche 没有像 quinn 那样提供”开箱即用”的异步顶层。你必须自己写 I/O 循环。这是设计选择——给调用者最大控制权。

quiche 的双拥塞控制引擎

quiche 独特的地方:它有两套拥塞控制实现:

┌──────────────────────────────────────────────────┐
│                quiche 拥塞控制                     │
├──────────────────────┬───────────────────────────┤
│    Native Engine     │    Chromium Engine         │
├──────────────────────┼───────────────────────────┤
│ 纯 Rust 实现          │ 从 Chromium 移植的 C++ 代码│
│ CUBIC, NewReno       │ BBR, BBR2                 │
│ Hystart++            │                           │
│                      │                           │
│ 优点:               │ 优点:                    │
│ - 代码清晰           │ - 经过亿级用户验证         │
│ - 易于修改           │ - 行为与Chrome一致         │
│ - 纯Rust依赖         │ - 包含所有边界情况处理     │
│                      │                           │
│ 缺点:               │ 缺点:                    │
│ - 验证不如Chromium多  │ - 代码风格是C++移植        │
│                      │ - 要跟踪Chromium更新       │
│                      │ - 包含Chromium的bug        │
└──────────────────────┴───────────────────────────┘

Cloudflare 在生产环境中使用 Chromium BBR 引擎——因为它已经在 Chrome 上被数十亿用户验证过了。

quiche 的 HTTP/3 支持

quiche 内置了完整的 HTTP/3 实现(quiche::h3 模块),这对 Cloudflare 的 CDN 场景至关重要:

// quiche 的 HTTP/3 使用(简化)
let mut h3_conn = quiche::h3::Connection::with_transport(
    &mut conn, &h3_config
)?;

// 发送 HTTP/3 请求
let headers = vec![
    quiche::h3::Header::new(b":method", b"GET"),
    quiche::h3::Header::new(b":path", b"/index.html"),
    quiche::h3::Header::new(b":authority", b"example.com"),
];
h3_conn.send_request(&mut conn, &headers, true)?;

// 接收响应
loop {
    match h3_conn.poll(&mut conn) {
        Ok((stream_id, quiche::h3::Event::Headers { list, .. })) => {
            // 处理响应头
        }
        Ok((stream_id, quiche::h3::Event::Data)) => {
            // 读取响应体
            h3_conn.recv_body(&mut conn, stream_id, &mut body)?;
        }
        Ok((_, quiche::h3::Event::Finished)) => break,
        Err(quiche::h3::Error::Done) => break,
        _ => {}
    }
}

quiche 的 C FFI:跨语言的桥梁

quiche 提供了完整的 C API,让非 Rust 项目也能使用:

// C 语言使用 quiche(简化)
quiche_config *config = quiche_config_new(QUICHE_PROTOCOL_VERSION);
quiche_config_set_application_protos(config, protos, protos_len);
quiche_config_set_max_idle_timeout(config, 5000);

// 创建连接
quiche_conn *conn = quiche_connect(
    "example.com", scid, scid_len, 
    local, local_len, peer, peer_len, config
);

// 处理收到的数据
quiche_recv_info recv_info = { from, from_len, to, to_len };
ssize_t done = quiche_conn_recv(conn, buf, buf_len, &recv_info);

// 获取要发送的数据
quiche_send_info send_info;
ssize_t written = quiche_conn_send(conn, out, out_len, &send_info);

这使得 quiche 被用在了很多非 Rust 项目中,比如 nginx 的 QUIC 模块(通过 FFI 调用 quiche)和 curl 的 HTTP/3 支持(quiche 作为后端之一)。

quiche 的代码结构

quiche/
├── quiche/              ← 核心库
│   ├── src/
│   │   ├── lib.rs          ← 主入口,Connection 实现
│   │   ├── packet.rs       ← 包解析和构造
│   │   ├── stream.rs       ← 流管理
│   │   ├── ranges.rs       ← ACK Range 管理
│   │   ├── recovery/       ← 丢包恢复
│   │   │   ├── mod.rs
│   │   │   ├── cubic.rs
│   │   │   └── ...
│   │   ├── h3/             ← HTTP/3 实现
│   │   │   ├── mod.rs
│   │   │   ├── frame.rs
│   │   │   └── ...
│   │   ├── ffi.rs          ← C FFI 接口
│   │   └── ...
│   ├── include/
│   │   └── quiche.h        ← C 头文件
│
├── quiche4nginx/        ← nginx 集成
├── apps/                ← 示例应用
│   ├── src/
│   │   ├── client.rs   ← 示例客户端
│   │   └── server.rs   ← 示例服务端
│
└── tools/               ← 工具和测试

quinn vs quiche:详细对比

架构对比

                quinn                          quiche
┌──────────────────────────┐    ┌──────────────────────────┐
│  quinn (async, tokio)    │    │  应用自己的 I/O 循环      │
│  ┌────────────────────┐  │    │  ┌────────────────────┐  │
│  │    quinn-proto      │  │    │  │      quiche        │  │
│  │    (sans-io)        │  │    │  │   (sans-io core)   │  │
│  │  ┌──────────────┐  │  │    │  │  ┌──────────────┐  │  │
│  │  │   rustls     │  │  │    │  │  │  BoringSSL   │  │  │
│  │  └──────────────┘  │  │    │  │  └──────────────┘  │  │
│  └────────────────────┘  │    │  └────────────────────┘  │
│  ┌────────────────────┐  │    │  ┌────────────────────┐  │
│  │    quinn-udp        │  │    │  │    C FFI layer     │  │
│  └────────────────────┘  │    │  └────────────────────┘  │
└──────────────────────────┘    └──────────────────────────┘

功能对比

维度 quinn quiche
crate 数 3 (quinn, proto, udp) 1 (单体)
异步支持 内置 tokio 无(sans-io only)
TLS 库 rustls BoringSSL
HTTP/3 无(用第三方如 h3 crate) 内置
C FFI 完整
拥塞控制 CUBIC, NewReno CUBIC, NewReno, BBR, BBR2
多路径 不支持 不支持
代码风格 惯用 Rust Rust + C 移植风格
学习曲线 中高
生产规模 中等 超大(Cloudflare CDN)
文档质量 优秀 良好

TLS 后端选择的影响

rustls(quinn 使用):
  ✓ 纯 Rust,内存安全保证
  ✓ 不需要 C 编译工具链
  ✓ 更小的二进制体积
  ✗ 不支持某些旧的加密算法
  ✗ 生态成熟度略低于 OpenSSL

BoringSSL(quiche 使用):
  ✓ Google 维护,经过 Chrome 验证
  ✓ 支持所有 TLS 特性
  ✓ 与 Chrome 行为一致
  ✗ 需要 C/C++ 编译器
  ✗ 编译慢(需要编译整个 BoringSSL)
  ✗ 跨平台编译更复杂

选择影响:
  如果你的项目是纯 Rust → quinn + rustls
  如果需要与 C/C++ 互操作 → quiche + BoringSSL
  如果需要在 nginx/curl 等 C 项目中集成 → quiche

性能对比

基准测试结果(典型场景):

批量传输(goodput):
  quinn:  ~8 Gbps(理想条件)
  quiche: ~10 Gbps(理想条件)
  差异原因:quiche 的 BoringSSL 有 AES-NI 硬件加速
            quinn 的 rustls 也支持但优化程度稍低

连接建立速率:
  quinn:  ~50K connections/sec
  quiche: ~60K connections/sec
  差异原因:quiche 的 sans-io 设计零分配更彻底

内存占用(每连接):
  quinn:  ~20-30 KB
  quiche: ~15-25 KB
  
注意:这些数字高度依赖配置和测试环境
      实际差异对大多数应用不显著

sans-io 模式的深入理解

sans-io 是 quinn 和 quiche 共同采用的核心架构模式。让我们更深入地理解它为什么重要:

sans-io 的测试优势

// quinn-proto 的测试示例(简化)
#[test]
fn test_handshake() {
    let mut client = Connection::new_client(...);
    let mut server = Connection::new_server(...);
    
    // 不需要网络!直接传递字节
    let client_initial = client.poll_transmit(now).unwrap();
    
    // 把客户端的包"喂给"服务端
    server.handle(now, client_addr, client_initial.data.into());
    
    let server_response = server.poll_transmit(now).unwrap();
    
    // 把服务端的包"喂给"客户端
    client.handle(now, server_addr, server_response.data.into());
    
    // 验证握手完成
    assert!(client.is_established());
    assert!(server.is_established());
}

没有 socket、没有网络、没有异步——就是纯函数调用。这让测试变得极其简单和可靠。

sans-io 的嵌入优势

场景:在嵌入式设备上运行 QUIC

没有 tokio?没有 epoll?只有一个裸金属板子?
  
用 quinn-proto:
  1. 你自己写收发 UDP 的代码(maybe 直接操作网卡寄存器)
  2. 你自己写时钟(maybe 用硬件定时器中断)
  3. quinn-proto 不关心这些——它只处理字节和时间

伪代码:
  loop {
      // 你的 I/O
      let packet = read_from_nic_register();
      
      // quinn-proto 处理
      proto_conn.handle(hardware_timer(), remote_addr, packet);
      
      // 取出要发的
      if let Some(transmit) = proto_conn.poll_transmit(hardware_timer()) {
          write_to_nic_register(transmit.destination, transmit.data);
      }
      
      // 检查超时
      if let Some(timeout) = proto_conn.poll_timeout() {
          set_hardware_timer(timeout);
      }
  }

sans-io 的模拟测试

// 可以模拟各种网络条件
struct SimulatedNetwork {
    loss_rate: f64,
    delay: Duration,
    jitter: Duration,
}

impl SimulatedNetwork {
    fn transfer(&self, packet: &[u8]) -> Option<(Vec<u8>, Duration)> {
        // 模拟丢包
        if random::<f64>() < self.loss_rate {
            return None;  // 丢了
        }
        // 模拟延迟 + 抖动
        let actual_delay = self.delay + random_jitter(self.jitter);
        Some((packet.to_vec(), actual_delay))
    }
}

// 用模拟网络测试 QUIC 行为
fn test_congestion_under_loss() {
    let network = SimulatedNetwork {
        loss_rate: 0.05,  // 5% 丢包
        delay: Duration::from_millis(50),
        jitter: Duration::from_millis(10),
    };
    // 驱动 client/server 通过模拟网络通信
    // 验证拥塞控制行为
}

学习建议:如何阅读 quinn 和 quiche 的代码

阅读 quinn 的路径

推荐顺序:

第1步:quinn/examples/
  → 看一个最简单的 client + server 怎么用
  → 建立"用户视角"

第2步:quinn/src/connection.rs
  → 看异步 Connection 如何包装 quinn-proto
  → 理解"胶水层"的角色

第3步:quinn-proto/src/endpoint.rs
  → 看 Endpoint 如何管理多个连接
  → 理解"sans-io 怎么被驱动"

第4步:quinn-proto/src/connection/mod.rs
  → 核心!连接状态机的主逻辑
  → handle() 和 poll_transmit() 的实现

第5步:quinn-proto/src/congestion/
  → 看拥塞控制的 trait 定义和 CUBIC 实现
  → 理解"可插拔"设计

关键文件清单(按重要性排序):
  1. quinn-proto/src/connection/mod.rs  (连接核心)
  2. quinn-proto/src/endpoint.rs        (端点管理)
  3. quinn-proto/src/connection/streams.rs (流管理)
  4. quinn-proto/src/congestion/mod.rs  (拥塞控制接口)
  5. quinn/src/connection.rs            (异步封装)

阅读 quiche 的路径

推荐顺序:

第1步:quiche/apps/src/client.rs
  → 示例客户端的 I/O 循环
  → 理解 quiche 的 API 风格

第2步:quiche/src/lib.rs(重点前 1000 行)
  → Connection 结构体定义
  → recv() 和 send() 的核心逻辑

第3步:quiche/src/recovery/mod.rs
  → 丢包恢复的主逻辑
  → 拥塞控制的调用入口

第4步:quiche/src/h3/mod.rs
  → HTTP/3 层如何建立在 QUIC 之上
  → 理解"分层"的实际效果

第5步:quiche/src/ffi.rs
  → C FFI 是怎么包装 Rust API 的
  → 理解跨语言设计

注意:quiche 的核心逻辑大部分在 lib.rs 一个文件里
      文件很大(5000+ 行),但结构清晰(按功能分区)

代码风格对比

错误处理

// quinn-proto: 使用 Rust 的 Result + 自定义错误类型
pub fn handle_initial(
    &mut self, packet: Packet
) -> Result<(), TransportError> {
    let payload = self.decrypt_initial(&packet)
        .map_err(|_| TransportError::PROTOCOL_VIOLATION)?;
    // ...
}

// quiche: 使用 Error 枚举 + C 风格的返回码(FFI 友好)
pub fn recv(&mut self, buf: &mut [u8], info: RecvInfo) -> Result<usize> {
    // 内部使用 Result
    let len = self.do_recv(buf, info)?;
    Ok(len)
}
// FFI 层转换为 C 错误码
#[no_mangle]
pub extern fn quiche_conn_recv(...) -> ssize_t {
    match conn.recv(buf, info) {
        Ok(v) => v as ssize_t,
        Err(e) => e.to_c() as ssize_t,  // 负数表示错误
    }
}

配置方式

// quinn: Builder 模式 + 类型安全
let server_config = ServerConfig::with_crypto(Arc::new(crypto_config));
let endpoint = Endpoint::server(server_config, addr)?;

// 传输参数通过 TransportConfig
let mut transport = TransportConfig::default();
transport.max_idle_timeout(Some(Duration::from_secs(30).try_into()?));

// quiche: 函数调用配置(C FFI 友好)
let mut config = quiche::Config::new(quiche::PROTOCOL_VERSION)?;
config.set_application_protos(&[b"h3"])?;
config.set_max_idle_timeout(30_000);
config.set_initial_max_data(10_000_000);
config.set_cc_algorithm(quiche::CongestionControlAlgorithm::BBR);

本章小结

维度 quinn quiche
定位 学习入口 + Rust 嵌入 生产部署 + 多语言集成
架构 3-crate 分层 单体 + FFI
sans-io 目的 运行时灵活性 跨语言可用性
TLS rustls(纯 Rust) BoringSSL(C)
HTTP/3 无(第三方) 内置
适合谁 Rust 开发者、学习者 需要生产级 QUIC 的团队
社区 社区驱动 企业(Cloudflare)驱动

一句话总结:quinn 是你学习 QUIC 源码的最佳入口——代码少、设计清晰、类型系统即文档。quiche 是你部署生产 QUIC 的首选之一——功能全面、经过 Cloudflare CDN 规模验证、多语言可用。两者都采用 sans-io 架构,但目标受众不同。


读完本章你能做什么

  1. 解释 sans-io 设计模式的三个好处(可测试、可嵌入、运行时无关)
  2. 画出 quinn 的三层架构图,标注每层职责
  3. 说出 quiche 双拥塞控制引擎(Native + Chromium)的设计原因
  4. 比较 rustls 和 BoringSSL 的优劣
  5. 给出”选 quinn 还是 quiche”的判断依据
  6. 描述阅读 quinn 源码的推荐路径

常见误区

误区一:sans-io 意味着不能做网络 I/O

正确理解:sans-io 的意思是”核心库不做 I/O”,不是”整个系统不做 I/O”。I/O 由调用者(或上层包装层)负责。quinn 的顶层 crate 就是一个做 I/O 的包装层——用 tokio 来收发 UDP 包,然后驱动 quinn-proto 这个 sans-io 核心。

误区二:quiche 没有异步支持所以不能用在异步程序中

正确理解:quiche 的 API 是同步的(recv/send 不是 async),但你完全可以在异步程序中使用它。只需要把 quiche 的 recv/send 包装到你的 async I/O 循环中。事实上 Cloudflare 的生产系统就是这么做的——他们有自己的异步框架包装 quiche。

误区三:quinn 和 quiche 是竞争对手

正确理解:它们面向不同的使用场景。quinn 更像一个”Rust 生态的 QUIC 库”——纯 Rust、类型安全、tokio 集成。quiche 更像一个”通用 QUIC 引擎”——C FFI、多语言、生产级。很多人在学习时用 quinn,部署时用 quiche(或者反过来:如果你的项目全是 Rust,quinn 也是很好的生产选择)。


导读目录:README.md 上一章:第 6 章 · 多路径 QUIC——同时走两条路 下一章:第 8 章 · C 语言双雄:msquic 与 ngtcp2——内核级 vs 万能适配