使用 WebTransport

WebTransport 是一种 API,可提供低延迟、双向的客户端-服务器消息传递。详细了解其使用场景,以及如何针对未来的实现提供反馈。

背景

什么是 WebTransport?

WebTransport 是一种 Web API,它使用 HTTP/3 协议作为双向传输。它旨在实现 Web 客户端与 HTTP/3 服务器之间的双向通信。它支持通过其数据报 API 不可靠地发送数据,也支持通过其流 API 可靠地发送数据。

数据报非常适合发送和接收不需要强交付保证的数据。单个数据包的大小受底层连接的最大传输单元 (MTU) 的限制,并且可能成功传输,也可能无法成功传输;即使成功传输,到达顺序也可能不确定。这些特性使得数据报 API 非常适合低延迟、尽力而为的数据传输。您可以将数据报视为用户数据报协议 (UDP) 消息,但经过加密并受拥塞控制。

相比之下,流式传输 API 可提供可靠的有序数据传输。它们非常适合需要发送或接收一个或多个有序数据流的场景。使用多个 WebTransport 流类似于建立多个 TCP 连接,但由于 HTTP/3 在底层使用更轻量级的 QUIC 协议,因此可以打开和关闭这些流,而不会产生过多的开销。

使用场景

以下列举了一些开发者可能会使用 WebTransport 的方式。

  • 以最小延迟时间通过小型、不可靠的无序消息定期向服务器发送游戏状态。
  • 接收从服务器推送的媒体流,延迟时间最短,不受其他数据流的影响。
  • 在网页打开时接收服务器推送的通知。

我们很想详细了解您计划如何使用 WebTransport。

浏览器支持

Browser Support

  • Chrome: 97.
  • Edge: 97.
  • Firefox: 114.
  • Safari: behind a flag.

Source

与所有未获得浏览器普遍支持的功能一样,通过功能检测进行防御性编码是一种最佳实践。

当前状态

步骤 状态
1. 创建说明 完成
2. 创建规范的初始草稿 完成
3. 收集反馈并迭代设计 完成
4. 源试用 完成
5. 发布 Chromium 97

WebTransport 与其他技术的关系

WebTransport 是否会取代 WebSocket?

或许可以。在某些使用情形下,WebSockets 或 WebTransport 可能是有效的通信协议。

WebSocket 通信是围绕单个可靠的有序消息流建模的,这对于某些类型的通信需求来说是合适的。如果您需要这些特性,WebTransport 的 Streams API 也可以提供。相比之下,WebTransport 的数据报 API 提供低延迟的传送,但不保证可靠性或顺序,因此不能直接替代 WebSocket。

通过数据报 API 或多个并发的 Streams API 实例使用 WebTransport,意味着您不必担心 head-of-line blocking,而这可能是 WebSockets 的一个问题。此外,在建立新连接时,由于底层 QUIC 握手比启动基于 TLS 的 TCP 更快,因此可以提高性能。

WebTransport 属于新的规范草稿,因此目前围绕客户端和服务器库的 WebSocket 生态系统更加强大。如果您需要一种能够与常见服务器设置“开箱即用”且具有广泛 Web 客户端支持的技术,那么 WebSocket 目前是更好的选择。

WebTransport 与 UDP 套接字 API 是否相同?

否。WebTransport 不是 UDP 套接字 API。虽然 WebTransport 使用 HTTP/3,而 HTTP/3 又在“底层”使用 UDP,但 WebTransport 在加密和拥塞控制方面有要求,因此它不仅仅是一个基本的 UDP 套接字 API。

WebTransport 是否是 WebRTC 数据通道的替代方案?

可以,对于客户端-服务器连接。WebTransport 与 WebRTC 数据通道有许多相同的属性,但底层协议不同。

一般来说,运行与 HTTP/3 兼容的服务器所需的设置和配置比维护 WebRTC 服务器要少,后者需要了解多种协议(ICEDTLSSCTP)才能获得可用的传输。WebRTC 涉及许多可能会导致客户端/服务器协商失败的活动部件。

WebTransport API 在设计时考虑到了 Web 开发者的使用情形,与使用 WebRTC 的数据通道接口相比,它更像是编写现代 Web 平台代码。与 WebRTC 不同,WebTransport 在 Web Worker 中受支持,这使您可以独立于给定的 HTML 网页执行客户端-服务器通信。由于 WebTransport 公开了符合 Streams 标准的接口,因此它支持围绕反压力的优化。

不过,如果您已经拥有一个运行正常的 WebRTC 客户端/服务器设置,并且对此感到满意,那么改用 WebTransport 可能不会带来太多优势。

试试看

试验 WebTransport 的最佳方式是启动兼容的 HTTP/3 服务器。然后,您可以将此网页与基本 JavaScript 客户端搭配使用,以尝试客户端/服务器通信。

此外,还有一个由社区维护的回显服务器,网址为 webtransport.day

使用此 API

WebTransport 基于现代 Web 平台原语(例如 Streams API)设计。它高度依赖于 Promise,并且可与 asyncawait 搭配使用。

Chromium 中当前的 WebTransport 实现支持三种不同的流量类型:数据报,以及单向和双向数据流。

连接到服务器

您可以通过创建 WebTransport 实例来连接到 HTTP/3 服务器。网址的架构应为 https。您需要明确指定端口号。

您应使用 ready promise 等待连接建立。在设置完成之前,此 promise 不会兑现;如果连接在 QUIC/TLS 阶段失败,此 promise 将被拒绝。

当连接正常关闭时,closed promise 会兑现;如果关闭是意外的,则会拒绝。

如果服务器因客户端指示错误(例如网址的路径无效)而拒绝连接,则会导致 closed 拒绝,而 ready 仍未解决。

const url = 'https://example.com:4999/foo/bar';
const transport = new WebTransport(url);

// Optionally, set up functions to respond to
// the connection closing:
transport.closed.t>hen(() = {
  console.log(`The HTTP/3 connection to ${url} closed gracefully.`);
}).catch((>error) = {
  console.error(`The HTTP/3 connection to ${url} closed due to ${error}.`);
});

// Once .ready fulfills, the connection can be used.
await transport.ready;

数据报 API

获得已连接到服务器的 WebTransport 实例后,您可以使用该实例发送和接收离散的数据位(称为数据报)。

writeable getter 会返回一个 WritableStream,网络客户端可以使用该对象将数据发送到服务器。readable getter 会返回 ReadableStream,以便您监听来自服务器的数据。这两个数据流本质上都不可靠,因此您写入的数据可能不会被服务器接收,反之亦然。

这两种类型的流都使用 Uint8Array 实例进行数据传输。

// Send two datagrams to the server.
const writer = transport.datagrams.writable.getWriter();
const data1 = new Uint8Array([65, 66, 67]);
const data2 = new Uint8Array([68, 69, 70]);
writer.write(data1);
writer.write(data2);

// Read datagrams from the server.
const reader = transport.datagrams.readable.getReader();
while (true) {
  const {value, done} = await reader.read();
  if (done) {
    break;
  }
  // value is a Uint8Array.
  console.log(value);
}

Streams API

连接到服务器后,您还可以使用 WebTransport 通过其 Streams API 发送和接收数据。

所有数据流的每个分块都是一个 Uint8Array。与数据报 API 不同,这些流是可靠的。不过,每个数据流都是独立的,因此无法保证数据流之间的数据顺序。

WebTransportSendStream

Web 客户端使用 WebTransport 实例的 createUnidirectionalStream() 方法创建 WebTransportSendStream,该方法会返回一个针对 WebTransportSendStream 的 promise。

使用 WritableStreamDefaultWriterclose() 方法关闭关联的 HTTP/3 流。浏览器会尝试发送所有待处理的数据,然后再实际关闭关联的流。

// Send two Uint8Arrays to the server.
const stream = await transport.createUnidirectionalStream();
const writer = stream.writable.getWriter();
const data1 = new Uint8Array([65, 66, 67]);
const data2 = new Uint8Array([68, 69, 70]);
writer.write(data1);
writer.write(data2);
try {
  await writer.close();
  console.log('All data has been sent.');
} catch (error) {
  console.error(`An error occurred: ${error}`);
}

同样,使用 WritableStreamDefaultWriterabort() 方法向服务器发送 RESET_STREAM。使用 abort() 时,浏览器可能会舍弃尚未发送的任何待处理数据。

const ws = await transport.createUnidirectionalStream();
const writer = ws.getWriter();
writer.write(...);
writer.write(...);
await writer.abort();
// Not all the data may have been written.

WebTransportReceiveStream

WebTransportReceiveStream 由服务器发起。对于 Web 客户端,获取 WebTransportReceiveStream 的过程分为两步。首先,它会调用 WebTransport 实例的 incomingUnidirectionalStreams 属性,该属性会返回一个 ReadableStream。该 ReadableStream 的每个块依次是一个 WebTransportReceiveStream,可用于读取服务器发送的 Uint8Array 实例。

async function readFrom(receiveStream) {
  const reader = receiveStream.readable.getReader();
  while (true) {
    const {done, value} = await reader.read();
    if (done) {
      break;
    }
    // value is a Uint8Array
    console.log(value);
  }
}

const rs = transport.incomingUnidirectionalStreams;
const reader = rs.getReader();
while (true) {
  const {done, value} = await reader.read();
  if (done) {
    break;
  }
  // value is an instance of WebTransportReceiveStream
  await readFrom(value);
}

您可以使用 ReadableStreamDefaultReaderclosed promise 来检测流关闭。当底层 HTTP/3 流通过 FIN 位关闭时,在读取所有数据后,closed promise 会得到兑现。当 HTTP/3 流突然关闭(例如,通过 RESET_STREAM 关闭)时,closed promise 会被拒绝。

// Assume an active receiveStream
const reader = receiveStream.readable.getReader();
reader.closed.then(() => {
  console.log('The receiveStream closed gracefully.');
}).ca>tch(() = {
  console.error('The receiveStream closed abruptly.');
});

WebTransportBidirectionalStream

WebTransportBidirectionalStream 可以由服务器或客户端创建。

Web 客户端可以使用 WebTransport 实例的 createBidirectionalStream() 方法创建一个,该方法会返回一个针对 WebTransportBidirectionalStream 的 promise。

const stream = await transport.createBidirectionalStream();
// stream is a WebTransportBidirectionalStream
// stream.readable is a ReadableStream
// stream.writable is a WritableStream

您可以使用 WebTransport 实例的 incomingBidirectionalStreams 属性来监听服务器创建的 WebTransportBidirectionalStream,该属性会返回 ReadableStream。该 ReadableStream 的每个块依次都是一个 WebTransportBidirectionalStream

const rs = transport.incomingBidirectionalStreams;
const reader = rs.getReader();
while (true) {
  const {done, value} = await reader.read();
  if (done) {
    break;
  }
  // value is a WebTransportBidirectionalStream
  // value.readable is a ReadableStream
  // value.writable is a WritableStream
}

WebTransportBidirectionalStream 只是 WebTransportSendStreamWebTransportReceiveStream 的组合。前两部分中的示例介绍了如何使用每种方法。

更多示例

WebTransport 草稿规范包含许多额外的内嵌示例,以及所有方法和属性的完整文档。

Chrome 开发者工具中的 WebTransport

遗憾的是,Chrome 的开发者工具目前不支持 WebTransport。您可以为此 Chrome 问题加注星标,以便在开发者工具界面有更新时收到通知。

Polyfill

我们提供了一个名为 webtransport-ponyfill-websocket 的 Polyfill(或者更确切地说,是一个提供功能的可用作独立模块的 Ponyfill),它实现了 WebTransport 的部分功能。请仔细阅读项目 README 中的限制,以确定此解决方案是否适用于您的使用情形。

隐私权和安全注意事项

如需权威指导,请参阅规范草稿的相应部分

反馈

Chrome 团队希望了解您对使用此 API 的想法和体验。

有关 API 设计的反馈

该 API 是否存在不便之处或无法按预期运行?或者,是否缺少实现您的想法所需的要素?

Web Transport GitHub 代码库中提交问题,或在现有问题中添加您的想法。

实现存在问题?

您是否发现 Chrome 的实现存在 bug?

请前往 https://new.crbug.com 提交 bug。请尽可能详细地说明问题,并提供简单的重现说明。

打算使用 API?

您的公开支持有助于 Chrome 确定功能优先级,并向其他浏览器供应商展示支持这些功能的重要性。

一般性讨论

对于不属于其他任何类别的常规问题或问题,您可以使用 web-transport-dev Google 群组

致谢

本文纳入了 WebTransport 说明规范草稿相关设计文档中的信息。感谢相关作者提供这一基础。

此博文中的主打图片由 Unsplash 上的 Robin Pierre 拍摄。