Prosto o WebSocket
WebSocket pozwala użytkownikowi wysyłać i odbierać wiadomości na serwerze. Zasadniczo jest to sposób komunikacji między klientem a serwerem. Postarajmy się zrozumieć tę komunikację, a za chwilę wrócimy do WebSocket.
Klient i serwer
Przeglądarki internetowe (klienci) i serwery komunikują się przez TCP/IP. Hypertext Transfer Protocol (HTTP) to standardowy protokół aplikacji zbudowany na TCP/IP. HTTP wspiera żądania (z przeglądarki internetowej) i ich odpowiedzi na nie (z serwera).
Jak to działa?
Przejdźmy przez te kroki:
- Klient (przeglądarka) wysyła żądanie do serwera
- Następuje połączenie
- Serwer odsyła odpowiedź
- Klient otrzymuje odpowiedź
- Połączenie zostaje zakończone
Tak wygląda komunikacja między klientem a serwerem. Przyjrzyjmy się krokowi nr 5.
Połączenie zostaje zakończone
Żądanie HTTP spełniło swoje zadanie i nie jest już potrzebne, dlatego połączenie zostało zamknięte.
A co, jeśli serwer chce wysłać wiadomość do klienta?
Połączenie musi zostać ustanowione pomyślnie, aby rozpocząć komunikację. Rozwiązanie jest takie, że to klient będzie musiał wysłać kolejne żądanie, by nawiązać połączenie i odebrać wiadomość..
Skąd klient będzie wiedział, że serwer chce wysłać wiadomość?
Spójrzmy na taki przykład:
Klient jest głodny i zamówił jedzenie przez Internet. Wysyła jedno żądanie na sekundę, aby sprawdzić, czy zamówienie jest gotowe.
0 sekund: Czy jedzenie jest gotowe? (Klient)
0 sekund: Nie, proszę czekać. (Serwer)
1 sekunda: Czy jedzenie jest gotowe? (Klient)
1 s: Nie, proszę czekać. (Serwer)
2 s: Czy jedzenie jest gotowe? (Klient)
2 sekundy: Nie, proszę czekać. (Serwer)
3 s: Czy jedzenie jest gotowe? (Klient)
3 s: Tak, oto Twoje zamówienie. (Serwer)
Nazywa się to odpytywaniem HTTP (HTTP Polling). Klient wysyła do serwera powtarzające się żądania i sprawdza, czy jest jakaś wiadomość do odebrania. Nie jest to zbyt wydajne. Zużywamy niepotrzebnie dużo zasobów, kolejnym problemem jest liczba nieudanych żądań.
Czy jest jakiś sposób na rozwiązanie tego problemu?
Tak, istnieje odmiana techniki odpytywania, która jest stosowana w celu zniwelowania nieefektywności i nazywa się ją Long-Polling.
Long Polling polega na wysłaniu żądania HTTP do serwera, a następnie utrzymaniu otwartego połączenia, aby umożliwić serwerowi odpowiedź w późniejszym terminie (o czym decyduje serwer).
Prześledźmy ten przykład dotyczący Long Pollingu:
0 s: Czy jedzenie jest gotowe? (Klient)
3 s: Tak, oto Twoje zamówienie. (Serwer)
Problem rozwiązany! Ale nie do końca. Mimo że Long Polling działa, jest ona bardzo droga pod względem użycia procesora, pamięci i przepustowości (blokujemy zasoby, utrzymując połączenie aktywnym).
Co robimy w takiej sytuacji? Wygląda na to, że sprawy wymykają się spod kontroli. Wróćmy do naszego wybawcy: WebSocket.
Dlaczego WebSocket?
Jak widać, Polling i Long Polling są dość drogimi opcjami w celu prowadzenia komunikacji w czasie rzeczywistym między klientem a serwerem.
Te ograniczenia związane z wydajnością są powodem, dla którego powinieneś zamiast tego użyć WebSocket. WebSockets nie wymaga wysyłania żądania w celu udzielenia odpowiedzi. Pozwalają na dwukierunkowy przepływ danych, więc wystarczy “nasłuchiwać” danych. Możesz po prostu “słuchać” serwera, który wyśle Ci wiadomość, gdy będzie dostępna.
Spójrzmy na wydajność WebSocket.
Zużycie zasobów
Poniższy wykres pokazuje transfer wymagany przez WebSockets i Long-Polling w trzech relatywnie częstych scenariuszach:
Różnica jest ogromna (dla stosunkowo większej liczby żądań).
Prędkość
Oto wyniki dla 1, 10 i 50 żądań obsłużonych na połączenie w ciągu jednej sekundy:
Jak widać, wykonanie pojedynczego żądania na połączenie jest o 50% wolniejsze przy użyciu Socket.io, ponieważ połączenie musi zostać najpierw ustanowione. Narzut ten jest mniejszy, ale wciąż zauważalny w przypadku dziesięciu żądań. Przy 50 żądaniach z tego samego połączenia, Socket.io jest już o 50% szybszy. Aby lepiej zrozumieć szczytową przepustowość, przyjrzymy się testowi porównawczemu z większą liczbą (500, 1000 i 2000) żądań na połączenie:
Tutaj widać, że test porównawczy HTTP osiąga maksimum przy około ~ 950 żądaniach na sekundę, podczas gdy Socket.io obsługuje około ~ 3900 żądań na sekundę. Efektywne, prawda?
Uwaga: Socket.io to biblioteka JavaScript do aplikacji internetowych w czasie rzeczywistym. Implementuje WebSocket wewnętrznie. Możesz myśleć o niej jak o wrapperze na WebSocket, które zapewnia wiele innych funkcji.
Jak działa WebSocket?
W ten sposób nawiążesz połączenie WebSocket:
- Klient (przeglądarka) wysyła żądanie HTTP do serwera.
- Połączenie jest nawiązywane za pomocą protokołu HTTP.
- Jeśli serwer obsługuje protokół WebSocket, zgadza się zaktualizować połączenie. To tzw. “handshake”.
- Po akceptacji początkowe połączenie HTTP zostaje zastąpione połączeniem WebSocket, które korzysta z tego samego protokołu TCP/IP.
- W tym momencie dane mogą swobodnie przepływać między klientem a serwerem.
Zakodujmy to
Stworzymy dwa pliki: serwer i klienta. Najpierw utwórz prosty dokument <html>
o nazwie client.html
, zawierający tag <script>
. Zobaczmy, jak to wygląda
Client.html
<html>
<script>
// Our code goes here
</script>
<body>
<h1>This is a client page</h1>
</body>
</html>
Teraz utwórz kolejny plik server.js
. Zaimportuj moduł HTTP i utwórz serwer. Niech nasłuchuje on portu 8000. To będzie działać jak zwykły serwer HTTP
, nasłuchujący na porcie 8000.
Server.js
//importing http module
const http = require('http');
//creating a http server
const server = http.createServer((req, res) => {
res.end("I am connected");
});
//making it listen to port 8000
server.listen(8000);
Uruchom komendę node server.js
, aby rozpocząć nasłuchiwanie na porcie 8000.
Uwaga: Możesz wybrać dowolny port. Ja wybrałem 8000 bez konkretnego powodu.
Nasza podstawowa konfiguracja klienta i serwera są już zakończone. Zrobiliśmy już naszego podstawowego klienta i serwer. Proste, prawda? Przejdźmy teraz do przyjemniejszych rzeczy.
Ustawienia klienta
Aby zbudować WebSocket, użyj konstruktora WebSocket()
, który zwraca obiekt WebSocket. Ten obiekt zapewnia API do tworzenia i zarządzania połączeniem WebSocket z serwerem.
Krótko mówiąc, obiekt WebSocket pomoże nam nawiązać połączenie z serwerem i stworzyć dwukierunkowy przepływ danych, tj. wysyłanie i odbieranie danych z obydwu stron.
<html>
<script>
//calling the constructor which gives us the websocket object: ws
let ws = new WebSocket('url');
</script>
<body>
<h1>This is a client page</h1>
</body>
</html>
Konstruktor WebSocket
oczekuje adresu URL z którym ma się połączyć. W naszym przypadku jest to ws://localhost:8000
, ponieważ tam właśnie działa nasz serwer.
Teraz może się to nieco różnić od tego, do czego jesteś przyzwyczajony. Nie używamy protokołu HTTP
, a protokołu WebSocket
. To powie klientowi, że używamy protokołu WebSocket, stąd ws://
zamiast http://
.
Teraz stwórzmy serwer WebSocket w server.js
.
Serwer
Będziemy potrzebować zewnętrznego modułu ws
w naszym serwerze node, by przekształcić go w serwer WebSocket
.
Najpierw zaimportujemy moduł ws
. Następnie stworzymy serwer WebSocket i przekażemy go serwerowi HTTP
, nasłuchującemu na porcie 8000.
Serwer HTTP nasłuchuje na porcie 8000, a serwer WebSocket nasłuchuje na tym serwerze HTTP. Zasadniczo słucha słuchacza.
Teraz nasz WebSocket obserwuje ruch na porcie 8000. Oznacza to, że spróbuje nawiązać połączenie, gdy tylko klient będzie dostępny. Nasz plik server.js
będzie wyglądał następująco:
const http = require('http');
//importing ws module
const websocket = require('ws');
const server = http.createServer((req, res) => {
res.end("I am connected");
});
//creating websocket server
const wss = new websocket.Server({ server });
server.listen(8000);
Jak powiedzieliśmy sobie wcześniej, konstruktor WebSocket()
zwraca obiekt WebSocket, zapewniający API do tworzenia i zarządzania połączeniem WebSocket z serwerem.
W tym przypadku obiekt wss
pomoże nam nasłuchiwać zdarzeń emitowanych, gdy coś się wydarzy. Np. gdy połączenie zostanie ustanowione lub zakończone itp.
Zobaczmy, jak nasłuchiwać wiadomości:
const http = require('http');
const websocket = require('ws');
const server = http.createServer((req, res) => {
res.end("I am connected");
});
const wss = new websocket.Server({ server });
//calling a method 'on' which is available on websocket object
wss.on('headers', (headers, req) => {
//logging the header
console.log(headers);
});
server.listen(8000);
Metoda on
oczekuje dwóch argumentów: nazwy zdarzenia i wywołania zwrotnego (callbacku). Nazwy zdarzenia, aby rozpoznać, które zdarzenie ma nasłuchiwać/emitować i wywołanie zwrotne, aby określiło, co z nim zrobić. Tutaj rejestrujemy tylko zdarzenie headers
.
To jest nasz nagłówek HTTP. Dokładnie to dzieje się za kulisami. Rozłóżmy to na czynniki pierwsze, aby lepiej to zrozumieć.
- Pierwszą rzeczą, którą zauważysz, jest to, że otrzymaliśmy kod statusu
101
. Być może widziałeś już wcześniej kody200
,201
,404
.101
jest statusem HTTP informującym o zmianie protokołu (101 Switching Protocols). Mówi „Hej, potrzebuję uaktualnienia”. - Drugi wiersz zawiera informacje o aktualizacji. Określa, że chce uaktualnienia do protokołu
WebSocket
. - To właśnie dzieje się podczas handshake’a. Przeglądarka używa połączenia
HTTP
do ustanowienia połączenia przy użyciu protokołuHTTP/1.1
, a następnieuaktualnia go
do protokołuWebSocket
.
Teraz wszystko nabiera sensu.
Zdarzenie “headers” jest emitowane, zanim nagłówki odpowiedzi zostaną zapisane do gniazda w ramach uzgadniania. Pozwala to na sprawdzenie/modyfikację nagłówków przed ich wysłaniem.
Oznacza to, że możesz akceptować nagłówek, odrzucać lub cokolwiek innego, czego będziesz potrzebował. Domyślna będzie akceptacja żądania.
Możemy dodać jeszcze jedno zdarzenie connection
, które będzie emitowane po zakończeniu handshake'u. Po pomyślnym nawiązaniu połączenia wyślemy wiadomość do klienta.
const http = require('http');
const websocket = require('ws');
const server = http.createServer((req, res) => {
res.end("I am connected");
});
const wss = new websocket.Server({ server });
wss.on('headers', (headers, req) => {
//console.log(headers); Not logging the header anymore
});
//Event: 'connection'
wss.on('connection', (ws, req) => {
ws.send('This is a message from server, connection is established');
//receive the message from client on Event: 'message'
ws.on('message', (msg) => {
console.log(msg);
});
});
server.listen(8000);
Nasłuchujemy również zdarzenia message
, które pochodzi od klienta. Zaimplementujmy to:
<html>
<script>
let ws = new WebSocket('url');
//logging the websocket property properties
console.log(ws);
//sending a message when connection opens
ws.onopen = (event) => ws.send("This is a message from client");
//receiving the message from server
ws.onmessage = (message) => console.log(message);
</script>
<body>
<h1>This is a client page</h1>
</body>
</html>
Tak to wygląda w przeglądarce:
Pierwszy log to WebSocket
z listą wszystkich właściwości obiektu WebSocket, a drugi to MessageEvent
, który ma właściowość data
. Jeśli przyjrzysz się uważnie, zobaczysz, że otrzymaliśmy naszą wiadomość z serwera.
Log serwera będzie wyglądał tak:
Otrzymaliśmy wiadomość od klienta. Oznacza to, że nasze połączenie zostało pomyślnie ustanowione. Tadam!
Oryginał tekstu w języku angielskim przeczytasz tutaj.