WebTransport 技术白皮书:从原理到多端实现

6 阅读19分钟

WebTransport 技术白皮书:从原理到多端实现

目录

  1. 一、执行摘要
  2. 二、背景与挑战:为何需要 WebTransport
  3. 三、技术概览:WebTransport 是什么
  4. 四、架构与数据模型
  5. 五、各端开发与实现指南
  6. 六、安全、部署与运维
  7. 七、实践案例与性能基准
  8. 八、迁移与实施路线图
  9. 九、未来展望与标准演进
  10. 附录 A:术语表
  11. 附录 B:完整代码示例
  12. 免责声明

一、执行摘要

1.1 核心观点

WebTransport 是构建于 HTTP/3QUIC 之上的现代 Web API,为客户端与服务器间提供低延迟、高吞吐、多路复用的实时通信能力。它同时支持可靠流不可靠数据报,在客户端-服务器架构下可替代或补充 WebSocket,是云游戏、互动直播、实时协作等场景的优选方案。

1.2 核心价值主张

价值说明
更低延迟QUIC 1-RTT/0-RTT 建连,无 TCP/HTTP/2 队头阻塞
灵活传输同一连接上兼得可靠流(聊天、文件)与不可靠数据报(游戏、传感器)
易集成浏览器原生 API,与现有 Web 安全模型一致,无需 P2P 打洞
可演进标准化进行中,Chromium/Firefox 已支持,可配合 WebSocket 降级

1.3 白皮书目的

本白皮书面向技术决策者各端开发团队,旨在:说明为何需要 WebTransport;厘清其技术定位、协议栈与数据模型;给出 Web/服务端/移动端的实现要点与完整示例代码;并涵盖安全、部署、迁移与未来演进,便于评估采用与落地实施。


二、背景与挑战:为何需要 WebTransport

2.1 现有 Web 实时通信方案的瓶颈

2.1.1 WebSocket:TCP 的局限
  • 队头阻塞 (Head-of-Line Blocking):基于 TCP,单包丢失会阻塞该连接上所有后续数据,多路复用场景下影响大。
  TCP/WebSocket:单连接单管道,一包丢失整条阻塞
  ───────────────────────────────────────────────────────────────>
  发送端    [P1][P2][P3][P4][P5]...    接收端
              │   ✗ 丢失
              ▼
  后续 P3 P4 P5 均需等待 P2 重传到达后才能被应用层读取(队头阻塞)

  QUIC/WebTransport:多流独立,一流丢包不影响其他流
  ───────────────────────────────────────────────────────────────>
  流 A     [A1][A2][A3]...    →  A2 丢失只阻塞流 AB     [B1][B2][B3]...    →  流 B 正常交付
  流 C     [C1][C2][C3]...    →  流 C 正常交付
  • 连接建立开销大:WSS 通常需 3-RTT(TCP + TLS + HTTP 升级),移动网络下延迟明显。
  • 连接迁移困难:连接由四元组标识,网络切换(如 Wi-Fi → 4G)导致 IP 变化时连接易中断。
2.1.2 WebRTC DataChannel:P2P 的复杂性
  • 架构复杂:面向 P2P,需 ICE、STUN、TURN 等穿透 NAT,部署与运维成本高。
  • 定位不符:核心是浏览器间直连,对常规客户端-服务器架构属于过度设计。

2.2 新兴应用场景的需求

  • 云游戏与互动直播:端到端延迟需低于约 100ms,且可容忍少量丢包,TCP 难以满足。
  • 大规模实时协作:需同时传输可靠文档数据与不可靠的鼠标/操作指令。
  • 物联网 (IoT):弱网与频繁换网下需要低开销、可迁移的连接。

2.3 结论:市场呼唤新协议

市场需要一种基于 UDP原生多路复用、支持可靠流与不可靠数据报、且易于在客户端-服务器架构中使用的统一传输方案。WebTransport 正是为此而生的标准技术。


三、技术概览:WebTransport 是什么

3.1 定义与定位

WebTransport 是基于 HTTP/3QUIC 的 Web API,为客户端与服务器提供双向、多路复用的通信能力。它不是一个全新协议,而是一套在 QUIC 连接之上承载可靠流不可靠数据报的通用语义与浏览器 API。

3.2 核心协议栈

协议作用
QUIC (RFC 9000)基于 UDP 的传输层:多路复用、1-RTT/0-RTT、连接迁移、拥塞控制与丢包重传
HTTP/3 (RFC 9114)基于 QUIC 的应用层:通过 CONNECT 建立 WebTransport 会话
TLS 1.3 (RFC 9001)内置于 QUIC,加密与传输一体

3.3 核心概念与特性

  • WebTransport Session:通过 HTTP/3 CONNECT 建立的持久、安全会话,是所有流与数据报的容器。
  • 可靠流 (Reliable Streams):有序、不丢包,适用于聊天、文件、RPC。
  • 不可靠数据报 (Unreliable Datagrams):尽力而为、低延迟,适用于实时状态、游戏、传感器。
  • 多路复用:单连接上并发多条流与数据报,流间无队头阻塞。
  • 连接迁移:通过 Connection ID 标识连接,网络切换时可持续使用。

3.4 Session 生命周期状态图

                    new WebTransport(url)
                            │
                            ▼
              ┌──────────────────────────┐
              │      connecting          │
              │  (QUIC 建连 + CONNECT)   │
              └─────────────┬────────────┘
                            │ transport.ready 兑现
                            ▼
              ┌──────────────────────────┐
              │      connected           │
              │  可创建流、收发数据报     │
              └─────────────┬────────────┘
                            │ close() 或 异常/网络断开
                            ▼
              ┌──────────────────────────┐
              │       closed             │
              │  transport.closed 兑现   │
              └──────────────────────────┘

四、架构与数据模型

4.1 系统架构示意

┌──────────────────────────────────────────────────────────────────┐
│  客户端:Web (浏览器) / Android (Cronet) / iOS (QUIC 库)          │
└─────────────────────────────┬────────────────────────────────────┘
                              │  WebTransport API / 各端 SDK
                              │  (CONNECT → 流 / 数据报)
┌─────────────────────────────▼────────────────────────────────────┐
│  网络:公网 / CDN·边缘节点 (UDP 443, QUIC)                        │
└─────────────────────────────┬────────────────────────────────────┘
                              │
┌─────────────────────────────▼────────────────────────────────────┐
│  服务端:WebTransport 网关 (Node / .NET / C++ / Go)              │
│         业务逻辑层、消息路由、媒体中转                             │
└──────────────────────────────────────────────────────────────────┘

4.2 从调用到 QUIC 包:协议栈与数据流

┌─────────────────────────────────────────────────────────────┐
│  应用层 (JS)  new WebTransport(url) → ready / Streams       │
└───────────────────────────┬─────────────────────────────────┘
                            │
┌───────────────────────────▼─────────────────────────────────┐
│  WebTransport API (浏览器 C++)  Session / CONNECT / 流映射    │
└───────────────────────────┬─────────────────────────────────┘
                            │
┌───────────────────────────▼─────────────────────────────────┐
│  HTTP/3  CONNECT 隧道;流 ID 与 DATA 帧                      │
└───────────────────────────┬─────────────────────────────────┘
                            │
┌───────────────────────────▼─────────────────────────────────┐
│  QUIC  流多路复用、拥塞控制、Connection ID                   │
└───────────────────────────┬─────────────────────────────────┘
                            │
┌───────────────────────────▼─────────────────────────────────┐
│  TLS 1.3 (内置于 QUIC)  加密握手与数据                        │
└───────────────────────────┬─────────────────────────────────┘
                            │
┌───────────────────────────▼─────────────────────────────────┐
│  UDP  →  服务器 UDP 443                                      │
└─────────────────────────────────────────────────────────────┘

4.2.1 WebTransport 会话建立时序图

  浏览器 (JS)                WebTransport/HTTP/3                服务器
       │                            │                                │
       │  new WebTransport(url)     │                                │
       │───────────────────────────>│                                │
       │                            │  建立 QUIC 连接 (1-RTT)        │
       │                            │  TLS 1.3 握手                  │
       │                            │──────────────────────────────>│
       │                            │<──────────────────────────────│
       │                            │                                │
       │                            │  HTTP/3 CONNECT /path          │
       │                            │  (建立 WebTransport 隧道)      │
       │                            │──────────────────────────────>│
       │                            │  200 OK / 隧道就绪              │
       │                            │<──────────────────────────────│
       │                            │                                │
       │  transport.ready 兑现     │                                │
       │<───────────────────────────│                                │
       │                            │                                │
       │  后续:createStream() /    │  QUIC Stream / DATAGRAM        │
       │  datagrams.write()         │<══════════════════════════════>│
       │                            │                                │

4.3 数据模型与 API 抽象

Session 生命周期
  • connectingnew WebTransport() 后、ready 未兑现前。
  • connectedawait transport.ready 成功,可创建流、读写数据报。
  • closedclose() 或异常后,transport.closed 兑现或 reject。
流 (Streams) 模型
  • 单向流createUnidirectionalStream()(客户端→服务端)、incomingUnidirectionalStreams(服务端→客户端)。
  • 双向流createBidirectionalStream()incomingBidirectionalStreams;可为流设置优先级以提示底层调度。
  • 使用标准 ReadableStream / WritableStream 读写。
数据报 (Datagrams) 模型
  • transport.datagrams.readable / transport.datagrams.writable,读写 Uint8Array
  • 只读属性 maxDatagramSize 表示当前会话允许的最大发出数据报字节数,超出可能失败或截断。
构造选项(部分浏览器支持)
  • congestionControl:如 'throughput''low-latency'
  • serverCertificateHashes:自签名证书场景下用哈希校验证书。

4.4 与 WebSocket / WebRTC 的对比矩阵

特性WebSocketWebTransportWebRTC DataChannel
底层协议TCPQUIC (UDP)SCTP over DTLS (UDP)
连接模型客户端-服务器客户端-服务器点对点 (P2P)
传输可靠性仅可靠可靠流 + 不可靠数据报可配置
多路复用
队头阻塞是 (TCP)
连接建立较慢 (3-RTT)快 (0–1 RTT)复杂 (信令)
NAT 穿透不适用不适用内置
浏览器支持通用良好(非全量)通用

4.5 直播示例:数据流与时序

下图表示附录 B 中“WebTransport 直播平台”的媒体流消息流路径,以及典型交互时序。

数据流总览:

  主播端                    服务器                      观众端 A / B / …
  ┌─────────┐              ┌─────────┐                 ┌─────────┐
  │ 摄像头   │              │         │                 │ <video> │
  │ 采集帧   │── Datagram ──>│ 路由/   │── Datagram ───>│ 显示    │
  └─────────┘   (视频帧)     │ 转发   │   (视频帧)      └─────────┘
       │                    │         │
       │ join/start_stream  │         │  system/chat
       │ chat               │         │  (单向流)
       └──── Stream ────────>│         │<──── Stream ──────┴─────────
                             │         │
                             └─────────┘
  说明:消息与信令走可靠流 (Stream),视频帧走不可靠数据报 (Datagram)。

消息交互时序(加入 + 聊天):

  观众 A              服务器                观众 B / 主播
     │                   │                        │
     │  join {name}      │                        │
     │── Stream ────────>│                        │
     │                   │  system "A 加入"       │
     │  system "A 加入"   │── Stream ─────────────>│
     │<── Stream ────────│                        │
     │                   │                        │
     │                   │  chat {from, text}    │
     │  chat             │<── Stream ────────────│  (B 发言)
     │<── Stream ────────│── Stream ─────────────>│
     │                   │                        │

视频帧推送时序(主播 → 观众):

  主播                  服务器                  观众
    │                      │                      │
    │  捕获帧 (ImageCapture)│                      │
    │  编码 JPEG           │                      │
    │  Datagram (帧数据)   │                      │
    │─────────────────────>│                      │
    │                      │  Datagram (帧数据)   │
    │                      │─────────────────────>│
    │                      │                      │  Blob → <video>.src
    │  (持续推送,无确认)   │  (持续转发,尽力而为) │

五、各端开发与实现指南

5.1 Web 端 (Browser)

  • 建立连接const transport = new WebTransport(url [, options]); await transport.ready;
  • 不可靠数据报transport.datagrams.writable.getWriter().write(...);注意 maxDatagramSize
  • 可靠流createUnidirectionalStream() / createBidirectionalStream();接收端用 incomingUnidirectionalStreams.getReader() 等。
  • 错误与监控WebTransportErrorsourcestreamErrorCode);getStats()(实验性)获取 RTT、丢包等。
  • 降级typeof WebTransport !== 'undefined' 检测,不支持时回退 WebSocket。

完整 Web 端示例(含登录、直播、聊天)见 附录 B

5.1.1 Web API 速查表

API / 属性说明
new WebTransport(url [, options])构造器;options.congestionControloptions.serverCertificateHashes
transport.readyPromise,会话就绪后兑现
transport.closedPromise,会话关闭后兑现或 reject
transport.datagrams.readableReadableStream,接收数据报
transport.datagrams.writableWritableStream,发送数据报
transport.datagrams.maxDatagramSize只读,当前允许的最大发出数据报字节数
transport.createUnidirectionalStream()创建客户端→服务端单向流
transport.createBidirectionalStream()创建双向流
transport.incomingUnidirectionalStreamsReadableStream of 流,接收服务端→客户端单向流
transport.incomingBidirectionalStreamsReadableStream of 流,接收服务端发起的双向流
transport.getStats()实验性,返回连接统计(如 RTT、丢包)
transport.close([closeInfo])关闭会话

5.2 服务端 (Server-Side)

通用要求:支持 HTTP/3 与 QUIC、强制 TLS 1.3、开放 UDP 443。

技术栈说明
Node.js无内置支持;可用 @fails-components/webtransport 或 Socket.IO 4.7+ 等
Goquic-go 等库可实现 HTTP/3 与 WebTransport,适合快速落地
.NET 10 + SignalRKestrel 启用 HTTP/3 后,SignalR 可协商 WebTransport
C++基于 mvfst+proxygen 或 QUICHE 自行实现会话与流管理,无现成 WebTransport 库;浏览器与 Cronet 底层均为 C++ 实现

完整 Node.js 服务端示例(会话管理、消息类型、广播、视频帧转发)见 附录 B

5.3 移动端 (Mobile)

平台建议
Android使用 Cronetorg.chromium.net:cronet)的 WebTransport 能力;或 C++ 核心 + JNI
iOS无官方 API;可基于 lsquic/ngtcp2 等 C++ 库实现后通过 ObjC++ 暴露,或使用社区 Swift 库

完整 Android (Kotlin) 示例见 附录 B


六、安全、部署与运维

6.1 网络安全

  • 加密:全部经 TLS 1.3,与 QUIC 握手一体。
  • 证书:生产环境使用受信任 CA 签发;自签名仅限内网/开发,并配合 serverCertificateHashes 或用户信任。
  • 防护:防范 UDP 放大攻击、连接耗尽;实施速率限制、连接数限制与超时。

6.2 部署拓扑

  直连模式:                        经 CDN/边缘模式:
  ┌────────┐      UDP 443      ┌────────┐    ┌────────┐   ┌────────┐
  │ 客户端  │◄─────────────────►│  WT    │    │ 客户端  │──►│ 边缘   │──►│ 源站   │
  │ 多端   │   QUIC/HTTP/3    │ 网关   │    │ 多端   │   │ WT     │   │ WT     │
  └────────┘                   └────────┘    └────────┘   │ 网关   │   │ 网关   │
      延迟最低、控制力强             └────────┘   └────────┘
                                           RTT 更小、扩展性更好

6.3 可观测性

  • 指标:建连成功率、RTT、丢包率、流创建/关闭速率、数据报收发量。
  • 工具:qlog、Wireshark (QUIC)、客户端 getStats()

6.4 常见坑与注意事项

问题说明与建议
Safari 不支持稳定版 Safari 目前不可依赖,必须做特性检测并降级 WebSocket。
自签名证书浏览器会拦截;需用户手动信任或使用 serverCertificateHashes(若支持)。
UDP 被封锁部分企业网络仅放行 TCP,QUIC 建连会失败,需评估并提供 TCP 降级。
Datagram 大小单次写入勿超过 transport.datagrams.maxDatagramSize
Writer 锁使用 getWriter() 后需 releaseLock()close(),否则同一 stream 无法再被其他 writer 使用。
标准仍在演进W3C/IETF 规范会更新,API 与行为可能随浏览器版本变化。

七、实践案例与性能基准

7.1 典型应用场景

  • 低延迟互动直播:视频帧经 Datagram 传输,聊天与信令经 Stream 传输。
  • 实时多人在线游戏:玩家状态用 Datagram 同步,游戏指令用 Stream。
  • 高频金融行情:可靠流保证有序、不丢。

7.2 性能对比(定性)

  • 建连时间:WebTransport (0–1 RTT) 显著快于 WebSocket (3-RTT)。
  • 端到端延迟:高丢包网络下,WebTransport 延迟波动通常小于 WebSocket。
  • 吞吐:可靠模式下 WebTransport 多路复用无队头阻塞,通常优于单路 WebSocket。

7.3 建连 RTT 对比示意

  WebSocket (WSS) 典型建连:
  ────────────────────────────────────────────────────────────────>
  客户端                    网络                    服务端
     │  TCP SYN             │                        │
     │─────────────────────>│                        │   RTT 1
     │  TCP SYN-ACK         │<─────────────────────│
     │<─────────────────────│                        │
     │  TLS ClientHello     │                        │
     │─────────────────────>│                        │   RTT 2
     │  TLS ServerHello+... │<─────────────────────│
     │<─────────────────────│                        │
     │  TLS Finished + HTTP Upgrade                  │
     │─────────────────────>│                        │   RTT 3101 Switching       │<─────────────────────│
     │<─────────────────────│                        │
  合计:约 3-RTT 后才能收发应用数据


  WebTransport (QUIC) 典型建连:
  ────────────────────────────────────────────────────────────────>
  客户端                    网络                    服务端
     │  QUIC Initial (含 TLS ClientHello 等)         │
     │─────────────────────>│                        │   RTT 1
     │  QUIC Handshake (含 TLS ServerHello + 加密)  │
     │<─────────────────────│<─────────────────────│
     │  CONNECT + 应用数据 (可携带)                  │
     │─────────────────────>│─────────────────────>│
  合计:1-RTT 后可发应用数据;0-RTT 恢复时首包即可带数据
对比项WebSocket (WSS)WebTransport
首次建连约 3-RTT约 1-RTT
会话恢复通常重新握手支持 0-RTT 恢复
单包丢失影响可能阻塞整连接仅影响该流,其他流不受阻

八、迁移与实施路线图

8.1 新项目采用策略

建议将 WebTransport 作为默认实时传输方案,并设计 WebSocket 降级路径与特性检测。

8.2 存量项目迁移策略

  1. 双协议共存:新功能优先 WebTransport,旧功能保留 WebSocket。
  2. 功能迁移:逐步将核心链路迁至 WebTransport。
  3. 全量切换:生态与监控就绪后,再考虑下线 WebSocket。

8.3 风险评估与应对

  • 浏览器兼容:明确支持矩阵(Chrome/Edge 97+、Firefox 114+、Safari 暂不可依赖),做好降级与提示。
  • 服务端复杂度:评估 QUIC/HTTP/3 带来的运维与技能成本;可先用 Node/Go 等成熟库降低门槛。

九、未来展望与标准演进

9.1 浏览器支持趋势

  • Chromium (Chrome/Edge):已稳定支持。
  • Firefox:114+ 默认启用。
  • Safari:截至当前仍不支持或默认关闭,需持续关注并做降级。

9.2 标准化进程

  • W3C:WebTransport 为 Working Draft,API 与语义仍在完善。
  • IETF:RFC 9000/9001/9114 已定稿;WebTransport 在 HTTP/3 上的会话与帧语义由 WEBTRANS 相关草案定义。

9.3 规范与参考资料

协议与标准
文档链接说明
W3C WebTransportTR/webtransport浏览器 API 与语义(Working Draft)
RFC 9000rfc9000QUIC:传输层协议
RFC 9001rfc9001QUIC 的 TLS 1.3 使用
RFC 9114rfc9114HTTP/3(基于 QUIC)
IETF WEBTRANSdraft-ietf-webtrans-http3WebTransport over HTTP/3(草案)
浏览器与运行环境
来源链接说明
MDNWebTransport APIAPI 说明与示例
MDNmaxDatagramSize数据报大小限制
ChromeWebTransport 能力说明Chrome 官方能力与示例
Can I useWebTransport各浏览器/版本支持情况
开源项目与仓库简介(扩展阅读)
技术栈GitHub 地址仓库简介
Node.jsgithub.com/fails-compo…基于 Node.js 的 WebTransport 服务端实现,依赖 HTTP/2 等,可与浏览器 WebTransport API 对接;适用于快速搭建演示或轻量网关。
Gogithub.com/quic-go/qui…纯 Go 实现的 QUIC 与 HTTP/3,支持 IETF 标准;可在此基础上实现 WebTransport 服务端/客户端,适合生产级高并发场景。
C++ (Google)github.com/google/quic…Chromium 使用的 QUIC/HTTP/3 库,被 Chrome 等采用;需自行实现 WebTransport 会话与流管理,适合对性能与可控性要求高的服务端。
C++ (Meta)github.com/facebook/mv…Meta 开源的 QUIC 实现;常与 proxygen 配合使用以支持 HTTP/3,在此基础上可开发 WebTransport 服务端。

上述仓库的许可证与 API 以各仓库首页为准;WebTransport 层多为“在 QUIC/HTTP/3 上自实现”,仅 Node 库提供开箱即用的 WebTransport 服务端抽象。

客户端与前端示例(扩展阅读)
类型GitHub 地址仓库简介
前端 / 浏览器GoogleChrome/samples (webtransport)Chrome 官方示例:含 client.html / client.js 网页客户端与 Python 示例服务端,可连接任意 WebTransport 服务、测试数据报与流;在线演示
前端 / 规范与示例w3c/webtransportW3C 规范仓库,内含 Echo、WebCodecs Echo 等示例实现,便于对照标准做客户端或服务端试验。
Rust 客户端BiagioFesta/wtransport异步 Rust WebTransport 库(crates.io: wtransport),支持流与数据报,基于 QUIC/HTTP/3,适合原生或服务端客户端。
Rust 客户端 + WASMkixelated/web-transport-rsRust WebTransport 库,支持 native 与 WASM,可在浏览器外或编译为 WASM 在 Web 中使用。
Python 客户端/服务端aiortc/aioquic纯 Python QUIC 与 HTTP/3 实现,支持 WebTransport over HTTP/3(如 H3Connection 等),自带 http3_server / http3_client 示例,适合脚本或工具链集成。

Android 端可依赖 Cronet(Chromium 网络库)的 WebTransport 能力,见正文 5.3 与附录 B;无单独“示例仓库”时可直接参考 Chromium 或官方文档。


附录 A:术语表与消息类型速查

A.1 术语表

术语说明
QUIC基于 UDP 的传输层协议,具备多路复用、快速建连、连接迁移等能力
HTTP/3基于 QUIC 的 HTTP 版本,使用 CONNECT 建立 WebTransport 隧道
Session一次 WebTransport 会话,由 HTTP/3 CONNECT 建立,包含流与数据报
可靠流有序、不丢包的字节流,可单向或双向
数据报 (Datagram)不可靠、不保证顺序的 UDP 风格报文,延迟最低
队头阻塞同一连接上因一个包丢失导致后续数据被阻塞的现象
0-RTT / 1-RTT会话恢复时 0 次额外往返、新连接时 1 次往返即可开始传输

A.2 附录 B 直播示例中的消息类型

类型 (type)方向载荷示例说明
join客户端 → 服务器{ type: 'join', name: '昵称' }加入直播间,登记昵称
start_stream客户端 → 服务器{ type: 'start_stream' }主播请求开始推流
stop_stream客户端 → 服务器{ type: 'stop_stream' }主播请求停止推流
chat客户端 → 服务器{ type: 'chat', text: '内容' }发送聊天消息
system服务器 → 客户端{ type: 'system', text: '...' }系统广播(如“某用户加入”)
chat服务器 → 客户端{ type: 'chat', from: '昵称', text: '...' }广播他人聊天内容
error服务器 → 客户端{ type: 'error', text: '...' }错误提示(如“已有主播在直播”)

消息在示例中经 Stream(可靠)或 WebSocket 文本帧传输;视频帧为二进制,经 Datagram(不可靠)发送。


附录 B:完整代码示例

以下为“WebTransport 直播平台”的简化实现:消息与信令走可靠流,视频帧走不可靠数据报,并带 WebSocket 降级。证书请置于项目根目录或 ./certs/(与代码中配置一致)。

B.1 服务端 (Node.js)

项目初始化:

mkdir webtransport-live-server && cd webtransport-live-server
npm init -y
npm install @fails-components/webtransport ws uuid

证书(若使用 ./certs/ 目录):

mkdir -p certs && cd certs
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes

server.js

// server.js
const { createWebTransportServer } = require('@fails-components/webtransport');
const { WebSocketServer } = require('ws');
const { v4: uuidv4 } = require('uuid');
const http2 = require('http2');
const fs = require('fs');

const CERT_PATH = './certs/cert.pem';
const KEY_PATH = './certs/key.pem';
const PORT = 4433;

if (!fs.existsSync(CERT_PATH) || !fs.existsSync(KEY_PATH)) {
  console.error("请先创建证书: openssl req -x509 -newkey rsa:4096 -keyout certs/key.pem -out certs/cert.pem -days 365 -nodes");
  process.exit(1);
}

const clients = new Map();
let activeStreamer = null;

const http2Server = http2.createSecureServer({
  key: fs.readFileSync(KEY_PATH),
  cert: fs.readFileSync(CERT_PATH),
});

const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws) => {
  const clientId = uuidv4();
  console.log(`[WS] 客户端 ${clientId} 已连接`);
  clients.set(clientId, { type: 'websocket', connection: ws });
  ws.on('message', (data) => handleMessage(clientId, data.toString()));
  ws.on('close', () => {
    console.log(`[WS] 客户端 ${clientId} 已断开`);
    clients.delete(clientId);
  });
});
console.log('WebSocket 降级服务运行在 ws://localhost:8080');

const wtServer = createWebTransportServer({ server: http2Server });

wtServer.on('session', async (session) => {
  const sessionId = uuidv4();
  console.log(`[WT] 新会话: ${sessionId}`);
  const sessionStreams = new Set();
  try {
    await session.ready;
    console.log(`[WT] 会话 ${sessionId} 已就绪`);
    clients.set(sessionId, { type: 'webtransport', connection: session, sessionId, id: sessionId, name: '', isStreamer: false });
    const reader = session.incomingUnidirectionalStreams.getReader();
    while (true) {
      const { done, value: stream } = await reader.read();
      if (done) break;
      sessionStreams.add(stream);
      handleIncomingStream(sessionId, stream);
    }
  } catch (err) {
    console.error(`[WT] 会话 ${sessionId} 错误:`, err);
  } finally {
    sessionStreams.forEach(s => s.cancel());
    clients.delete(sessionId);
    console.log(`[WT] 会话 ${sessionId} 已关闭`);
  }
});

function handleMessage(clientId, message) {
  let msg;
  try {
    msg = JSON.parse(message);
  } catch (e) { return; }
  console.log(`[MSG] 来自 ${clientId}:`, msg);
  switch (msg.type) {
    case 'join':
      clients.set(clientId, { ...clients.get(clientId), name: msg.name, isStreamer: false });
      broadcast({ type: 'system', text: `${msg.name} 加入了直播间` }, clientId);
      break;
    case 'start_stream':
      if (activeStreamer) {
        sendToClient(clientId, { type: 'error', text: '已有主播在直播' });
        return;
      }
      activeStreamer = clientId;
      const client = clients.get(clientId);
      if (client) client.isStreamer = true;
      broadcast({ type: 'system', text: `${client.name} 开始直播!` });
      break;
    case 'stop_stream':
      if (activeStreamer === clientId) {
        activeStreamer = null;
        broadcast({ type: 'system', text: '直播已结束' });
      }
      break;
    case 'chat':
      const sender = clients.get(clientId);
      broadcast({ type: 'chat', from: sender?.name || 'Anonymous', text: msg.text });
      break;
  }
}

async function handleIncomingStream(sessionId, stream) {
  const reader = stream.readable.getReader();
  const clientId = sessionId;
  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      if (clients.get(clientId)?.isStreamer && activeStreamer === clientId) {
        broadcastVideoFrame(value, clientId);
      } else {
        const text = new TextDecoder().decode(value);
        handleMessage(clientId, text);
      }
    }
  } catch (e) {
    console.error('[Stream] 读取错误:', e);
  }
}

function broadcast(msg, excludeId = null) {
  const data = JSON.stringify(msg);
  clients.forEach((client, id) => {
    if (id === excludeId) return;
    if (client.type === 'websocket') {
      client.connection.send(data);
    } else if (client.connection) {
      sendViaWebTransport(client.connection, data).catch(console.error);
    }
  });
}

async function sendViaWebTransport(session, data) {
  const stream = await session.createUnidirectionalStream();
  const writer = stream.writable.getWriter();
  await writer.write(new TextEncoder().encode(data));
  await writer.close();
}

function broadcastVideoFrame(frameData, fromStreamerId) {
  clients.forEach((client, id) => {
    if (id === fromStreamerId) return;
    if (!client.isStreamer && client.connection?.datagrams?.writable) {
      client.connection.datagrams.writable.getWriter().write(frameData).catch(console.error);
    }
  });
}

function sendToClient(clientId, msg) {
  const client = clients.get(clientId);
  if (!client) return;
  const data = JSON.stringify(msg);
  if (client.type === 'websocket') {
    client.connection.send(data);
  } else if (client.connection) {
    sendViaWebTransport(client.connection, data).catch(console.error);
  }
}

http2Server.listen(PORT, () => {
  console.log(`WebTransport 服务器运行在 https://localhost:${PORT}`);
});

B.2 Web 端:项目结构

web-client/
├── index.html
├── style.css
└── app.js

index.html

<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>WebTransport 直播</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="container">
    <h1>WebTransport 直播平台</h1>
    <div id="publisher-view" class="hidden">
      <video id="local-video" autoplay muted playsinline></video>
      <button id="btn-start-stream">开始直播</button>
      <button id="btn-stop-stream" disabled>停止直播</button>
    </div>
    <div id="viewer-view">
      <video id="remote-video" autoplay playsinline></video>
      <p id="status">等待主播...</p>
    </div>
    <div class="chat-area">
      <div id="messages"></div>
      <input type="text" id="chat-input" placeholder="输入消息..." />
      <button id="btn-send">发送</button>
    </div>
    <div id="login-modal">
      <input type="text" id="username-input" placeholder="输入昵称" />
      <label><input type="checkbox" id="role-publisher"> 我要当主播</label>
      <button id="btn-join">进入直播间</button>
    </div>
  </div>
  <script src="app.js"></script>
</body>
</html>

style.css

body { font-family: sans-serif; background: #1a1a2e; color: white; display: flex; justify-content: center; padding-top: 20px; }
.container { width: 90%; max-width: 800px; }
video { width: 100%; background: black; border-radius: 8px; }
.hidden { display: none; }
.chat-area { margin-top: 20px; border: 1px solid #444; padding: 10px; border-radius: 8px; }
#messages { height: 150px; overflow-y: scroll; margin-bottom: 10px; }
#messages div { margin-bottom: 5px; }
#chat-input { width: calc(100% - 70px); padding: 8px; }
button { padding: 8px 15px; cursor: pointer; margin-left: 5px; }
#login-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #16213e; padding: 30px; border-radius: 10px; box-shadow: 0 0 20px rgba(0,0,0,0.5); z-index: 100; }
#login-modal input[type="text"] { width: 100%; padding: 10px; margin-bottom: 15px; box-sizing: border-box; }
#login-modal label { display: block; margin-bottom: 15px; }
#login-modal button { width: 100%; padding: 12px; }

app.js

// app.js
document.addEventListener('DOMContentLoaded', () => {
  const loginModal = document.getElementById('login-modal');
  const usernameInput = document.getElementById('username-input');
  const rolePublisher = document.getElementById('role-publisher');
  const btnJoin = document.getElementById('btn-join');
  const publisherView = document.getElementById('publisher-view');
  const viewerView = document.getElementById('viewer-view');
  const localVideo = document.getElementById('local-video');
  const remoteVideo = document.getElementById('remote-video');
  const btnStartStream = document.getElementById('btn-start-stream');
  const btnStopStream = document.getElementById('btn-stop-stream');
  const chatInput = document.getElementById('chat-input');
  const btnSend = document.getElementById('btn-send');
  const messagesDiv = document.getElementById('messages');
  const statusEl = document.getElementById('status');

  let transport = null;
  let isPublisher = false;

  btnJoin.onclick = async () => {
    const username = usernameInput.value.trim();
    if (!username) return alert('请输入昵称');
    isPublisher = rolePublisher.checked;
    loginModal.classList.add('hidden');
    if (isPublisher) {
      publisherView.classList.remove('hidden');
      await startCamera();
    } else {
      viewerView.classList.remove('hidden');
    }
    connectToServer(username);
  };

  async function startCamera() {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
      localVideo.srcObject = stream;
      return stream;
    } catch (err) {
      console.error("无法访问摄像头:", err);
      alert("请确保已授予摄像头权限");
      return null;
    }
  }

  async function connectToServer(username) {
    try {
      const url = 'https://localhost:4433/stream';
      transport = new WebTransport(url);
      await transport.ready;
      console.log('WebTransport 连接成功');
      addSystemMessage('已连接到服务器');
      if (isPublisher) {
        sendMessage({ type: 'join', name: username });
        btnStartStream.disabled = false;
      } else {
        sendMessage({ type: 'join', name: username });
      }
      setupMessageReceiver(transport);
      if (!isPublisher) setupVideoReceiver(transport);
    } catch (err) {
      console.error('连接失败:', err);
      addSystemMessage('连接失败,尝试 WebSocket 降级...');
      connectWebSocketFallback(username);
    }
  }

  function connectWebSocketFallback(username) {
    const ws = new WebSocket('ws://localhost:8080');
    ws.onopen = () => {
      ws.send(JSON.stringify({ type: 'join', name: username }));
      addSystemMessage('已通过 WebSocket 连接');
    };
    ws.onmessage = (event) => handleReceivedMessage(JSON.parse(event.data));
    ws.onclose = () => addSystemMessage('WebSocket 连接断开');
    window.fallbackWs = ws;
  }

  function sendMessage(msg) {
    const data = JSON.stringify(msg);
    if (transport?.ready) {
      const writer = transport.datagrams.writable.getWriter();
      writer.write(new TextEncoder().encode(data));
      writer.releaseLock();
    } else if (window.fallbackWs) {
      window.fallbackWs.send(data);
    }
  }

  function setupMessageReceiver(transport) {
    const reader = transport.incomingUnidirectionalStreams.getReader();
    (async function readLoop() {
      while (true) {
        const { done, value: stream } = await reader.read();
        if (done) break;
        const sReader = stream.readable.getReader();
        const { value } = await sReader.read();
        const msg = JSON.parse(new TextDecoder().decode(value));
        handleReceivedMessage(msg);
      }
    })();
  }

  function handleReceivedMessage(msg) {
    switch (msg.type) {
      case 'system': addSystemMessage(msg.text); break;
      case 'chat': addChatMessage(msg.from, msg.text); break;
      case 'error': alert(msg.text); break;
    }
  }

  function setupVideoReceiver(transport) {
    setInterval(async () => {
      if (!transport?.datagrams?.readable) return;
      const reader = transport.datagrams.readable.getReader();
      try {
        const { done, value } = await reader.read();
        if (!done && value) {
          const blob = new Blob([value], { type: 'image/jpeg' });
          const url = URL.createObjectURL(blob);
          remoteVideo.srcObject = null;
          remoteVideo.src = url;
        }
      } catch (e) { /* ignore */ }
    }, 1000 / 30);
  }

  btnStartStream.onclick = async () => {
    if (!transport) return;
    const mediaStream = localVideo.srcObject;
    if (!mediaStream) return;
    const track = mediaStream.getVideoTracks()[0];
    const imageCapture = new ImageCapture(track);
    sendMessage({ type: 'start_stream' });
    btnStartStream.disabled = true;
    btnStopStream.disabled = false;
    statusEl.textContent = "直播中...";
    const captureAndSend = async () => {
      if (!transport || btnStopStream.disabled) return;
      try {
        const bitmap = await imageCapture.grabFrame();
        const canvas = document.createElement('canvas');
        canvas.width = bitmap.width;
        canvas.height = bitmap.height;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(bitmap, 0, 0);
        canvas.toBlob(async (blob) => {
          if (blob && transport?.datagrams?.writable) {
            const arrayBuffer = await blob.arrayBuffer();
            const writer = transport.datagrams.writable.getWriter();
            await writer.write(arrayBuffer);
            writer.releaseLock();
          }
        }, 'image/jpeg', 0.8);
      } catch (e) { console.error(e); }
      requestAnimationFrame(captureAndSend);
    };
    requestAnimationFrame(captureAndSend);
  };

  btnStopStream.onclick = () => {
    sendMessage({ type: 'stop_stream' });
    btnStartStream.disabled = false;
    btnStopStream.disabled = true;
    statusEl.textContent = "直播已停止";
  };

  btnSend.onclick = () => {
    const text = chatInput.value.trim();
    if (!text) return;
    sendMessage({ type: 'chat', text });
    chatInput.value = '';
  };
  chatInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') btnSend.onclick(); });

  function addSystemMessage(text) {
    const div = document.createElement('div');
    div.className = 'system';
    div.textContent = `[系统] ${text}`;
    messagesDiv.appendChild(div);
    messagesDiv.scrollTop = messagesDiv.scrollHeight;
  }
  function addChatMessage(from, text) {
    const div = document.createElement('div');
    div.innerHTML = `<strong>${from}:</strong> ${text}`;
    messagesDiv.appendChild(div);
    messagesDiv.scrollTop = messagesDiv.scrollHeight;
  }
});

B.3 移动端 (Android / Kotlin)

app/build.gradle 依赖:

dependencies {
    implementation 'org.chromium.net:cronet-embedded:121.0.6167.101'
}

MainActivity.kt 核心逻辑:

// MainActivity.kt (节选)
val engineParams = CronetEngine.Builder(this).apply {
    setStoragePath(cacheDir.absolutePath)
    enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK, 1024 * 1024 * 50)
    enableQuic(true)
}.build()
cronetEngine = engineParams

scope.launch {
    val session = cronetEngine.createWebTransportSession("https://your-server:4433/stream")
    session.connect()
    webTransportSession = session
    receiveMessages(session)  // 从 incomingUnidirectionalStreams 读消息
    // sendVideoStream(session)  // 用 datagramsWritable 发视频帧
}
// onDestroy: webTransportSession?.close(null); cronetEngine.shutdown()

B.4 运行与测试

  1. 证书:在服务端项目下执行 openssl req -x509 -newkey rsa:4096 -keyout certs/key.pem -out certs/cert.pem -days 365 -nodes(或与 server.js 中路径一致)。
  2. 启动服务端cd webtransport-live-server && node server.js
  3. Web 端:用本地静态服务打开 web-client/index.html;浏览器会提示自签名证书,需信任“继续前往 localhost”;可开两个标签分别以主播、观众身份加入。
  4. 移动端:将 your-server 改为本机 IP 或域名,并处理证书信任或使用正式 CA 证书。

免责声明

本文整理自公开技术资料与社区实践,仅供学习与参考。实现细节、API 与浏览器支持请以最新官方文档与标准为准。


参考资料:W3C WebTransport 草案、IETF QUIC/HTTP/3 RFC、MDN WebTransport API、公开技术问答与实现文档整理。