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.
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).
Przejdźmy przez te kroki:
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.
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ść..
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ń.
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.
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.
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ń).
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.
W ten sposób nawiążesz połączenie WebSocket:
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
<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.
//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.
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
.
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ć.
101
. Być może widziałeś już wcześniej kody 200
, 201
, 404
. 101
jest statusem HTTP informującym o zmianie protokołu (101 Switching Protocols). Mówi „Hej, potrzebuję uaktualnienia”.WebSocket
.HTTP
do ustanowienia połączenia przy użyciu protokołu HTTP/1.1
, a następnie uaktualnia go
do protokołu WebSocket
.
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.