WebTransport: как устроен «WebSocket для HTTP/3» — разбор до фреймов и байтов
TL;DR:
- WebTransport — не новый транспорт, а тонкий слой поверх обычного HTTP/3: сессии, потоки и датаграммы внутри одного QUIC-соединения.
- Сессия открывается расширенным CONNECT, её идентификатор — номер стрима этого самого запроса.
- Потоки опознаются сигнальными байтами 0x54/0x41 и Session ID в varint; датаграммы — QUIC-фреймы с «четвертью» номера стрима в префиксе.
- Для пассивного наблюдателя всё это неотличимо от обычного HTTPS/3-трафика: различимо по поведению, не по сигнатурам. Активный наблюдатель может узнать о поддержке WT из SETTINGS — но не больше.
WebSocket появился в 2011 году и с тех пор почти не менялся. При всей вездесущности у него две хронические болезни. Во-первых, он живёт поверх TCP, а TCP — это один упорядоченный байтовый поток: потеря одного сегмента останавливает доставку всего, что за ним едет, включая кадры других логических каналов. Во-вторых, доставка только надёжная и упорядоченная — а играм, живому видео и телеметрии нужна семантика «лучше потерять, чем задержать». WebRTC DataChannel умеет и то и другое, но тянет за собой весь стек ICE/SDP/DTLS, спроектированный под p2p-медиа-звонки.
WebTransport — ответ W3C и IETF на эту дырку в стеке: простой клиент-серверный транспорт поверх HTTP/3 с мультиплексированными потоками и ненадёжными датаграммами. В этой статье разберём его не как обзор API, а до механики: как сессия открывается через extended CONNECT, как опознаются потоки и зачем делят номер стрима на четыре — и что из всего этого видит наблюдатель на проводе.
Статья пригодится тем, кто пишет реалтайм-сервисы, и тем, кто просто хочет понимать, что происходит под капотом HTTP/3.
1. Дырка в стеке: почему WebSocket и WebRTC не закрыли задачу
Пройдясь по столбцам:
WebSocket. Все кадры — текстовые, бинарные, ping/pong — едут в одном TCP-потоке, и между ними нет никакого мультиплексирования. Хотите десять логических каналов — режьте кадры своего протокола сами и живите с тем, что потеря сегмента на канале №1 стопорит каналы №2–№10. HTTP/2 (RFC 8441) убрал Upgrade-танец, но не саму проблему: стрим-то по-прежнему один.
WebRTC DataChannel. SCTP поверх DTLS даёт потоки и частичную надёжность, и это работает — но обвязка убийственна: сигналинг, обмен ICE-кандидатами, сертификаты, SDP-строки на полстраницы. Всё это спроектировано под медиа-звонки между двумя браузерами. Для «браузер ↔ свой сервер» это лишний этаж со своей эксплуатационной ценой.
WebTransport занял пустую клетку: простота WebSocket + стримы и датаграммы поверх QUIC, без p2p-обвязки.
2. Слои: что именно стандартизовано
Главная мысль: WebTransport — не новый транспорт. Это тонкий слой мультиплексирования поверх обычного HTTP/3-соединения. Внутри одного H3-соединения может жить несколько независимых WT-сессий — например, к разным приложениям, — и все они делят одно шифрование, один сетевой путь и один congestion control.
Уточнение по номерам стандартов, чтобы не запутаться (в интернете их до сих пор путают): связку «HTTP-датаграммы и капсульный протокол» стандартизирует RFC 9297 — и он общий для WebTransport и MASQUE. Само же привязывание WebTransport к HTTP/3 — пока активный IETF-черновик draft-ietf-webtrans-http3 (на 2026 год — в WGLC), а API браузера — спецификация W3C. Черновик живой: детали ниже приведены по редакции -16, и именно здесь за последние редакции менялись токены и коды — при отладке всегда сверяйтесь с актуальной версией.
Существует и WebTransport over HTTP/2 — черновой фолбэк для сетей, где UDP недоступен: стримы там работают, а датаграммная семантика эмулируется капсулами поверх надёжного потока — API-совместимо, но потери чинит транспорт, «отправил и забыл» перестаёт быть ненадёжным. Дальше говорим только про вариант над HTTP/3.
3. Как открывается сессия: extended CONNECT
Механизм — расширенный CONNECT, тот же, что использует WebSocket над HTTP/2 и HTTP/3 (RFC 8441 / RFC 9220):
Детали, которые важны:
- Почему «расширенный». Классический CONNECT (из прокси-мира) имел только :authority. Extended CONNECT добавил :scheme, :protocol и :path — без них не объяснить, какой протокол мультиплексируется и по какому пути приложения.
- Токен протокола — webtransport-h3. В ранних редакциях черновика (и в H2-варианте WebTransport) использовался токен webtransport; интероп между редакциями ещё наводится, поэтому при отладке первым делом смотрите, какой токен шлёт ваш клиент.
- Возможность сессии согласуется заранее. Сервер анонсирует поддержку WT настройкой HTTP/3 SETTINGS_WT_ENABLED (0x2c7cf000), а поддержку датаграмм — настройкой SETTINGS_H3_DATAGRAM из RFC 9297 плюс транспортным параметром QUIC max_datagram_frame_size. Если чего-то нет, сессию не поднять, и это выясняется при установке соединения, а не в рантайме посреди игры.
- Session ID = номер стрима, на котором прошёл CONNECT. В примере выше — 0 (первый клиентский двунаправленный стрим). Из этого номера однозначно восстанавливается и владелец, и тип стрима.
- Origin-модель безопасности. Браузер всегда присылает origin, и сервер обязан его валидировать. Иначе любой сайт в открытой вкладке сможет открыть WT-сессию к вашему серверу от лица посетителя — класс CSRF, только с транспортной семантикой.
4. Потоки: как опознаётся WT-стрим
WT-поток — это самый обычный QUIC-стрим, но его первые байты — «паспорт», и устроен он по-разному для разных типов потока:
- Унидирекционный поток начинается с типа потока 0x54 (QUIC-varint), затем Session ID (varint), затем — ваши данные. Это в духе всех унистримов HTTP/3: у них первый «полезный» байты всегда тип (0x00 — control, 0x01 — QPACK-encoder и так далее), WT просто завёл себе следующий номер.
- Двунаправленный поток начинается с сигнального значения 0x41 (в реестре H3-фреймов оно зарегистрировано как WT_STREAM), затем Session ID, затем тело.
Зачем два механизма? Двунаправленные стримы в HTTP/3 — это запросные стримы: их первый фрейм обычно HEADERS (0x01). Получатель обязан по первым байтам понять, что перед ним не очередной HTTP-запрос, а поток WT-сессии, — отсюда отдельный сигнал 0x41. Унистримам хватает типа потока: запросов там не бывает by construction.
Дальше — никакой обвязки: ваш байтовый поток и есть поток. Session ID в паспорте адресует его нужной сессии, потому что сессий на одном соединении может быть несколько.
Семантика доставки: стримы всегда надёжные и упорядоченные — как TCP. Но упорядоченность — в пределах одного стрима, между стримами порядка нет. Потеря пакета с данными стрима A не останавливает доставку стрима B. Это и есть «нет head-of-line blocking», ради которого всё затевалось.
5. Датаграммы: UDP-семантика внутри QUIC
RFC 9221 добавил к QUIC фрейм DATAGRAM: полезная нагрузка без подтверждений, без ретрансмиссий, без порядка. Дошло — дошло; потерялось — потерялось навсегда. В QUIC-пакете это фрейм типа 0x30 (длина неявная — данные до конца пакета) или 0x31 (с явным полем длины).
Как устроена полезная нагрузка фрейма — определяет слой HTTP-датаграмм из RFC 9297 (общий у WebTransport и MASQUE), а поверх него — уже семантика WebTransport:
Зачем «четверть стрима»: младшие два бита QUIC stream ID кодируют тип стрима, а для Session ID они всегда нули (CONNECT-стрим — клиентский двунаправленный, биты 00). Деление на четыре отбрасывает эти гарантированно нулевые биты и держит заголовок компактным. Честная оговорка о выигрыше: байт экономится только когда полный ID пересекает границу длины varint (64, 16384…). Для сессии 0 экономии нет вовсе — это оптимизация под соединения с десятками сессий, а не «байт с каждой посылки».
Ограничения, о которых молчат туториалы:
- размер датаграммы ограничен PMTU — примерно 1200 байт с учётом заголовков; фрагментации нет, резать полезную нагрузку обязано приложение;
- если приложение пишет быстрее, чем endpoint отправляет, лишние датаграммы молча сбрасываются — очередей «дождаться» не предусмотрено.
6. Капсулы: WT_CLOSE_SESSION и WT_DRAIN_SESSION
Управляющий канал сессии — сам CONNECT-стрим. Капсульный протокол (TLV-фрейминг) определён в RFC 9297, а конкретные типы капсул для WebTransport регистрирует WT-черновик:
- WT_CLOSE_SESSION (0x2843) — немедленное закрытие: все потоки сбрасываются, код ошибки и причина едут в value. «RST всем сразу».
- WT_DRAIN_SESSION (0x78ae) — вежливое завершение: «новых потоков не открывайте, существующие доделайте». Например, перед деплоем новой версии сервера.
Свежие редакции драфта добавили и собственное управление потоком на уровне сессии — капсулы семейства WT_MAX_STREAMS / WT_MAX_DATA с парными «blocked»-капсулами: сессия тюнит свои окна, не трогая настройки всего H3-соединения. Капсулы едут внутри CONNECT-стрима и зашифрованы так же, как весь трафик после рукопожатия.
7. Один обмен под лупой
Соединение: браузер game.example ↔ сервер wt.example.com. Что реально происходит в байтах.
- QUIC Initial, клиент → сервер. Первый пакет рукопожатия. Свойство, о котором часто забывают: Initial-пакеты QUIC шифруются ключами, выводимыми из Destination Connection ID по публичному salt из спецификации версии. Эта защита — от случайной порчи middlebox'ами, а не от наблюдателя: любой на пути может расшифровать Initial и увидеть TLS ClientHello — SNI (wt.example.com) и ALPN (h3). Современный штрих: с постквантовым key share (браузеры шлют X25519+Kyber) ClientHello перестал влезать в один Initial и занимает несколько пакетов.
- 1-RTT пакеты. Всё после рукопожатия — под ключами сессии и для наблюдателя непрозрачно. Внутри: клиент открывает двунаправленный стрим 0 и шлёт HEADERS-фрейм (тип 0x01) с QPACK-блоком: :method CONNECT, :protocol webtransport-h3, :path /session.
- Сервер отвечает на том же стриме HEADERS с :status 200. Сессия установлена, Session ID = 0.
- Датаграмма. Клиент отправляет позицию игрока: в 1-RTT пакете едет DATAGRAM-фрейм 0x31, длина, quarter stream ID = 0/4 = 0 (однобайтовый varint 0x00), дальше — байты приложения.
- Новый унидирекционный стрим. С номерами осторожнее, чем кажется: первые клиентские унистримы — 2 (control-стрим HTTP/3), 6 и 10 (QPACK encoder и decoder) — уже заняты протоколом. Первый свободный — 14. Его первые байты: 0x54 (тип WT-потока), varint(0) — Session ID, затем данные.
- Завершение. Сервер дописывает в стрим 0 капсулу WT_DRAIN_SESSION; клиент доделывает существующие потоки и перестаёт открывать новые.
Ни одного «специального» пакета — весь обмен это обычный HTTP/3-трафик с обычным ALPN.
8. Клиент за 20 строк
Браузерный API (W3C):
Поддержка браузеров на 2026 год: Chrome/Edge — стабильно с 97-й (2022), Firefox — с 114-й (июнь 2023), Safari — последним из большой тройки: без экспериментального флага — с 26.4 (март 2026); с этого момента WebTransport считается Baseline. Актуальный статус — webstatus.dev / caniuse.
Серверная сторона: webtransport-go — референсная реализация на Go поверх quic-go; @fails-components/webtransport для Node/JS; строительные блоки (QUIC + HTTP/3 + датаграммы) есть в quiche (Rust) и aioquic (Python). Готового «nginx для WT» пока нет: обратные прокси, понимающие WT-сессии, — редкость, закладывайте терминирование на своём сервере.
Две практические заметки, сэкономящие вечер:
- Самоподписанные сертификаты в разработке. Конструктор принимает опцию serverCertificateHashes — пиннинг хэша сертификата сервера вместо проверки цепочки. Не нужно городить локальный CA ради стенда.
- Ловушка с ready. Если сервер не поддерживает WT и молчит на CONNECT, промис ready висит бесконечно — вежливой ошибки не будет. Оборачивайте установку сессии в свой таймаут.
9. Что видит наблюдатель
Разбор наблюдаемости — часть инженерной гигиены: понимая, что видно на проводе, вы осознанно принимаете решения о топологии и защите.
Пассивный DPI до рукопожатия: версия QUIC, DCID, SNI, ALPN — открыты по построению (п. 1 выше). Спрятать это должен ECH — но это отдельная история с отдельной драмой.
Пассивный DPI после рукопожатия: ничего. 1-RTT пакеты непрозрачны: ни имён, ни путей, ни фреймов, ни настроек. WT-сессия снаружи выглядит как «долгоживущее H3-соединение к wt.example.com» — таких на любой странице десяток: fetch, EventSource, медиа.
Отличительных маркеров протокола пассивно нет: тот же ALPN h3, те же типы фреймов, что у обычного HTTP/3.
Активный наблюдатель — сильнее, чем обычно пишут. Сервер, поддерживающий WT, объявляет это открыто в SETTINGS_WT_ENABLED; любой, кто завершил рукопожатие, узнает о поддержке, даже не пытаясь открыть сессию. Так что честная формулировка такая: WT неотличим от HTTPS/3 для пассивного наблюдателя; активная проба одним рукопожатием отличает «сервер с WT» от «сервера без WT». Всё, что дальше, — уже не скрытность, а авторизация: origin-проверка из §3 отвалит чужие сайты, неавторизованный CONNECT получит отказ.
Что остаётся у пассивного аналитика:
- поведенческий анализ — устойчивый каденс датаграмм, паттерны открытия стримов, симметричные объёмы; статистика, а не сигнатуры.
Цена адресной фильтрации: отличить WT от «просто HTTP/3» дёшево нельзя — можно только давить UDP/443 целиком. Но QUIC сегодня — это Google, Cloudflare, Apple (Private Relay), Яндекс, банки. Практика отдельных сетей показывает, что давление на UDP случается — и тогда страдают все QUIC-приложения без разбора. Честный вывод: «едет внутри HTTP/3» означает «наследует судьбу HTTP/3», а не «неуязвим». WebTransport решает задачи мультиплексирования и латентности; выживание в конкретной сети — свойство самого H3 и политики вокруг него, а не этого слоя.
10. Подводные камни
- PMTU. Датаграмма ~1200 байт максимум, без фрагментации: для крупных сообщений нужен свой framing поверх или стримы.
- Ненадёжность — ваша забота. Ретрансмиссий нет: протокол поверх датаграмм обязан терпеть потери и дубликаты, иначе не используйте датаграммы вовсе. И учтите: датаграммы, как и стримы, подчиняются congestion control — «отправил и забыл» означает «без ACK», а не «мимо перегрузки».
- Один congestion control на всё соединение. Все сессии и потоки принадлежат одному клиенту — и конкурируют между собой: одна жадная сессия легко съедает окно у остальных ваших же сессий. Приоритезация — уровень H3 (extensible priorities); в свежих редакциях WT-черновика появились и свои капсулы управления потоком (§6), но зрелой поддержки в реализациях пока мало.
- Нет NAT traversal. Строгая клиент-серверная модель: ICE и релеев нет. Если UDP до вашего сервера не доходит — фолбэк на WT over HTTP/2, где датаграммная семантика эмулируется капсулами надёжного стрима.
- Молодая и подвижная экосистема. Редакции черновика меняли токены, коды капсул и настройки — реализации расходятся ровно на этих швах. Тестируйте конкретную пару клиент–сервер, а не «WebTransport вообще».
11. Куда это движется
Главный массовый заказчик стека — MoQ, Media over QUIC (рабочая группа IETF): переосмысление живого видео, стартовавшее именно поверх WebTransport (позже появились варианты и поверх сырого QUIC). Дальше по списку — облачный гейминг, коллаборативные редакторы, удалённые столы, IoT-телеметрия.
Отдельной линией из того же семейства extended CONNECT развивается MASQUE (RFC 9298, CONNECT-UDP) — туннелирование UDP через HTTP; это то, на чём работает Apple Private Relay. Разбор MASQUE — кандидат на следующую статью.
Ссылки
- RFC 9297 — HTTP Datagrams and the Capsule Protocol (общий слой для WebTransport и MASQUE)
- draft-ietf-webtrans-http3 — WebTransport over HTTP/3 (актуальная редакция спецификации, WGLC; факты в статье — по редакции -16)
- draft-ietf-webtrans-overview — The WebTransport Protocol Framework
- RFC 9221 — Unreliable Datagram Extension to QUIC
- RFC 9114 — HTTP/3
- RFC 9000 — QUIC: A UDP-Based Multiplexed and Secure Transport
- RFC 9220 / RFC 8441 — Bootstrapping WebSockets с HTTP/3 / HTTP/2 (extended CONNECT)
- RFC 9298 — Proxying UDP in HTTP (MASQUE, CONNECT-UDP)
- W3C — спецификация WebTransport API