Java EE 7 e WebSocket (con WildFly e AngularJS) – Parte 2

Nel post precedente 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:
  • data: contiene il corpo del messaggio ricevuto.
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:
  • code: contiene il codice di chiusura.
  • reason: contiene la frase testuale inviata in fase di chiusura.

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:

Websocket client log

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.

Andrea Como

Sono un software engineer focalizzato nella progettazione e sviluppo di applicazioni web in Java. Presso OmniaGroup ricopro il ruolo di Tech Leader sulle tecnologie legate alla piattaforma Java EE 5 (come WebSphere 7.0, EJB3, JPA 1 (EclipseLink), JSF 1.2 (Mojarra) e RichFaces 3) e Java EE 6 con JBoss AS 7, in particolare di CDI, JAX-RS, nonché di EJB 3.1, JPA2, JSF2 e RichFaces 4. Al momento mi occupo di ECM, in particolar modo sulla customizzazione di Alfresco 4 e sulla sua installazione con tecnologie da devops come Vagrant e Chef. In passato ho lavorato con la piattaforma alternativa alla enterprise per lo sviluppo web: Java SE 6, Tomcat 6, Hibernate 3 e Spring 2.5. Nei ritagli di tempo sviluppo siti web in PHP e ASP. Per maggiori informazioni consulta il mio curriculum pubblico. Follow me on Twitter - LinkedIn profile - Google+