Come utilizzare WebSocket in Node.js per creare applicazioni in tempo reale

Questo tutorial dimostra come utilizzare WebSocket in Node.js per una comunicazione bidirezionale e interattiva tra un browser e un server. La tecnica è essenziale per applicazioni veloci e in tempo reale come dashboard, app di chat e giochi multiplayer.

Table of Contents

Il Web si basa su messaggi HTTP di tipo richiesta-risposta. Il tuo browser effettua una richiesta URL e un server risponde con dati. Ciò può portare a ulteriori richieste del browser e risposte del server per immagini, CSS, JavaScript, ecc., ma il server non può inviare dati arbitrariamente a un browser.

Le tecniche di polling lungo Ajax possono far apparire le app web che si aggiornano in tempo reale, ma il processo è troppo limitato per applicazioni vere e proprie in tempo reale. Il polling ogni secondo sarebbe inefficiente in certi momenti e troppo lento in altri.

Dopo una connessione iniziale da parte di un browser, gli eventi inviati dal server sono una risposta HTTP standard (trasmessa) che può inviare messaggi dal server in qualsiasi momento. Tuttavia, il canale è unidirezionale e il browser non può inviare messaggi indietro. Per una vera e veloce comunicazione bidirezionale, è necessario WebSockets.

Panoramica su WebSockets

Il termine WebSocket si riferisce a un protocollo di comunicazione TCP su ws:// o il sicuro ed encriptato wss://. È diverso da HTTP, anche se può funzionare sui port 80 o 443 per garantire il funzionamento in luoghi che bloccano il traffico non web. La maggior parte dei browser rilasciati dopo il 2012 supporta il protocollo WebSocket.

In un’applicazione web in tempo reale tipica, è necessario avere almeno un server web per servire contenuti web (HTML, CSS, JavaScript, immagini, ecc.) e un server WebSocket per gestire la comunicazione bidirezionale.

Il browser effettua ancora una richiesta iniziale WebSocket a un server, il quale apre un canale di comunicazione. Entrambi il browser o il server possono quindi inviare un messaggio su quel canale, il che innesca un evento sull’altro dispositivo.

Comunicazione con altri browser connessi

Dopo la richiesta iniziale, il browser può inviare e ricevere messaggi a/dal server WebSocket. Il server WebSocket può inviare e ricevere messaggi a/da qualsiasi dei suoi browser client connessi.

La comunicazione peer-to-peer è non possibile. BrowserA non può inviare direttamente messaggi a BrowserB anche quando sono in esecuzione sullo stesso dispositivo e connessi allo stesso server WebSocket! BrowserA può solo inviare un messaggio al server e sperare che venga reindirizzato ad altri browser come necessario.

Supporto Server WebSocket

Node.js non ha ancora supporto nativo per WebSocket, anche se si vocifera che arriverà presto! Per questo articolo, sto utilizzando il modulo di terze parti ws, ma ci sono dozzine di altri.

Il supporto WebSocket integrato è disponibile nei runtime JavaScript Deno e Bun.

Le librerie WebSocket sono disponibili per runtime tra cui PHP, Python e Ruby. Opzioni SaaS di terze parti come Pusher e PubNub offrono anche servizi WebSocket ospitati.

Dimostrazione WebSockets Quickstart

Le app di chat sono il Ciao, mondo! delle dimostrazioni WebSocket, quindi mi scuso per:

  1. Essere poco originali. Detto questo, le app di chat sono un ottimo modo per spiegare i concetti.

  2. Non essere in grado di fornire una soluzione online completamente ospitata. Preferirei non dover monitorare e moderare un flusso di messaggi anonimi!

Clona o scarica il repository node-wschat da GitHub:

git clone https://github.com/craigbuckler/node-wschat

Installa le dipendenze Node.js:

cd node-wschat
npm install

Avvia l’applicazione di chat:

npm start

Apri http://localhost:3000/ in un numero di browser o schede (puoi anche definire il nome del tuo chat nella stringa di query — come ad esempio http://localhost:3000/?Craig). Digita qualcosa in una finestra e premi INVIA o premi Invio vedrai che appare in tutti i browser collegati.

Panoramica del Codice Node.js

Il file di ingresso index.js dell’applicazione Node.js avvia due server:

  1. Un’app Express in esecuzione su http://localhost:3000/ con un modello EJS per servire una singola pagina con HTML, CSS e JavaScript lato client. Il JavaScript del browser utilizza l’API WebSocket per stabilire la connessione iniziale e poi inviare e ricevere messaggi.

  2. A WebSocket server running at ws://localhost:3001/, which listens for incoming client connections, handles messages, and monitors disconnections. The full code:

    // WebSocket server
    import WebSocket, { WebSocketServer } from 'ws';
    
    const ws = new WebSocketServer({ port: cfg.wsPort });
    
    // client connection
    ws.on('connection', (socket, req) => {
    
      console.log(`connection from ${ req.socket.remoteAddress }`);
    
      // received message
      socket.on('message', (msg, binary) => {
    
        // broadcast to all clients
        ws.clients.forEach(client => {
          client.readyState === WebSocket.OPEN && client.send(msg, { binary });
        });
    
      });
    
      // closed
      socket.on('close', () => {
        console.log(`disconnection from ${ req.socket.remoteAddress }`);
      });
    
    });

Il libreria Node.js ws:

  • Solleva un evento "connection" quando un browser vuole connettersi. La funzione gestore riceve un oggetto socket utilizzato per comunicare con quel dispositivo individuale. Deve essere mantenuto per tutta la durata della connessione.

  • Solleva un evento socket "message" quando un browser invia un messaggio. La funzione gestore trasmette il messaggio a tutti i browser connessi (incluso quello che lo ha inviato).

  • Solleva un evento socket "close" quando il browser si disconnette — tipicamente quando la scheda viene chiusa o aggiornata.

Panoramica del codice JavaScript lato client

Il file static/main.js dell’applicazione esegue la funzione wsInit() e passa l’indirizzo del server WebSocket (dominio della pagina più un valore di porta definito nel modello della pagina HTML):

wsInit(`ws://${ location.hostname }:${ window.cfg.wsPort }`);

// gestisci la comunicazione WebSocket
function wsInit(wsServer) {

  const ws = new WebSocket(wsServer);

  // connetti al server
  ws.addEventListener('open', () => {
    sendMessage('entered the chat room');
  });

L’evento open si attiva quando il browser si connette al server WebSocket. La funzione gestore invia un messaggio è entrato nella chat room chiamando sendMessage():

// invia messaggio
function sendMessage(setMsg) {

  let
    name = dom.name.value.trim(),
    msg =  setMsg || dom.message.value.trim();

  name && msg && ws.send( JSON.stringify({ name, msg }) );

}

La funzione sendMessage() recupera il nome dell’utente e il messaggio dall’HTML form, anche se il messaggio può essere sovrascritto da qualsiasi argomento setMsg passato. I valori vengono convertiti in un oggetto JSON e inviati al server WebSocket utilizzando il metodo ws.send().

Il server WebSocket riceve il messaggio in arrivo che attiva il gestore "message" (vedi sopra) e lo trasmette a tutti i browser. Ciò attiva un evento "message" su ciascun client:

// ricevi messaggio
ws.addEventListener('message', e => {

  try {

    const
      chat = JSON.parse(e.data),
      name = document.createElement('div'),
      msg  = document.createElement('div');

    name.className = 'name';
    name.textContent = (chat.name || 'unknown');
    dom.chat.appendChild(name);

    msg.className = 'msg';
    msg.textContent = (chat.msg || 'said nothing');
    dom.chat.appendChild(msg).scrollIntoView({ behavior: 'smooth' });

  }
  catch(err) {
    console.log('invalid JSON', err);
  }

});

Il gestore riceve i dati JSON trasmessi sulla proprietà .data dell’oggetto evento. La funzione lo analizza in un oggetto JavaScript e aggiorna la finestra di chat.

Infine, i nuovi messaggi vengono inviati utilizzando la funzione sendMessage() ogni volta che si attiva il gestore "submit" del form:

// invio del modulo
dom.form.addEventListener('submit', e => {
  e.preventDefault();
  sendMessage();
  dom.message.value = '';
  dom.message.focus();
}, false);

Gestione degli errori

Un evento "error" si attiva quando la comunicazione WebSocket fallisce. Questo può essere gestito sul server:

// gestisci errore WebSocket sul server
socket.on('error', e => {
  console.log('WebSocket error:', e);
});

e/o sul client:

// gestisci errore WebSocket sul client
ws.addEventListener('error', e => {
  console.log('WebSocket error:', e);
})

Solo il client può ripristinare la connessione eseguendo nuovamente il costruttore new WebSocket().

Chiusura delle connessioni

Entrambi i dispositivi possono chiudere il WebSocket in qualsiasi momento utilizzando il metodo .close() della connessione. È possibile fornire opzionalmente argomenti interi code e stringa reason (massimo 123 byte), che vengono trasmessi al dispositivo remoto prima che si disconnetta.

WebSocket avanzati

Gestire i WebSocket è facile in Node.js: un dispositivo invia un messaggio utilizzando il metodo .send(), che attiva un evento "message" sull’altro. Come ogni dispositivo crea e risponde a tali messaggi può essere più impegnativo. Le sezioni seguenti descrivono problemi che potresti dover considerare.

Sicurezza WebSocket

Il protocollo WebSocket non gestisce autorizzazione o autenticazione. Non puoi garantire che una richiesta di comunicazione in entrata provenga da un browser o da un utente collegato al tuo web application — specialmente quando il web e i server WebSocket potrebbero essere su dispositivi diversi. La connessione iniziale riceve un’intestazione HTTP contenente cookie e il server Origin, ma è possibile falsificare questi valori.

La seguente tecnica garantisce che le comunicazioni WebSocket siano limitate agli utenti autorizzati:

  1. Prima di effettuare la richiesta iniziale WebSocket, il browser contatta il server web HTTP (forse utilizzando Ajax).

  2. Il server verifica le credenziali dell’utente e restituisce un nuovo biglietto di autorizzazione. Il biglietto di solito fa riferimento a una registrazione in un database che contiene l’ID dell’utente, l’indirizzo IP, il momento della richiesta, il tempo di scadenza della sessione e qualsiasi altro dato richiesto.

  3. Il browser trasmette il biglietto al server WebSocket nel handshake iniziale.

  4. Il server WebSocket verifica il biglietto e controlla fattori come l’indirizzo IP, il tempo di scadenza, ecc. prima di consentire la connessione. Esegue il metodo WebSocket .close() quando un biglietto è invalido.

  5. Il server WebSocket potrebbe dover ricontrollare la registrazione del database di tanto in tanto per assicurarsi che la sessione dell’utente rimanga valida.

Importante, validare sempre i dati in ingresso:

  • Come HTTP, il server WebSocket è suscettibile agli attacchi di SQL injection e altri tipi di attacchi.

  • Il client non dovrebbe mai iniettare valori grezzi nel DOM o valutare codice JavaScript.

Separate vs multiple istanze del server WebSocket

Si consideri un gioco multiplayer online. Il gioco ha molti universi che giocano separate istanze del gioco: universeA, universeB, e universeC. Un giocatore si collega a un universo:

  • universeA: connesso da player1, player2, e player3
  • universeB: connesso da player99

Si potrebbero implementare le seguenti opzioni:

  1. A separate WebSocket server for each universe.

    A player action in universeA would never be seen by those in universeB. However, launching and managing separate server instances could be difficult. Would you stop universeC because it has no players, or continue to manage that resource?

  2. Utilizzare un singolo server WebSocket per tutti gli universi del gioco.

    Questo utilizza meno risorse ed è più facile da gestire, ma il server WebSocket deve tenere traccia di quale universo si collega ogni giocatore. Quando player1 esegue un’azione, deve essere trasmessa a player2 e player3 ma non a player99.

Multipli server WebSocket

L’applicazione chat di esempio può gestire centinaia di utenti simultanei, ma si bloccherebbe una volta che la popolarità e l’uso della memoria superano i livelli critici. Dovrai alla fine scalare orizzontalmente aggiungendo ulteriori server.

Ogni server WebSocket può gestire solo i propri client connessi. Un messaggio inviato da un browser a serverX non potrebbe essere trasmesso a quelli connessi a serverY. Potrebbe diventare necessario implementare sistemi di messaggistica backend publisher–subscriber (pub-sub). Ad esempio:

  1. WebSocket serverX desidera inviare un messaggio a tutti i client. Pubblica il messaggio sul sistema pub–sub.

  2. Tutti i server WebSocket iscritti al sistema pub–sub ricevono un evento di nuovo messaggio (incluso serverX). Ogni server può gestire il messaggio e trasmetterlo ai propri client connessi.

Efficienza della messaggistica WebSocket

La comunicazione WebSocket è veloce, ma il server deve gestire tutti i client connessi. È necessario considerare le dinamiche e l’efficienza dei messaggi, specialmente quando si sviluppano giochi d’azione multiplayer:

  • Come si sincronizzano le azioni di un giocatore su tutti i dispositivi dei client?

  • Se player1 si trova in una posizione diversa da player2, è necessario inviare a player2 informazioni sugli azioni che non può vedere?

  • Come si affronta la latenza di rete — o il ritardo nella comunicazione? Qualcuno con una macchina veloce e una connessione avrebbe un vantaggio sleale?

I giochi veloci devono fare compromessi. Pensateci come giocare al gioco sul vostro dispositivo locale, ma alcuni oggetti sono influenzati dalle attività degli altri. Piuttosto che inviare la posizione esatta di ogni oggetto in ogni momento, i giochi spesso inviano messaggi più semplici e meno frequenti. Ad esempio:

  • objectX è apparso al puntoX
  • objectY ha una nuova direzione e velocità
  • objectZ è stato distrutto

Ogni gioco del cliente riempie i vuoti. Quando objectZ esplode, non avrà importanza se l’esplosione appare diversa su ogni dispositivo.

Conclusione

Node.js rende facile gestire WebSockets. Non necessariamente rende più facili da progettare o codificare le applicazioni in tempo reale, ma la tecnologia non ti trattenerà!

I principali svantaggi:

  • WebSockets richiedono una propria istanza server separata. Le richieste Ajax Fetch() e eventi inviati dal server possono essere gestiti dal web server che stai già eseguendo.

  • I server WebSocket richiedono i propri controlli di sicurezza e autorizzazione.

  • Le connessioni WebSocket interrotte devono essere ristabilite manualmente.

Ma non lasciarti scoraggiare!

Domande Frequenti (FAQs) su App in Tempo Reale con WebSockets e Eventi Inviati dal Server

In che modo i WebSockets differiscono da HTTP in termini di prestazioni e funzionalità?

WebSockets forniscono un canale di comunicazione full-duplex su una singola connessione TCP, il che significa che i dati possono essere inviati e ricevuti simultaneamente. Questa è una significativa miglioria rispetto a HTTP, dove ogni richiesta richiede una nuova connessione. WebSockets consentono anche il trasferimento di dati in tempo reale, rendendoli ideali per applicazioni che richiedono aggiornamenti istantanei, come app di chat o aggiornamenti di eventi sportivi in diretta. D’altra parte, HTTP è senza stato e ogni coppia richiesta-risposta è indipendente, il che può essere più adatto per applicazioni in cui gli aggiornamenti in tempo reale non sono necessari.

Puoi spiegare il ciclo di vita di una connessione WebSocket?

Il ciclo di vita di una connessione WebSocket inizia con un handshake, che aggiorna una connessione HTTP a una connessione WebSocket. Una volta stabilita la connessione, i dati possono essere scambiati tra il client e il server fino a quando una delle parti decide di chiudere la connessione. La connessione può essere chiusa da entrambe le parti inviando un frame di chiusura, seguito dall’altra parte che conferma il frame di chiusura.

Come posso implementare WebSockets in un’applicazione Android?

L’implementazione di WebSockets in un’applicazione Android comporta la creazione di un client WebSocket che può connettersi a un server WebSocket. Questo può essere fatto utilizzando librerie come OkHttp o Scarlet. Una volta configurato il client, è possibile aprire una connessione al server, inviare e ricevere messaggi e gestire diversi eventi come l’apertura della connessione, la ricezione dei messaggi e la chiusura della connessione.

Cosa sono gli Eventi Trasmessi da Server e come si confrontano con WebSockets?

I Server-Sent Events (SSE) sono uno standard che consente a un server di inviare aggiornamenti a un client tramite HTTP. A differenza dei WebSockets, gli SSE sono unidirezionali, il che significa che consentono solo l’invio di dati dal server al client. Ciò li rende meno adatti per le applicazioni che richiedono una comunicazione bidirezionale, ma possono essere una soluzione più semplice ed efficiente per le applicazioni che hanno solo bisogno di aggiornamenti dal server.

Quali sono alcuni casi d’uso comuni per i WebSockets e gli Server-Sent Events?

I WebSockets sono comunemente utilizzati nelle applicazioni che richiedono una comunicazione in tempo reale, bidirezionale, come app di chat, giochi multiplayer e strumenti collaborativi. Gli Server-Sent Events, d’altra parte, sono spesso utilizzati nelle applicazioni che necessitano di aggiornamenti in tempo reale dal server, come aggiornamenti di notizie in diretta, aggiornamenti dei prezzi delle azioni o rapporti di avanzamento per attività di lunga durata.

Come posso gestire le connessioni WebSocket in un’applicazione Spring Boot?

Spring Boot fornisce supporto per la comunicazione WebSocket attraverso il modulo Spring WebSocket. È possibile utilizzare l’annotazione @EnableWebSocket per abilitare il supporto WebSocket e quindi definire un WebSocketHandler per gestire il ciclo di vita della connessione e la gestione dei messaggi. È inoltre possibile utilizzare il SimpMessagingTemplate per inviare messaggi ai client connessi.

Quali sono le considerazioni di sicurezza quando si utilizzano i WebSockets?

Come qualsiasi altra tecnologia web, WebSockets può essere vulnerabile a vari rischi di sicurezza, come l’Hacking Cross-Site di WebSocket (CSWSH) e gli attacchi di Denial of Service (DoS). Per mitigare questi rischi, dovresti sempre utilizzare connessioni WebSocket sicure (wss://) e convalidare e sanitizzare tutti i dati in ingresso. Dovresti anche considerare l’uso di meccanismi di autenticazione e autorizzazione per controllare l’accesso al tuo server WebSocket.

Posso usare WebSockets con un’API REST?

Sì, puoi utilizzare WebSockets in combinazione con un’API REST. Mentre le API REST sono ottime per la comunicazione stateless request-response, WebSockets possono essere utilizzati per una comunicazione bidirezionale in tempo reale. Ciò può essere particolarmente utile nelle applicazioni che richiedono aggiornamenti istantanei, come app di chat o aggiornamenti live di eventi sportivi.

Come posso testare un server WebSocket?

Ci sono diversi strumenti disponibili per testare i server WebSocket, come il Test di Eco di WebSocket.org o Postman. Questi strumenti ti consentono di aprire una connessione WebSocket a un server, inviare messaggi e ricevere risposte. Puoi anche scrivere test automatizzati per il tuo server WebSocket utilizzando librerie come Jest o Mocha.

Quali sono le limitazioni di WebSockets e Server-Sent Events?

Mentre WebSockets e Server-Sent Events offrono potenti capacità per la comunicazione in tempo reale, presentano anche limitazioni. Ad esempio, non tutti i browser e le reti supportano queste tecnologie, e possono consumare una quantità significativa di risorse se non gestiti correttamente. Inoltre, possono essere più complessi da implementare e gestire rispetto alla comunicazione HTTP tradizionale.

Source:
https://www.sitepoint.com/real-time-apps-websockets-server-sent-events/