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.
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 open
message
close
.
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łówekOrigin
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.
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łówekSec-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 Blob
ArrayBuffer
, 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 dostarczonocode
), -
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:
- Otworzyć połączenie.
- Na przesłaniu formularza –
socket.send(message)
dla wiadomości. - 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:
- Utwórz
clients = new Set()
– zestaw gniazd. - Dla każdego zaakceptowanego websocket’a dodaj go do zestawu
clients.add(socket)
i skonfigurujmessage
słuchacza zdarzeń, aby otrzymywać jego wiadomości. - Gdy wiadomość zostanie odebrana: iteruj po klientach i wyślij ją do wszystkich.
- 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.