WebSocketStream: integrowanie strumieni z interfejsem WebSocket API

Zapobiegaj zalewaniu aplikacji wiadomościami WebSocket lub zalewaniu serwera WebSocket wiadomościami, stosując mechanizm backpressure.

Tło

Interfejs WebSocket API udostępnia interfejs JavaScriptu do protokołu WebSocket, który umożliwia otwieranie dwukierunkowej interaktywnej sesji komunikacyjnej między przeglądarką użytkownika a serwerem. Za pomocą tego interfejsu API możesz wysyłać wiadomości na serwer i otrzymywać odpowiedzi oparte na zdarzeniach bez odpytywania serwera o odpowiedź.

Streams API

Interfejs Streams API umożliwia JavaScriptowi programowy dostęp do strumieni fragmentów danych otrzymywanych przez sieć i przetwarzanie ich w dowolny sposób. Ważnym pojęciem w kontekście strumieni jest przeciwciśnienie. Jest to proces, w którym pojedynczy strumień lub łańcuch potoków reguluje szybkość odczytu lub zapisu. Gdy strumień lub strumień w dalszej części łańcucha jest zajęty i nie jest jeszcze gotowy do przyjęcia kolejnych fragmentów, wysyła sygnał wstecz przez łańcuch, aby odpowiednio spowolnić dostarczanie.

Problem z obecnym interfejsem WebSocket API

Nie można zastosować mechanizmu backpressure do odebranych wiadomości

W przypadku obecnego interfejsu WebSocket API reakcja na wiadomość następuje w WebSocket.onmessage, czyli w EventHandler wywoływanym po otrzymaniu wiadomości z serwera.

Załóżmy, że masz aplikację, która musi wykonywać operacje przetwarzania dużych ilości danych za każdym razem, gdy otrzyma nową wiadomość. Prawdopodobnie skonfigurujesz przepływ podobny do kodu poniżej, a ponieważ await wynik wywołania process(), wszystko powinno być w porządku, prawda?

// 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);
};

Źle! Problem z obecnym interfejsem WebSocket API polega na tym, że nie ma możliwości zastosowania mechanizmu backpressure. Gdy wiadomości przychodzą szybciej, niż może je obsłużyć metoda process(), proces renderowania albo zapełni pamięć, buforując te wiadomości, albo przestanie odpowiadać z powodu 100-procentowego wykorzystania procesora, albo jedno i drugie.

Stosowanie mechanizmu backpressure do wysłanych wiadomości jest nieergonomiczne

Stosowanie mechanizmu backpressure do wysyłanych wiadomości jest możliwe, ale wymaga sondowania właściwości WebSocket.bufferedAmount, co jest nieefektywne i nieergonomiczne. Ta właściwość tylko do odczytu zwraca liczbę bajtów danych, które zostały umieszczone w kolejce za pomocą wywołań funkcji WebSocket.send(), ale nie zostały jeszcze przesłane do sieci. Ta wartość resetuje się do zera po wysłaniu wszystkich danych w kolejce, ale jeśli będziesz nadal wywoływać funkcję WebSocket.send(), będzie ona rosnąć.

Co to jest interfejs WebSocketStream API?

Interfejs WebSocketStream API rozwiązuje problem z nieistniejącym lub nieergonomicznym ciśnieniem zwrotnym, integrując strumienie z interfejsem WebSocket API. Oznacza to, że mechanizm backpressure może być stosowany „bezpłatnie”, bez dodatkowych kosztów.

Sugerowane przypadki użycia interfejsu WebSocketStream API

Przykłady witryn, które mogą korzystać z tego interfejsu API:

  • Aplikacje WebSocket o dużej przepustowości, które muszą zachować interaktywność, w szczególności aplikacje do udostępniania wideo i ekranu.
  • Podobnie jest w przypadku aplikacji do nagrywania filmów i innych aplikacji, które generują w przeglądarce duże ilości danych wymagających przesłania na serwer. Dzięki mechanizmowi backpressure klient może przestać generować dane, zamiast gromadzić je w pamięci.

Obecny stan,

Krok Stan
1. Tworzenie wyjaśnienia Zakończono
2. Tworzenie wstępnej wersji specyfikacji W toku
3. Zbieranie opinii i ulepszanie projektu W toku
4. Wersja próbna origin Zakończono
5. Uruchom Nie rozpoczęto

Jak korzystać z interfejsu WebSocketStream API

Interfejs WebSocketStream API jest oparty na obietnicach, co sprawia, że korzystanie z niego jest naturalne w nowoczesnym świecie JavaScriptu. Zacznij od utworzenia nowego obiektu WebSocketStream i przekazania mu adresu URL serwera WebSocket. Następnie czekasz na nawiązanie połączenia opened, co powoduje wyświetlenie ReadableStream lub WritableStream.

Wywołując metodę ReadableStream.getReader(), uzyskasz w końcu obiekt ReadableStreamDefaultReader, z którego możesz read() dane, dopóki strumień nie zostanie zakończony, czyli dopóki nie zwróci obiektu w formie {value: undefined, done: true}.

W związku z tym wywołując metodę WritableStream.getWriter() uzyskujesz ostatecznie WritableStreamDefaultWriter, do którego możesz następnie write() dane.

  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);
  }

Ciśnienie zwrotne

A co z obiecaną funkcją ograniczenia ciśnienia zwrotnego? Otrzymasz go „bezpłatnie” i nie musisz wykonywać żadnych dodatkowych czynności. Jeśli process() zajmie więcej czasu, następna wiadomość zostanie przetworzona dopiero wtedy, gdy potok będzie gotowy. Podobnie WritableStreamDefaultWriter.write() krok jest wykonywany tylko wtedy, gdy jest to bezpieczne.

Przykłady zaawansowane

Drugi argument WebSocketStream to pakiet opcji, który umożliwia przyszłe rozszerzenia. Jedyną opcją jest protocols, która działa tak samo jak drugi argument konstruktora WebSocket:

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

Wybrany protocol oraz potencjalny extensions są częścią słownika dostępnego za pomocą obietnicy WebSocketStream.opened. Wszystkie informacje o połączeniu na żywo są dostarczane przez tę obietnicę, ponieważ nie ma znaczenia, czy połączenie się nie powiedzie.

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

Informacje o zamkniętym połączeniu WebSocketStream

Informacje, które były dostępne w przypadku zdarzeń WebSocket.oncloseWebSocket.onerror w interfejsie WebSocket API, są teraz dostępne za pomocą obietnicy WebSocketStream.closed. Obietnica jest odrzucana w przypadku nieprawidłowego zamknięcia. W przeciwnym razie jest ona rozwiązywana z użyciem kodu i przyczyny wysłanych przez serwer.

Wszystkie możliwe kody stanu i ich znaczenie znajdziesz na liście CloseEvent kodów stanu.

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

Zamykanie połączenia WebSocketStream

Strumień WebSocketStream można zamknąć za pomocą elementu AbortController. Dlatego przekaż obiekt AbortSignal do konstruktora WebSocketStream. AbortController.abort() działa tylko przed uzgodnieniem połączenia, a nie po nim.

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

Możesz też użyć metody WebSocketStream.close(), ale jej głównym celem jest umożliwienie określenia kodu i przyczyny wysyłanej na serwer.

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

Progresywne ulepszanie i współdziałanie

Obecnie Chrome jest jedyną przeglądarką, która implementuje interfejs WebSocketStream API. W przypadku interoperacyjności z klasycznym interfejsem WebSocket API nie można stosować mechanizmu backpressure do otrzymywanych wiadomości. Stosowanie mechanizmu backpressure do wysyłanych wiadomości jest możliwe, ale wymaga sondowania właściwości WebSocket.bufferedAmount, co jest nieefektywne i nieergonomiczne.

Wykrywanie cech

Aby sprawdzić, czy interfejs WebSocketStream API jest obsługiwany, użyj tego kodu:

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

Prezentacja

W obsługiwanych przeglądarkach możesz zobaczyć działanie interfejsu WebSocketStream API w osadzonym elemencie iframe lub bezpośrednio na platformie Glitch.

Prześlij opinię

Zespół Chrome chce poznać Twoje wrażenia związane z interfejsem WebSocketStream API.

Opisz projekt interfejsu API

Czy w API jest coś, co nie działa tak, jak oczekujesz? Czy brakuje metod lub właściwości, które są potrzebne do realizacji Twojego pomysłu? Masz pytania lub uwagi dotyczące modelu zabezpieczeń? Zgłoś problem ze specyfikacją w odpowiednim repozytorium GitHub lub dodaj swoje uwagi do istniejącego problemu.

Zgłaszanie problemu z implementacją

Czy w implementacji Chrome występuje błąd? A może implementacja różni się od specyfikacji? Zgłoś błąd na stronie new.crbug.com. Podaj jak najwięcej szczegółów, proste instrukcje odtwarzania i wpisz Blink>Network>WebSockets w polu Komponenty.

Wyrażanie poparcia dla interfejsu API

Czy zamierzasz używać interfejsu WebSocketStream API? Twoje publiczne wsparcie pomaga zespołowi Chrome ustalać priorytety funkcji i pokazuje innym dostawcom przeglądarek, jak ważne jest ich obsługiwanie.

Wyślij tweeta do @ChromiumDev z hasztagiem #WebSocketStream i napisz, gdzie i jak korzystasz z tej funkcji.

Przydatne linki

Podziękowania

Interfejs WebSocketStream API został zaimplementowany przez Adama Rice’a i Yutakę Hirano.