WebTransport 技术白皮书:从原理到多端实现
目录
- 一、执行摘要
- 二、背景与挑战:为何需要 WebTransport
- 三、技术概览:WebTransport 是什么
- 四、架构与数据模型
- 五、各端开发与实现指南
- 六、安全、部署与运维
- 七、实践案例与性能基准
- 八、迁移与实施路线图
- 九、未来展望与标准演进
- 附录 A:术语表
- 附录 B:完整代码示例
- 免责声明
一、执行摘要
1.1 核心观点
WebTransport 是构建于 HTTP/3 和 QUIC 之上的现代 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 丢失只阻塞流 A
流 B [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/3 和 QUIC 的 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 生命周期
- connecting:
new WebTransport()后、ready未兑现前。 - connected:
await transport.ready成功,可创建流、读写数据报。 - closed:
close()或异常后,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 的对比矩阵
| 特性 | WebSocket | WebTransport | WebRTC DataChannel |
|---|---|---|---|
| 底层协议 | TCP | QUIC (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()等。 - 错误与监控:
WebTransportError(source、streamErrorCode);getStats()(实验性)获取 RTT、丢包等。 - 降级:
typeof WebTransport !== 'undefined'检测,不支持时回退 WebSocket。
完整 Web 端示例(含登录、直播、聊天)见 附录 B。
5.1.1 Web API 速查表
| API / 属性 | 说明 |
|---|---|
new WebTransport(url [, options]) | 构造器;options.congestionControl、options.serverCertificateHashes |
transport.ready | Promise,会话就绪后兑现 |
transport.closed | Promise,会话关闭后兑现或 reject |
transport.datagrams.readable | ReadableStream,接收数据报 |
transport.datagrams.writable | WritableStream,发送数据报 |
transport.datagrams.maxDatagramSize | 只读,当前允许的最大发出数据报字节数 |
transport.createUnidirectionalStream() | 创建客户端→服务端单向流 |
transport.createBidirectionalStream() | 创建双向流 |
transport.incomingUnidirectionalStreams | ReadableStream of 流,接收服务端→客户端单向流 |
transport.incomingBidirectionalStreams | ReadableStream 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+ 等 |
| Go | quic-go 等库可实现 HTTP/3 与 WebTransport,适合快速落地 |
| .NET 10 + SignalR | Kestrel 启用 HTTP/3 后,SignalR 可协商 WebTransport |
| C++ | 基于 mvfst+proxygen 或 QUICHE 自行实现会话与流管理,无现成 WebTransport 库;浏览器与 Cronet 底层均为 C++ 实现 |
完整 Node.js 服务端示例(会话管理、消息类型、广播、视频帧转发)见 附录 B。
5.3 移动端 (Mobile)
| 平台 | 建议 |
|---|---|
| Android | 使用 Cronet(org.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 3
│ 101 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 存量项目迁移策略
- 双协议共存:新功能优先 WebTransport,旧功能保留 WebSocket。
- 功能迁移:逐步将核心链路迁至 WebTransport。
- 全量切换:生态与监控就绪后,再考虑下线 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 WebTransport | TR/webtransport | 浏览器 API 与语义(Working Draft) |
| RFC 9000 | rfc9000 | QUIC:传输层协议 |
| RFC 9001 | rfc9001 | QUIC 的 TLS 1.3 使用 |
| RFC 9114 | rfc9114 | HTTP/3(基于 QUIC) |
| IETF WEBTRANS | draft-ietf-webtrans-http3 | WebTransport over HTTP/3(草案) |
浏览器与运行环境
| 来源 | 链接 | 说明 |
|---|---|---|
| MDN | WebTransport API | API 说明与示例 |
| MDN | maxDatagramSize | 数据报大小限制 |
| Chrome | WebTransport 能力说明 | Chrome 官方能力与示例 |
| Can I use | WebTransport | 各浏览器/版本支持情况 |
开源项目与仓库简介(扩展阅读)
| 技术栈 | GitHub 地址 | 仓库简介 |
|---|---|---|
| Node.js | github.com/fails-compo… | 基于 Node.js 的 WebTransport 服务端实现,依赖 HTTP/2 等,可与浏览器 WebTransport API 对接;适用于快速搭建演示或轻量网关。 |
| Go | github.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/webtransport | W3C 规范仓库,内含 Echo、WebCodecs Echo 等示例实现,便于对照标准做客户端或服务端试验。 |
| Rust 客户端 | BiagioFesta/wtransport | 异步 Rust WebTransport 库(crates.io: wtransport),支持流与数据报,基于 QUIC/HTTP/3,适合原生或服务端客户端。 |
| Rust 客户端 + WASM | kixelated/web-transport-rs | Rust 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 运行与测试
- 证书:在服务端项目下执行
openssl req -x509 -newkey rsa:4096 -keyout certs/key.pem -out certs/cert.pem -days 365 -nodes(或与server.js中路径一致)。 - 启动服务端:
cd webtransport-live-server && node server.js。 - Web 端:用本地静态服务打开
web-client/index.html;浏览器会提示自签名证书,需信任“继续前往 localhost”;可开两个标签分别以主播、观众身份加入。 - 移动端:将
your-server改为本机 IP 或域名,并处理证书信任或使用正式 CA 证书。
免责声明
本文整理自公开技术资料与社区实践,仅供学习与参考。实现细节、API 与浏览器支持请以最新官方文档与标准为准。
参考资料:W3C WebTransport 草案、IETF QUIC/HTTP/3 RFC、MDN WebTransport API、公开技术问答与实现文档整理。