Nel abbiamo conosciuto WebSocket nelle sue caratteristiche architetturali e affrontato la sua implementazione lato server con WildFly, in riferimento ad un progetto di esempio ben preciso che trovate su GitHub e in live demo su OpenShift. E’ il momento adesso di vedere come gestire l’invio e la ricezione di oggetti lato client, e come integrare il tutto in una applicazione AngularJS.
Il client
Riprendendo il progetto di esempio su GitHub, in questo post, vedremo come interagire con la nostra nuova WebSocket a partire da una pagina web, in quei browser che, come abbiamo visto, le supportano.
ECMAScript5 definisce la JavaScript WebSocket API, dove, guarda caso, tutto ruota attorno all’oggetto WebSocket
. Questo è capace di gestire gli eventi e la comunicazione con l’altro capo della socket, in modo molto simile a quanto visto lato server:
var websocket = new WebSocket("ws://localhost:8080/my-app/my-socket"); websocket.onopen = function(event) { ... }; websocket.onclose = function(event) { ... }; websocket.onmessage = function(event) { ... }; websocket.onerror = function(event) { ... };
L’unica cosa strana che si può vedere è che l’url non comincia per http:// ma per ws://!!! Chiamando infatti un endpoint websocket in http si ottiene un bel 404 perché non è in http! Una volta aperta la connessione (dopo l’evento websocket.onopen
), è possibile gestire le situazioni di errore (websocket.onerror
), stare in ascolto ai messaggi in arrivo (websocket.onmessage
) oppure inviarne di propri:
// Invio una stringa websocket.send('Hello!'); // Invio un oggetto websocket.send(JSON.stringify({message: 'Hello!'}));
In JavaScript possiamo quindi inviare un oggetto JSON semplicemente serializzandolo in stringa, oppure dati binari grazie all’oggetto Blob
, oppure più semplicemente appoggiandoci a BinaryJS.
Quando la comunicazione è terminata, entrambi gli endpoint del canale possono decidere di chiudere la connessione. E’ sempre possibile indicare una “Close Reason“, ovvero un codice di chiusura e volendo anche un testo, e in JavaScript si fa così:
websocket.close(1000, 'Thank you');
Il listener websocket.onclose
a questo punto riceve l’evento di chiusura.
In rete è disponibile l’elenco dei codici ammessi, anche se quelli più interessanti sono:
Codice | Nome | Descrizione |
---|---|---|
1000 | CLOSE_NORMAL | Chiusura normale della connessione. |
1001 | CLOSE_GOING_AWAY | L’endpoint se n’è andato. Questo per esempio succede quando si lascia una pagina che ha aperto una connessione senza chiuderla: il browser la chiude per noi con questo codice. |
1006 | CLOSE_ABNORMAL | Identifica solitamente gli errori di connessione: la connessione infatti si è chiusa senza il frame di chiusura. |
1009 | CLOSE_TOO_LARGE | L’endpoint ha ricevuto un frame troppo grande, cioè che la lunghezza massima consentita è stata superata. |
Se si chiude la connessione senza parametri, il codice di chiusura sarà CLOSE_GOING_AWAY. In generale però gli unici codici che si possono usare esplicitamente sono il 1000 (CLOSE_NORMAL) e i codici dal 4000 al 4999, che sono quelli riservati agli applicativi. Usare altri codici non è ammesso.
Finora abbiamo sempre parlato di messaggi e mai di frame: finché stiamo sotto il limite, in effetti possono essere la stessa cosa. Più in generale però, un messaggio può essere costituito da un numero illimitato di frame (per questo lato Java abbiamo visto dei decoders ed encoders di stream…).
In teoria questo limite è spropositato, ovvero è di 263 bytes, ma in realtà varia da browser a browser e mediamente sembra si aggiri intorno ai 231 bytes, ovvero 2Gb.
Gestione degli eventi
Abbiamo visto quindi che la comunicazione tramite WebSocket è fatta da azioni, eventi e listener su eventi: ad ogni azione, solitamente corrisponde un evento al quale può essere associato un listener. Ogni listener riceve come parametro un oggetto evento che ha una specifica tipologia (nell’attributo type):
Azione | Tipo Evento | Listener | Descrizione |
---|---|---|---|
var socket = new WebSocket(...) |
open | socket.onopen | la socket è aperta e pronta alla comunicazione. L’evento si scatena contemporaneamente sia sul client che sul server. |
(ricevo un messaggio dal server) | message | socket.onmessage |
E’ l’evento principale, quello che scatta quando si riceve un messaggio dalla socket. L’evento possiede un attributi fondamentale:
|
socket.send(...) | Invia un messaggio all’altro capo della socket, non genera nessun evento (a meno di errori di comunicazione). | ||
socket.close() | close | socket.onclose |
L’evento ricevuto contiene due attributi fondamentali:
Questo evento viene scatenato contemporaneamente sia sul client che sul server. |
(può avvenire ad ogni comunicazione) | error | socket.onerror | Viene lanciato in caso di errore di comunicazione. Solitamente, di seguito viene scatenato l’evento close, il quale conterrà le ragioni della chiusura. |
WebSocket e AngularJS
Visto come funzionano gli eventi, vediamo di integrarli nella logica di una applicazione AngularJS, fatta di controller, servizi e promesse!
La parte del progetto di demo che stiamo analizzando ha semplicemente una pagina con un campo di testo: il messaggio viene inviato e ci si attende un “Echo” dal server, dopo 5 secondi (secondo il diagramma di sequenza del post precedente). I passi della comunicazione vengono loggati sotto il campo:
Al bottone “Send” è associata una azione che esegue le seguenti operazioni:
- apre la socket
- una volta aperta, invia il test
- alla ricezione del messaggio che contiene “Echo” chiude la connessione
Per riuscire a rendere questo processo fluido, è necessario conciliare il modello a callback della gestione eventi delle WebSocket con le promises di Angular, in modo da poter inviare il messaggio appena la connessione è aperta, oppure gestire subito l’errore in caso contrario. Per far questo, possiamo usare il servizio $q di Angular, ispirato al framework Q di Kris Kowal.
Grazie a Q, siamo liberi così di disaccoppiare l’apertura della WebSocket in un service che renderà una promessa di una socket aperta ad un controller, il quale ne farà quel che vuole. Il service potrà così essere riusato per produrre socket valide:
angular.module('appServices', []) .factory('messageService', ['$http', '$q', function($http, $q) { var service = { openSocket : function() { var socket = new WebSocket(getRootUri() + '/socket/echo-message'); var deferred = $q.defer(); socket.onopen = function(event) { deferred.resolve(socket); }; socket.onerror = function(event) { socket.onerror = function(event) { alert(event); }; deferred.reject(event); }; function getRootUri() { return "ws://" + (document.location.hostname == "" ? "localhost" : document.location.hostname) + (document.location.port == "" ? "" : ":" + document.location.port); } return deferred.promise; } }; return service; }]);
La promessa verrà quindi risolta nel caso in cui venga scatenato l’evento WebSocket open; in caso di evento error, la promessa sarà rifiutata.
Il controller è quindi libero di gestire entrambe i casi:
angular.module('wsApp', []) .controller('messageController', ['$scope', 'messageService', '$log', function($scope, messageService, $log){ $scope.logs = []; $scope.sendMessage = function(message) { messageService.openSocket() .then(function(socket) { // do something }, function(event) { alert("Unable to open socket :("); }); }; }]);
La funzione anonima che riceve la socket sa che sicuramente è aperta (altrimenti sarebbe stato mostrato un alert
) e quindi può avviare la comunicazione:
log("Before send message: " + message); socket.send(angular.toJson({message: message})); log("After send message: " + message); socket.onmessage = function(event) { $scope.$apply(function() { var response = angular.fromJson(event.data); log(response.message + ', session id: ' + response.sessionId); }); if (event.data.indexOf('Echo') != -1) { socket.close(1000, 'Thank you'); } }
Il controller quindi invia un messaggio e poi imposta il listener per ricevere messaggi dal server. Quando riceve l'”Echo”, ringrazia e chiude.
L’evento message, a cui il listener socket.onmessage
sta in ascolto, verrà chiamato al di fuori del contesto di Angular: è necessario quindi aggiornare il modello (tramite la funzione log
) all’interno di una funzione passata a $scope.$apply
.
Conclusioni
In questo post abbiamo visto l’API JavaScript per WebSocket, fatta da eventi e da listener che ci danno un ampio controllo sulla comunicazione, in modo speculare a quanto avviene lato server. Grazie al servizio $q di AngularJS, è stato semplice trasformare due callback opposte in una promise: l’esempio di fatto è molto semplice (diciamo didattico, anche se plausibile), ma questa tecnica è molto efficace e permette di manipolare qualsiasi evento asincrono (come l’evento message) in modo da essere trasformato in una promessa. Grazie a queste, siamo riusciti a separare la logica di apertura della socket (in un service), da quella di comunicazione (in un controller), permettendoci di gestire l’accesso all’endpoint in un solo post nell’applicativo.
Nel prossimo (e ultimo) post sulle WebSocket, vedremo come l’aiuto di RxJava e le nuove Concurrency Utilities di Java EE 7, al posto di EJB Asincroni ed eventi CDI, possono semplificare notevolmente il codice lato server.
Pingback: ()