Protokół WebSocket, opisany w specyfikacji RFC 6455, zapewnia sposób wymiany danych pomiędzy przeglądarką a serwerem za pomocą trwałego połączenia. Dane mogą być przekazywane w obu kierunkach jako „pakiety”, bez konieczności zrywania połączenia i dodatkowych zapytań HTTP.

WebSocket jest szczególnie przydatny dla usług wymagających ciągłej wymiany danych, np. gier online, systemów transakcyjnych działających w czasie rzeczywistym itp.

Prosty przykład

Aby otworzyć połączenie websocket, musimy utworzyć new WebSocket używając specjalnego protokołu ws w adresie url:

let socket = new WebSocket("ws://javascript.info");

Istnieje również protokół szyfrowany wss://. Jest to jak HTTPS dla websockets.

Zawsze preferuj wss://

Protokół wss:// jest nie tylko szyfrowany, ale także bardziej niezawodny.

To dlatego, że ws:// dane nie są zaszyfrowane, widoczne dla każdego pośrednika. Stare serwery proxy nie wiedzą o WebSocket, mogą zobaczyć „dziwne” nagłówki i przerwać połączenie.

Z drugiej strony, wss:// to WebSocket over TLS, (tak samo jak HTTPS to HTTP over TLS), warstwa bezpieczeństwa transportu szyfruje dane u nadawcy i deszyfruje u odbiorcy. Więc pakiety danych są przekazywane zaszyfrowane przez serwery proxy. Nie widzą one co jest w środku i nie mogą ich przepuścić.

Po utworzeniu gniazda, powinniśmy nasłuchiwać zdarzeń na nim. Istnieją w sumie 4 zdarzenia:

  • open – połączenie nawiązane,
  • message – dane odebrane,
  • error – błąd websocket,
  • close – połączenie zamknięte.

…A jeśli chcielibyśmy coś wysłać, to socket.send(data) to zrobi.

Tutaj przykład:

let socket = new WebSocket("wss://javascript.info/article/websocket/demo/hello");socket.onopen = function(e) { alert(" Connection established"); alert("Sending to server"); socket.send("My name is John");};socket.onmessage = function(event) { alert(` Data received from server: ${event.data}`);};socket.onclose = function(event) { if (event.wasClean) { alert(` Connection closed cleanly, code=${event.code} reason=${event.reason}`); } else { // e.g. server process killed or network down // event.code is usually 1006 in this case alert(' Connection died'); }};socket.onerror = function(error) { alert(` ${error.message}`);};

Dla celów demonstracyjnych uruchomiony jest mały serwer server.js napisany w Node.js, dla powyższego przykładu. Odpowiada on komunikatem „Hello from server, John”, następnie czeka 5 sekund i zamyka połączenie.

Więc zobaczysz zdarzenia openmessageclose.

To właściwie tyle, możemy już mówić WebSocket. Całkiem proste, nieprawdaż?

Teraz porozmawiajmy bardziej dogłębnie.

Otwarcie websocket

Po utworzeniu new WebSocket(url) od razu zaczyna się łączyć.

Podczas połączenia przeglądarka (używając nagłówków) pyta serwer: „Czy obsługujesz Websocket?”. I jeśli serwer odpowie „tak”, to rozmowa jest kontynuowana w protokole WebSocket, który wcale nie jest HTTP.

Tutaj przykład nagłówków przeglądarki dla żądania wykonanego przez new WebSocket("wss://javascript.info/chat").

GET /chatHost: javascript.infoOrigin: https://javascript.infoConnection: UpgradeUpgrade: websocketSec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==Sec-WebSocket-Version: 13

  • Origin – pochodzenie strony klienta, np. https://javascript.info. Obiekty WebSocket są z natury cross-origin. Nie ma żadnych specjalnych nagłówków ani innych ograniczeń. Stare serwery i tak nie są w stanie obsłużyć WebSocket, więc nie ma problemów z kompatybilnością. Ale nagłówek Origin jest ważny, ponieważ pozwala serwerowi zdecydować, czy chce rozmawiać przez WebSocket z tą stroną, czy nie.
  • Connection: Upgrade – sygnalizuje, że klient chciałby zmienić protokół.
  • Upgrade: websocket – żądany protokół to „websocket”.
  • Sec-WebSocket-Key – losowy klucz wygenerowany przez przeglądarkę w celu zapewnienia bezpieczeństwa.
  • Sec-WebSocket-Version – wersja protokołu WebSocket, aktualna to 13.
WebSocket handshake nie może być emulowany

Nie możemy użyć XMLHttpRequest lub fetch do wykonania tego rodzaju żądania HTTP, ponieważ JavaScript nie może ustawiać tych nagłówków.

Jeśli serwer zgadza się na przejście na WebSocket, powinien wysłać odpowiedź o kodzie 101:

101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=

Tutaj Sec-WebSocket-Accept znajduje się Sec-WebSocket-Key, przekodowany przy użyciu specjalnego algorytmu. Przeglądarka używa go, aby upewnić się, że odpowiedź odpowiada żądaniu.

Później dane są przesyłane za pomocą protokołu WebSocket, którego strukturę („ramki”) zobaczymy już wkrótce. A to już w ogóle nie jest HTTP.

Rozszerzenia i podprotokoły

Mogą istnieć dodatkowe nagłówki Sec-WebSocket-Extensions i Sec-WebSocket-Protocol, które opisują rozszerzenia i podprotokoły.

Na przykład:

  • Sec-WebSocket-Extensions: deflate-frame oznacza, że przeglądarka obsługuje kompresję danych. Rozszerzenie to coś związanego z przesyłaniem danych, funkcjonalność rozszerzająca protokół WebSocket. Nagłówek Sec-WebSocket-Extensions jest wysyłany automatycznie przez przeglądarkę, wraz z listą wszystkich obsługiwanych przez nią rozszerzeń.

  • Sec-WebSocket-Protocol: soap, wamp oznacza, że chcemy przesyłać nie byle jakie dane, ale dane w protokołach SOAP lub WAMP („The WebSocket Application Messaging Protocol”). Podprotokoły WebSocket są zarejestrowane w katalogu IANA. Tak więc nagłówek ten opisuje formaty danych, z których będziemy korzystać.

    Ten opcjonalny nagłówek ustawia się za pomocą drugiego parametru new WebSocket. Jest to tablica podprotokołów, np. jeśli chcielibyśmy używać SOAP lub WAMP:

    let socket = new WebSocket("wss://javascript.info/chat", );

Serwer powinien odpowiedzieć listą protokołów i rozszerzeń, których zgadza się używać.

Na przykład żądanie:

GET /chatHost: javascript.infoUpgrade: websocketConnection: UpgradeOrigin: https://javascript.infoSec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==Sec-WebSocket-Version: 13Sec-WebSocket-Extensions: deflate-frameSec-WebSocket-Protocol: soap, wamp

Odpowiedź:

101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=Sec-WebSocket-Extensions: deflate-frameSec-WebSocket-Protocol: soap

W tym miejscu serwer odpowiada, że obsługuje rozszerzenie „deflate-frame”, oraz tylko SOAP żądanych podprotokołów.

Przesyłanie danych

Komunikacja WebSocket składa się z „ramek” – fragmentów danych, które mogą być wysyłane z każdej strony i mogą być kilku rodzajów:

  • „ramki tekstowe” – zawierają dane tekstowe, które strony wysyłają do siebie.
  • „ramki danych binarnych” – zawierają dane binarne, które strony wysyłają do siebie.
  • „ramki ping/pong” służą do sprawdzania połączenia, wysyłane są z serwera, przeglądarka odpowiada na nie automatycznie.
  • jest jeszcze „ramka zamknięcia połączenia” i kilka innych ramek serwisowych.

W przeglądarce bezpośrednio pracujemy tylko z ramkami tekstowymi lub binarnymi.

WebSocket .send() metoda może wysyłać zarówno dane tekstowe jak i binarne.

Wywołanie socket.send(body) umożliwia body w formacie łańcuchowym lub binarnym, w tym BlobArrayBuffer, itp. Nie są wymagane żadne ustawienia: po prostu wyślij go w dowolnym formacie.

Gdy otrzymamy dane, tekst zawsze przychodzi jako ciąg. A w przypadku danych binarnych, możemy wybrać pomiędzy formatami Blob i ArrayBuffer.

To jest ustawiane przez właściwość socket.binaryType, domyślnie jest to "blob", więc dane binarne przychodzą jako obiekty Blob.

Blob jest obiektem binarnym wysokiego poziomu, bezpośrednio integruje się z <a><img> i innymi tagami, więc jest to rozsądna wartość domyślna. Ale dla przetwarzania binarnego, aby uzyskać dostęp do poszczególnych bajtów danych, możemy zmienić go na "arraybuffer":

socket.binaryType = "arraybuffer";socket.onmessage = (event) => { // event.data is either a string (if text) or arraybuffer (if binary)};

Ograniczanie prędkości

Wyobraźmy sobie, że nasza aplikacja generuje dużo danych do wysłania. Ale użytkownik ma powolne połączenie sieciowe, może w mobilnym internecie, poza miastem.

Możemy zadzwonić socket.send(data) ponownie i ponownie. Ale dane będą buforowane (przechowywane) w pamięci i wysyłane tylko tak szybko, jak pozwala na to prędkość sieci.

Właściwość socket.bufferedAmount przechowuje, ile bajtów pozostaje w tej chwili zbuforowanych, czekając na wysłanie przez sieć.

Możemy ją zbadać, aby sprawdzić, czy gniazdo jest rzeczywiście dostępne do transmisji.

// every 100ms examine the socket and send more data// only if all the existing data was sent outsetInterval(() => { if (socket.bufferedAmount == 0) { socket.send(moreData()); }}, 100);

Zamknięcie połączenia

Normalnie, gdy strona chce zamknąć połączenie (zarówno przeglądarka jak i serwer mają równe prawa), wysyła „connection close frame” z kodem numerycznym i tekstowym powodem.

Metoda na to jest następująca:

socket.close(, );

  • code to specjalny kod zamykający WebSocket (opcjonalny)
  • reason to ciąg znaków, który opisuje powód zamknięcia (opcjonalnie)

Wtedy druga strona w close event handler dostaje kod i powód, e.g.:

// closing party:socket.close(1000, "Work complete");// the other partysocket.onclose = event => { // event.code === 1000 // event.reason === "Work complete" // event.wasClean === true (clean close)};

Najczęściej spotykane wartości kodu:

  • 1000 – domyślne, normalne zamknięcie (używane, jeśli nie dostarczono code),
  • 1006 – brak możliwości ręcznego ustawienia takiego kodu, wskazuje, że połączenie zostało utracone (brak ramki zamykającej).

Są też inne kody jak:

  • 1001 – strona się rozjeżdża, np. serwer się wyłącza, lub przeglądarka opuszcza stronę,
  • 1009 – wiadomość jest zbyt duża do przetworzenia,
  • 1011 – nieoczekiwany błąd na serwerze,
  • …i tak dalej.

Pełną listę można znaleźć w RFC6455, §7.4.1.

Kody WebSocket są trochę jak kody HTTP, ale inne. W szczególności, wszelkie kody mniejsze niż 1000 są zarezerwowane, przy próbie ustawienia takiego kodu pojawi się błąd.

// in case connection is brokensocket.onclose = event => { // event.code === 1006 // event.reason === "" // event.wasClean === false (no closing frame)};

Stan połączenia

Aby uzyskać stan połączenia, dodatkowo istnieje socket.readyState właściwość z wartościami:

  • 0 – „CONNECTING”: połączenie nie zostało jeszcze nawiązane,
  • 1 – „OPEN”: komunikacja,
  • 2 – „CLOSING”: połączenie jest zamykane,
  • 3 – „CLOSED”: połączenie jest zamykane.

Przykład czatu

Przejrzyjmy przykład czatu z wykorzystaniem przeglądarkowego WebSocket API i modułu Node.js WebSocket https://github.com/websockets/ws. Główną uwagę zwrócimy na stronę klienta, ale serwer też jest prosty.

HTML: potrzebujemy <form> do wysyłania wiadomości oraz <div> dla wiadomości przychodzących:

<!-- message form --><form name="publish"> <input type="text" name="message"> <input type="submit" value="Send"></form><!-- div with messages --><div></div>

Z poziomu JavaScript chcemy uzyskać trzy rzeczy:

  1. Otworzyć połączenie.
  2. Na przesłaniu formularza – socket.send(message) dla wiadomości.
  3. Na przychodzącej wiadomości – dołącz ją do div#messages.

Tutaj jest kod:

let socket = new WebSocket("wss://javascript.info/article/websocket/chat/ws");// send message from the formdocument.forms.publish.onsubmit = function() { let outgoingMessage = this.message.value; socket.send(outgoingMessage); return false;};// message received - show the message in div#messagessocket.onmessage = function(event) { let message = event.data; let messageElem = document.createElement('div'); messageElem.textContent = message; document.getElementById('messages').prepend(messageElem);}

Kod po stronie serwera jest trochę poza naszym zakresem. Tutaj będziemy używać Node.js, ale nie musisz tego robić. Inne platformy również mają swoje środki do pracy z WebSocket.

Algorytm po stronie serwera będzie:

  1. Utwórz clients = new Set() – zestaw gniazd.
  2. Dla każdego zaakceptowanego websocket’a dodaj go do zestawu clients.add(socket) i skonfiguruj message słuchacza zdarzeń, aby otrzymywać jego wiadomości.
  3. Gdy wiadomość zostanie odebrana: iteruj po klientach i wyślij ją do wszystkich.
  4. Gdy połączenie zostanie zamknięte: clients.delete(socket).
const ws = new require('ws');const wss = new ws.Server({noServer: true});const clients = new Set();http.createServer((req, res) => { // here we only handle websocket connections // in real project we'd have some other code here to handle non-websocket requests wss.handleUpgrade(req, req.socket, Buffer.alloc(0), onSocketConnect);});function onSocketConnect(ws) { clients.add(ws); ws.on('message', function(message) { message = message.slice(0, 50); // max message length will be 50 for(let client of clients) { client.send(message); } }); ws.on('close', function() { clients.delete(ws); });}

Tutaj jest działający przykład:

Możesz go również pobrać (prawy górny przycisk w iframe) i uruchomić lokalnie. Nie zapomnij tylko zainstalować Node.js i npm install ws przed uruchomieniem.

Podsumowanie

WebSocket to nowoczesny sposób na trwałe połączenia przeglądarka-serwer.

  • WebSockets nie mają ograniczeń cross-origin.
  • Są dobrze obsługiwane w przeglądarkach.
  • Mogą wysyłać/odbierać ciągi znaków i dane binarne.

Aplikacja API jest prosta.

Metody:

  • socket.send(data),
  • socket.close(, ).

Wydarzenia:

  • open,
  • message,
  • error,
  • close.

WebSocket sam w sobie nie zawiera rekoneksji, uwierzytelniania i wielu innych mechanizmów wysokiego poziomu. Istnieją więc do tego biblioteki klient/serwer, można też zaimplementować te możliwości ręcznie.

Czasami, aby zintegrować WebSocket z istniejącym projektem, ludzie uruchamiają serwer WebSocket równolegle z głównym serwerem HTTP i współdzielą jedną bazę danych. Żądania do WebSocketa używają wss://ws.site.com, subdomeny prowadzącej do serwera WebSocket, podczas gdy https://site.com idzie do głównego serwera HTTP.

Zapewne inne sposoby integracji są również możliwe.

Współpraca z WebSocketem jest możliwa.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *