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.
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 open
message
close
.
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 esempiohttps://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’intestazioneOrigin
è 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.
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-Accept
Sec-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’intestazioneSec-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)
permettebody
in formato stringa o binario, inclusoBlob
ArrayBuffer
, 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
eArrayBuffer
.Questo è impostato dalla proprietà
socket.binaryType
"blob"
di default, quindi i dati binari arrivano come oggettiBlob
.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 nessuncode
), -
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:
- Aprire la connessione.
- Sull’invio del modulo –
socket.send(message)
per il messaggio. - 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à:
- Creare
clients = new Set()
– un insieme di socket. - Per ogni websocket accettato, aggiungerlo all’insieme
clients.add(socket)
e impostaremessage
ascoltatore di eventi per ricevere i suoi messaggi. - Quando un messaggio viene ricevuto: iterare sui client e inviarlo a tutti.
- 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, mentrehttps://site.com
va al server HTTP principale.Sicuramente, sono possibili anche altri modi di integrazione.