DataChannels, основанные на QUIC, считаются альтернативой нынешнему SCTP-транспорту. Рабочая группа WebRTC в Google уже экспериментирует с ними:
Давайте мы тоже попробуем. Для этого мы создадим одностраничное приложение, похожее на пример WebRTC-канала для передачи текста – это полностью рабочий пример (причем без сигнальных серверов), который, к тому же, позволит легко сравнить подходы к реализации WebRTC DataChannels.
Прежде чем мы начнем, давайте вспомним основы DataChannel.
Вкратце о DataChannel
DataChannels в WebRTC позволяют участниками обмениваться произвольными данными. Они могут быть как надежными – что очень полезно при передаче файлов – так и ненадежными, что приемлемо для информации о позициях в играх. API представляет из себя расширение RTCPeerConnection
и выглядит так:
const dc = pc.createDataChannel("some label string");
// подождите, когда канал откроется – например, навесив хендлер –
// затем вызывайте метод send
dc.send("some string");
// на другой стороне
otherPc.addEventListener('datachannel', e => {
const channel = e.channel;
channel.onmessage = event => {
console.log('received', event.data);
});
});
На официальной странице WebRTC samples есть примеры отправки строк и бинарных данных.
DataChannel использует протокол SCTP. Он работает параллельно с RTP-транспортом для аудио- и видеопотоков. В отличие от UDP, который обычно используется аудио- и видеопотоками, SCTP предлагает много другой функциональности вроде мультиплексирования каналов по одному соединению или надежного, частично надежного (т.е. надежного, но неупорядоченного) и ненадежного режимов.
Google представил QUIC в 2012 (подробно про историю протокола и его нюансы можно почитать в другом нашем материале – прим. переводчика). Подобно WebRTC, протокола QUIC также был взят под крыло IETF и теперь это HTTP/3. У QUIC есть ряд отличных нововведений, как то: снижение задержки, расчет полосы пропускания на основе управления перегрузкой, прямая коррекция задержек (FEC) и реализация в пространстве пользователя (вместо ядра) для более быстрой раскатки.
QUIC может стать альтернативой RTCP для WebRTC – как транспорт для DataChannel. Текущий эксперимент пытается избежать использования RTCPeerConnection API (и SDP!), используя отдельную версию ICE-транспорта. Думайте об этом как о виртуальном соединении, которое добавляет немного безопасности и много NAT traversal.
В видео ниже – объяснение этой концепции от Ian Swett из команды Chrome networking. И хотя этому выступлению уже несколько лет, оно все же дает дополнительную информацию по теме:
Первые шаги с QUIC
К счастью, большая часть кода из статьи 2015 года остается актуальной и легко адаптируется под новое API. Давайте разберемся.
Склонируйте код отсюда или попробуйте его здесь. Обратите внимание, что Chrome (версия 73+ сейчас – это Canary) должен быть запущен со специальными флагами, чтобы эксперимент сработал локально:
google-chrome-unstable --enable-blink-features=RTCQuicTransport,RTCIceTransportExtension
Настройка ICE-транспорта
Спецификация RTCIceTransport основана на ORTC, поэтому настройка схожа со старым кодом:
const ice1 = new RTCIceTransport();
ice1.onstatechange = function() {
console.log('ICE transport 1 state change', ice1.state);
};
const ice2 = new RTCIceTransport();
ice2.onstatechange = function() {
console.log('ICE transport 2 state change', ice2.state);
};
// обмен ICE-кандидатами
ice1.onicecandidate = function(evt) {
console.log('1 -> 2', evt.candidate);
if (evt.candidate) {
ice2.addRemoteCandidate(evt.candidate);
}
};
ice2.onicecandidate = function(evt) {
console.log('2 -> 1', evt.candidate);
if (evt.candidate) {
ice1.addRemoteCandidate(evt.candidate);
}
};
// запуск ICE-транспортов
ice1.start(ice2.getLocalParameters(), 'controlling');
ice2.start(ice1.getLocalParameters(), 'controlled');
ice1.gather(iceOptions);
ice2.gather(iceOptions);
Заметьте, что в этом API нет RTCIceGatherer, в отличие от ORTC. Потому что у нас уже все необходимое, чтобы установить ICE-транспорт.
Настройка QUIC-транспорта
const quic1 = new RTCQuicTransport(ice1);
quic1.onstatechange = function() {
console.log('QUIC transport 1 state change', quic1.state);
};
const quic2 = new RTCQuicTransport(ice2);
quic2.onstatechange = function() {
console.log('QUIC transport 2 state change', quic2.state);
};
// добавляем хендлер для потока QUIC
quic2.addEventListener('quicstream', (e) => {
console.log('QUIC transport 2 got a stream', e.stream);
receiveStream = e.stream;
});
Здесь эксперимент отходит от спецификации, в которой используется идентификация на основе сертификата. Вместо этого в ход идет открытый ключ, как сказано в посте Google Developers:
Соединение RTCQuicTransport настраивается с открытым ключом API. На данный момент мы не планируем, чтобы это API заменило оригинальную проверку. Оно будет заменено сигнализацией о факте идентификации удаленных сертификатов, чтобы валидировать самоподписанные сертификаты – когда QUIC начнет поддерживать это в Chromium.
Пока все идет неплохо.
QUICStream для отправки и получения данных
Задействовать QUICStream немного сложнее, чем WebRTC DataChannel. Streams API (см. подробности на MDN), созданное рабочей группой WHATWG, было принято, но не внедрено.
Мы создаем sendStream
только после того, как QUIC-транспорт переходит в состояние «connected» – в другом состоянии это бы привело к ошибке:
quic1.onstatechange = function() {
console.log('QUIC transport 1 state change', quic1.state);
if (quic1.state === 'connected' && !sendStream) {
sendStream = quic1.createStream('webrtchacks'); // похоже на createDataChannel.
document.getElementById('sendButton').disabled = false;
document.getElementById('dataChannelSend').disabled = false;
}
};
Затем навешиваем обработчики на кнопку отправки и поле ввода: после клика по кнопке, текст из поля ввода кодируется в Uint8Array и пишется в поток:
document.getElementById('sendButton').onclick = () => {
const rawData = document.getElementById('dataChannelSend').value;
document.getElementById('dataChannelSend').value = '';
// нужен Uint8Array. Хорошо, что это легко сделать с помощью TextEncoder.
const data = encoder.encode(rawData);
sendStream.write({
data,
});
};
Первая запись вызовет событие onquicstream
на удаленном QUIC-транспорте:
// добавляем хендлер для потока QUIC
quic2.addEventListener('quicstream', (e) => {
console.log('QUIC transport 2 got a stream', e.stream);
receiveStream = e.stream;
receiveStream.waitForReadable(1)
.then(ondata);
});
… и затем мы ожидаем, когда данные станут доступны для прочтения:
function ondata() {
const buffer = new Uint8Array(receiveStream.readBufferedAmount);
const res = receiveStream.readInto(buffer);
const data = decoder.decode(buffer);
document.getElementById('dataChannelReceive').value = data;
receiveStream.waitForReadable(1)
.then(ondata);
}
Все данные из receiveStream
будут прочтены, декодированы в текст и помещены в поле вывода. И так каждый раз, когда будут появляться доступные для чтения данные.
Вывод и комментарии
Надеюсь, этот пример легче для понимания, чем аналогичный в блоге Google. Такой способ едва ли подходит для P2P-соединений, для них уже отлично справляется DataChannel на SCTP. Тем не менее, это может стать интересной альтернативой веб-сокетам с QUIC-сервером на другом конце. Пока это не случилось, следует определить достойный способ работы с ненадежными и неупорядоченными каналами. По моему мнению, предложения из вышеупомянутого поста больше выглядят хаками, нежели решениями.
Также неясно, какой обратной связи извне ждут разработчики. «Внедрите уже спецификацию вместо того, чтобы снова лепить шорткаты, которые останутся с нами на несколько лет», – звучит слишком очевидно. Плюс, общее мнение коммьюнити склоняется к тому, чтобы использовать потоки WHATWG, что выставляет в странном свете разработчиков, просящих потестировать собственное API для чтения данных.
Еще бы мне хотелось, чтобы SCTP в Chromium имел дополнительные функции. Например, вот этот запрос про DataChannel – самый рейтинговый, к слову – остается практически нетронутым уже три года. Не совсем понятно, почему имеет место фокус на QUIC, когда еще есть задачи по SCTP; однако это не должно никого останавливать от тестирования QUIC и обратной связи о результатах.
Комментарий Voximplant
Слово нашему Frontend-лиду irbisadm:
Достаточно давно наши SDK используют для сигнализации web socket. Это отличный, проверенный временем стандарт, но с ним есть некоторые проблемы. Во-первых, это TCP. А TCP не то чтобы хорош и быстр на мобильных сетях, плюс никак не поддерживает роуминг между сетями. Во-вторых, он зачастую текстовый (бинарный режим тоже есть, но его нечасто увидишь).
Недавно мы запустили закрытое бета-тестирование сигнального протокола на DataChannel. Это тоже протокол не без минусов, но так как он работает в плохих сетях и при роуминге, то покоряет с первого взгляда. Поменяли сеть? Не нужно пересоздавать подключение.
ICE Restart
в большинстве случаев поможет найти новый путь для трафика. Но, как я и говорил, у протокола есть пока недостатки: не все браузеры поддерживают все расширения протокола, такие как гарантированная доставка и поддержка порядка пакетов; также протокол не поддерживает gzip для текстового режима из коробки. Но все эти проблемы можно решить на стороне приложения.
Автор: nvpushkarskiy2