Nasza strona używa cookies. Dowiedz się więcej o celu ich używania i zmianie ustawień w przeglądarce. Korzystając ze strony, wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki. Rozumiem

Prosto o WebSocket

Mohd Shad Mirza Mobile Application Developer / SheeVoot International Pvt. Ltd
Sprawdź, kiedy warto wykorzystać WebSocket oraz jak zaimplementować prosty klient i serwer w JavaScript.
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:

  1. Klient (przeglądarka) wysyła żądanie do serwera
  2. Następuje połączenie
  3. Serwer odsyła odpowiedź
  4. Klient otrzymuje odpowiedź
  5. 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:

  1. Klient (przeglądarka) wysyła żądanie HTTP do serwera.
  2. Połączenie jest nawiązywane za pomocą protokołu HTTP.
  3. Jeśli serwer obsługuje protokół WebSocket, zgadza się zaktualizować połączenie. To tzw. “handshake”.
  4. Po akceptacji początkowe połączenie HTTP zostaje zastąpione połączeniem WebSocket, które korzysta z tego samego protokołu TCP/IP.
  5. 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 kody 200, 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ł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.

Masz coś do powiedzenia?

Podziel się tym z 120 tysiącami naszych czytelników

Dowiedz się więcej