Le WebSockets API introducono, nella loro estrema semplicità, una funzionalità tra le più attese ed emulate: la possibilità di stabilire e mantenere una connessione dati tra browser e server remoto sulla quale far transitare messaggi in entrambe le direzioni. Le attuali specifiche, che lasciano ben poco spazio per implementazioni del genere, hanno, nel corso degli anni, dato luogo a workaround più o meno esotici tra i quali l'utilizzo di socket in Flash pilotati via Javascript e della famosa tecnica di long polling (l'utilizzo continuo di chiamate AJAX mantenute aperte fino alla ricezione del dato o al tempo di timeout). Le nuove API offrono invece un meccanismo ben più semplice grazie all'oggetto WebSocket
, al metodo send e all'evento onmessage
.
Prima di passare alla visione delle specifiche e al dovuto esempio di implementazione è importante ricordare che questa tecnologia non consente di creare connessioni verso altri, ben conosciuti protocolli, come ad esempio telnet, SMTP, IRC, etc., per due distinti motivi: in primo luogo lo user agent implementa una policy che blocca l'accesso verso porte riservate a servizi conosciuti (fanno eccezione solo la 80 e la 443). In seconda istanza le comunicazioni viaggiano all'interno di un nuovo e specifico protocollo, chiamato con molta fantasia 'The WebSocket Protocol' che richiama per certi aspetti, soprattutto durante l'handshake, una conversazione HTTP.
JavaScript websocket: le specifiche
L'utilizzo di queste API è assolutamente didascalico, ci troviamo di fronte infatti a poco più di tre metodi; vediamoli insieme:
var echo_service = new WebSocket('ws://echo.websocket.org');
La creazione di un nuovo WebSocket richiede come unico parametro obbligatorio l'url verso la quale si vuole stabilire la connessione. Il protocollo può essere ws
o wss
, dove il secondo indica la richiesta di una connessione sicura. Opzionalmente è possibile passare al costruttore anche una stringa o un array di sub-protocolli: valori arbitrari utili per comunicare al server un elenco di servizi che l'oggetto in costruzione può supportare. Ad esempio un server di chat potrebbe rispondere solo a richieste con protocollo 'server_di_chat', e via dicendo...
echo_service.onmessage = function(event){
alert(event.data);
}
Una volta creato un nuovo WebSocket
, il funzionamento dello stesso diventa praticamente identico, nella forma, a quello già esposto per la comunicazione tra Worker: la funzione associata all'handler onmessage
viene invocata ogniqualvolta dal server proviene un messaggio,
echo_service.onopen = function(){
echo_service.send("hello!");
}
mentre la funzione send
provvede all'invio, verso il server remoto, del testo passato come argomento. Da notare che l'invio deve essere necessariamente subordinato alla condizione di avvenuta connessione, notificata tramite l'evento onopen
. Esistono altri eventi all'interno del ciclo vita di un WebSocket: onclose
e onerror
; vediamoli insieme completando l'esempio in HTML:
<!doctype html>
<html>
<head>
<title> WebSocket: Echo Server </title>
<script>
append = function(text){
document.getElementById("eventi_websocket").insertAdjacentHTML('beforeend',
"<li>" + text + ";</li>"
);
}
window.onload = function(){
var echo_service = new WebSocket('ws://echo.websocket.org');
echo_service.onmessage = function(event){
append("messaggio ricevuto")
alert(event.data);
echo_service.close();
}
echo_service.onopen = function(){
append("connessione effettuata")
echo_service.send("hello!");
}
echo_service.onclose = function(){
append("connessione chiusa");
}
echo_service.onerror = function(){
append("errore nella connessione");
}
}
</script>
</head>
<body>
<ul id="eventi_websocket">
</ul>
</body>
</html>
In questo esempio è stato introdotto anche il metodo close
, utile per terminare una connessione. Eseguiamo l'esempio all'interno di Chromium (figura 1):
Prima di proseguire è bene ricordare che il server utilizzato per questo esempio: echo.websocket.org
ha la peculiarità, come il nome stesso suggerisce, di rispondere ad ogni messaggio con lo stesso testo ricevuto.
Un esempio
Nel prossimo capitolo estenderemo il progetto guida in modo che ogni FiveBoard sia connessa ad un server centrale; creeremo quindi un viewer: una pagina html capace di connettersi ad una FiveBoard registrata presso il server e leggerne il testo mano mano esso viene digitato. Ecco lo schema (figura 2):
Per poter raggiungere questo obiettivo è necessario scrivere anche del codice in un linguaggio server-side. Tale codice servirà per creare e istruire il WebSocket Server, al quale le varie pagine dovranno connettersi; ai fini di questo esempio non è necessario cercare un servizio di hosting sul quale installare il codice del WebSocket Server: la nostra macchina di sviluppo andrà benissimo. Per questa implementazione utilizzeremo Ruby, un linguaggio di programmazione elegante e conciso. L'installazione dell'interprete Ruby è veramente facile ed il codice che utilizzeremo molto leggibile. Per prima cosa colleghiamoci al portale ufficiale: http://www.ruby-lang.org/it/downloads/ e selezioniamo la procedura di installazione dedicata al nostro sistema operativo, quindi apriamo una console di comando (a volte chiamata anche terminale) e digitiamo:
gem install em-websocket
Per installare la libreria necessaria allo sviluppo del WebSocket Server (per alcuni sistemi operativi è necessario anteporre sudo all'istruzione).
Creiamo ora un file 'websocket_server.rb' e modifichiamone il contenuto come segue:
require 'rubygems'
require 'em-websocket'
EventMachine.run {
@channels = Hash.new {|h,k| h[k] = EM::Channel.new }
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 8080, :debug => true) do |
ws|
ws.onopen do
sid = nil
fiveboard_channel = nil
ws.onmessage do |msg|
command, value = msg.split(":", 2);
case command
when 'registra'
fiveboard_channel = @channels[value]
sid = fiveboard_channel.subscribe { |txt| ws.send(txt) }
when 'aggiorna'
fiveboard_channel.push('testo:' + value)
end
end
ws.onclose do
fiveboard_channel.unsubscribe(sid)
end
end
end
puts "Il server è correttamente partito"
}
Seppur possiate essere non abituati a questo linguaggio il codice è tutto sommato comprensibile e succinto, ecco la spiegazione dell'algoritmo:
- Con l'istruzione
WebSocket.start
(.. l'applicazione si mette in attesa di connessioni websocket sulla porta 8080; ogni connessione in ingresso viene memorizzata nella variabilews
e causa l'esecuzione delle successive istruzioni (quelle comprese nell'attiguo bloccodo..end
). - Alla ricezione di un messaggio attraverso una connessione
ws
(ws.onmessage
) il server si comporta dividendo il testo ricevuto secondo la solita convenzione 'comando:valore
' ed agendo in modo diverso a seconda che il comando sia 'registra' o 'aggiorna'. - Nel caso il messaggio sia '
registra:titolo_del_documento
' il server aggiungerà la connessione attuale ad un canale che porta il nome del valore del messaggio (in questo caso 'titolo_del_documento
'). In questo modo tutte le pagine che vorranno 'osservare' il documento 'A
' non dovranno far altro che inviare al WebSocket Server il messaggio 'registra:A
'. - Nel caso il messaggio sia '
aggiorna:testo_del_documento
' il server si comporterà semplicemente inviando lungo il canale associato alla connessione corrente il valore del messaggio (in questo caso 'testo_del_documento
'), propagandolo in questo modo a tutte le connessioni registrate al canale. - Infine con l'istruzione
ws.onclose do...
si gestisce, in caso di disconnessione del client, la rimozione diws
dal canale presso il quale si era registrata.
Eseguiamo il server portandoci con il terminale nella posizione dello script e digitando:
ruby websocket_server.rb
Un messaggio, 'Il server è correttamente partito', dovrebbe confermare la bontà del nostro operato. Dedichiamoci ora alle API Javascript ed alla loro implementazione, per prima cosa editiamo il file 'js/application.js' per fare in modo che ogni FiveBoard si registri presso il server ed invii segnali di aggiornamento ad ogni modifica del testo, ecco il codice da inserire all'interno della funzione window.onload
:
// all'interno di window.onload, js/application.js
// queste due istruzioni sono state spostate dalla loro precedente posizione
titolo_fiveboard = prompt("Seleziona il titolo per questa FiveBoard");
document.title = "FB: " + titolo_fiveboard;
// creazione di un nuovo socket verso il server Ruby
websocket = new WebSocket('ws://0.0.0.0:8080');
websocket.onopen = function(){
// invio del comando 'registra'
websocket.send("registra:" + titolo_fiveboard);
}
// ad ogni variazione di input segue l'invio del comando 'aggiorna' /
/ verso il server Ruby
document.forms['form_da_ricordare'].elements['testo_da_ricordare'].oninput = function
(event){
websocket.send("aggiorna:" + event.target.value);
}
Anche in questo caso l'implementazione risulta abbastanza leggibile; unico appunto da fare sull'evento oninput
, anch'esso novità introdotta dalle specifiche HTML5, che viene invocato ad ogni attività di input (pressione di un tasto, copia ed incolla, drag and drop,...) sull'elemento in oggetto.
Completiamo l'esempio con la creazione della semplicissima pagina 'viewer.html':
<!doctype html>
<html>
<head>
<title>FiveBoard Viewer</title>
<script>
window.onload = function(){
var documento_da_visionare = prompt("Inserisci il nome del documento che vuoi
osservare");
var websocket = new WebSocket('ws://0.0.0.0:8080');
websocket.onopen = function(){
document.title = "VB: " + documento_da_visionare;
websocket.send("registra:" + documento_da_visionare);
}
websocket.onmessage = function(evento){
nome_comando = evento.data.split(":")[0]
valore_comando = evento.data.substr(nome_comando.length + 1);
switch (nome_comando){
case 'testo':
document.getElementById('documento_in_visione').value = valore_comando;
break;
}
}
}
</script>
</head>
<body>
<textarea id="documento_in_visione" readonly>Aspettando il primo aggiornamento...</ textarea>
</body>
</html>
In questo caso la pagina è istruita nel reagire alla ricezione di un messaggio da parte del WebSocket Server; il valore ricevuto viene infatti gestito con la solita convenzione 'comando:valore
' e, nel caso il comando sia 'testo
', il valore viene inserito all'interno di una textarea preposta.
Bene, eseguiamo una prova di funzionamento: sincerandoci di aver lanciato il WebSocket Server navighiamo prima sulla pagina 'index.html' e creiamo un nuovo documento 'esempio1
', quindi apriamo una nuova finestra e puntiamola all'indirizzo di 'viewer.html': alla richiesta del nome del documento inseriamo anche qui 'esempio1
' in modo da legare il viewer alla fiveboard.
Ora proviamo a digitare alcuni caratteri sulla prima finestra e noteremo che, attraverso il server, questi sono propagati in tempo reale alla seconda! Ovviamente il tutto funzionerebbe anche se la conversazione avvenisse tra due macchine distinte attraverso un server remoto; nell'immagine seguente si può notare una fiveboard aperta sull'iPad ed il suo corrispondente viewer visibile su di un portatile; in alto i messaggi di log del WebSocket Server (figura 3).
Conclusioni su WebSocket
I WebSocket, sono nella loro estrema semplicità, degli strumenti incredibilmente potenti; a riprova di questo fatto la rete ha già incominciato ad offrire interessanti prospettive di utilizzo nonostante l'innegabile giovinezza di queste API. Tra le soluzioni degne di nota merita sicuramente una citazione Pusher, un servizio che offre la possibilità di gestire eventi real-time attraverso l'utilizzo di WebSocket. Un po' più artigianale, ma altrettanto valido è http://jsterm.com/, un'applicazione che consente di collegarsi a server remoti utilizzando un proxy, scritto in node.js, tra il protocollo WebSocket e Telnet.
Recentemente lo sviluppo delle API è stato frenato dalla scoperta di una seria vulnerabilità [documento PDF] legata al potenziale comportamento di un caching proxy che può essere indotto, attraverso l'utilizzo di WebSocket ad-hoc, a modificare il contenuto delle informazioni consegnate ad altri client. La vulnerabilità è stata correttamente risolta dalla versione 0.7 del protocollo ma sfortunatamente le versioni 4 e 5 di Firefox, rilasciate prima del fix, hanno i websocket disabilitati per default; la versione 6 del browser dovrebbe però ripristinare il funzionamento out-of-the-box di questa interessantissima feature.
Tabella del supporto sui browser
API e Web Applications | |||||
---|---|---|---|---|---|
WebSockets | No | 4.0+ (parziale) | 5.0+ | 7.0+ | 11.0+ (parziale) |