Пожарный шланг, из которого вытекает вода

WebSocketStream: интеграция потоков с WebSocket API

Как с помощью обратной реакции не дать приложению утонуть в сообщениях WebSocket и не переполнить данными сервер WebSocket.

Published on Updated on

Translated to: English, Português, 한국어, 中文, 日本語

Общая информация

WebSocket API

WebSocket API предоставляет интерфейс JavaScript для протокола WebSocket, что позволяет открывать сеанс двусторонней интерактивной передачи данных между браузером пользователя и сервером. Используя этот API, можно отправлять сообщения на сервер и получать ответы на основе событий, не опрашивая сервер.

Streams API

Streams API позволяет JavaScript программно получать доступ к потокам фрагментов данных, полученных по сети, и обрабатывать их. Важным понятием в контексте потоков является обратная реакция — процесс, посредством которого отдельный поток или цепочка перенаправления регулирует скорость чтения или записи. Когда исходный поток или поток далее в цепочке перенаправления еще занят и не готов принимать следующие фрагменты, он отправляет обратно по цепочке сигнал о необходимости замедлить доставку данных.

Проблема с текущим WebSocket API

Невозможность применять обратную реакцию к полученным сообщениям

В текущем WebSocket API реакция на сообщение происходит в WebSocket.onmessage — обработчике EventHandler, который вызывается при получении сообщения от сервера.

Предположим, у нас есть приложение, которое с каждым полученным сообщением должно выполнять ресурсоемкую обработку данных. Соответствующий код наверняка будет похож на приведенный ниже. В нем мы ожидаем (await) результат вызова process(), поэтому всё должно работать, верно?

// Ресурсоемкая обработка данных.
const process = async (data) => {
return new Promise((resolve) => {
window.setTimeout(() => {
console.log('Сообщение WebSocket обработано:', data);
return resolve('done');
}, 1000);
});
};

webSocket.onmessage = async (event) => {
const data = event.data;
// Ждем результат этапа обработки в обработчике сообщений.
await process(data);
};

Неверно! Проблема с текущим WebSocket API в том, что мы не можем использовать обратную реакцию. Если сообщения приходят быстрее, чем их может обработать метод process(), процесс отрисовки либо забьет память, буферизируя эти сообщения, либо перестанет отвечать из-за 100 % загрузки ЦП (или произойдет и то, и другое).

Применять обратную реакцию к отправляемым сообщениям — неудобно

Применять обратную реакцию к отправляемым сообщениям можно, но для этого нужно запрашивать свойство WebSocket.bufferedAmount, что неэффективно и неудобно. Это доступное только для чтения свойство возвращает количество байтов данных, помещенных в очередь посредством вызовов WebSocket.send(), но еще не переданных в сеть. Значение сбрасывается на ноль после отправки всех данных в очереди, но если продолжить вызывать WebSocket.send(), оно будет повышаться.

Что представляет собой WebSocketStream API

WebSocketStream API решает проблему отсутствия надлежащей обратной реакции путем интеграции потоков с WebSocket API. То есть, обратную реакцию можно применять «бесплатно» — без дополнительных ресурсозатрат.

Предлагаемые варианты использования для WebSocketStream API

Примеры сайтов, которые могут использовать этот API:

  • Приложения WebSocket с высокой сетевой нагрузкой, которые должны оставаться интерактивными, в частности — решения для трансляции видео и демонстрации экрана.
  • Средства для видеозахвата и другие приложения, генерирующие в браузере большие объемы данных, который необходимо загрузить на сервер. Используя обратную реакцию, клиент может остановить производство данных и не накапливать их в памяти.

Текущее состояние

ЭтапСостояние
1. Написание поясненияГотово
2. Создание первоначального проекта спецификацииВыполняется
3. Сбор отзывов и доработка проектаВыполняется
4. Испытание в OriginГотово
5. ЗапускНе запущено

Как использовать WebSocketStream API

Вводный пример

WebSocketStream API работает на промисах, и поэтому естественным образом встраивается в современный мир JavaScript. Для начала мы создаем новый объект WebSocketStream передаем ему URL-адрес сервера WebSocket. Затем ждем , пока установится подключение (connection). В результате мы получаем ReadableStream и (или) WritableStream.

Вызывая метод ReadableStream.getReader(), мы в итоге получаем ReadableStreamDefaultReader, с помощью которого можно читать (read()) данные из потока до его завершения, то есть пока поток не вернет объект в форме {value: undefined, done: true}.

Аналогичным образом, вызывая метод WritableStream.getWriter(), мы в итоге получаем WritableStreamDefaultWriter, с помощью которого можно записывать (write()) данные.

  const wss = new WebSocketStream(WSS_URL);
const {readable, writable} = await wss.connection;
const reader = readable.getReader();
const writer = writable.getWriter();

while (true) {
const {value, done} = await reader.read();
if (done) {
break;
}
const result = await process(value);
await writer.write(result);
}

Обратная реакция

А что насчет обещанной функции обратной реакции? Как я писал выше, она дается «бесплатно» — ничего дополнительно делать не нужно: если для process() требуется больше времени, следующее сообщение будет потреблено, только когда конвейер будет готов. Соответственно, и действие WritableStreamDefaultWriter.write() будет выполняться только в том случае, когда это допустимо.

Более сложные примеры

Второй аргумент в WebSocketStream — это набор параметров, число которых может быть увеличено в будущем. Пока что есть только параметр protocols, который ведет себя так же, как второй аргумент в конструкторе WebSocket:

const chatWSS = new WebSocketStream(CHAT_URL, {protocols: ['chat', 'chatv2']});
const {protocol} = await chatWSS.connection;

Выбранный протокол (protocol) и возможные расширения (extensions) входят в словарь, доступный по промису WebSocketStream.connection. В этом промисе содержится вся информация о действующем подключении (если связь не установлена, эти данные неактуальны).

const {readable, writable, protocol, extensions} = await chatWSS.connection;

Информация о закрытом подключении WebSocketStream

Информацию, которая была доступна в событиях WebSocket.onclose и WebSocket.onerror в WebSocket API, теперь можно получить через промис WebSocketStream.closed. Если подключение закрыто ненадлежащим образом, промис отклоняется. В противном случае он разрешается с кодом и причиной, полученными от сервера.

Все возможные коды состояния и их значение объяснены в списке для CloseEvent.

const {code, reason} = await chatWSS.closed;

Закрытие подключения WebSocketStream

Закрыть WebSocketStream можно с помощью AbortController: достаточно передать AbortSignal в конструктор WebSocketStream.

const controller = new AbortController();
const wss = new WebSocketStream(URL, {signal: controller.signal});
setTimeout(() => controller.abort(), 1000);

Также можно использовать метод WebSocketStream.close(), но его основное предназначение — указание кода и причины, которые отправляются на сервер.

wss.close({code: 4000, reason: 'Игра закончена'});

Прогрессивное улучшение и совместимость

Пока что WebSocketStream API реализован только в браузере Chrome. Для совместимости с классическим WebSocket API применение обратной реакции к полученным сообщениям невозможно. Применять обратную реакцию к отправляемым сообщениям можно, но для этого нужно запрашивать свойство WebSocket.bufferedAmount, что неэффективно и неудобно.

Обнаружение функции

Как проверить, поддерживается ли WebSocketStream API:

if ('WebSocketStream' in window) {
// `WebSocketStream` поддерживается!
}

Демонстрация

Увидеть работу WebSocketStream API (если поддержка в браузере реализована) можно во встроенном iframe здесь или на странице Glitch.

Отзывы

Команде Chrome хотелось бы услышать ваши отзывы о работе с WebSocketStream API.

Расскажите, что вы думаете о структуре API

Что-то в API не работает должным образом? Или, может, отсутствуют методы или свойства, необходимые для реализации вашей идеи? Есть вопрос или комментарий по модели безопасности? Отправьте заявку о проблеме со спецификацией (Spec issue) в GitHub-репозиторий или прокомментируйте существующую.

Сообщите о проблеме с реализацией

Нашли ошибку в реализации функции в браузере Chrome? Реализация отличается от спецификации? Сообщите об ошибке на странице new.crbug.com. Опишите проблему как можно подробнее, дайте простые инструкции по ее воспроизведению и в поле Components укажите Blink>Network>WebSockets. Для демонстрации этапов воспроизведения ошибки удобно использовать Glitch.

Окажите поддержку API

Планируете использовать WebSocketStream API? Ваш публичный интерес помогает команде Chrome определять приоритет функций и показывает важность их поддержки разработчикам других браузеров.

Упомяните в твите @ChromiumDev, поставьте хештег #WebSocketStream и расскажите, как вы используете эту функцию.

Полезные ссылки

Благодарности

Авторы реализации WebSocketStream API — Адам Райс и Ютака Хирано. Автор главного изображения — Даан Муйдж, платформа Unsplash.

Updated on Improve article

We serve cookies on this site to analyze traffic, remember your preferences, and optimize your experience.