Una volta visto il pattern long-polling, possiamo realizzare una piccola applicazione per metterne in pratica i concetti teorici.
L'applicazione realizzeremo permetterà di monitorare la presenza di utenti in una applicazione aggiornando in tempo diretto il conto sui diversi client. L'esempio non terrà conto di alcuni aspetti secondari come la sicurezza (per esempio evitare che un utente si registri più volte) o la consistenza (essendo un'applicazione di rete in real-time possono accadere una serie di eventi da gestire come ad esempio la caduta temporanea della connessione).
Quest'esempio servirà per apprendere il pattern e per offrire uno spunto per realizzare nelle prossime lezioni una libreria a supporto di questa tipologia di applicazioni.
I casi d'uso e le specifiche funzionali
L'applicazione prevederà diversi casi d'uso:
- Accesso alla piattaforma e invio agli utenti già registrati del "gettone di presenza"
- Attivazione del canale di long-polling
- Ricezione di un nuovo "gettone di presenza"
- Disconnessione alla piattaforma
Queste azioni verranno realizzate grazie a tre entry point server side che saranno identificati da
rispettive callback:
Entry point | Descrizione |
---|---|
/register |
si occuperà di registrare il nuovo utente |
/channel |
si occuperà di mantenere attivo il canale di comunicazione e risponderà a seguito di un evento server-side (nel nostro caso quando un utente si registra o si disconnette) |
/logout |
si occuperà di disconnettere l'utente |
L'applicazione server-side
Una volta compreso quello che l'applicazione dovrà mettere in pratica possiamo partire con lo sviluppo. Come al solito approfitteremo di httpdispatcher per gestire le rotte e di due nuove librerie: ajncookie per gestire i cookie per mantenere una sessione attiva con gli utenti e hat per la generazione di hash randomici.
Iniziamo lo sviluppo definendo una serie di funzioni di utilità generica. Dato che la comunicazione sarà basata sullo scambio di messaggi JSON avremo bisogno di metodi per serializzare un oggetto e ritornarlo al client.
var dataResponse = function(res, data) {
data.status = true;
res.end(JSON.stringify(data));
}
var okResponse = function(res) {
res.end(JSON.stringify({
status: true
}));
}
var errorResponse = function(res) {
res.end(JSON.stringify({
status: false
}));
}
La funzione dataResponse
avrà lo scopo di rispondere con un messaggio complesso, mentre okResponse
e errorResponse
segnaleranno solamente che tutto è stato eseguito con successo o meno. Entrambe le funzioni richiedono come attributo primario un oggetto http.ServerResponse
.
Nelle applicazioni basate su un modello di long-polling, gli oggetti ServerResponse
e ServerRequest
possono essere considerati al pari di altri oggetti, quindi possono essere inseriti in strutture dati complesse (array o oggetti) ed essere "parcheggiati" per essere riutilizzati dopo (ad esempio quando un nuovo utente accede all'applicazione).
Per questo motivo abbiamo bisogno anche di un contenitore di contesti (un contesto rappresenta l'insieme di una ServerResponse
, una ServerRequest
, una data e un id univoco. La miglior struttura per gestirli è senza dubbio un oggetto che abbia come chiavi gli id dei contesti.
var channelContext = { }
var getCount = function() {
return Object.keys(channelContext).length
}
Una volta inizializzato il container possiamo definire l'ultima funzione generica che permette di rispondere a tutte le ServerResponse tenute in sospeso comunicando il nuovo numero di utenti collegati all'applicazione.
var pushCount = function() {
var count = getCount();
for(id in channelContext) {
var res = channelContext[id].res;
if(res) {
dataResponse(res, { count: count });
console.log("Rispondo alla sessione " + id);
}
}
}
La funzione recupera il totale dei contesti attivi in un determinato momento (che dovrebbe rappresentare anche il numero di utenti collegati all'applicazione) e ad ognuno di essi, a patto che non ci siano problemi con la response, invia una response di successo contenente il totale recuperato in precedenza. Questa funzione dovrà essere eseguita ad ogni cambiamento di stato (quindi del numero di utenti) dell'applicazione nel suo insieme per comunicare a tutti i clienti ancora collegati un evento server-side.
Definite le funzioni generiche possiamo concentrarci sulla parte web dell'applicativo server. Come detto in precedenza utilizzeremo la libreria httpdispatcher
.
dispatcher.onGet('/register', function(req, res) {
var id = hat();
channelContext[id] = {
id: id,
d: new Date()
}
console.log("Nuova sessione registrata " + id);
pushCount();
cookie.setCookie(res, "long-polling-session", id, false, false, false, true);
dataResponse(res, { count: getCount() });
});
All'url /register
risponderà un handler che si occupererà di definire un nuovo contesto generandone l'id univoco grazie ad hat, di registrare il nuovo contesto nel container globale di notificare a tutti gli altri utenti dell'aggiornamento di stato (grazie a pushCount) e di rispondere all'utente segnalando gli utenti attualmente collegati e, tramite cookie, l'id di sessione associato a lui.
dispatcher.onGet('/channel', function(req, res) {
var id = cookie.getCookie(req, "long-polling-session");
if(id && channelContext[id]) {
console.log("Canale aperto per la sessione " + id);
channelContext[id].req = req;
channelContext[id].res = res;
channelContext[id].d = new Date();
} else errorResponse(res);
});
L'handler in ascolto su /channel
è il vero componente alternativo dell'applicazione. Rispetto agli altri handler non risponderà in maniera sincrona al client (a meno di errori) ma si occuperà di aggiornare il contesto (recuperato grazie al cookie impostato nella chiamata precedente) inserendone ServerResponse, ServerRequest e aggiornandone la data. Gli oggetti verranno virtualmente parcheggiati in attesa di un evento server-side (nel nostro caso scatenato da altri utenti) che farà scattare la risposta.
dispatcher.onGet('/logout', function(req, res) {
var id = cookie.getCookie(req, "long-polling-session");
if(id && channelContext[id]) {
console.log("Elimino la sessione: " + id);
if(channelContext[id].res) okResponse(channelContext[id].res);
delete channelContext[id];
pushCount();
cookie.delCookie(res, "long-polling-session", false, false, false, true);
okResponse(res);
} else errorResponse(res);
});
L'ultimo handler /logout
si occuperà di eliminare il contesto legato alla sessione dell'utente attuale (recuperata sempre tramite cookie), di eliminare il cookie e di notificare agli altri utenti del cambio di stato dell'applicazione (sempre tramite pushCount
).
Una volta completati gli handler richiesti possiamo definire quale percorso verrà gestito come statico (per l'applicazione client-side) e avviare finalmente l'applicazione.
dispatcher.setStatic('static');
http.createServer(function (req, res) {
dispatcher.dispatch(req, res);
}).listen(1337, '127.0.0.1');
L'applicazione client-side
L'applicazione client-side, nonostante nella maggior parte delle applicazioni web presenta complessità solamente legate all'aspetto grafico e di usabilità, in applicazioni basate su long-polling ricopre un ruolo fondamentale anche nelle logiche di business dell'applicazione, trasformandosi di fatto in un componente vero e proprio e non più come semplice interfaccia.
Anche in questo caso partiamo dalle strutture dati accessorie come HTML, CSS e funzioni JavaScript generiche.
HTML
<body>
<div id="counter-container"></div>
<button id="login-button">Login</button>
<button id="logout-button">Logout</button>
<div id="logger"></div>
</body>
CSS
#logout-button { display: none }
#counter-container { font-size: 20px; color: red }
JS
var logText = function(message) {
$("#logger").html(message+""+$("#logger").html());
};
Il markup della pagina è molto semplice anche perchè le informazioni da mostrare all'utente sono davvero poche: abbiamo un contenitore per il conteggio degli utenti, due bottoni di login e logout visibili in maniera mutuamente esclusiva e un contenitore per i messaggi di log.
Il CSS non richiede approfondimenti così come il JavaScript. Come stato fatto per la parte server-side, anche in questo caso approfondiremo ciascun entry point autonomamente.
var doLogin = function() {
$.getJSON('/register', function(data) {
if(data.status) {
logText("Utente registrato con successo!");
$("#login-button").hide();
$("#logout-button").show();
if(data.count) $("#counter-container").html(data.count);
doChannel();
} else logText("Errore in fase di registrazione!");
});
};
La funzione doLogin
si occupa della registrazione del nuovo utente al sistema e in caso di risposta positiva da parte del server, oltre alla loggata, aggiornerà l'interfaccia nascondendo/mostrando i bottoni e popolando il counter-container con il numero di utenti collegati e invochera la seconda funziona doChannel
.
var doChannel = function() {
$.getJSON('/channel', function(data) {
if(data.status) {
if(data.count) {
doChannel();
logText("Nuova cambio presenza!");
$("#counter-container").html(data.count);
}
} else logText("Errore nel channel!");
});
};
La seconda funzione attiverà subito il canale di comunicazione asincrono (all'url /channel
) e nel momento in cui riceverà una risposta (che ricordo non sarà immediata) oltre ad allineare il numero degli utenti riaprirà immediatamente un nuovo canale di comunicazione con il server per eventuali eventi successivi.
var doLogout = function() {
$.getJSON('/logout', function(data) {
if(data.status) {
logText("Utente disconnesso con successo!");
$("#login-button").show();
$("#logout-button").hide();
$("#counter-container, #logger").html("");
} else logText("Errore in fase di logout!");
});
};
L'ultima funzione doLogout
non farà nient'altro che notificare al server la disconnessione dell'utente e ripristinare l'interfaccia al suo stato iniziale.
Una volta definiti i tre comportamenti principali possiamo impostarli come callback di eventi frontend finalizzando di fatto l'applicazione.
$(function() {
$("#login-button").click(doLogin);
$("#logout-button").click(doLogout);
$(window).unload(doLogout);
});
Criticità
Come ricordato all'inizio di questo articolo, un'applicazione fortemente basata sull'interazione client/server come questa deve, non solo essere funzionante, ma deve offrire un'affidabilità superiore alle classiche applicazioni web che usano in maniera standard i protocolli esistenti.
Proviamo ad immaginare per esempio un utente con una latenza notevole che non riesce immediatamente a ricreare la connessione con il server una volta ricevuto il messaggio e rischia di perdere degli aggiornamenti, oppure un proxy che invalida le chiamate che rimangono appese per più di 10 secondi senza avere nessuna risposta oppure ancora un crash del browser che non fa terminare correttamente la sessione di un utente.
In questo articolo ci siamo soffermati sullo specifico pattern, andando a sottolineare le specifiche di base. Alcune di queste problematiche verranno affrontate nelle prossime lezioni dove ritorneremo sull'argomento long-polling implementando una libreria a supporto delle future applicazioni.