Come funzionano i WebSocket: Una guida completa alle connessioni in tempo reale

Nei primi giorni del web, il browser era un semplice visualizzatore di documenti. Richiedevi una pagina, il server la renderizzava e la connessione si chiudeva. Questo ciclo di richiesta-risposta è il cuore di HTTP (Hypertext Transfer Protocol).

Tuttavia, con l’evoluzione delle applicazioni web in esperienze interattive e dinamiche — come chat in tempo reale, quotazioni finanziarie in tempo reale, editing collaborativo e giochi multiplayer — il modello HTTP tradizionale ha iniziato a mostrare i suoi limiti.

Per ottenere aggiornamenti in tempo reale, inizialmente gli sviluppatori si affidavano a soluzioni temporanee:

  • Short Polling: Il browser invia ripetutamente richieste HTTP al server ogni pochi secondi per verificare la presenza di nuovi dati. Ciò crea un enorme overhead di intestazioni e spreca risorse del server.
  • Long Polling (Comet): Il browser invia una richiesta e il server la tiene aperta fino a quando non sono disponibili nuovi dati. Una volta inviati i dati, la connessione si chiude e il browser apre immediatamente una nuova richiesta. Questa soluzione è complessa da gestire e comporta comunque un notevole overhead per la configurazione della connessione.

I WebSocket hanno risolto queste limitazioni introducendo un protocollo standardizzato per comunicazioni persistenti, bidirezionali e full-duplex su una singola connessione TCP.


Cos’è un WebSocket?

I WebSocket (definiti in RFC 6455) operano a fianco di HTTP. Mentre HTTP è un protocollo senza stato in cui solo il client può avviare richieste, una connessione WebSocket rimane aperta a tempo indeterminato, consentendo sia al client che al server di inviarsi dati in qualsiasi momento con una latenza minima.

Ecco la regola fondamentale dei WebSocket:

Una volta stabilita la connessione, ciascuna parte può inviare messaggi in qualsiasi momento senza avviare una nuova richiesta di connessione.


Guida dettagliata: Il ciclo di vita della connessione

Una connessione WebSocket attraversa tre fasi distinte: l’Handshake, il Trasferimento dei Dati e la Chiusura.

WebSocket Connection Lifecycle Diagram

1. L’HTTP Handshake (Upgrade del Protocollo)

Poiché i firewall e i router sono configurati per consentire il normale traffico web sulle porte 80 (HTTP) e 443 (HTTPS), i WebSocket iniziano il loro percorso come una normale richiesta HTTP/1.1. Questo processo prende il nome di Upgrade Handshake.

La Richiesta del Client

Il client invia una richiesta HTTP GET con intestazioni specifiche che richiedono un cambio di protocollo:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
  • Upgrade: websocket e Connection: Upgrade: Informano il server che il client desidera cambiare protocollo.
  • Sec-WebSocket-Key: Un valore casuale a 16 byte codificato in Base64. Serve a dimostrare che il server ha ricevuto l’handshake e comprende il protocollo WebSocket.
  • Sec-WebSocket-Version: Specifica la versione del protocollo WebSocket (solitamente la 13).
  • Origin: Utilizzato dal server per decidere se consentire la connessione (controllo di sicurezza contro i siti non autorizzati).

La Risposta del Server

Se il server supporta i WebSocket, convalida la richiesta e risponde con un codice di stato HTTP 101 Switching Protocols:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
  • Come il Server Calcola Sec-WebSocket-Accept:
    1. Il server prende la chiave del client Sec-WebSocket-Key (dGhlIHNhbXBsZSBub25jZQ==).
    2. La concatena con una stringa magica standard GUID: "258EAFA5-E914-47DA-95CA-C5AB0DC85B11".
    3. Calcola l’hash SHA-1 della stringa combinata.
    4. Codifica l’hash risultante in Base64.
    5. Se il client verifica che questo valore corrisponde alle sue aspettative, l’handshake ha successo, la connessione HTTP passa a un socket TCP grezzo e entrambe le parti passano al protocollo WebSocket.

2. Framing dei Dati e Trasferimento

A differenza di HTTP, che invia intestazioni in formato testo seguite da un corpo, i WebSocket trasmettono dati in pacchetti binari strutturati chiamati frame.

Un frame WebSocket ha un’intestazione molto leggera (da 2 a 14 byte) seguita dal payload. Questa intestazione contiene:

  • Bit FIN (1 bit): Indica se questo è l’ultimo frame di un messaggio.
  • Opcode (4 bit): Definisce il tipo di frame:
    • 0x1: Frame di testo (codificato in UTF-8)
    • 0x2: Frame binario
    • 0x8: Richiesta di chiusura della connessione
    • 0x9: Ping
    • 0xA: Pong
  • Bit Mask (1 bit): Specifica se i dati del payload sono mascherati.
  • Payload Length: La dimensione dei dati.
  • Masking Key (4 byte): Requisito di sicurezza fondamentale: Tutti i frame inviati dal client al server devono essere mascherati (offuscati tramite XOR) utilizzando una chiave casuale a 4 byte. Questo impedisce alle cache proxy di leggere il traffico o di eseguire attacchi di cache poisoning. I frame da server a client non devono essere mascherati.

Heartbeats (Ping/Pong)

Per evitare che router e load balancer chiudano le connessioni inattive, ciascuna parte può inviare un frame di Ping. La parte ricevente deve rispondere immediatamente con un frame di Pong contenente lo stesso payload.

3. Chiusura della Connessione

Per chiudere una connessione in modo pulito:

  1. Un peer invia un frame di Close contenente un codice di stato (ad esempio, 1000 per la normale chiusura, 1006 per chiusura anomala) e un motivo testuale facoltativo.
  2. L’altro peer risponde con il proprio frame di Close.
  3. Il socket TCP sottostante viene chiuso.

Esempio di codice: Implementazione WebSocket in Node.js

Per vedere i WebSocket in azione, scriviamo una semplice applicazione in Node.js. Creeremo un server WebSocket locale che restituisce qualsiasi messaggio riceva, insieme a uno script client per connettersi ad esso.

Il Server WebSocket (server.js)

const { WebSocketServer } = require('ws');
const http = require('http');

// 1. Crea un server HTTP standard
const server = http.createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('HTTP Server running. Use WebSocket to connect.\n');
});

// 2. Collega un server WebSocket al server HTTP
const wss = new WebSocketServer({ server });

wss.on('connection', (ws, req) => {
    const clientIp = req.socket.remoteAddress;
    console.log(`[Server] Nuovo client connesso da ${clientIp}`);

    // Invia un messaggio di benvenuto al client
    ws.send(JSON.stringify({ type: 'welcome', message: 'Connesso al server WebSocket Ghaznix!' }));

    // Ascolta i messaggi in arrivo da questo client
    ws.on('message', (message) => {
        console.log(`[Server] Ricevuto: ${message}`);
        
        try {
            const data = JSON.parse(message);
            ws.send(JSON.stringify({
                type: 'echo',
                message: `Eco del server: ${data.text.toUpperCase()}`,
                timestamp: new Date().toISOString()
            }));
        } catch (e) {
            ws.send(JSON.stringify({ type: 'error', message: 'Formato JSON non valido' }));
        }
    });

    // Gestisci la disconnessione del client
    ws.on('close', (code, reason) => {
        console.log(`[Server] Client disconnesso (Codice: ${code}, Motivo: ${reason.toString() || 'Nessuno'})`);
    });

    ws.on('error', (error) => {
        console.error(`[Server] Errore del socket: ${error.message}`);
    });
});

server.listen(8080, () => {
    console.log('Il server WebSocket è in ascolto su ws://localhost:8080');
});

Il Client del Browser (JavaScript lato client)

Puoi eseguire questo client direttamente nella console del tuo browser:

// 1. Stabilisci la connessione al server
const socket = new WebSocket('ws://localhost:8080');

// 2. Gestore connessione aperta
socket.addEventListener('open', (event) => {
    console.log('[Client] Connesso al server.');
    
    const payload = JSON.stringify({ text: 'ciao, server!' });
    socket.send(payload);
    console.log(`[Client] Inviato: ${payload}`);
});

// 3. Ascolta i messaggi dal server
socket.addEventListener('message', (event) => {
    const response = JSON.parse(event.data);
    console.log('[Client] Messaggio ricevuto dal server:', response);
});

// 4. Ascolta la chiusura della connessione
socket.addEventListener('close', (event) => {
    console.log(`[Client] Connessione chiusa (Codice: ${event.code})`);
});

// 5. Ascolta eventuali errori
socket.addEventListener('error', (error) => {
    console.error('[Client] Errore WebSocket:', error);
});

HTTP vs. WebSocket: Un confronto dettagliato

Caratteristica HTTP/1.1 WebSocket
Comunicazione Unidirezionale (avviata dal client) Bidirezionale (client o server)
Modello di connessione Richiesta-Risposta (breve durata) Persistente (lunga durata)
Overhead Alto (intestazioni inviate con ogni richiesta) Molto basso (overhead di framing minimo)
Stato Senza stato Con stato (il contesto di connessione viene mantenuto)
Protocollo http:// o https:// ws:// o wss://
Ideale per Recupero documenti, API REST Chat in tempo reale, dashboard, feed live

Considerazioni sulla sicurezza per i WebSocket

Poiché i WebSocket bypassano il routing HTTP standard dopo l’handshake, introducono vettori di sicurezza unici:

  1. Usa WebSocket Secure (wss://): Esegui sempre i WebSocket su TLS/SSL (porta 443). WSS crittografa il payload di framing, impedendo intercettazioni e manomissioni da parte di intermediari.
  2. Validazione dell’Origine: I WebSocket non sono limitati dalla Same-Origin Policy (SOP). Convalida sempre l’intestazione Origin sul server durante l’handshake per impedire l’accesso non autorizzato.
  3. Autenticazione all’Handshake: Autentica gli utenti prima che venga stabilita la connessione, ad esempio passando un token (come un JWT) nei parametri di query o verificando i cookie di sessione.
  4. Sanificazione dell’Input: Gestisci ogni messaggio ricevuto tramite WebSocket come input non attendibile. Valida e bonifica i payload per prevenire attacchi di Cross-Site Scripting (XSS).

I WebSocket hanno trasformato le applicazioni web in tempo reale eliminando l’overhead del polling HTTP tradizionale. Mantenendo una singola connessione TCP persistente, consentono la messaggistica bidirezionale istantanea, alimentando le moderne dashboard live, i giochi multiplayer e le app di chat. Comprendere l’upgrade HTTP, l’architettura di framing e le pratiche di sicurezza fondamentali ti assicurerà di sviluppare servizi in tempo reale veloci e sicuri.


Esplora altri tutorial e guide per sviluppatori sul Blog di Ghaznix →