如何使用 WebTransport

发布日期:2020 年 6 月 8 日

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: 26.4.

Source

与所有缺乏通用浏览器支持的功能一样,我们建议添加功能检测

与其他技术的关系

WebTransport 是否会取代 WebSockets?

或许可以。在某些使用场景中,WebSockets 或 WebTransport 可能是有效的通信协议。

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

使用 WebTransport 时,借助数据报 API 或多个并发的 Streams API 实例,您无需担心 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 阶段连接失败,则会拒绝。

当连接正常关闭时,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.then(() => {
  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.');
}).catch(() => {
  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 的组合。前两部分中的示例介绍了如何使用每种方法。

Polyfill

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

隐私权和安全注意事项

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

反馈

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

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

一般性讨论

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

致谢

我们纳入了 WebTransport 说明规范草稿中的信息。感谢各位作者提供这些基础知识。