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

Не допускайте переполнения вашего приложения сообщениями WebSocket или переполнения сервера WebSocket сообщениями с помощью обратного давления.

Фон

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

API потоков

API потоков позволяет JavaScript программно обращаться к потокам данных, полученным по сети, и обрабатывать их по мере необходимости. Важным понятием в контексте потоков является обратное давление (backpressure) . Это процесс, посредством которого отдельный поток или цепочка каналов регулирует скорость чтения или записи. Когда сам поток или поток, расположенный далее в цепочке каналов, всё ещё занят и не готов принять новые блоки данных, он отправляет сигнал обратно по цепочке, чтобы замедлить доставку при необходимости.

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

Применение обратного давления к полученным сообщениям невозможно.

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

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

// A heavy data crunching operation.
const process = async (data) => {
  return new Promise((resolve) => {
    window.setTimeout(() => {
      console.log('WebSocket message processed:', data);
      return resolve('done');
    }, 1000);
  });
};

webSocket.onmessage = async (event) => {
  const data = event.data;
  // Await the result of the processing step in the message handler.
  await process(data);
};

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

Применение обратного давления к отправленным сообщениям неэргономично.

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

Что такое API WebSocketStream?

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

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

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

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

Текущий статус

Шаг Статус
1. Создайте пояснитель Полный
2. Создайте первоначальный проект спецификации В ходе выполнения
3. Собирайте отзывы и дорабатывайте дизайн В ходе выполнения
4. Испытание происхождения Полный
5. Запуск Не начато

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

API WebSocketStream основан на промисах, что делает работу с ним естественной в современном мире JavaScript. Вы начинаете с создания нового WebSocketStream и передаёте ему URL-адрес сервера WebSocket. Затем вы ожидаете opened соединения, что приводит к созданию 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.opened;
  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.opened;

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

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

Информация о закрытом соединении WebSocketStream

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

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

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

Закрытие соединения WebSocketStream

WebSocketStream можно закрыть с помощью AbortController . Поэтому передайте AbortSignal конструктору WebSocketStream . AbortController.abort() работает только до установления связи, а не после.

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

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

wss.close({closeCode: 4000, reason: 'Game over'});

Прогрессивное улучшение и взаимодействие

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

Обнаружение особенностей

Чтобы проверить, поддерживается ли API WebSocketStream, используйте:

if ('WebSocketStream' in window) {
  // `WebSocketStream` is supported!
}

Демо

В поддерживаемых браузерах вы можете увидеть API WebSocketStream в действии во встроенном iframe или непосредственно в Glitch .

Обратная связь

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

Расскажите нам о дизайне API

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

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

Вы обнаружили ошибку в реализации Chrome? Или реализация отличается от спецификации? Сообщите об ошибке на сайте new.crbug.com . Опишите проблему как можно подробнее, предоставьте простые инструкции по воспроизведению и введите Blink>Network>WebSockets в поле «Компоненты» .

Показать поддержку API

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

Отправьте твит @ChromiumDev , используя хэштег #WebSocketStream , и расскажите, где и как вы его используете.

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

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

API WebSocketStream был реализован Адамом Райсом и Ютакой Хирано .