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):
1 2 3 4 5 6 7 8 9 10 11 12 |
<dependency> <groupId>javax.websocket</groupId> <artifactId>javax.websocket-api</artifactId> <version>1.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.enterprise</groupId> <artifactId>cdi-api</artifactId> <version>1.2</version> <scope>provided</scope> </dependency> |
L’Endpoint
L’endpoint del servizio sarà la classe BingoWebSocketService
che quindi dovrà essere annotata con l’annotazione @ServerEndpoint.
1 2 |
@ServerEndpoint("/host") public class BingoWebSocketService {} |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@ServerEndpoint("/host") public class BingoService { @OnOpen public void open(Session session) {} @OnClose public void close(Session session) {} @OnError public void error(Session session, Throwable error) {} @OnMessage public void message(String message, Session session) {} } |
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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class Tablou extends ArrayList<Integer> { private Random random = new Random( Calendar.getInstance().getTimeInMillis() ); synchronized public Integer next() { Integer next = 0; do { next = random.nextInt( 90 ) + 1; } while ( contains( next ) ); add( next ); return next; } synchronized public boolean isCompleted() { return super.size() == 90; } } |
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
:
1 2 3 4 5 |
<dependency> <groupId>de.mirkosertic.cdicron</groupId> <artifactId>cdi-cron-quartz-scheduler</artifactId> <version>1.0</version> </dependency> |
TablouService
ha quindi il seguente aspetto:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
@ApplicationScoped public class TablouService { private static final Log log = LogFactory.getLog( TablouService.class ); private static AtomicBoolean bingo = new AtomicBoolean( false ); private static Tablou tablou = new Tablou(); @Inject private Event<DrawEvent> drawEvent; /* Used to notify a new draw */ @Inject private Event<StartEvent> startEvent; /* Used to notify a new game */ /* Start every minute*/ @Cron(cronExpression = "0 0/1 * 1/1 * ? *") public void start() { log.info( "Started ..." ); tablou.clear(); bingo.set( false ); startEvent.fire( new StartEvent() ); do { DrawEvent de = new DrawEvent(); de.setNumber( tablou.next() ); drawEvent.fire( de ); try { Thread.sleep( 250 ); } catch (InterruptedException e) { log.error( "Sleep interrupted", e ); } } while ( !tablou.isCompleted() && !bingo.get() ); log.info( "Stopped ..." ); } public void onBingo(@Observes BingoEvent event) { bingo.set( true ); } } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
private static Set<Session> sessions = new HashSet<Session>(); @OnOpen public void open(Session session) { log.info( "New player id " + session.getId() ); synchronized (sessions) { sessions.clear(); sessions.addAll( session.getOpenSessions() ); try { session.getBasicRemote().sendText( "A game every minute ... please wait" ); } catch (IOException e) { log.error( "Error sending greetings", e ); } } } @OnClose public void close(Session session) { synchronized (sessions) { sessions.clear(); sessions.addAll( session.getOpenSessions() ); } } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public void onDraw(@Observes DrawEvent event) { sendToAll( event.getNumber().toString() ); } public void onStart(@Observes StartEvent event) { sendToAll("start"); } private void sendToAll( String message ) { try { if ( sessions != null && !sessions.isEmpty() ) { synchronized (sessions) { for ( Session session : sessions ) { if ( session.isOpen() ) { session.getBasicRemote().sendText( message ); } } } } } catch (IOException e) { log.error("Error sending message", e); } } |
Si noti che per il corretto funzionamento della gestione venti CDI è stato necessario inserire l’annotation @Singleton
alla classe:
1 2 3 |
@ServerEndpoint("/host") @Singleton public class BingoWebSocketService {...} |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
@ClientEndpoint public class BingoPlayer { private static Random random = new Random( Calendar.getInstance().getTimeInMillis() ); private int id; private Set<String> card = new HashSet<String>(); private int found = 0; private boolean started = false; public BingoPlayer(){} public BingoPlayer(int number ) { this.id = number; } @OnError public void error(Session session, Throwable error) { error.printStackTrace(); } @OnMessage public void handleMessage(String message, Session session) throws IOException { if ( message.equalsIgnoreCase( "start" ) ) { /* Generate the card */ card.clear(); for ( int i = 0; i < 5; i++ ) { card.add( String.valueOf( random.nextInt( 90 ) + 1 ) ); } found = 0; started = true; System.out.println( "Player " + id + ": " + card ); } else if ( started && card.contains( message ) ) { System.out.println( "Player " + id + ": found " + message ); found++; if ( found == card.size() ) { System.out.println( "Player " + id + ": BINGO !!!" ); session.getBasicRemote().sendText( "bingo" ); } } else if ( started && message.equalsIgnoreCase( "bingo" ) ) { started = false; } session.getBasicRemote().sendText( "ok" ); } } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var ws = new WebSocket("ws://localhost:8080/bingo/host"); ws.onmessage = function(event) { var mySpan = document.getElementById("messageGoesHere"); mySpan.innerHTML = event.data; if ( event.data == 'start' ) { for ( i = 1; i <= 90; i++ ) { document.getElementById("cell" + i ).style.backgroundColor = "white"; } } else if ( event.data == 'bingo' ) { mySpan.innerHTML = "BINGO !!!"; } else { document.getElementById("cell" + event.data ).style.backgroundColor = "red"; } }; |
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>
.
1 2 3 4 5 6 |
<jboss-web version="7.2" xmlns="http://www.jboss.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee schema/jboss-web_7_2.xsd"> <enable-websockets>true</enable-websockets> </jboss-web> |
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.