发布日期:2020 年 6 月 8 日
WebTransport 是一种 Web API,它使用 HTTP/3 协议作为双向传输。它旨在实现 Web 客户端与 HTTP/3 服务器之间的双向通信。它支持通过数据报 API 不可靠地发送数据,也支持通过流 API 可靠地发送数据。
数据报非常适合发送和接收不需要强交付保证的数据。单个数据包的大小受底层连接的最大传输单元 (MTU) 的限制,并且可能成功传输,也可能无法成功传输;即使成功传输,到达顺序也可能不确定。这些特性使得数据报 API 非常适合低延迟、尽力而为的数据传输。您可以将数据报视为用户数据报协议 (UDP) 消息,但经过加密并受拥塞控制。
相比之下,流式传输 API 可提供可靠的有序数据传输。它们非常适合需要发送或接收一个或多个有序数据流的场景。使用多个 WebTransport 流类似于建立多个 TCP 连接,但由于 HTTP/3 在底层使用更轻量级的 QUIC 协议,因此可以打开和关闭这些流,而不会产生过多的开销。
使用场景
以下是开发者可能使用 WebTransport 的一些方式。
- 以最小延迟定期向服务器发送游戏状态,但消息量小、不可靠且无序。
- 接收从服务器推送的媒体流,延迟时间最短,不受其他数据流的影响。
- 在网页打开时接收从服务器推送的通知。
我们很想详细了解您计划如何使用 WebTransport。
浏览器支持
与所有缺乏通用浏览器支持的功能一样,我们建议添加功能检测。
与其他技术的关系
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 服务器要少,后者需要了解多种协议(ICE、DTLS 和 SCTP)才能获得可用的传输。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,并且可与 async 和 await 搭配使用。
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。
使用 WritableStreamDefaultWriter 的 close() 方法关闭关联的 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}`);
}
同样,使用 WritableStreamDefaultWriter 的 abort() 方法向服务器发送 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);
}
您可以使用 ReadableStreamDefaultReader 的 closed 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 只是 WebTransportSendStream 和 WebTransportReceiveStream 的组合。前两部分中的示例介绍了如何使用每种方法。
Polyfill
我们提供了一个名为 webtransport-ponyfill-websocket 的 Polyfill(或者更确切地说,是一个可作为独立模块使用的 Ponyfill),用于实现 WebTransport 的部分功能。请仔细阅读项目 README 中的限制,以确定此解决方案是否适用于您的使用情形。
隐私权和安全注意事项
如需权威指导,请参阅规范草稿的相应部分。
反馈
该 API 是否存在不便之处或无法按预期运行?或者,您是否缺少实现想法所需的要素?
- 在 Web Transport GitHub 代码库中提交问题,或在现有问题中添加您的想法。
- 提交有关 Chromium 实现的 bug。请尽可能详细地说明问题,并提供重现问题的步骤。
您的公开支持有助于 Chrome 确定功能优先级,并向其他浏览器供应商展示支持这些功能的重要性。
- 请使用 #
#WebTransport标签向 @ChromiumDev 发送推文,详细说明您在何处以及如何使用该功能。
一般性讨论
对于不属于其他任何类别的常规问题或问题,您可以使用 web-transport-dev Google 群组。
致谢
我们纳入了 WebTransport 说明和规范草稿中的信息。感谢各位作者提供这些基础知识。