Moduł 6 - Zajęcia 11 - Websockets

1.1 WebSockets

WebSocket to protokół dwustronnej wymiany danych. W praktyce oznacza to, że WebSockets ustanawia jedno połączenie klienta z serwerem. Po niezbędnych potwierdzeniach, że zarówno klient jak i serwer może pracować z WebSockets, serwer i klient mogą wysyłać przez nie wiadomości tekstowe, przy czym przesyłanie zachodzi od razu, ponieważ tworzone są dwustronne kanały. Połączenie jest cały czas aktywne, co pozwala nie przekazywać m.in. zbędnych nagłówków HTTP.

Przy użyciu WebSocketów nie ma ograniczeń w ilości połączeń, ani w kolejności zapytań.

1.2 Moduł ws

W celu rozpoczęcia pracy z WebSocketami potrzebne są tylko dwie rzeczy: przeglądarka wspierająca WebSocket i serwer realizujący tę technologię. Po stronie przeglądarki wszystko jest proste - WebSockets jest wspierane przez większość współczesnych wersji przeglądarek. Sprawdzić wsparcie tej technologii przez konkretną przeglądarkę można pod tym linkiem https://echo.websocket.events/.ws - możemy tam przetestować wysyłanie i otrzymywanie wiadomości poprzez socket

Stwórzmy jednak własny serwer obsługujący socket

Na serwerze należy zainstalować moduł:

            npm install ws
          

A następnie, zadeklarujmy instancję WebSocket

            const WebSocketServer = new require('ws');
          

Korzystając z niej uruchamiamy serwer WebSocket na wybranym porcie:

            const wss = new WebSocketServer.Server({ port: 8080 });
          

Serwer WebSocket będzie działał, czekając na zapytania zgodnie z protokołem ws:// na wprowadzony port (w naszym przypadku 8080). Napiszemy funkcję callback na zdarzenie połączenia:

            wss.on('connection', ws => {
              console.log('Nowe połączenie');
            });
          

Dodatkowo należy napisać niedużego klienta, w tym przypadku po prostu - stronę internetową która może zostać umieszczona na dowolnym serwerze internetowym (lub uruchomiona lokalnie na naszym komputerze)

            <!DOCTYPEhtml>
            <html lang="en">
              <head>
                <meta charset="UTF-8" />
                <meta name="viewport" content="width=device-width, initial-scale=1.0" />
                <meta http-equiv="X-UA-Compatible" content="ie=edge" />
                <title>WebSocket</title>
              </head>
              <script>
                window.onload = () => {
                  const ws = new WebSocket('ws://localhost:8080');
                };
              </script>
              <body></body>
            </html>
          

Teraz przy otwarciu strony w przeglądarce zostanie utworzone połączenie i odpowiednio w konsoli serwera pojawi się napis - Nowe połączenie.

Na razie są to interakcje zachodzące od serwera do klienta, dodajmy więc wymianę wiadomości. Dodajmy potrzebną funkcjonalność do serwera:

            const WebSocketServer = new require('ws');

            const wss = new WebSocketServer.Server({ port: 8080 });

            let clients = [];

            wss.on('connection', ws => {
              let id = clients.length;
              clients[id] = ws;
              console.log(`Nowe połączenie #${id}`);
            // wysyłamy do klienta wiadomość
              clients[id].send(`Cześć, został nadany numer №${id}`);
            // wysyłamy wszystko pozostałym
              clients.forEach((item, index) => {
                if (index !== id) {
                  item.send(`Do nas przypisany został numer - ${id}`);
                }
              });
            });
          

Do klienta należy dopisać kod dla przyjmowania wiadomości z serwera:

            window.onload = () => {
              const ws = new WebSocket('ws://localhost:8080');
              ws.onmessage = e => {
                console.log(e.data);
              };
            };
          

Teraz spróbujemy otworzyć stronę w kilku oknach przeglądarki. Zobaczymy wiadomości w konsolach zarówno przeglądarek, jak i serwera. Zaszła więc wymiana danych w obie strony.

2.1 Socket.io

Ta biblioteka pozwala zapewniać współpracę w czasie rzeczywistym, w nieco prostszy sposób niż ws. Socket.io również zapewnia obustronne połączenie w czasie rzeczywistym na podstawie zdarzeń. Dostępna jest na dowolnej platformie, przeglądarce lub urządzeniu, jest szybka i niezawodna. Socket.io składa się z dwóch części:

  • Serwer, na którym socket.io integruje się się z serwerem HTTP Node.JS;
  • Biblioteki klienta, ładowanej na stronie przeglądarki czyli socket.io-client

Wystarczy, że zainstalujemy paczkę w klasyczny sposób:

            npm install socket.io
          

Następnie dodajmy do naszej aplikacji express i utwórzmy plik app.js.

            const express = require('express');
            const app = express();
            const http = require('http').createServer(app);
            const io = require('socket.io')(http);

            app.use(express.static('public'));

            io.on('connection', socket => {
              console.log('User connected!');
              socket.emit('message', 'User connected!');
            });

            http.listen(3000, () => {
              console.log('listening on *:3000');
            });
          

Zwróć uwagę, że przy inicjalizacji instancji socket.io, przekazujemy obiekt http (HTTP-serwera).

Następnie, zaczynamy nasłuchiwać zdarzenia connection które wywołane zostanie dla nowych podpiętych użytkowników, a gdy to nastąpi, wyprowadzamy wiadomość User connected! do konsoli serwera. Następnie wysyłamy zdarzenie message do danego klienta z taką samą wiadomością.

W folderze public umieścimy plik index.html, który będzie naszym klientem.

            <!DOCTYPEhtml>
            <html lang="en">
              <head>
                <meta charset="UTF-8" />
                <meta name="viewport" content="width=device-width, initial-scale=1.0" />
                <meta http-equiv="X-UA-Compatible" content="ie=edge" />
                <script src="/socket.io/socket.io.js"></script>
                <title>SocketIO</title>
              </head>
              <body>
                <script>
                  const socket = io.connect('/');
                  socket.on('message', data => {
                    console.log(data);
                  });
                </script>
              </body>
            </html>
          

Wszystko, co trzeba zrobić, aby zacząć pracować z WebSocketami po stronie przeglądarki to załadować socket.io-client jako skrypt. Dostarczy on globalną zmienną io, dzięki której możemy podłączyć się do serwera przy użyciu metody io.connect('/') i obsłużyć nasze zdarzenie message.

Jeżeli uruchomimy serwer, to przy wejściu przez przeglądarkę na adres http://localhost:3000/ w konsoli przeglądarki zobaczymy wiadomość User connected!.

Przeanalizujemy bardziej szczegółowo zdarzenia serwerowej i przeglądarkowej części socket.io.

Idea budowania aplikacji socket.io zawiera się w tym, że obie jej części - serwerowa i klienta - mają dużo wspólnych właściwości i metod, natomiast inaczej reagują one na poszczególne zdarzenia.

Na serwerze są tylko trzy zdefiniowane wcześniej zdarzenia:

  1. connection — zdarzenie następuje przy ustanowieniu połączenia z klientem;
  2. on — zdarzenie następuje przy otrzymaniu wiadomości od klienta o podanej nazwie;
  3. disconnect — przerwanie połączenia danego klienta.

W przeglądarkowej części socket.io jest więcej określonych zdarzeń:

  1. connecting — zdarzenie następuje podczas nawiązywania połączenia z serwerem;
  2. connect_failed — zdarzenie następuje przy nieudanej próbie połączenia;
  3. connect — zdarzenie następuje przy nawiązaniu połączenia z serwerem;
  4. on — zdarzenie następuje przy otrzymaniu wiadomości o określonej nazwie od serwera;
  5. disconnect — zdarzenie następuje przy zerwaniu połączenia z serwerem;
  6. reconnecting (może pojawić się nie jeden raz) - zdarzenie następuje przy próbie ustanowienia połączenia;
  7. reconnect — zdarzenie następuje przy ponownym nawiązaniu połączenia;
  8. error — zdarzenie błędu;

Po tym wprowadzeniu przyszedł czas na zrealizowanie prostego czatu w bibliotece Socket.io.

3.1 Tworzymy czat

Przeanalizujmy utworzenie prostego czatu przy pomocy biblioteki Socket.io. To nasz przykład, który będziemy rozpatrywać - Chat example

Realizacja części serwerowej jest dość prosta:

            const express = require('express');
            const app = express();
            const http = require('http');
            const server = http.createServer(app);
            const io = require('socket.io')(server);

            server.listen(process.env.PORT || 3000, function () {
              console.log('Server running in port 3000');
            });

            app.use(express.static(__dirname + '/public'));
          

Podłączamy wszystkie niezbędne moduły, uruchamiamy serwer i wskazujemy, gdzie będą znajdować się zasoby statystyczne, w naszym przypadku strona html i skrypty związane z zachowaniem klienta.

Następnie tworzymy obiekt:

            const users = {};
          

będziemy tam zapisywać, zgodnie z unikalnym kluczem klienta, nazwę dla każdego podłączanego użytkownika.

Cała praca czatu będzie zamknięta wewnątrz konstrukcji:

            io.sockets.on('connection', (client) => { … })
          

Ten kod wykona się dla każdego nowo podłączanego użytkownika w zdarzeniu connection.

Wewnątrz tworzymy funkcję:

            const broadcast = (event, data) => {
              client.emit(event, data);
              client.broadcast.emit(event, data);
            };
          

Ta funkcja wykonuje zdarzenie event i przesyła dane data konkretnie dla bieżącego użytkownika client.emit(event, data), następnie inicjuje zdarzenie dla wszystkich pozostałych podłączonych użytkowników client.broadcast.emit(event, data).

Przy pierwszym podłączeniu danego użytkownika wykonujemy zdarzenie user

            broadcast('user', users);
          

i powiadamiamy wszystkich uczestników czatu o naszej aktualnej liście użytkowników.

Przydadzą się nam jeszcze dwie funkcje dla zdarzeń

  • message - wysłanie wiadomości na czacie
  • disconnect - użytkownik wyszedł z czatu (zamknął zakładkę przeglądarki).

Wysłanie wiadomości:

            client.on('message', message => {
            if (users[client.id] !== message.name) {
              users[client.id] = message.name;
              broadcast('user', users);
            }
            broadcast('message', message);
          });
          

Sprawdzamy, czy użytkownik jest już na liście users czy też zmienił nazwę przy wysłaniu wiadomości i jeśli tak, to następnie informujemy wszystkich użytkowników przez zdarzenie user, że dany użytkownik zmienił swoją nazwę. Później wywołujemy zdarzenie message i wysyłamy otrzymaną wiadomość do wszystkich użytkowników.

Jeżeli użytkownik zaktualizował stronę lub zamknął zakładkę przeglądarki nastąpi zdarzenie disconnect.

            client.on('disconnect', () => {
              delete users[client.id];
              client.broadcast.emit('user', users);
            });
          

Usuwamy bieżącego użytkownika z listy users i wysyłamy do wszystkich pozostałych użytkowników przez zdarzenie user zaktualizowaną listę. Zwróć uwagę, że wysyłamy to zdarzenie tylko do pozostałych użytkowników. Na nasze potrzeby to wszystko co potrzebujemy po stronie serwera

Kod klienta jest bardziej skomplikowany.

Tworzymy zmienne ze wszystkimi niezbędnymi dla DOM elementami:

            const usersList = document.getElementById('users');
            const board = document.getElementById('board');
            const userMessage = document.getElementById('msg_txt');
            const userName = document.getElementById('msg_name');
            const sendButton = document.getElementById('msg_btn');
          

Podłączamy socket.io:

            const socket = io();
          

Tworzymy tablicę, w której będziemy zapisywać otrzymane od serwera wiadomości:

            const messages = [];
          

i ustawiamy limit dla maksymalnej ilości wiadomości na ekranie:

            const LIMIT_MESSAGES = 10;
          

Funkcja renderListOfMessages za każdym razem po otrzymaniu zdarzenia message rysuje zaktualizowaną listę wiadomości od użytkowników na stronie.

Funkcja renderListOfUsers za każdym razem przy otrzymaniu zdarzenia user rysuje zaktualizowaną listę użytkowników na stronie.

Za podłączenie handlers do odpowiednich zdarzeń odpowiada następujący fragment kodu:

            socket.on('user', renderListOfUsers);
            socket.on('message', renderListOfMessages);
          

Samo wysłanie wiadomości na serwer wykonuje się w funkcji sendUserMessage.

            const sendUserMessage = () => {
            let name = userName.value;
            const message = userMessage.value;

            if (message === '' || name === '') {
              return;
            }

            socket.emit('message', {
              message,
              name,
            });

            userMessage.value = '';
            userMessage.focus();
          };

          sendButton.addEventListener('click', sendUserMessage);
          

Funkcja

            const pressEnterKey = e => {
            if (e.keyCode === 13) {
              sendUserMessage();
            }
          };
          

będzie wywoływać funkcję sendUserMessage, jeżeli naciśniemy klawisz Enter.

Jak widać, cała logika pracy kodu klienta zbudowana jest na obsługiwaniu zdarzeń user i message, które generuje dla nas serwer, a także generowaniu zdarzenia message które służy do wysyłania na serwer wiadomości (funkcja sendUserMessage).