Chi sviluppa per il Web, si scontra ogni giorno con le “limitazioni” del modello request-response di HTTP, il quale non è pensato “ad hoc” per applicazioni real time, o comunicazione bidirezionale client-server. Per questi scenari, spesso vengono utilizzate tecniche di polling, da parte del client, per verificare l’eventuale presenza di dati “freschi”, eseguendo continuamente delle richieste HTTP, comportando dei “costi”: il server deve utilizzare differenti connessioni TCP per ogni client (una per inviare informazioni ed una per rispondere alla richiesta di aggiornamenti), un continuo scambio di messaggi client-server (comportando quindi un aumento del traffico di rete). Potrebbe sembrare cosa da poco, ma se pensiamo ad applicazioni web con migliaia di client connessi, le risorse di server e banda potrebbero essere velocemente consumate (e dato che la nostra epoca è
affamata di risorse potremmo trovarci in situazioni poco piacevoli).
Quindi, quale potrebbe essere la soluzione ? utilizzare una singola connessione TCP per gestire il traffico in entrambe le direzioni, ovvero il protocollo WebSocket, che se gestito mediante le WebSocket API (WSAPI), fornisce un’ottima alternativa alla tecnica di polling di cui parlavamo in precedenza. Possiamo utilizzare i WebSocket in diversi scenari: giochi, applicazioni di editing real time multi-utente, UI web che espongono dati lato server in tempo reale ecc .… tutto questo condividendo le porte standard HTTP 80 e 443, “attraversando” firewalls, proxies e router senza problemi. Possiamo riassumere le due “tecniche” discusse in precedenza con la figura seguente;
dalla quale si evince come l’utilizzo del “polling” comporti una maggiore latenza nella trasmissione delle informazioni utili. Nell’immagine, nella comunicazione bidirezionale offerta dai WebSocket, come primo passo (dal client verso il server) c’è la voce “WebSocket upgrade”, cosa sarà mai ?, un passo per volta.
Il protocollo è diviso in due parti: handshakes e transfer data. Durante la prima parte (di negoziazione), client e server comunicano scambiandosi dei pacchetti di controllo per stabilire una connessione WebSocket. Il client invia una richiesta di questo tipo:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Version: 13
ed il server (normalmente) dovrebbe rispondere con:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
In dettaglio, il processo di connessione parte con una standard GET HTTP, e fin qui tutto regolare:
GET /echo HTTP/1.1
Host: example.microsoft.com
poi, l’header “Upgrade” richiede al server di cambiare il protocollo a livello di application-layer da HTTP a WebSocket:
Upgrade: websocket
Connection: Upgrade
Il valore presente in Sec-WebSocket-Key inviato dal client verrà utilizzato dal server per far capire al chiamante che “comprende” la richiesta:
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
L’header Sec-WebSocket-Version identifica la versione del protocollo da utilizzare:
Sec-WebSocket-Version: 13
A questo punto, se il server “comprende” la nostra richiesta , il server eseguirà un “Upgrade” dell’application-layer e risponderà con lo stato HTTP 101. Il valore contenuto in Sec-WebSocket-Accept verrà utilizzato dal client per validare che il server è un WebSocket server e non un “semplice” HTTP server.
Una volta che client e server hanno entrambi inviati i loro handshakes, (e l’handshaking ha avuto successo) può iniziare il trasferimento dati su un unico canale di comunicazione bidirezionale, tra client e server, sfruttando la connessione TCP precedentemente stabilita.
I dati trasferiti tra le due parti sono racchiusi in unità logiche chiamate “messaggi”, ogni messaggio è fisicamente composto da uno o più frames.
Dopo la teoria la pratica…
Il protocollo WebSocket, definisce due nuove tipologie di URI:
- ws://host [: port] path [? query] (di default usa la porta 80 e connessione non cifrata)
- wss://host [:port] path [? query] (di default utilizza la porta 443, ed una connessione cifrata HTTP over TLS)
Le ultime specifiche del protocollo sono definite nel documento RFC 6455. Per “sporcarci le mani” con i WebSocket, possiamo sviluppare un semplice pagina web con estensione .html, come la seguente:
I bottoni Open e Close, sono rispettivamente utilizzati per aprire e chiudere la connessione del WebSocket, mentre il campo text per inviare un messaggio al server, il quale risponderà inviando al client la lunghezza della stringa inviata (ovviamente il livello pratico dell’esempio è praticamente nullo ).
Per stabilire una connessione con il server, utilizziamo le seguenti righe di codice client-script:
var uri = 'ws://localhost:9915/WebSocketHandler.ashx';
websocket = new WebSocket(uri);
Per conoscere quando la connessione con il server è stata creata è necessario sottoscriversi all’evento
onopen:
websocket.onopen = function () {
appendData("Connected.");
appendData(uri);
$('#wsform').submit(function (event) {
websocket.send($('#inputbox').val());
$('#inputbox').val('');
event.preventDefault();
});
$("#closeButton").removeAttr("disabled");
};
Con lo script precedente, all’apertura della connessione visualizziamo il messaggio “Connected.” e l’URI utilizzato per la connessione. Dopodiché ci registriamo all’evento
submit per inviare al server, utilizzando l’API
Send, messaggi al WebSocket server sotto forma di testo UTF-8 o Blobs.
Per ricevere messaggi, dobbiamo registrarsi all’evento onmessagge dell’oggetto websocket il quale riceve messageEvent che espone la proprietà data, contenente le informazioni inviate al client:
websocket.onmessage = function (messageEvent) {
var receivedData = messageEvent.data.toString();
appendData(receivedData);
};
Il codice precedente è veramente banale, in quanto si limita a recuperare il testo inviato dal server in risposta al client e visualizzarlo sul browser tramite un div.
Come per l’apertura, c’è una sorta di handshaking per la chiusura della connessione, che può essere iniziata indipendentemente dal client o dal server. Chi inizia il processo di chiusura, invia uno speciale frame, detto close frame, che può contenere, opzionalmente, un codice di stato (il protocollo fornisce un insieme di codici di stato appropriati per la chiusura) e la ragione (una descrizione testuale) della chiusura. Quando una delle due parti riceve un close frame, invia lo stesso frame all’altra parte (eventualmente, prima di rinviarlo, invia i messaggi pending) e la comunicazione viene chiusa.
Per iniziare il processo di close handshake, bisogna invocare l’API close:
function closeWebSocket() {
websocket.close(1000, "Normal.");
}
Il valore 1000 indica lo stato “Normal Closure”. Come “ragione” della chiusura specifichiamo il testo “Normal”. Per sapere quando la chiusura è stata completata, ci sottoscriviamo all’evento onclose:
websocket.onclose = function () {
appendData("Closed.");
$("#closeButton").attr("disabled", "disabled");
};
Con la funzione precedente, visualizziamo il messaggio “Closed.”, e disabilitiamo il button per la chiusura del WebSocket.
La parte server è composta da una classe IHttpHandler che fa uso della classe WebSocketContext (.NET 4.5) utilizzata per accedere alle informazioni di WebSocket handshake. Attraverso altri semplici post, l’obiettivo è realizzare una semplice “lavagna” interattiva real time, utilizzando sia un browser web, che un client WPF, come mostrato seguente:
A tal fine, utilizzeremo lato Windows, la classe ClientWebSocket, presente nel namespace System.Net.WebSockets, la quale fornisce un client per connettersi a servizi esposti tramite WebSocket.