Il protocollo WebSocket, descritto nella specifica RFC 6455 fornisce un modo per scambiare dati tra browser e server tramite una connessione persistente. I dati possono essere passati in entrambe le direzioni come “pacchetti”, senza interrompere la connessione e ulteriori richieste HTTP.

WebSocket è particolarmente grande per i servizi che richiedono un continuo scambio di dati, ad esempio giochi online, sistemi di trading in tempo reale e così via.

Un semplice esempio

Per aprire una connessione websocket, dobbiamo creare new WebSocket usando il protocollo speciale ws nell’url:

let socket = new WebSocket("ws://javascript.info");

C’è anche il protocollo criptato wss://. È come HTTPS per i websockets.

Preferisci sempre wss://

Il protocollo wss:// non è solo criptato, ma anche più affidabile.

Questo perché i dati ws:// non sono criptati, visibili per qualsiasi intermediario. I vecchi server proxy non conoscono WebSocket, possono vedere intestazioni “strane” e interrompere la connessione.

D’altra parte, wss:// è WebSocket su TLS, (come HTTPS è HTTP su TLS), il livello di sicurezza del trasporto cripta i dati al mittente e li decripta al ricevitore. Quindi i pacchetti di dati vengono passati criptati attraverso i proxy. Non possono vedere cosa c’è dentro e lasciarli passare.

Una volta creato il socket, dovremmo ascoltare gli eventi su di esso. Ci sono totalmente 4 eventi:

  • open – connessione stabilita,
  • message – dati ricevuti,
  • error – errore websocket,
  • close – connessione chiusa.

…E se vogliamo inviare qualcosa, allora socket.send(data) lo farà.

Ecco un esempio:

let socket = new WebSocket("wss://javascript.info/article/websocket/demo/hello");socket.onopen = function(e) { alert(" Connection established"); alert("Sending to server"); socket.send("My name is John");};socket.onmessage = function(event) { alert(` Data received from server: ${event.data}`);};socket.onclose = function(event) { if (event.wasClean) { alert(` Connection closed cleanly, code=${event.code} reason=${event.reason}`); } else { // e.g. server process killed or network down // event.code is usually 1006 in this case alert(' Connection died'); }};socket.onerror = function(error) { alert(` ${error.message}`);};

Per scopi dimostrativi, c’è un piccolo server server.js scritto in Node.js, per l’esempio sopra, in esecuzione. Risponde con “Ciao dal server, John”, poi aspetta 5 secondi e chiude la connessione.

Quindi vedrete gli eventi openmessageclose.

Ecco fatto, possiamo già parlare di WebSocket. Abbastanza semplice, no?

Ora parliamo più approfonditamente.

Apertura di un websocket

Quando new WebSocket(url) viene creato, inizia subito la connessione.

Durante la connessione il browser (usando gli header) chiede al server: “Supporti Websocket?” E se il server risponde “sì”, allora la conversazione continua nel protocollo WebSocket, che non è affatto HTTP.

Ecco un esempio di intestazioni del browser per la richiesta fatta da new WebSocket("wss://javascript.info/chat").

GET /chatHost: javascript.infoOrigin: https://javascript.infoConnection: UpgradeUpgrade: websocketSec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==Sec-WebSocket-Version: 13

  • Origin – l’origine della pagina client, per esempio https://javascript.info. Gli oggetti WebSocket sono cross-origine per natura. Non ci sono intestazioni speciali o altre limitazioni. I vecchi server non sono in grado di gestire WebSocket in ogni caso, quindi non ci sono problemi di compatibilità. Ma l’intestazione Origin è importante, in quanto permette al server di decidere se parlare WebSocket con questo sito web o meno.
  • Connection: Upgrade – segnala che il client vorrebbe cambiare il protocollo.
  • Upgrade: websocket – il protocollo richiesto è “websocket”.
  • Sec-WebSocket-Key – una chiave casuale generata dal browser per la sicurezza.
  • Sec-WebSocket-Version – versione del protocollo WebSocket, 13 è quella attuale.
L’handshake WebSocket non può essere emulato

Non possiamo usare XMLHttpRequest o fetch per fare questo tipo di richiesta HTTP, perché JavaScript non può impostare questi header.

Se il server accetta di passare a WebSocket, dovrebbe inviare il codice 101 di risposta:

101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=

Qui Sec-WebSocket-AcceptSec-WebSocket-Key, ricodificato usando un algoritmo speciale. Il browser lo usa per assicurarsi che la risposta corrisponda alla richiesta.

In seguito, i dati vengono trasferiti utilizzando il protocollo WebSocket, vedremo presto la sua struttura (“frames”). E questo non è affatto HTTP.

Estensioni e sottoprotocolli

Ci possono essere ulteriori intestazioni Sec-WebSocket-Extensions e Sec-WebSocket-Protocol che descrivono estensioni e sottoprotocolli.

Per esempio:

  • Sec-WebSocket-Extensions: deflate-frame significa che il browser supporta la compressione dei dati. Un’estensione è qualcosa relativo al trasferimento dei dati, una funzionalità che estende il protocollo WebSocket. L’intestazione Sec-WebSocket-Extensions viene inviata automaticamente dal browser, con la lista di tutte le estensioni che supporta.

  • Sec-WebSocket-Protocol: soap, wamp significa che vorremmo trasferire non qualsiasi dato, ma i dati nei protocolli SOAP o WAMP (“The WebSocket Application Messaging Protocol”). I sottoprotocolli WebSocket sono registrati nel catalogo IANA. Quindi, questa intestazione descrive i formati di dati che useremo.

    Questa intestazione opzionale è impostata utilizzando il secondo parametro di new WebSocket. Questo è l’array di sottoprotocolli, ad esempio se vogliamo usare SOAP o WAMP:

    let socket = new WebSocket("wss://javascript.info/chat", );

    Il server dovrebbe rispondere con una lista di protocolli ed estensioni che accetta di usare.

    Per esempio, la richiesta:

    GET /chatHost: javascript.infoUpgrade: websocketConnection: UpgradeOrigin: https://javascript.infoSec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==Sec-WebSocket-Version: 13Sec-WebSocket-Extensions: deflate-frameSec-WebSocket-Protocol: soap, wamp

    Risposta:

    101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=Sec-WebSocket-Extensions: deflate-frameSec-WebSocket-Protocol: soap

    Qui il server risponde che supporta l’estensione “deflate-frame”, e solo SOAP dei sottoprotocolli richiesti.

    Trasferimento dati

    La comunicazione WebSocket consiste in “frames” – frammenti di dati, che possono essere inviati da entrambe le parti, e possono essere di diversi tipi:

    • “text frames” – contengono dati di testo che le parti si inviano a vicenda.
    • “binary data frames” – contengono dati binari che le parti si inviano a vicenda.
    • “ping/pong frames” sono usati per controllare la connessione, inviati dal server, il browser risponde a questi automaticamente.
    • ci sono anche “connection close frame” e alcuni altri frame di servizio.

    Nel browser, lavoriamo direttamente solo con i frame di testo o binari.

    WebSocket .send() metodo può inviare sia dati testuali che binari.

    Una chiamata socket.send(body) permette body in formato stringa o binario, incluso BlobArrayBuffer, ecc. Non servono impostazioni: basta inviarlo in qualsiasi formato.

    Quando riceviamo i dati, il testo arriva sempre come stringa. E per i dati binari, possiamo scegliere tra i formati Blob e ArrayBuffer.

    Questo è impostato dalla proprietà socket.binaryType"blob" di default, quindi i dati binari arrivano come oggetti Blob.

    Blob è un oggetto binario di alto livello, si integra direttamente con <a><img> e altri tag, quindi è un default sano. Ma per l’elaborazione binaria, per accedere a singoli byte di dati, possiamo cambiarlo in "arraybuffer":

    socket.binaryType = "arraybuffer";socket.onmessage = (event) => { // event.data is either a string (if text) or arraybuffer (if binary)};

    Rate limiting

    Immaginate, la nostra app sta generando molti dati da inviare. Ma l’utente ha una connessione di rete lenta, forse su un internet mobile, fuori da una città.

    Possiamo chiamare socket.send(data) ancora e ancora. Ma i dati saranno bufferizzati (immagazzinati) in memoria e inviati solo alla velocità consentita dalla rete.

    La proprietà socket.bufferedAmount memorizza quanti byte rimangono bufferizzati in questo momento, in attesa di essere inviati in rete.

    Possiamo esaminarla per vedere se il socket è effettivamente disponibile per la trasmissione.

    // every 100ms examine the socket and send more data// only if all the existing data was sent outsetInterval(() => { if (socket.bufferedAmount == 0) { socket.send(moreData()); }}, 100);

    Chiusura della connessione

    Normalmente, quando una parte vuole chiudere la connessione (sia il browser che il server hanno uguali diritti), invia un “connection close frame” con un codice numerico e un motivo testuale.

    Il metodo per questo è:

    socket.close(, );

    • code è un codice speciale di chiusura WebSocket (opzionale)
    • reason è una stringa che descrive il motivo della chiusura (opzionale)

    Quindi l’altra parte in close gestore di eventi ottiene il codice e il motivo, e.g.

    // closing party:socket.close(1000, "Work complete");// the other partysocket.onclose = event => { // event.code === 1000 // event.reason === "Work complete" // event.wasClean === true (clean close)};

    I valori di codice più comuni:

    • 1000 – la chiusura normale e predefinita (usata se non viene fornito nessun code),
    • 1006 – nessun modo di impostare tale codice manualmente, indica che la connessione è stata persa (nessun frame di chiusura).

    Ci sono altri codici come:

    • 1001 – la parte sta andando via, ad es. il server si sta spegnendo, o un browser lascia la pagina,
    • 1009 – il messaggio è troppo grande da elaborare,
    • 1011 – errore inaspettato sul server,
    • …e così via.

    L’elenco completo può essere trovato in RFC6455, §7.4.1.

    I codici WebSocket sono un po’ come i codici HTTP, ma diversi. In particolare, qualsiasi codice inferiore a 1000 è riservato, ci sarà un errore se cerchiamo di impostare un tale codice.

    // in case connection is brokensocket.onclose = event => { // event.code === 1006 // event.reason === "" // event.wasClean === false (no closing frame)};

    Stato della connessione

    Per ottenere lo stato della connessione, inoltre c’è la proprietà socket.readyState con valori:

    • 0 – “CONNECTING”: la connessione non è ancora stata stabilita,
    • 1 – “OPEN”: in comunicazione,
    • 2 – “CLOSING”: la connessione è in chiusura,
    • 3 – “CLOSED”: la connessione è chiusa.

    Esempio di chat

    Esaminiamo un esempio di chat utilizzando le API WebSocket del browser e il modulo WebSocket di Node.js https://github.com/websockets/ws. Presteremo l’attenzione principale al lato client, ma anche il server è semplice.

    HTML: abbiamo bisogno di un <form> per inviare messaggi e un <div> per i messaggi in arrivo:

    <!-- message form --><form name="publish"> <input type="text" name="message"> <input type="submit" value="Send"></form><!-- div with messages --><div></div>

    Da JavaScript vogliamo tre cose:

    1. Aprire la connessione.
    2. Sull’invio del modulo – socket.send(message) per il messaggio.
    3. Sul messaggio in arrivo – appendilo a div#messages.

    Ecco il codice:

    let socket = new WebSocket("wss://javascript.info/article/websocket/chat/ws");// send message from the formdocument.forms.publish.onsubmit = function() { let outgoingMessage = this.message.value; socket.send(outgoingMessage); return false;};// message received - show the message in div#messagessocket.onmessage = function(event) { let message = event.data; let messageElem = document.createElement('div'); messageElem.textContent = message; document.getElementById('messages').prepend(messageElem);}

    Il codice lato server è un po’ oltre il nostro scopo. Qui useremo Node.js, ma non è necessario. Anche altre piattaforme hanno i loro mezzi per lavorare con WebSocket.

    L’algoritmo lato server sarà:

    1. Creare clients = new Set() – un insieme di socket.
    2. Per ogni websocket accettato, aggiungerlo all’insieme clients.add(socket) e impostare message ascoltatore di eventi per ricevere i suoi messaggi.
    3. Quando un messaggio viene ricevuto: iterare sui client e inviarlo a tutti.
    4. Quando una connessione viene chiusa: clients.delete(socket).
    const ws = new require('ws');const wss = new ws.Server({noServer: true});const clients = new Set();http.createServer((req, res) => { // here we only handle websocket connections // in real project we'd have some other code here to handle non-websocket requests wss.handleUpgrade(req, req.socket, Buffer.alloc(0), onSocketConnect);});function onSocketConnect(ws) { clients.add(ws); ws.on('message', function(message) { message = message.slice(0, 50); // max message length will be 50 for(let client of clients) { client.send(message); } }); ws.on('close', function() { clients.delete(ws); });}

    Ecco l’esempio funzionante:

    Si può anche scaricare (pulsante in alto a destra nell’iframe) ed eseguire in locale. Basta non dimenticare di installare Node.js e npm install ws prima dell’esecuzione.

    Summario

    WebSocket è un modo moderno per avere connessioni browser-server persistenti.

    • I WebSockets non hanno limitazioni cross-origin.
    • Sono ben supportati nei browser.
    • Possono inviare/ricevere stringhe e dati binari.

    L’API è semplice.

    Metodi:

    • socket.send(data),
    • socket.close(, ).

    Eventi:

    • open,
    • message,
    • error,
    • close.

    WebSocket da solo non include riconnessione, autenticazione e molti altri meccanismi di alto livello. Quindi ci sono librerie client/server per questo, ed è anche possibile implementare queste capacità manualmente.

    A volte, per integrare WebSocket in un progetto esistente, le persone eseguono il server WebSocket in parallelo con il principale server HTTP, e condividono un unico database. Le richieste a WebSocket usano wss://ws.site.com, un sottodominio che porta al server WebSocket, mentre https://site.com va al server HTTP principale.

    Sicuramente, sono possibili anche altri modi di integrazione.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *