WebSocket in Java

Alle origini del protocollo HTML il paradigma di comunicazioni tra client e server (web) prevedeva ruoli ben definiti. La comunicazione iniziava sempre con una request del client a cui il server rispondeva inviando il contenuto richiesto e nulla più accadeva fino alla successiva request. Per sua natura, infatti, il protocollo HTTP è half-duplex, ovvero le informazioni tra client e server avvengono sempre in una delle due direzioni e mai contemporaneamente.

Con l’avvento di AJAX le applicazioni web sono diventate dinamiche, ma ancora la comunicazione col server era sempre iniziata da una request del client. Diverse soluzioni sono state ideate per “simulare” l’avvio della comunicazione dal server, come Long Polling e Comet.

Tuttavia, l’esigenza di una soluzione standard per la realizzazione di un canale bidirezionale e full-duplex tra client e server è aumentata, fino alla produzione della specifica RFC 6455 che ha standardizzato le WebSocket.

La specifica WebSocket definisce un’API per stabilire connessioni “socket” tra un browser web e un server creando un collegamento permanente in cui le due parti possono iniziare a inviare i dati in qualsiasi momento.

JSR 356

La specifica JSR 356 definisce le API che gli sviluppatori Java possono utilizzare quando vogliono integrare le WebSockets nelle loro applicazioni, sia lato server che lato client. Ogni implementazione del protocollo WebSocket conforme con tale specifica deve implementare queste API, consentendo agli sviluppatori di scrivere le loro applicazioni basate su WebSocket indipendentemente dall’implementazione sottostante.

JSR 356 è una parte della specifica Java EE 7 ed è contenuta nel package javax.websocket. Conseguentemente tutti gli Application Server ed i Servlet Engine conformi a tale specifica hanno una implementazione di tale protocollo secondo lo standard JSR 356 (come ad esempio WildFly).

La specifica JSR 356 è stata ideata in modo da supportare pattern e tecniche comuni ai developer Java EE, quali le annotazioni e l’injection. In generale sono due i modelli di programmazione supportati: annotation-driven ed interface-driven.

Nel post presentiamo un esempio di applicazione che utilizza le WebSocket secondo il paradigma annotation-driven in cui è possibile interagire con gli eventi del lifecycle definito dal protocollo semplicemente annotando i metodo di una classe POJO.

Il Progetto

Il progetto di esempio che presentiamo è un server che gestisce le estrazioni del gioco del bingo (o tombola). Vari client possono connettersi all’end-point della WebSocket e ricevere i numeri estratti in tempo reale. Seguiamo le istruzioni del post Creare una Web Application con Maven per implementare con Maven lo scheletro della web application. Quindi apriamo il pom.xml ed inseriamo le dipendenze che ci interessano, ovvero alle WebSocket e a CDI (entrambe provided dall’application server WildFly che abbiamo utilizzato):

L’Endpoint

L’endpoint del servizio sarà la classe BingoWebSocketService che quindi dovrà essere annotata con l’annotazione @ServerEndpoint.

Questa ammette come parametro un URI che concatenato con il contesto dell’applicazione web fornisce l’URL di pubblicazione del servizio che i client devono utilizzare. Nel nostro caso l’URL sarà: ws://localhost:8080/bingo/host. La porzione iniziale ws nell’URL identifica appunto un endpoint WebSocket che eventualmente può essere protetto con protocollo SSL. IN questo caso l’URL diverrebbe wss://localhost:8080/bingo/host.

Ad ogni connessione verso l’endpoint un nuovo oggetto Session viene generato ed una nuova istanza della classe BingoService viene assegnata alla sessione. Il ciclo di vita della comunicazione è gestito attraverso l’utilizzo di quattro annotazioni: OnOpen, OnClose, OnError e OnMessage, con le quali è possibile annotare i metodi del service e che saranno invocati in risposta agli eventi associati alle annotazioni.

Il Tablou

Per l’estrazione dei numeri implementiamo due classi: un Tablou e un TablouService. La prima implementa il tabellone dove segnare i numeri estratti ed è ottenuta estendendo la classe ArrayList:

L’Estrazione

La classe TablouService implementa il servizio di estrazione che deve avviarsi ogni minuto. Poiché CDI non standardizza uno scheduler, utilizziamo una nuova dipendenza a CDI Cron, una estensione di CDI che utilizza il framework Quartz per l’implementazione e lo sheduling dei job. Inseriamola quindi nel pom.xml:

La classe TablouService ha quindi il seguente aspetto:

Come si nota la classe ha un metodo start() la  cui esecuzione è avviata da quartz configurato per mezzo dell’annotazione @Cron. Il metodo utilizza la classe Tablou per la generazione dei numeri e mediante la gestione degli eventi offerta da CDI notifica lo start del gioco e l’estrazione dei numero: startEvent.fire() e drawEvent.fire().

La classe ha poi un metodo onBingo() che osserva un evento BingoEvent che interrompe il processo di estrazione corrente. Per farlo è utilizzato un oggetto AtomicBoolean che gestisce autonomamente la concorrenza.

Si noti infine che la classe ha uno scope ApplicationScoped che consente l’accesso concorrente da parte di più thread. Se fosse stata annotata come Singleton avremmo avuto una eccezione di Concurrent Access.

Gestione delle Connessioni

I numeri estratti devono essere inviati a tutti i client connessi. Allo scopo l’oggetto Session dispone di un metodo getOpenSessions() che restituisce tutte le sessioni attive. Nella classe BingoWebSocketService utilizzeremo quindi una variabile statica che conserva tutte le sessioni attive e che viene aggiornata all’aperture e chiusura di una connessione.

Un blocco synchronized consente di aggiornare la collection in modo thread safe.

Comunicazione

Vediamo infine la gestiamo della comunicazione da e verso i client. Ad ogni avvio di gioco e ad ogni estrazione l’evento deve essere inviati ai client. Per farlo osserviamo gli eventi sollevati dalla classe TablouService ed utilizziamo un metodo generico sendToAll() per l’invio del messaggio:

Si noti che per il corretto funzionamento della gestione venti CDI è stato necessario inserire l’annotation @Singleton alla classe:

I Client

La specifica JSR 356 prevede anche una implementazione per i client che vogliono connettersi all’endpoint del server mediante WebSocket. A tale scopo il client di connessione deve essere annotato con @ClientEndpoint e, esattamente come per il server, può utilizzare le annotazioni OnOpen, OnClose, OnError e OnMessage per interagire con il ciclo di vita della WebSocket.

Implementiamo quindi la classe BingoClient che genera una cartella di gioco ad ogni avvio e verifica i numeri ricevuti fino all’eventuale ottenimento del bingo che comunica tempestivamente al server.

Come ulteriore esempio implementiamo un semplice Tablou, che nel progetto è inserito nella pagina index.jsp, in cui è presente uno script javascript che utilizza la classe WebSocket offerta da HTML5 per ascoltare gli eventi del server e colorare i numeri estratti nel tablou:

Le due immagini seguenti mostrano un round di gioco con due giocatori ed il tablou colorato con i numeri estratti sino al bingo.

bingo player tablou

Deploy

Il pacchetto generato dalla compilazione del progetto con maven (bingo.war) può essere eseguito su  WildFly 8 senza alcuna modifica. E’ sufficiente copiare il pacchetto nella directory deploy.

Se si intende eseguire il deploy su JBoss EAP 6.4 le websocket devono essere abilitate. Per farlo è necessario inserire un file jboss-web.xml nella cartella WEB-INF (presente sotto src/main/webapp) contenente il tag <enable-websockets>true</enable-websockets>.

Inoltre nel file standalone.xml nella cartella configuration di JBoss il protocollo HTTP/1.1 nella sezione:

<connector name="http" protocol="HTTP/1.1" scheme="http" socket-binding="http" max-connections="200"/>

deve essere sostituita conorg.apache.coyote.http11.Http11NioProtocol nel modo seguente:

<connector name="http" protocol="org.apache.coyote.http11.Http11NioProtocol" scheme="http" socket-binding="http" max-connections="200"/>

Il Codice Sorgente

Il codice sorgente del progetto è scaricabile qui bingo.