WebTransport API: Новая эра веб-коммуникаций

WebTransport API — революционная технология, обеспечивающая высокоэффективную связь с минимальной задержкой между веб-клиентами и серверами. Этот инновационный протокол оптимизирует передачу данных в режиме реального времени, поддерживает двунаправленные потоки и повышает производительность веб-приложений. Рассмотрим варианты использования, преимущества и рекомендации по внедрению для успешной веб-разработки.
WebTransport API предлагает современную альтернативу WebSockets, используя HTTP/3 Transport для упрощения передачи данных. Он поддерживает множественные потоки, однонаправленные потоки и внеочередную доставку. WebTransport обеспечивает надежную связь через потоки и ненадежную передачу данных через UDP-подобные датаграммы.
Описание и возможности
WebTransport API — это интерфейс для передачи данных между клиентами и серверами посредством HTTP/3.
Он поддерживает надежную, упорядоченную доставку данных через один или несколько одно- или двунаправленных потоков, а также ненадежную, неупорядоченную доставку через датаграммы. В первом случае он заменяет WebSockets, а во втором — RTCDataChannel из WebRTC API.
В настоящее время поддержка WebTransport API отсутствует в Node.js, Deno и Bun.
В июне 2023 года поддержка WebTransport появилась в Socket.io. Однако эта реализация основана на пакете @fails-components/webtransport
, который носит экспериментальный характер и не рекомендуется для промышленного использования. Тем не менее, рассмотрим этот вариант подробнее, учитывая репутацию Socket.io как надежной библиотеки для обмена данными в реальном времени.
Обзор HTTP/3
HTTP/3 базируется на протоколе QUIC от Google, использующем UDP и призванном решить ряд проблем TCP:
- Задержка обработки очереди (HOL blocking): в отличие от HTTP/2 с его мультиплексированием (несколько потоков по одному соединению), в HTTP/2 сбой одного потока блокирует остальные. QUIC решает эту проблему, обеспечивая независимость потоков.
- Повышенная производительность: QUIC превосходит TCP по многим показателям. Это обусловлено встроенной безопасностью (в отличие от TCP, использующего TLS), сокращающей количество циклов передачи данных, и более эффективным механизмом передачи потоков по сравнению с пакетной передачей TCP, что особенно актуально в условиях высокой сетевой нагрузки.
- Плавный переход между сетями: QUIC использует уникальный идентификатор соединения для корректной доставки пакетов в разных сетях. Этот идентификатор сохраняется при смене сети, обеспечивая непрерывную загрузку при переключении между Wi-Fi и мобильной связью. В HTTP/2 используются IP-адреса, что может приводить к сбоям при переходе между сетями.
- Ненадежная доставка: HTTP/3 поддерживает ненадежную доставку, что может быть эффективнее гарантированной доставки в определенных случаях.
Включить поддержку HTTP/3 (QUIC) в Google Chrome можно, активировав опцию «Экспериментальный протокол QUIC» в chrome://flags
.

Принципы работы API WebTransport
Установление соединения
Чтобы установить соединение HTTP/3 с сервером, URL-адрес должен быть передан конструктору WebTransport()
. Обратите внимание, что URL-адрес должен использовать схему HTTPS, а порт должен быть указан явно. Обещание WebTransport.ready
разрешается после успешного установления соединения.
Соединение можно закрыть с помощью обещания WebTransport.closed
. Любые обнаруженные ошибки являются экземплярами WebTransportError
, которые включают дополнительные сведения поверх стандартного набора DOMException.
const url = "https://example.com:4999/wt";
async function initTransport(url) {
// Initialize the connection
const transport = new WebTransport(url);
// Resolving this promise indicates readiness to handle requests
await transport.ready;
// ...
}
async function closeTransport(transport) {
// Handle connection closing
try {
await transport.closed;
console.log(`HTTP/3 connection to ${url} closed gracefully.`);
} catch (error) {
console.error(`HTTP/3 connection to ${url} closed due to error: ${error}.`);
}
}
Эта конфигурация обеспечивает эффективное управление соединениями и обработку ошибок при использовании WebTransport API по протоколу HTTP/3.
Ненадежная передача данных через датаграммы
При ненадежной передаче нет гарантии полной доставки или упорядоченности данных. В некоторых случаях это допустимо, главное преимущество — повышенная скорость передачи.
Обработка ненадежной доставки осуществляется через свойство WebTransport.datagrams
, возвращающее объект WebTransportDatagramDuplexStream
, содержащий все необходимое для отправки и получения датаграмм.
Свойство WebTransportDatagramDuplexStream.writable
предоставляет объект WritableStream
для отправки данных на сервер:
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);
Свойство WebTransportDatagramDuplexStream.readable
для передачи дейтаграмм предоставляет ReadableStream, позволяющий считывать данные, полученные с сервера:
async function readData() {
const reader = transport.datagrams.readable.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}
console.log(value); // value is a Uint8Array
}
}
Надежная передача данных с использованием потоков
Надежная передача данных гарантирует полную и упорядоченную доставку информации, хотя этот процесс занимает больше времени по сравнению с использованием датаграмм. Тем не менее, надежность играет ключевую роль во многих сценариях, таких как чат-приложения.
При использовании потоков для передачи данных можно задавать приоритеты потоков.
Однонаправленная передача данных
Для открытия однонаправленного потока используется метод WebTransport.createUnidirectionalStream()
, который возвращает объект WritableStream
. Данные отправляются на сервер с помощью writer
, полученного через метод getWriter
:
async function writeData() {
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 successfully sent");
} catch (error) {
console.error(`An error occurred while sending data: ${error}`);
}
}
Метод WritableStreamDefaultWriter.close()
применяется для закрытия соединения HTTP/3 после завершения отправки всех данных.
Для получения данных из однонаправленного потока, открытого на сервере, используется свойство WebTransport.incomingUnidirectionalStreams
, которое возвращает объекты ReadableStream
типа WebTransportReceiveStream
.
Для чтения данных из WebTransportReceiveStream
создается специальная функция. Эти объекты наследуются от класса ReadableStream
, что упрощает их реализацию:
async function readData(receiveStream) {
const reader = receiveStream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
console.log(value); // value is a Uint8Array
}
}
Получите ссылку на reader
с помощью getReader()
и прочитайте данные из incomingUnidirectionalStreams
по частям (каждая часть — это WebTransportReceiveStream
):
async function receiveUnidirectional() {
const uds = transport.incomingUnidirectionalStreams;
const reader = uds.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
await readData(value);
}
}
Эти механизмы позволяют эффективно обрабатывать как надежные, так и ненадежные передачи данных с использованием API WebTransport.
Двунаправленная передача данных
Чтобы открыть двунаправленный поток, используйте метод WebTransport.createBidirectionalStream()
, который возвращает объект WebTransportBidirectionalStream
. Этот поток содержит свойства readable
и writable
, предоставляющие ссылки на экземпляры WebTransportReceiveStream
и WebTransportSendStream
. Их можно использовать для чтения данных, полученных с сервера, и отправки данных на сервер соответственно.
async function setUpBidirectional() {
const stream = await transport.createBidirectionalStream();
// stream is WebTransportBidirectionalStream
// stream.readable is WebTransportReceiveStream
const readable = stream.readable;
// stream.writable is WebTransportSendStream
const writable = stream.writable;
// Additional setup code can follow
}
Чтение из WebTransportReceiveStream
может быть реализовано следующим образом:
async function readData(readable) {
const reader = readable.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}
console.log(value); // value is Uint8Array
}
}
Запись в WebTransportSendStream
может быть реализована следующим образом:
async function writeData(writable) {
const writer = writable.getWriter();
const data1 = new Uint8Array([65, 66, 67]);
const data2 = new Uint8Array([68, 69, 70]);
writer.write(data1);
writer.write(data2);
}
Чтобы извлечь данные из двунаправленного потока, открытого на сервере, используйте свойство WebTransport.incomingBidirectionalStreams
, которое возвращает объекты ReadableStream
типа WebTransportBidirectionalStream
. Каждый поток может использоваться для чтения и записи экземпляров Uint8Array
. Вам понадобится функция для обработки чтения из двунаправленного потока:
async function receiveBidirectional() {
const bds = transport.incomingBidirectionalStreams;
const reader = bds.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
await readData(value.readable);
await writeData(value.writable);
}
}
Такой подход позволяет эффективно обрабатывать двунаправленные потоки данных, в полной мере используя API WebTransport.
Как реализовать API WebTransport с помощью JS?
// Create a WebTransport connection
class TransportClient {
constructor(url) {
this.url = url;
this.transport = null;
this.streams = new Map();
this.datagramWriter = null;
this.datagramReader = null;
}
async connect() {
try {
this.transport = new WebTransport(this.url);
console.log('Initiating connection...');
// Wait for connection establishment
await this.transport.ready;
console.log('Connection established successfully');
// Set up error handling
this.transport.closed
.then(() => {
console.log('Connection closed normally');
})
.catch((error) => {
console.error('Connection closed due to error:', error);
});
// Initialize datagram handlers
this.setupDatagrams();
} catch (error) {
console.error('Failed to establish connection:', error);
throw error;
}
}
// Set up datagram sending and receiving
setupDatagrams() {
// Set up datagram writer
this.datagramWriter = this.transport.datagrams.writable.getWriter();
// Set up datagram reader
this.handleDatagrams();
}
async handleDatagrams() {
try {
const reader = this.transport.datagrams.readable.getReader();
while (true) {
const {value, done} = await reader.read();
if (done) {
console.log('Datagram reader done');
break;
}
// Process received datagram
const decoded = new TextDecoder().decode(value);
console.log('Received datagram:', decoded);
}
} catch (error) {
console.error('Error reading datagrams:', error);
}
}
// Send a datagram
async sendDatagram(data) {
try {
const encoded = new TextEncoder().encode(data);
await this.datagramWriter.write(encoded);
console.log('Datagram sent successfully');
} catch (error) {
console.error('Error sending datagram:', error);
throw error;
}
}
// Create and handle a bidirectional stream
async createBidirectionalStream() {
try {
const stream = await this.transport.createBidirectionalStream();
const streamId = crypto.randomUUID();
this.streams.set(streamId, stream);
// Handle incoming data
this.handleStreamInput(stream, streamId);
return {
streamId,
writer: stream.writable.getWriter()
};
} catch (error) {
console.error('Error creating bidirectional stream:', error);
throw error;
}
}
async handleStreamInput(stream, streamId) {
try {
const reader = stream.readable.getReader();
while (true) {
const {value, done} = await reader.read();
if (done) {
console.log(`Stream ${streamId} reading complete`);
break;
}
const decoded = new TextDecoder().decode(value);
console.log(`Received on stream ${streamId}:`, decoded);
}
} catch (error) {
console.error(`Error reading from stream ${streamId}:`, error);
} finally {
this.streams.delete(streamId);
}
}
// Send data through a specific stream
async sendOnStream(streamId, data) {
const stream = this.streams.get(streamId);
if (!stream) {
throw new Error(`Stream ${streamId} not found`);
}
try {
const writer = stream.writable.getWriter();
const encoded = new TextEncoder().encode(data);
await writer.write(encoded);
await writer.close();
console.log(`Data sent successfully on stream ${streamId}`);
} catch (error) {
console.error(`Error sending data on stream ${streamId}:`, error);
throw error;
}
}
// Close the WebTransport connection
async close() {
try {
await this.transport.close();
console.log('Connection closed successfully');
} catch (error) {
console.error('Error closing connection:', error);
throw error;
}
}
}
Вот как использовать реализацию WebTransport:
// Usage example
async function main() {
// Create a new transport client
const client = new TransportClient('https://example.com/webtransport');
try {
// Connect to the server
await client.connect();
// Send a datagram
await client.sendDatagram('Hello via datagram!');
// Create a bidirectional stream
const {streamId, writer} = await client.createBidirectionalStream();
// Send data through the stream
await client.sendOnStream(streamId, 'Hello via stream!');
// Close the connection when done
await client.close();
} catch (error) {
console.error('Error:', error);
}
}
Демонстрация связи в реальном времени с использованием WebTransport
// server.js
import { createServer } from "http";
import { WebTransport } from "@fails-components/webtransport";
const server = createServer();
const port = 8080;
// Create WebTransport server instance
const wtServer = new WebTransport({
port: port,
host: "localhost",
certificates: [] // Add your SSL certificates for production
});
wtServer.on("session", async (session) => {
console.log("New WebTransport session established");
// Handle bidirectional streams
session.on("stream", async (stream) => {
const reader = stream.readable.getReader();
const writer = stream.writable.getWriter();
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
// Echo received data back to client
const response = `Server received: ${new TextDecoder().decode(value)}`;
await writer.write(new TextEncoder().encode(response));
}
} catch (err) {
console.error("Stream error:", err);
} finally {
reader.releaseLock();
writer.releaseLock();
}
});
});
server.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
// client.js
class WebTransportClient {
constructor() {
this.transport = null;
this.stream = null;
}
async connect() {
try {
this.transport = new WebTransport("https://localhost:8080/webtransport");
await this.transport.ready;
console.log("WebTransport connection established");
// Handle connection close
this.transport.closed
.then(() => console.log("Connection closed normally"))
.catch((error) => console.error("Connection closed with error:", error));
} catch (err) {
console.error("Failed to establish WebTransport connection:", err);
}
}
async createStream() {
try {
this.stream = await this.transport.createBidirectionalStream();
console.log("Bidirectional stream created");
// Set up stream reader
this.startReading();
return this.stream;
} catch (err) {
console.error("Failed to create stream:", err);
}
}
async startReading() {
const reader = this.stream.readable.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
console.log("Received:", new TextDecoder().decode(value));
}
} catch (err) {
console.error("Error reading from stream:", err);
} finally {
reader.releaseLock();
}
}
async sendMessage(message) {
if (!this.stream) {
console.error("No active stream");
return;
}
const writer = this.stream.writable.getWriter();
try {
await writer.write(new TextEncoder().encode(message));
console.log("Message sent:", message);
} catch (err) {
console.error("Error sending message:", err);
} finally {
writer.releaseLock();
}
}
async close() {
if (this.transport) {
await this.transport.close();
console.log("Connection closed");
}
}
}
// Usage example
async function main() {
const client = new WebTransportClient();
// Connect to server
await client.connect();
// Create bidirectional stream
await client.createStream();
// Send test message
await client.sendMessage("Hello WebTransport!");
// Close connection after 5 seconds
setTimeout(async () => {
await client.close();
}, 5000);
}
main().catch(console.error);
Демонстрация WebTransport с Socket.IO
Вот демонстрационное приложение, показывающее коммуникацию в реальном времени с использованием API WebTransport.
Сначала создайте новый каталог, перейдите в него и инициализируйте проект Node.js:
mkdir webtransport-socket-example
cd webtransport-socket-example
npm init -yp
Примечание: WebTransport работает только в защищенном контексте (HTTPS), поэтому даже localhost
не является исключением. Необходимо сгенерировать SSL-сертификат и ключ.
openssl req -newkey rsa:2048 -keyout PRIVATEKEY.key -out MYCSR.csr
Подробнее о сертификатах — [https://www.ssl.com/how-to/manually-generate-a-certificate-signing-request-csr-using-openssl/](https://www.ssl.com/how-to/manually-generate-a-certificate-signing-request-csr-using-openssl/)
Далее установим несколько пакетов:
npm i express socket.io @fails-components/webtransport
npm i -D nodemon
Теперь определите код сервера и его сценарий запуска в файле package.json
:
"main": "server.js",
"scripts": {
"start": "nodemon"
},
"type": "module",
Создайте файл server.js
со следующим содержимым:
import { readFileSync } from 'node:fs'
import path from 'node:path'
import { createServer } from 'node:https'
import express from 'express'
// Read SSL key and certificate
const key = readFileSync('./key.pem')
const cert = readFileSync('./cert.pem')
// Create the Express app
const app = express()
// Serve `index.html` for all requests
app.use('*', (req, res) => {
res.sendFile(path.resolve('./index.html'))
})
// Create the HTTPS server
const httpsServer = createServer({ key, cert }, app)
const port = process.env.PORT || 443
// Start the server
httpsServer.listen(port, () => {
console.log(`Server listening at https://localhost:${port}`)
})
Создайте файл index.html
со следующим содержимым:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebTransport</title>
<link rel="icon" href="data:." />
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<h1>WebTransport</h1>
<p>Connection: <span id="connection">Disconnected</span></p>
<p>Transport: <span id="transport">Not Defined</span></p>
</body>
</html>
У нас есть два абзаца: один для статуса соединения и другой для механизма передачи данных.
Запустите npm start
, чтобы запустить сервер разработки. Перейдите по адресу https://localhost:3000
, примите использование самоподписанного сертификата, и вы готовы к работе!
Редактирование server.js для добавления поддержки WebSocket с помощью Socket.IO
Чтобы включить поддержку WebSocket на сервере, внесите следующие изменения:
// ...
import { Server } from 'socket.io'
// ...
const io = new Server(httpsServer)
// Handle connections
io.on('connection', (socket) => {
// Log the initial transport type: pooling, websocket, or webtransport (not yet available)
console.log(`connected with transport ${socket.conn.transport.name}`)
// Handle transport upgrade: pooling → websocket → webtransport
socket.conn.on('upgrade', (transport) => {
console.log(`transport upgraded to ${transport.name}`)
})
// Handle disconnections
socket.on('disconnect', (reason) => {
console.log(`disconnected due to ${reason}`)
})
})
Редактирование index.html для добавления поддержки WebSocket на клиенте
Вставьте следующий код перед тегом </head>
, чтобы включить библиотеку Socket.IO:
<script src="/socket.io/socket.io.js"></script>
Добавьте следующее перед тегом </body>
для обработки событий WebSocket:
<script>
const $connection = document.getElementById('connection')
const $transport = document.getElementById('transport')
const socket = io()
// Handle connection
socket.on('connect', () => {
console.log(`connected with transport ${socket.io.engine.transport.name}`)
$connection.textContent = 'Connected'
$transport.textContent = socket.io.engine.transport.name
// Handle transport upgrade
socket.io.engine.on('upgrade', (transport) => {
console.log(`transport upgraded to ${transport.name}`)
$transport.textContent = transport.name
})
})
// Handle connection errors
socket.on('connect_error', (err) => {
console.log(`connect_error due to ${err.message}`)
})
// Handle disconnections
socket.on('disconnect', (reason) => {
console.log(`disconnected due to ${reason}`)
$connection.textContent = 'Disconnected'
$transport.textContent = 'Unavailable'
})
</script>
Перезапустите сервер.
Для перезапуска сервера выполните следующую команду:
npm start
Проверка обновления WebSocket
После запуска сервера откройте приложение в браузере и убедитесь, что транспорт успешно обновлен до websocket
.
Добавление поддержки WebTransport
Расширьте файл server.js
, включив возможности WebTransport для расширенных транспортных опций. Вот как это сделать:
// ...
import { Http3Server } from '@fails-components/webtransport'
// ...
const io = new Server(httpsServer, {
// `webtransport` must be explicitly specified
transports: ['polling', 'websocket', 'webtransport'],
})
// Create an HTTP/3 server
const h3Server = new Http3Server({
port,
host: '0.0.0.0',
secret: 'changeit',
cert,
privKey: key,
})
// Start the HTTP/3 server
h3Server.startServer();
// Create a stream and pass it to `socket.io`
(async () => {
const stream = await h3Server.sessionStream('/socket.io/')
// Familiar processing logic
const sessionReader = stream.getReader()
while (true) {
const { done, value } = await sessionReader.read()
if (done) {
break
}
io.engine.onWebTransportSession(value)
}
})()
Редактирование index.html
Добавьте следующее, чтобы указать webtransport
в качестве параметра транспорта для Socket.IO:
<script>
// ...
const socket = io({
transportOptions: {
// Explicitly specify `webtransport`
webtransport: {
hostname: '127.0.0.1',
},
},
})
// ...
</script>
Перезапустите сервер.
Разрешение вопросов, связанных с сертификатами
Вы можете столкнуться с ошибкой, связанной с ненадежным сертификатом. Чтобы устранить эту ошибку, Chrome требует определенные флаги для правильной обработки протоколов HTTP/3 и QUIC. После исследования выяснилось, что необходимы три флага Chrome:
--ignore-certificate-errors-spki-list
: Игнорирует ошибки сертификата SSL для определенного сертификата (требуется хэш сертификата, см. ниже).--origin-to-force-quic-on
: Принудительное использование протокола QUIC для определенных источников.--user-data-dir
: Указывает каталог данных профиля пользователя (требуется по непонятным причинам).
Генерация хеша сертификата
Создайте скрипт generate_hash.sh
, который будет генерировать хэш сертификата:
#!/bin/bash
openssl x509 -pubkey -noout -in cert.pem |
openssl pkey -pubin -outform der |
openssl dgst -sha256 -binary |
base64
Запустите скрипт:
bash generate_hash.sh
Это создаст хэш вашего SSL-сертификата.
Запуск Chrome с необходимыми флагами
Создайте скрипт open_chrome.sh
для запуска Chrome с необходимыми флагами:
#!/bin/bash
google-chrome-stable \
--ignore-certificate-errors-spki-list="<INSERT_HASH_HERE>" \
--origin-to-force-quic-on="127.0.0.1:443" \
--user-data-dir="/path/to/your/profile"
Замените <INSERT_HASH_HERE>
хэшем, сгенерированным из generate_hash.sh
, и укажите соответствующий путь для --user-data-dir
.
Важные примечания
Конфигурация пути Chrome
Чтобы запустить Chrome с помощью команды chrome
, убедитесь, что путь к chrome.exe
включен в системную переменную среды Path
.
Пример пути: C:\Program Files\Google\Chrome\Application\chrome.exe
Хэш сертификата
Хэш для --ignore-certificate-errors-spki-list
— это тот, который был сгенерирован ранее с помощью скрипта generate_hash.sh
.
Запустите скрипт:
bash open_chrome.sh
Если вы столкнулись с ошибкой chrome: command not found
, просто выполните команду Chrome прямо в терминале:
chrome --ignore-certificate-errors-spki-list=AbpC9VJaXAcTrUG38g2lcCqobfGecqNmdIvLV1Ukkf8= --origin-to-force-quic-on=127.0.0.1:443 --user-data-dir=quic-user-data https://localhost :443
Выполнив эти шаги, вы сможете запустить Chrome с необходимыми настройками для работы с WebTransport через QUIC.
Благодаря этим изменениям ваш сервер и клиент теперь должны поддерживать WebTransport через Socket.IO.
Заключение
Выполнив шаги, описанные в этом руководстве, вы успешно настроили сервер WebTransport с использованием `socket.io` и Node.js, а также включили поддержку протокола HTTP/3 (QUIC) в Google Chrome. От создания SSL-сертификатов до обработки подключений WebTransport и настройки Chrome для доверия вашей пользовательской конфигурации, вы изучили практическую реализацию этого современного протокола.
WebTransport все еще является новой технологией, и хотя она пока не поддерживается нативно в Node.js, такие инструменты, как @fails-components/webtransport
, предоставляют возможность экспериментировать с ее возможностями. При правильной настройке и гибкости socket.io
вы можете использовать скорость и эффективность WebTransport для передачи данных в реальном времени в современных веб-приложениях.
Имейте в виду, что по мере развития WebTransport, скорее всего, появятся более стабильные и готовые к производству решения, что еще больше упростит интеграцию этого протокола в ваши проекты. На данный момент эта настройка служит отличной отправной точкой для экспериментов с будущим обмена данными в реальном времени.