Introduzione
La qualità di servizio delle connessioni a Internet ha tristemente diffuso nel senso comune l'idea che le applicazioni web siano lente, poco responsive, inaffidabili e a tratti inattendibili. Sebbene questa convinzione, per qualche oscura legge di Murphy, venga sempre confermata da cattive pratiche di programmazione o cattiva qualità di connessione, l'estensione delle web app nei più disparati settori è un dato di per se' eloquente: operando attraverso la rete, le applicazioni web godono di tutti i vantaggi offerti dal mezzo di comunicazione, in primo piano la logistica e il forte disaccoppiamento spaziale, temporale e di carico.
Non essendo il mondo fatto solo di vantaggi, esse ricevono in dote anche tutte le criticità strutturali delle tecnologie utilizzate. All'aumentare della complessità, aumentano anche le aspettative e così i requisiti, fino ad arrivare al punto che le tecnologie in uso non sono più in grado di fornire strumenti capaci di soddisfare queste mutevoli esigenze. Un fattore critico nel quale applicazioni desktop e web si sono sempre differenziate è la responsività.
Le applicazioni odierne risentono, in determinate circostanze, dell'esigenza di poter usfruire di comunicazioni di tipo event-driven che le infrastrutture attuali non sono in grado di offrire se non attraverso particolari espedienti. Le tecnologie attuali non sono in grado di garantire trasmissioni dati che abbiano un certo grado di affidabilità e che siano soggette a una latenza più bassa possibile, in sostanta, abbastanza bassa da poter essere definita, molto sommariamente, tempo reale. Il rispetto di vincoli temporali così stringenti è una esigenza nata in tempi recenti con l'esplosione di nuovi settori di mercato come i giochi online, i social network e applicazioni time-critical, come le applicazioni finanziarie o transazionali.
Il sistema di comunicazione attuale, basato su protocollo HTTP, dopo anni di inaspettata gloria si è, improvvisamente, rivelato in buona parte inadatto e in parte inefficiente a gestire questi particolari scenari: da qui la necessità di nuovo strumento capace di sopperire a questa mancanza. La comunicazione HTTP è stata concepita per funzionare in modalità half duplex, ovvero utilizzando un modello di comunicazione che permette il transito delle informazione in una sola direzione per volta; ogni pacchetto è altresì appesantito da una prolissa intestazione HTTP che non solo è fonte di overhead in termini traffico, ma anche una possibile fonte di latenza. Ovviamente il progetto iniziale prevedeva poche variazioni al classico Request-Response: l'evolversi di diversi modelli, dall'information pushing al modello a eventi, hanno costretto gli sviluppatori ad arrangiarsi e "forzare" il sistema con un po' inventiva.
La soluzione AJAX
Attualmente, per ottenere qualcosa di vagamente somigliante a una interazione Publish-Subscribe, si utilizzano due tecniche basate sull'ormai onnipresente AJAX: Short Polling e Long Polling.
Lo Short Polling, denominato comunemente AJAX Polling, è la forma più semplice di polling, nel quale il client si incarica di effettuare regolarmente delle chiamate al server, per ottenere dati aggiornati. Convenzionalmente, se non vi sono dati aggiornati, il server risponderà con un messaggio vuoto. Come tutte le declinazioni di polling, questo metodo, sebbene sia il più semplice e il più diffuso, implica uno spreco di risorse computazionali, sia sul server che sul client, e un notevole utilizzo della banda per l'invio di mesaggi request-response il cui contenuto sarà, per la maggior parte, irrilevante (messaggio di risposta vuoto). Se concettualmente possiamo affermare che con questo approccio l'attesa è delegata al client, esiste anche una metodologia di comunicazione denominata polling lungo, nella quale idealmente l'attesa viene delegata invece al server.
Il Long Polling, familiarmente conosciuto come Reverse AJAX, è un modello di comunicazione web nel quale, a fronte di una singola richiesta di inizializzazione, si cerca di mantenere una connessione HTTP costantemente aperta fra client e server. Quando il server dovrà notificare ai client, non farà altro che inviare dei chunk di dati sulla connessione persistente. Nella sua forma più diffusa il Long Polling è implementato mediante l'utilizzo di un IFRAME nascosto, nel quale il client richiede una determinata pagina (azione di subscribe) e il server risponde servendo una pagina HTML a chunk distruibuiti nel tempo. Questa implementazione è nota come Comet, ed è quella che possiamo facilmente incontrare per gestire, ad esempio, gli instant messaging HTTP che si possono trovare sui vari social network. Ogni qualvolta il server desidera inviare un evento, aggiunge un chunk alla pagina Comet contente un blocco \<script\> con del codice da eseguire: il client riceve il blocco JavaScript come contenuto della sua pagina sapientemente centellinata e lo manda in esecuzione nientedimeno come fosse una funzione di callback. Simulando l'invio di una pagina estremamente lenta o grande, si riesce a mantenere aperta la connessione.
Che si tratti di Short o Long Polling, la natura da "workaround" dei due approcci è abbastanza palese: si pensi che esistono implementazioni di fallback basate persino su Flash o Siverlight, e la padronanza di queste metodologie può risultare in un ulteriore aggravio di tempo e risorse, anche se mitigate da librerie JavaScript wrapper che riescano a nasconderne la complessità.
Nella mente degli sviluppatori è chiaro che, anche se questo approccio comunque funziona, sarebbe necessario mettere finalmente a punto una soluzione veramente pulita a queste problematiche: è lo scopo che, in un futuro prossimo, dovrebbero raggiungere le WebSocket.
WebSocket
Quando parliamo di WebSocket intendiamo due standard precisi che fanno da corollario al grande ecosistema che ruota intorno a HTML5. Le WebSocket API sono API JavaScript per accedere e gestire la rispettiva implementazione del browser, direttamente standardizzate dal W3C. Insieme a queste nuove API, è stato introdotto anche un nuovo protocollo di comunicazione, costruito "on top" a HTTP per garantirne la coesistenza, standardizzato dall'IETF.
Perchè un protocollo nuovo?
Il protocollo WebSocket prevede due nuovi URI scheme (ws:, per connessioni in chiaro, e wss:, per connessioni criptate) per differenziarsi dall'HTTP classico, rimanendo con esso compatibile. Il sistema prevede l'invio e la ricezione di pacchetti UTF-8 e, in futuro, di binary frame attraverso un singolo canale TCP operante in modalità full duplex, ovvero supportando la trasmissione dati contemporanea in entrambe le direzioni.
Il funzionamento e il nome ricordano molto da vicino le socket TCP-IP, ma vi sono alcune sostanziali differenze: le WebSocket non sono porte TCP-IP raw, si posizionano a un livello di astrazione più alto, devono sottostare al protocollo HTTP e, solo dopo una fase di handshake, si passa a operare sul nuovo protocollo.
HandShake
Il primo messaggio per effettuare l'handshake è a tutti gli effetti una richiesta HTTP GET al server verso il quale si desidera aprire una connessione WebSocket. Questo messaggio iniziale contiene una richiesta di upgrade al protocollo WebSocket, nella forma:
Client Request
GET /resource HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
Host: example.com:8080
Origin: http://www.example.com
Sec-WebSocket-Key1: W1529l9 6i4b{{a 9 z55
Sec-WebSocket-Key2: F13 O05 q q6 - 4k7D 97
-#&nD\[fU
Per essere totalmente retrocompatibili, la prima riga specifica una richiesta "HTTP/1.1 GET", mentre le successive due sono l'offerta di upgrade. Seguono l'host e la porta al quale si richiede la connessione e un campo di origine che indica il dominio che richiede la risorsa, non sempre coincidente con l'HTTP referer. Le due chiavi inviate dal client sono rispettivamente di 8 byte, vengono generate casualmente e serviranno al server per costruire la risposta alla challenge per concludere l'handshake.
Se il server accetta la connessione risponderà con:
Server Response
HTTP/1.1 101 Web Socket Protocol Handshake Upgrade: WebSocket Connection: Upgrade Sec-WebSocket-Origin: http://www.example.com Sec-WebSocket-Location: ws://example.com:8080/resource 8jKS'y:G\*Co,Wxa- dove in coda è presente la risposta alla challenge, ottenuta estraendo i valori numerici delle chiavi che accompagnano la request, combinandole ed estranedone un hash MD5. La porta di funzionamento è generalmente la medesima di HTTP, l'80 in chiaro e la 413 per il protocollo sicuro, in maniera tale da aggirare qualsiasi problema di autorizzazioni su porte non convenzionali, sebbe sia possibile instradare una connessione anche attraverso le porte TCP 81 e 415.
Il protocollo ws: supporta la stessa forma di autenticazione base via cookie di HTTP, ma una volta inizializzata la comunicazione e svolto con successo l'handshake, l'header HTTP non viene più utilizzato, in favore di un più agile header di soli 2 byte. Ogni messaggio consiste in un marker byte 0x00, un dato UTF-8 e un marker byte di fine messaggio 0xFF. Una qualsiasi deviazione da questo modello, ad esempio la mancata ricezione di uno dei due marker delimitatori, provoca la chiusura immediata della connessione con un messaggio di errore. Lato client, ogni messaggio ricevuto, fa scattare la funzione di handler onMessage(), permettendo al client di processare il messaggio e agire di conseguenza.
Il senso delle WebSocket
L'utilizzo delle WebSocket elimina in toto la necessità di utilizzare una qualsiasi forma di polling, in quanto i messaggi vengono scambiati solo all'occorrenza. Per dare un ordine di grandezza si può immaginare una ipotetica applicazione di chat uno-a-molti, nella quale ciò che uno scrive venga inviato, carattere per carattere agli altri partecipanti, un po' come avviene quando più persone editano il medesimo documento su servizi come Google Docs. Una volta costruita la socket, ogni singolo messaggio potrà essere dell'ordine di una ventina di byte; in uno scenario AJAX a polling, invece, lo stesso comportamento richiederebbe di istanziare ogni volta una oggetto XHR e il transito di coppie di messaggi di una magnitudine almeno 1 volta superiore. Inoltre la comunicazione WebSocket stabilisce un tunnel di comunicazione che attraversa in maniera trasparente tutti gli agenti sul percorso di rete, siano essi proxy, router o firewall, risultando in una comunicazione end-to-end esente da tutti i problemi che affliggono generalmente le tecniche Comet. Salvo utilizzare librerie JavaScript particolari, la creazione di una WebSocket è triviale, anche se utilizzarle in maniera corretta è tutta un'altra questione: ne' il server ne' il browser si fanno carico del canale, la cui gestione è delegata, con tutti i suoi pro e i suoi contro, interamente al programmatore. Ammettiamo che per qualche ragione una connessione ballerina faccia traballare la vostra socket: nonesiste alcun meccanismo per capire se la chiusura di una socket sia voluta o accidentale, non esiste una politica da seguire nel caso di un fallimento, non esistono politiche di riconnessione o di time-out detection. Implementare queste features ex novo ogni volta può risultare estremamente dispendioso. La specifica non tratta per ora nemmeno la tematica della qualità di servizio:se la connessione viene interrotta, è nuovamente competenza dello sviluppatore l'implementazione di un sistema di code di messaggi e altri meccanismi di persistenza o di posticipazione dei messaggi. Spesso mi capita di assistere ad affermazioni alquanto "naif" di sedicenti visionari che vedono le WebSocket come la panacea, la soluzione naturale alle problematiche di comunicazione di un ecosistema di applicazioni web sempre più complesse. Che le applicazioni siano destinate a esplodere ulteriormente in complessità è innegabile; nondimeno ritengo sia importante non cadere nell'ingenuità di considerare le WebSocket come l'unico strumento di comunicazione possibile. Le WebSocket introdotte con HTML5, la prima specifica HTML a prevedere una socket di comunicazione client server, sono destinate a cambiare radicalmente il modo in cui scriviamo le applicazioni. Non dobbiamo dimenticare tuttavia che esse nascono per soddisfare una serie di requisiti limitati e precisi, insoddisfatti dall'attuale modello asincrono a messaggi. Cosa serve per sviluppare con le WebSocket? Allo stato attuale quasi tutti i browser di nuova generazione supportano le WebSocket:
- Chrome 4
- FireFox 4
- Opera 11
- Safari 5 Desktop
- Safari 5 IOS dalla versione 4.2 a seguire
- WebKit da Android 2.3 Gingerbread a seguire
- Internet Exlorer 9 non nativamente, ma attraverso un plugin da installare
È importante sottolineare che il supporto alle WebSocket è correntemente disabilitato di default sulla maggior parte di questi browser, sia per il carattere ancora sperimentale di queste implementazioni, sia a causa di una vulnerabilità evidenziata dalla specifica stessa. Di fatto alcuni proxy potrebbero non riconoscere l'handshake WebSocket, interpretando le conseguenti intestazioni come comandi HTTP validi, aprendo la strada a tecniche di poisoning o quant'altro. Fin tanto che questa problematica non sarà affrontata, i vendor hanno concordato nel mantenere disabilitata questa funzione dei browser. Lato client si possono utilizzare diverse tecnologie. Per partire con Java, le tecnologie adatte sono le seguenti:
- Jetty +7.0
- Glassfish +3.1
- Caucho Resin +4.0.2
- Kaazing Gateway
- jWebSocket [5]
- Netty
- xLightWeb
In PHP si può usare phpwebsocket. In altri linguaggi sono disponibili web-socket-ruby (evidentemente per Ruby) e Socket IO-node (per node.js).
Al lavoro!
Nonostante sia ancora una tecnologia embrionale, possiamo comunque dilettarci a scrivere un progettino client che utilizzi le WebSocket. Lo sviluppo della parte server esula da questo articolo, utilizziamo invece per convenienza il servizio di echo gratuito presente su WebSocket.Org [1].
Una semplice pagina HTML
<!DOCTYPE html> <meta charset="utf-8" /> <html> <head> <title>HTML5 WebSocket Echo Test</title> <script> // Inseriremo qua i nostri script </script> </head> <body> <h1>HTML5 WebSocket Echo Test</h1> <div style="padding:10px; margin: 4px; border: 1px solid #000;"> <label>Server Url:</label> <input id="serverUrl" size="25" value="ws://echo.websocket.org/" /> <button id="connetti">Connetti</button> <button id="disconnetti">Disconnetti</button> <br /> <label>Messaggio:</label> <input id="messaggio" size="25" value="" /> <button id="invia">Invia</button> </div> <div id="output" style="padding:10px; margin: 4px; color: #FFF; background-color: #000;" ></div> </body> </html>
Lo script
Ci teniamo solo due variabili globali, per non inquinare troppo il global scope, uno che punterà alla nostra WebSocket e uno contenente tutte i metodi che ci servono, in questo caso una funzione di inizializzazione init(), una funzione di setup degli handler setHandler() e una funzione di log per visualizzare i messaggi nella div#output.
var websocket;
var test = {
init: function(){ ... },
setHandler: function(){ ... },
log: function(message){ ... }
};
La funzione di log() non è nulla di estremamente evoluto:
...
log: function(message){
var p = document.createElement("p");
p.innerHTML = message;
document.getElementById("output").appendChild(p);
}
...
Creiamo un elemento p, lo riempiamo con la stringa passata come parametro e ne facciamo un append al div#output. Nella funzione di init() dobbiamo assicurarci che il browser che stiamo utilizzando supporti la tecnologia WebSocket e per far ciò controlliamo che "WebSocket" sia una proprietà di window.
... init: function(){
if("WebSocket" in window){
test.log("WebSocket Supportate :)");
test.setHandler();
}else{
test.log("Spiacente, WebSocket non Supportate");
}
},
...
Avremmo potuto controllare anche con una più tradizionale:
if (window.WebSocket){ ... }
Se il browser ha questa capability, possiamo invocare il setup degli handler, altrimenti logghiamo dispiaciuti.
L'oggetto WebSocket
L'oggetto WebSocket è relativamente semplice da istanziare: ha i due metodi principali, per aprire e chiudere la connessione, una variabile di stato e tre handler che vengono invocati allo scatenarsi del rispettivo evento.
// Connessione a un server
var websocket = new WebSocket("ws://example.com:8080/resource");
// handler invocato ad apertura ultimata
websocket.onopen = function(evt) { ... };
// handler invocato alla ricezione di un messaggio
websocket.onmessage = function(evt) { ... };
// handler invocato in caso di errore
websocket.onerror = function(evt) { ... };
// handler invocato sulla chiusura da ambo le parte della socket
websocket.onclose = function() { ... };
// metodo di invio dati
websocket.send(data);
// chiusura della connessione
websocket.close();
// stato della socket
// 0 == In connessione
// 1 == Aperta
// 2 == Chiusa
var state = websocket.readyState;
Da qui, possiamo costruirci la nostra funzione setHandler() nella quale facciamo il grosso del lavoro:
...
setHandler: function(){
//binding delle funzioni ai 3 pulsanti dell'interfaccia
document.getElementById("connetti").onclick = function() {
var url = document.getElementById("serverUrl").value;
try {
// Connessione alla socket
websocket = new WebSocket(url);
test.log("Websocket Status: "+websocket.readyState+ " ( In connessione )");
//setup degli handler
websocket.onopen = function(evt) {
test.log("Websocket Status: "
+websocket.readyState
+" ( Connesso a: "+url+")");
};
websocket.onclose = function(evt) {
test.log("Websocket Status: "
+websocket.readyState
+" ( Disconnesso )");
};
websocket.onmessage = function(evt) { test.log("Ricevuto: " + evt.data); };
websocket.onerror = function(evt) { test.log("Errore: " + evt.data); };
} catch(ex) {
test.log("Errore: "+ex);
}
};
document.getElementById("disconnetti").onclick = function() {
websocket.close();
};
document.getElementById("invia").onclick = function(){
var messaggio = document.getElementById("messaggio").value;
try {
websocket.send(messaggio);
test.log("Inviato: "+messaggio+" ...");
} catch(ex){
test.log("Errore: "+ex);
}
};
},
...
Non ci resta ora che aggiungere l'esecuzione dello script dell'init() all'evento di onload della pagina usando l'addEventListener(), occupandoci anche delle versioni di Internet Explorer < 9 che non supportano questo metodo di binding.
if (window.addEventListener){
window.addEventListener("load", test.init, false); //binding per tutti
} else {
window.attachEvent( "onload" , test.init); // binding per IE
}
Lo script completo
<!DOCTYPE html>
<meta charset="utf-8" />
<html>
<head>
<title>HTML5 WebSocket Echo Test</title>
<script>
var websocket;
var test = {
init: function(){
if("WebSocket" in window){
test.log("WebSocket Supportate :)");
test.setHandler();
}else{
test.log("Spiacente, WebSocket non Supportate");
}
},
setHandler: function(){
document.getElementById("connetti").onclick = function() {
var url = document.getElementById("serverUrl").value;
try {
websocket = new WebSocket(url);
test.log("Websocket Status: "
+websocket.readyState
+ " ( In connessione )");
websocket.onopen = function(evt) {
test.log("Websocket Status:
"+websocket.readyState+
" ( Connesso a: "+url+")");
};
websocket.onclose = function(evt) {
test.log("Websocket Status: "
+websocket.readyState
+" ( Disconnesso )");
};
websocket.onmessage
= function(evt) { test.log("Ricevuto: " + evt.data); };
websocket.onerror
= function(evt) { test.log("Errore: " + evt.data); };
} catch(ex) {
test.log("Errore: "+ex);
}
};
document.getElementById("disconnetti").onclick = function() {
websocket.close();
};
document.getElementById("invia").onclick = function(){
var messaggio
= document.getElementById("messaggio").value;
try {
websocket.send(messaggio);
test.log("Inviato: "+messaggio+" ...");
} catch(ex){
test.log("Errore: "+ex);
}
};
},
log: function(message){
var p = document.createElement("p");
p.innerHTML = message;
document.getElementById("output").appendChild(p);
}
};
if (window.addEventListener){
window.addEventListener("load", test.init, false);
} else {
window.attachEvent( "onload" , test.init);
}
</script>
</head>
<body>
<h1>HTML5 WebSocket Echo Test</h1>
<div style="padding:10px; margin: 4px; border: 1px solid #000;">
<label>Server Url:</label>
<input id="serverUrl" size="25" value="ws://echo.websocket.org/" />
<button id="connetti">Connetti</button>
<button id="disconnetti">Disconnetti</button>
<br />
<label>Messaggio:</label>
<input id="messaggio" size="25" value="" />
<button id="invia">Invia</button>
</div>
<div id="output" style="padding:10px; margin: 4px;
color: #FFF; background-color: #000;" ></div>
</body>
</html>
E se le Web Socket non sono disponibili?
Come abbiamo visto, un utilizzo ragionato delle WebSocket non è lineare come ci si potrebbe aspettare e spesso è richiesto allo sviluppatore di implementare obbligatoriamente un set minimo di funzionalità ripetutamente attraverso progetti diversi. Inoltre l'eterogeneità delle piattaforme rischia, in un progetto nel quale è necessario raggiungere una audience relativamente estesa, di rendere la vostra applicazione inacessibile. In questi casi è abbastanza consigliabile prendere in considerazione anche soluzioni di ripiego nel caso le WebSocket non siano disponibili, come tecnologie di long polling mediate da framework come Atmosphere [6] o framework JavaScript come socket.io [7] che automaticamente rileva le capability del browser e impiega la tecnologia migliore disponibile esponendo comunque un set uniforme di API.
Conclusioni: lo stato dell'arte
Attualmente come abbiamo accennato, la specifica delle WebSocket è in fase di revisione in quanto ancora soggetta a vulnerabilità strutturali, e non è ancora stata esplicitamente delineata tutta una serie di casistiche e di problematiche che potrebbero portare al fallimento della comunicazione. Ad esempio non sono ancora gestite corse critiche nelle quali entrambe gli estremi inviino contemporaneamente grandi flussi di dati, caso che negli attuali browser porta ad un deadlock. Nei prossimi articoli, verranno affrontate le tematiche relative alle interazioni/comportamenti, e gli importanti aspetti legati alle funzioni e ai formati multimediali in HTML5
Riferimenti
- [1] Il servizio di echo lato server cui appoggiarsi per provare il codice
http://websocket.org/echo.html
- [2] La specifica WebSocket API, ancora in versione draft
http://dev.w3.org/html5/websockets/
- [3] Il nuovo protocollo ws
http://www.whatwg.org/specs/web-socket-protocol/
- [4] La precedente specifica sul nuovo protocollo websocket
http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
- [5] jWebSocket
http://jwebsocket.org/
- [6] Il framework Atmosphere per il long polling
http://atmosphere.java.net/
- [7] Il framework JavaScript socket.io
http://socket.io/
PAGE \* MERGEFORMAT 9
