Sommario
< Home
Stampa

WebSocket

WebSocket è un tipo di connessione TCP di livello applicazione, ed è a tutti gli effetti una estensione di http, con cui condivide l’architettura client-server e la tecnologia basata su web server. Websocket aggiunge la possibilità, rispetto ad http, di creare una connessione permanente e bidirezionale tra client-server e quindi superare i limiti di HTTP, dove è il client a dover iniziare sempre la connessione.

Come HTTP, WebSocket si basa su TCP, protocollo di comunicazione di livello trasporto che crea una comunicazione permanente tra due nodi, comunicazione che viene chiamata socket1. WebSocket ne riprende il concetto ma lo applica a livello applicazione: non sono quindi due nodi della rete a stabilire una connessione, ma sono delle applicazioni, il client ed il server appunto.

Come funziona?

In una chiamata http abbiamo visto che la comunicazione avviene in questo modo:

1) il client effettua una richiesta di handshake al server sulla porta indicata (di solito la 80) (SYN);

2) il server avvia l’handshake comunicando la porta effettiva di comunicazione (SYN/ACK);

3) quando il client conferma (ACK) viene stabilita la connessione socket;

4) il client invia la richiesta;

5) il server invia la risposta.

Protocollo HTTP

In WebSocket il punto 5 è sostituito da una risposta che attiva la creazione di un canale di comunicazione sempre aperto. Sia il server che il client possono quindi inviare in qualsiasi momento flussi di dati. Una WebSocket comporta quindi che non c’è più nessuna attesa di avviare un aconnessione tra client server, inoltre il server può inviare anch’esso dati senza attendere richieste: in pratica la comunicazione è interamente bidirezionale. 

Protocollo WebSocket

Elenchiamo quindi i vantaggi di questa soluzione tecnologica:

  • la connessione è permanente: quindi non serve rifare handshake per fare una richiesta. Le websocket quindi hanno meno latenza.
  • la connessione è bidirezionale: il server può inviare informazioni al client, cosa impossibile in HTTP. Questo elimina la necessità del polling.
  • sono richieste meno risorse: una websocket richiede meno risorse lato server quindi è possibile tenere attive a parità di memoria più websocket che connessioni http.

Ci sono però degli svantaggi che vanno presi in considerazione:

  • Richiede un minimo di apprendimento.
  • non è sempre supportata da tutti i client e server
  • è vero che una websocket richiede meno risorse, ma queste risorse sono sempre attive, anche quando non transitano dati. E’ quindi meno efficiente a livello di cpu.

Gli utilizzi pratici sono numerosi: chat, videogiochi, aste on line, notizie, sistemi in tempo reale, IoT (dispositivi che comunicano informazioni al server), notifiche dal server al browser. Le websocket sono di fatto presenti oggi nella maggior parte delle applicazioni web e costituiscono una alternativa spesso più efficace del classico HTTP.

Websocket è disponibile, lato server, per la maggior parte delle piattaforme Web, come NodeJS, PHP, Java, Python, C++ e i linguaggi .NET (C#), anche se probabilmente l’implementazione meno complessa è il NodeJs.

Lato client viene usato Javascript oppure i linguaggi nativi per app mobile (Swift, Java, Kotlin) o desktop (Java, C#, Python, C++).

Websocket in NodeJS

Come esempio pratico in NodeJS vediamo passo passo la creazione di una WebSocket lato client e lato server.

Partiamo con l’impostazione del progetto:

git init
npm init

e la definizione del file .gitignore

node_modules
package-lock.json
.DS_Store
conf.json

e del file conf.json

{
   "port": XXXX // scegliere numero porta
}

Per utilizzare le WebSocket useremo una libreria molto popolare, socket.io, che va installata tramite npm.

npm install socket.io
npm install express

Il resto del codice è quello che abbiamo già visto nelle lezioni precedenti.

const fs = require('fs');
const express = require("express");
const http = require("http");
const app = express();
const path = require("path");
const bodyParser = require("body-parser");
const { Server } = require('socket.io'); // importazione oggetto Server da socket.io
const conf = JSON.parse(fs.readFileSync("./conf.json"));

app.use(bodyParser.json());
app.use(
  bodyParser.urlencoded({
    extended: true,
  }),
);

app.use("/", express.static(path.join(__dirname, "public")));
const server = http.createServer(app);
const io = new Server(server);
server.listen(conf.port, () => {
  console.log("server running on port: " + conf.port);

});

Dopo aver importato la libreria e l’oggetto Server, si inizializza una istanza di socket.io passando al costruttore l’istanza del server Express.

La gestione delle socket è quella tipica di Javascript, quindi tramite eventi e callback.

Ad esempio possiamo verificare se un nuovo client è connesso con queste istruzioni:

io.on('connection', (socket) => {
  console.log("socket connected: " + socket.id);
});

L’oggetto socket è la rappresentazione della comunicazione tra il server ed uno specifico client. Va ricordato infatti che un server può tenere attive in contemporanea molte socket quindi è necessario memorizzare in una qualche struttura dati l’elenco delle socket attive.

Passiamo al client. Creiamo prima di tutto una pagina html base (con già Bootstrap):

<!doctype html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" />    
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js"></script>
    <script src="/socket.io/socket.io.js"></script>
  </head>
  <body>    
    
    <script type="application/javascript" src="index.js"></script>
  </body>
</html>

JS:

const socket = io();

La url “/socket.io/socket.io.js” viene messa a disposizione dalla libreria socket.io e contiene il codice lato client per far funzionare le websocketindex. Quando viene eseguita la paginasi vedrà che automaticamente JS crea una connessione socket, e lato server vedremo in console un messaggio come questo:

socket connected: 11d8ZNn0l7zeWAkLAAAB

Eseguiamo una commit del progetto. Sarà il punto di partenza di ogni progetto con Websocket.

Chat con NodeJS

Adesso proviamo a creare una chat rudimentale dove ogni client che si connette ha una form dove inserire del testo e ad ogni invio questo testo viene rimandato a tutti i client connessi. Per semplicità useremo come nome del client l’id della socket utilizzata.

Torniamo al server. Il codice da aggiungere (eliminiamo il vecchio codice io.on('connection')... ) è il seguente:

app.use("/", express.static(path.join(__dirname, "public")));
const server = http.createServer(app);
const io = new Server(server);
io.on('connection', (socket) => {
   console.log("socket connected: " + socket.id);
   io.emit("chat", "new client: " + socket.id);
   socket.on('message', (message) => {
      const response = socket.id + ': ' + message;
      console.log(response);
      io.emit("chat", response);
   });
});

Il principio generale è quello di attivare ad ogni nuova connessione un middleware che gestisce gli eventi di quella connessione e comunica con il client o con i client. L’oggetto

Vediamo quali sono le funzioni possibili.

API lato serverDescrizione
io.on('connection', (socket) => {...} )eseguita ad ogni nuova connessione di un nuovo client.
L’oggetto socket rappresenta la websocket ed è un oggetto con diverse proprietà, di cui la più importante è l’id (socket.id).
io.emit(key, message);questa funzione invia in broadcast a tutte le socket il messaggio message. Tutti i messaggi che vengono inviati alle socket vanno marcati con una chiave, indicata dalla stringa key.
Il messaggio può essere qualsiasi oggetto JS (number, string, boolean, array, dizionario).

Ad esempio io.emit(“message”, “ciao”);
In questo caso tutti i client connessi riceveranno un evento con chiave “message” e valore “ciao”.

E’ quindi possibile utilizzare chiavi diverse per inviare messaggi di tipo diverso e configurare il client come vedremo tra poco per ascoltare messaggi solo con una determinata key.
socket.emit(key, message) permette di inviare ad una specifica socket un messaggio, utile per inviare comunicazioni riservate. Valgono le considerazioni di cui sopra, tuttavia in questo caso il messaggio è per la specifica socket.
socket.on(key, (message) => {...})questa funzione server per gestire la comunicazioni dalla socket, quando il client ci sta mandando un messaggio.
Ad esempio:

socket.on('message', (message) => {
const response = socket.id + ': ' + message;
console.log(response);
io.emit("chat", response);
});


In questo esempio specifico il messaggio ricevuto viene reinviato a tutte le socket (compreso il mittente) aggiungendo come prefisso l’id della socket.  
socket.on(‘disconnect’, () => {… }questo codice, che va inserito sempre nella gestione della connessione con la socket, consente di gestire una eventuale disconnessione (ad esempio eliminando dati non più usati).

Vediamo il resto del codice client dove creiamo una semplice form.

<body>    
    <div class="container">
      <ul id="chat" class="list-group">
      </ul>
    </div>
    <div class="row mt-4">
      <input id="input" class="form-control" />
      <button id="sendButton" type="button" class="btn btn-success">Send</button>
    </div>    
    <script type="application/javascript" src="index.js"></script>
  </body>

JS:

const input = document.getElementById("input");
const button = document.getElementById("sendButton");
const chat = document.getElementById("chat");

const template = "<li class=\"list-group-item\">%MESSAGE</li>";
const messages = [];

const socket = io();

input.onkeydown = (event) => {
  
  if (event.keyCode === 13) {
      event.preventDefault();
      button.click();
  }
}

button.onclick = () => {
  socket.emit("message", input.value);
  input.value = "";
}

socket.on("chat", (message) => {
  console.log(message);
  messages.push(message);
  render();
})

const render = () => {
  let html = "";
  messages.forEach((message) => {
    const row = template.replace("%MESSAGE", message);
    html+=row;
  });
  chat.innerHTML = html;
  window.scrollTo(0, document.body.scrollHeight);
}

Vediamo quindi le API lato client.

API lato clientDescrizione

const socket = io();
Attiva una connessione col server (il server lo intercetterà con io.on(‘connection’) visto sopra.
socket.on(key, (message) => {... })Questa funzione serve per gestire i messaggi in arrivo dal server (inviati con io.emit o socket.emit). La chiave è quella indicata dal server.
socket.emit(key, message);questa funzione invia al server un messaggio. Si ricorda che si tratta di qualsiasi oggetto JS inviabile (come lato server).

Provare a testarlo con almeno due client connessi (due pagine del browser). Vedrete che sarà possibile chattare tra loro.

Il grande vantaggio dell’uso di Websocket con NodeJs è che si usa lo stesso linguaggio e le stesse logiche di connessione sia lato client che lato server.

  1. il termine può essere ambiguo perché socket è in realtà qualsiasi connessione TCP (quindi livello 4 del TCP/IP). Anche HTTP utilizza connessioni socket. Una Websocket è invece una connessione di livello 5, che non va confusa con le socket di livello 4. ↩︎