Instant messaging con Atmosphere

Notifiche tramite estensione per GWTdi

In questo articolo introduciamo Atmosphere, un framework Java/JavaScript, potente ed espressivo, per lo sviluppo di applicazioni asincrone e real-time che comunicano tramite il paradigma publish-subscribe. Tra le tante feature proposte, Atmosphere fornisce una estensione per GWT: vedremo come integrarla in un'applicazione web.

Introduzione

Atmosphere [1] è un framework per lo sviluppo di applicazioni web in Java asincrone e real time che implementa vari meccanismi di push technology.

Atmosphere è anche un framework maturo e in continua evoluzione che ha visto crescere rapidamente la sua popolarità grazie alle feature proposte: tra l'altro, è compatibile con i maggiori servlet container, è di facile integrazione in uno stack tecnologico tramite opportuno modulo (ad esempio GWT, oppure, Jersey, JMS, etc.), permette l'esecuzione di push tramite l'impiego di web socket, implementa un meccanismo trasparente con cui il framework impiega la push technology più adatta, in base al servlet container su cui è in esecuzione e al browser che lo interroga.

La versione ufficiale al momento in cui è stato scritto questo articolo è la versione 2.1.0.

Come funziona Atmosphere

Atmosphere si colloca tra il client e il servlet container. In modo completamente trasparente, Atmosphere individua il tipo di push technology utilizzabile in base al server e al browser che lo sta interrogando. Ad esempio, se il server o il browser non supportano web socket, allora il framework implementa una comunicazione basata su long-polling.

I componenti principali

Cuore pulsante di Atmosphere sono i componenti Broadcaster, AtmosphereResource e AtmosphereHandler.

Pensiamo a Broadcaster come la chat room a cui si collegano gli utenti, il componente che implementa il meccanismo di publish-subscribe, l'analogo del Topic in JMS.

AtmosphereResource, invece, rappresenta una connessione remota, il canale di comunicazione che collega il client al server. Quando il client invia un messaggio, il server incapsula la nuova connessione in una AtmosphereResource. Un oggetto AtmosphereResource è associato ad uno o più Broadcaster. I Broadcaster hanno il compito di inoltrare i messaggi ricevuti a tutti gli oggetti AtmosphereResource di cui hanno riferimento. In questo modo, un client viene notificato dei messaggi che gli sono spediti. La classe AtmosphereResource espone due metodi:

  • getRequest(), che restituisce un oggetto di tipo AtmosphereRequest;
  • getResponse(), che restituisce un oggetto di tipo AtmosphereResponse;

AtmosphereRequest e AtmosphereResponse estendono, rispettivamente, i tipi HttpRequest e HttpResponse.  Possiamo infatti guardare ad un oggetto AtmosphereResource come ad un wrapper di una connessione HTTP.

AtmosphereHandler è il componente server-side, una specie di servlet Atmosphere. AtmosphereHandler espone tre metodi:

  • onRequest(...)
  • onStateChange(...)
  • destroy()

Il metodo onRequest(...) riceve come parametro di input un oggetto di tipo AtmosphereResource. Questo metodo è chiamato ogni volta che il client invia un messaggio al server. Il corpo di questo metodo, quindi, deve implementare la logica di scambio dei messaggi dell'applicazione. Il metodo onStateChange(...) viene chiamato quando si verifica un evento che afferisce alla connessione remota con il client. L'evento viene notificato tramite il parametro di input, un oggetto di tipo AtmosphereResourceEvent. Il metodo destroy() viene chiamato quando l'applicazione viene fermata. Nel corpo di questo metodo, quindi, dev'essere implementata la logica di interruzione dell'applicazione.

Il meccanismo in breve

Ricapitoliamo. La connessione tra client e server è implementata dal framework tramite una AtmosphereResource. Ogni AtmosphereResource incapsula un Broadcaster. Tramite Broadcaster il server può eseguire push dei messaggi al client, il Broadcaster invia i messaggi attraverso tutte le AtmosphereResource di cui possiede un riferimento, notificando in questo modo al client.

Atmosphere, inoltre, presenta una libreria JavaScript. Questa libreria implementa i meccanismi di invio e ricezione dei messaggi, e, nel caso dell'estensione GWT, integra il codice JavaScript prodotto dal compilatore nella comunicazione con il server.

Get your feet wet!

Implementiamo una semplice applicazione di instant messaging con l'estensione Atmosphere-GWT. Creiamo un Google Web Project con Eclipse. Recuperiamo i JAR di Atmosphere che ci servono; abbiamo bisogno dei seguenti file:

  • atmosphere-gwt20-client-2.1.0-RC2.jar
  • atmosphere-gwt20-common-2.1.0-RC2.jar
  • atmosphere-gwt20-server-2.1.0-RC2.jar
  • atmosphere-runtime-2.1.0-RC2.jar
  • slf4j-api-1.6.1.jar
  • atmosphere.js

I suddetti file possono essere scaricati dal repository Maven indicato sul sito ufficiale [2]. Come detto in precedenza, il file JavaScript è necessario al corretto funzionamento dell'applicazione e deve essere incluso nel file HTML a cui si collega il modulo GWT. In figura 1 è riportata la struttura del progetto Eclipse dell'applicazione.

 

 

Figura 1 - Struttura del progetto.

 

Come in ogni progetto GWT, distinguiamo tra parte client e parte server.

Parte client

Prima di iniziare a descrivere la parte client dell'applicazione, modifichiamo il file .gwt.xml in modo da includere il modulo di Atmosphere per GWT.


La parte client dell'applicazione si articola in una piccola parte model con cui rappresentare i messaggi, un'interfaccia, un oggetto con mansioni di controller, un semplice helper che ci permette di deserializzare i messaggi che arrivano dalla parte server.

L'interfaccia non presenta sorprese per chi abbia già visto un'applicazione GWT. L'elemento controller e l'elemento helper, invece, meritano maggiore attenzione. Il nostro controller è un oggetto di tipo AtmosphereRequestConfig.

// serializer: helper per serializzare e deserializzare
RawMessageSerializer serializer = GWT
  .create(RawMessageSerializer.class);
// rpcRequestConfig: configurazione della comunicazione con il server
AtmosphereRequestConfig rpcRequestConfig = AtmosphereRequestConfig
  .create(serializer);
rpcRequestConfig.setUrl(GWT.getModuleBaseURL() + "gwtchatsrv/rpc");
rpcRequestConfig.setTransport(AtmosphereRequestConfig.Transport.WEBSOCKET);
rpcRequestConfig
  .setFallbackTransport(AtmosphereRequestConfig.Transport.LONG_POLLING);
rpcRequestConfig.setMessageHandler(new AtmosphereMessageHandler() {
  @Override
  public void onMessage(AtmosphereResponse response) {
     logger.info("RPC response. "+response.toString());
     List messages = response.getMessages();
     for (RawMessage event : messages) {
       logger.info("received message through RPC: "
            + event.getFrom() + ", " + event.getTo()
            + ", " + event.getMsg()
            +".");
     }
  }
});

AtmosphereRequestConfig permette di impostare le proprietà che definiscono la connessione con il server, l'URL, il tipo di trasporto, ad esempio web socket, server side event, long polling, un eventuale protocollo di trasporto alternativo qualora il primo indicato non fosse praticabile, un riferimento all'oggetto helper per deserializzare i messaggi in arrivo dal server.

L'helper è definito tramite una classe astratta dove, grazie ad apposita annotation, dobbiamo indicare i .class di tutti gli oggetti che serializzeremo/deserializzeremo nell'interazione con il server.

import org.atmosphere.gwt20.client.GwtRpcClientSerializer;
import org.atmosphere.gwt20.client.GwtRpcSerialTypes;

@GwtRpcSerialTypes({ RawMessage.class, DummyData.class })
public abstract class RawMessageSerializer extends GwtRpcClientSerializer {
}

Indicando un tipo, potranno essere serializzati anche tutti gli oggetti di un tipo estensione del primo. Importante indicare anche i tipi degli attributi incapsulati nei nostri messaggi.

All'oggetto AtmosphereRequestConfig viene effettuata notifica dal server in merito ai messaggi arrivati o ai cambi di stato della connessione. Per gestire queste notifiche, si possono associare degli opportuni listener all'oggetto. Sicuramente l'utente vuole vedere i messaggi che gli sono inviati, quindi l'oggetto AtmosphereRequestConfig della nostra applicazione è stato dotato di un listener di tipo AtmosphereMessageHandler. Questo listener implementa il metodo onMessage per la gestione dei messaggi in arrivo.

L'oggetto AtmosphereRequestConfig non ha ancora assolto completamente alla sua funzione. Lo stesso, infatti, è impiegato per istanziare un oggetto di tipo AtmosphereRequest. Tramite l'oggetto AtmosphereRequest siamo in grado di inviare gli oggetti che implementano i nostri messaggi al server.

Atmosphere atmosphere = Atmosphere.create();
final AtmosphereRequest rpcRequest = atmosphere
  .subscribe(rpcRequestConfig);
 
sendRPC.addClickHandler(new ClickHandler() {
  @Override
  public void onClick(ClickEvent event) {
     if (messageInput.getText().trim().length() > 0) {
       try {
          SimpleMessage evt = new SimpleMessage();
          evt.setFrom(authorInput.getText());
          evt.setTo(toInput.getText());
          evt.setMsg(messageInput.getText());
          DummyData dd = new DummyData();
          dd.setA("A");
          dd.setB(0);
          dd.setC(new Date());
          evt.setDd(dd);
          // l'oggetto AtmosphereRequest 
                    // viene impiegato per inviare un messaggio
          rpcRequest.push(evt);
       } catch (SerializationException ex) {
          logger.log(Level.SEVERE, "Failed to serialize message",
              ex);
       }
     }
  }
});

Parte server

Il lato server, invece, è costituito da un'unica classe,  AtmosphereChatHandler, vero motore del servizio. Questa classe estende la classe AbstractReflectorAtmosphereHandler  e sovrascrive i suoi metodi destroy, onRequest e onStateChange.

Il metodo destroy implementa la logica di stop del servizio. Il metodo destroy, ad esempio, viene invocato in fase di undeploy dell'applicazione.

Il metodo onRequest viene invocato quando un'informazione raggiunge il nostro servizio. Per capire cosa implementare in questo metodo è necessario aprire una piccola parentesi. Il file web.xml del servizio presenta un riferimento a una servlet di tipo org.atmosphere.cpr.AtmosphereServlet. AtmosphereServlet estende HttpServlet e raccoglie tutte le richieste HTTP inviate al suo url-pattern. Il collegamento tra la AtmosphereServlet e l'oggetto di tipo AbstractReflectorAtmosphereHandler risiede nel file /META-INF/atmosphere.xml.



       class-name
       ="edu.pezzati.atmosphere.chatgwt.server.AtmosphereChatHandler">
     

Di seguito è riportato il file /WEB-INF/web.xml.


  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
          http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
  version="3.0" xmlns="http://java.sun.com/xml/ns/javaee">
  
     AtmosphereServlet
     AtmosphereServlet
     org.atmosphere.cpr.AtmosphereServlet
     
       org.atmosphere.cpr.AtmosphereInterceptor
       org.atmosphere.gwt20.server.GwtRpcInterceptor
    
    
       org.atmosphere.cpr.broadcasterCacheClass
       org.atmosphere.cache.UUIDBroadcasterCache
     
     1
     true
  
  
     AtmosphereServlet
     /atmospherechatgwtcase/gwtchatsrv/rpc/*
  
 
  
     AtmosphereChatGWTCase.html

Il valore di url-pattern della servlet AtmosphereServlet indicato nel file web.xml è espresso come valore dell'attributo context-root del file atmosphere.xml; l'attributo class-name, invece, riporta il nome della classe AtmosphereChatHandler. Quindi, il file atmosphere.xml indica alla servlet che le richieste HTTP dirette al suo url-pattern siano gestite tramite un oggetto di tipo AtmosphereChatHandler.

Torniamo al metodo onRequest. Nel corpo di questo metodo devono essere gestite le richieste HTTP. Il codice del metodo esegue una distinzione tra le richieste http di tipo GET e quelle di tipo POST.

@Override
public void onRequest(AtmosphereResource arg0) throws IOException {
  if (arg0.getRequest().getMethod().equals("GET")) {
     doGet(arg0);
  } else if (arg0.getRequest().getMethod().equals("POST")) {
     doPost(arg0);
  } else {
     doTrash(arg0);
  }
}

La prima interazione con il servizio avviene per mezzo di una chiamata GET. Nel metodo che implementa la gestione delle chiamate GET, l'oggetto handler gestisce un oggetto di tipo AtmosphereResource, che incapsula la richiesta HTTP. Il metodo assegna un Broadcaster all'oggetto AtmosphereResource e ne invoca il metodo suspend().

private void doGet(AtmosphereResource ar) {
  ar.setBroadcaster(DefaultBroadcasterFactory.getDefault().get());
  ar.suspend();
}

In questo modo, la connessione remota con il client viene sospesa. La connessione sospesa viene usata successivamente dal servizio per inviare messaggi a quel client.

Il metodo che gestisce le richieste di tipo POST, invece, presenta la logica di inoltro dei messaggi. Nel semplice servizio proposto, il primo messaggio POST implementa un messaggio di login al servizio. Questo messaggio vuole emulare una fase di login e serve a comunicare un nome utente da associare alla AtmosphereResource.

private void doPost(AtmosphereResource ar) {
  Object msg = ar.getRequest().getAttribute(Constants.MESSAGE_OBJECT);
  if (msg != null) {
     if (msg instanceof BroadcastMessage) {
       handleBroadCastMessage(ar, msg);
     } else if (msg instanceof ListenMessage) {
       // gestione del messaggio che vuole simulare una
       // operazione di login
       handleListenMessage(ar, msg);
     } else if (msg instanceof SimpleMessage) {
       handleSimpleMessage(ar, msg);
     } else if (msg instanceof LoginMessage) {
       handleLoginMessage(ar, msg);
     } else {
       doTrash(ar);
     }
  }
}
 
private void handleLoginMessage(AtmosphereResource ar, Object msg) {
  // tramite inline il server tiene traccia degli utenti connessi
  // e delle loro risorse AtmosphereResource
  inline.put(
     ((LoginMessage) msg).getFrom(),
     (String) ar.getRequest().getAttribute(
       ApplicationConfig.SUSPENDED_ATMOSPHERE_RESOURCE_UUID));
}

Tramite l'oggetto inline di tipo ConcurrentHashMap viene registrata l'associazione tra nome utente e identificativo univoco della sua AtmosphereResouce, ovvero la risorsa AtmosphereResource sospesa durante la prima interazione con il servizio. L'identificativo univoco può essere recuperato come attributo della request HTTP incapsulata nella AtmosphereResource, oppure tramite il suo metodo uuid(). Quando il client invia un messaggio, il server recupera il nome del destinatario e lo usa per ottenere l'identificativo univoco della sua AtmosphereResource. Recupera la AtmosphereResource del destinatario e usa il broadcaster associato per inviare il messaggio mediante invocazione del metodo broadcast.

Un ultimo accorgimento. La classe Broadcaster espone tre overload del metodo broadcast. Se il metodo viene invocato indicando il solo messaggio, allora il broadcaster trasmetterà attraverso tutte le AtmosphereResource che hanno visto invocato il loro metodo suspend; di fatto, il messaggio potrebbe essere trasmesso a tutti gli utenti. Invece, indicando al metodo il parametro messaggio e un parametro AtmosphereResource, allora il Broadcaster trasmette il messaggio solo a quella AtmosphereResource.

Conclusioni

In questo articolo abbiamo visto come funziona Atmosphere e ne abbiamo osservato il suo impiego in una semplice applicazione GWT dandole la capacità di comunicare in modo real time e asincrono. Salta agli occhi quanto il framework sia potente ed espressivo. Oltre a ciò, il buon numero di estensioni con cui si presenta permettono una sua facile integrazione in diversi contesti.

Riferimenti

[1] Atmosphere

https://github.com/Atmosphere

 

[2] Il repository da cui scaricare i file necessari

http://search.maven.org/#search|ga|1|atmosphere

 

 

 

Condividi

Pubblicato nel numero
193 marzo 2014
Appassionato di programmazione object oriented e pattern, si occupa di sviluppo web su piattaforma Java EE, utilizzando varie tecnologie: GWT, EJB 3.1, SOAP e REST web services. Vive a Firenze dove lavora come consulente per varie aziende, dopo aver conseguito una laurea triennale in informatica.
Ti potrebbe interessare anche