Nella lezione precedente abbiamo appreso le basi del pattern di comunicazione basato su longpolling. Per offrire un'applicazione "live" agli utenti però abbiamo introdotto una serie di complessità notevoli che rendono diffocoltoso lo sviluppo.
Tutte le applicazioni long-polling condividono una serie di aspetti, per questo in questa lezione esamineremo una libreria che ho creato per semplificare l'implementazione di questo pattern. La libreria si chiama Polly ed è disponibile sia su su NPM, sia su github.
L'applicazione che analizzeremo sfrutta Polly ed è una semplice chat (pollychat). Attualmente l'applicazione pubblicata su Nodester (piattaforma che approfondiremo nei prossimi articoli).
I concetti chiave di Polly
Prima di iniziare con l'analisi del codice dobbiamo introdurre alcuni concetti teorici che faciliteranno la comprensione della libreria. Questi concetti sono stati indirettamente introdotti nella lezione precedente ma per realizzare una libreria, e quindi un'astrazione, è necessario specificarli meglio.
Un'applicazione basata su long-polling implementa due concetti chiave:
- gli eventi
message
user_join
user_part
- i contesti
identificata da un id segreto (che permette di identificare l'utente), da oggetti request e
response che permettono l'asincronia nelle risposte e una serie di dati aggiuntivi sull'utente
(per esempio lo username).
Entrambi i tipi di oggetto verranno inseriti in un hash e identificati dal proprio id univoco. L'hash degli eventi sarà incrementale e non verrà mai modificato mentre quello dei contesti sarà soggetto a continue modifiche in base al flusso di utenti nell'applicazione.
La libreria sarà composta da due componenti:
- componente server-side
di facile comprensione e che ridurranno il codice del server a poche linee di codice. - componente client-side
chiamate esposte dal server.
Essa dipenderà da due moduli aggiuntivi già visti in precedenza: cookie per la gestione appunto dei "biscottini" e ajn per la generazione di id univoci.
Polly, componente server-side
Partiamo analizzando il modulo server-side da importare nelle nostre applicazioni.
var Polly = function() {
this.contexts = { };
this.events = { };
this.eventCounter = 0;
this.backEvents = 5;
this.cookieName = "POLLY_ID";
}
Il costruttore inizializza una serie di variabili tra le quali i due hash contexts e events, un contatore di eventi, la variabile backEvents
(che analizzeremo dopo) e il nome del cookie dove verrà stoccato l'id della sessione.
Dopo il costruttore ecco una serie di metodi pubblici di utilità:
Polly.prototype.getSessionCount = function() {
return Object.keys(this.contexts).length;
}
Polly.prototype.getSession = function(id) {
return this.contexts[id];
}
Polly.prototype.getUserData = function(id) {
return this.contexts[id].data;
}
Polly.prototype.getSessionId = function(req) {
return cookie.getCookie(req, this.cookieName);
}
Polly.prototype.getAllSessionsData = function() {
var datas = [];
for(var i in this.contexts) {
datas.push(this.getSession(i).data);
}
return datas;
}
Metodo | Descrizione |
---|---|
getSessionCount
|
ritorna il numero di sessioni attive in un determinato momento |
getSession
|
ritorna una sessione a partire dall'id (letto da cookie) |
getUserData
|
ritorna una mappa di proprietà legate all'utente di una particolare sessione |
getSessionId
|
legge il valore del cookie a partire dalla request |
getAllSessionData
|
aggrega in un vettore tutte le proprietà di tutti gli utenti (utile per mostrare lo stato dell'applicazione in un determinato istante) |
Ecco ora alcune funzionalità legate agli eventi:
Polly.prototype._getLastEventsById = function(eventId) {
var eventsToSend = [];
for(var i in this.events) {
if(this.events[i].id > eventId) eventsToSend.push(this.events[i]);
}
eventsToSend = eventsToSend.splice(-this.backEvents,this.backEvents);
return eventsToSend;
}
Polly.prototype._createEvent = function(eventType, eventData, user) {
var event = {
id: this.eventCounter++,
type: eventType,
data: eventData,
user: user
}
this.events[event.id] = event;
return event;
}
Il metodo privato _getLastEventsById
- Utente che per problemi di rete non riceve un particolare evento: alla chiamata successiva riceverà anche l'evento precedente non ricevuto;
- Utente appena entrato nell'applicazione che riceverà gli ultimi eventi accaduti (la proprietà vista in precedenza backEvents serve proprio a indicare quanti eventi dovrà ricevere l'utente appena entrato nell'applicazione).
Il metodo privato _createEvent
eventType
Spostiamoci ora sui metodi principali di Polly che permettono di mettere in piedi un flusso di comunicazione long-polling.
Polly.prototype.registerSession = function(req, res, data) {
var id = hat();
var uid = hat();
data.id = uid;
this.contexts[id] = {
id: id,
data: data,
d: new Date()
}
cookie.setCookie(res, this.cookieName, id, false, false, false, true);
this.pushEvent(id, "user_join", {});
console.log("Session " + id + " registered!");
return id;
}
RegisterSession
Innanzitutto vediamo la generazione di due id (id e uid). Il primo identifica l'id di sessione del client (infatti viene impostato nel cookie) e deve essere segreto. Recuperare questo id da qualche altro utente significa potersi autenticare come se si fosse lui, e questo non è cosa buona e giusta. Il secondo id invece rappresenta l'id pubblico di un utente, può essere comunicato a terzi e permette di identificare univocamente un determinato utente (per esempio nel caso di messaggi privati).
L'altro aspetto curioso è la chiamata al metodo pushEvent
user_join
Polly.prototype.unregisterSession = function(id) {
if(this.contexts[id]) {
this.contexts[id].res.end(JSON.stringify({ status: false }));
this.pushEvent(id, "user_part", {});
delete this.contexts[id];
console.log("Session " + id + " destroyed!");
}
}
L'opposto di registerSession
unregisterSession
user_part
Polly.prototype.holdSession = function(req, res) {
var id = this.getSessionId(req);
if(this.contexts[id]) {
var lastEventId = req.params.lastEventId;
var eventsToSend = this._getLastEventsById(lastEventId);
if(eventsToSend.length > 0) {
res.end(JSON.stringify({ status: true, events: eventsToSend }));
console.log(eventsToSend.length + " events sent to session " + id);
return true;
}
if(id && this.contexts[id]) {
this.contexts[id].req = req;
this.contexts[id].res = res;
this.contexts[id].d = new Date();
console.log("Session " + id + " holded!");
return true;
}
} return false;
}
Il metodo holdSession
Polly.prototype.pushEvent = function(id, eventType, eventData) {
var user = this.getUserData(id);
var event = this._createEvent(eventType, eventData, user);
for(var i in this.contexts) {
if(this.getSession(i).res)
this.getSession(i).res.end(JSON.stringify({ status: true, events: [event] }));
}
}
Il metodo pushEvent
holdSession
Gli ultimi due metodi sono "di pulizia":
Polly.prototype._refreshContext = function() {
var now = new Date();
for(var i in this.contexts) {
var c = this.contexts[i];
if(now - c.d > this.refreshTimeout) {
this.contexts[i].res.end(JSON.stringify({ status: true }));
console.log("Session "+c.id+" refreshed!");
}
}
}
Polly.prototype._purgeContext = function() {
var now = new Date();
for(var i in this.contexts) {
var c = this.contexts[i];
if(now - c.d > this.purgeTimeout) {
this.unregisterSession(i);
console.log("Session "+c.id+" purged!");
}
}
}
_refreshContext refreshTimeout _purgeContext purgeTimeout
Queste funzioni sono fondamentali per avere un'applicazione affidabile che, anche a fronte di problemi di rete o di utenti "maleducati", risulta coerente e stabile. Entrambe sono da eseguire in maniera temporizzata quindi è necessario modificare il costruttore in questo modo:
var Polly = function() {
// [...]
this.refreshTimeout = 3333;
this.purgeTimeout = 10000;
var polly = this;
this.refresher = setInterval(function() {
polly.refreshContext();
}, this._refreshTimeout);
this.purger = setInterval(function() {
polly._purgeContext();
}, this.refreshTimeout);
}
Ogni 3,33 secondi:
- le sessioni più vecchie di 3,33 secondi verranno rinfrescate senza nessun nuovo evento
- le sessioni più vecche di 10 secondi verranno considerate scadute
Questi numeri sono abbastanza esosi in termini di risorse. A fronte di un aumento delle richieste sarà necessario ritararli per non appesantire troppo sia il server che i vari client.
Il file server.js
Una volta definita la libreria possiamo spostare la nostra attenzione sul server vero e proprio, concentrandoci quindi sulla tipologia di applicazione che vogliamo realizzare: una semplice chat.
Il componente server si appoggierà come sempre al modulo httpdispatcher. Gli entry point implementati saranno 4: /register
, /hold
, /push
e /part
:
dispatcher.onGet('/register', function(req, res) {
var data = {
username: req.params['username']
}
var users = polly.getAllSessionsData();
var id = polly.registerSession(req, res, data);
res.end(JSON.stringify({
status: true,
users: users
}));
});
dispatcher.onGet('/hold', function(req, res) {
polly.holdSession(req, res);
});
dispatcher.onGet('/push', function(req, res) {
polly.pushEvent(polly.getSessionId(req), "message", {
message: req.params.message
});
res.end(JSON.stringify({
status: true
}));
});
dispatcher.onGet('/part', function(req, res) {
polly.unregisterSession(polly.getSessionId(req));
res.end(JSON.stringify({
status: true
}));
});
Come è facilmente intuibile anche dal semplice numero di righe, la struttura del server è diventata
banale grazie alle chiamate alle API di polly.
Entry point | Funzione |
---|---|
/register
|
recupera le sessioni attuali (per mostrare gli utenti collegati alla chat) e registra una nuova sessione anche grazie allo username ricevuto dal client |
/hold
|
mette la chiamata in attesa di nuovi eventi |
/push
|
crea un nuovo evento "message" a partire dal messaggio (req.params.message
|
/part
|
elimina la sessione attuale |
Polly, il componente client-side
Gli aspetti client-side dell'applicazione sono notevolmente più semplici. Il modulo polly-client implementa le seguenti funzioni:
init: function(registerUrl, holdUrl, pushUrl, partUrl eventHandler) {
polly.registerUrl = registerUrl;
polly.holdUrl = holdUrl;
polly.eventHandler = eventHandler;
polly.pushUrl = pushUrl;
polly.partUrl = partUrl;
polly.lastEventId = -1;
},
register: function(data, cb) {
$.getJSON(polly.registerUrl, data,function(data) {
if(data.status) {
polly.hold();
cb(data);
}
});
},
hold: function() {
$.getJSON(polly.holdUrl, {
lastEventId: polly.lastEventId
},
function(data) {
if(data.status) {
var events = data.events;
if(events && events.length > 0)
polly.lastEventId = events[events.length-1].id;
polly.hold();
for(i in events) {
var event = events[i];
var handler = polly.eventHandler[event.type];
if(handler && typeof handler == "function") {
handler(event);
}
}
}
});
},
pushEvent: function(eventType, data) {
data.eventType = eventType;
$.getJSON(polly.pushUrl, data);
},
part: function(cb) {
$.getJSON(polly.partUrl, function(data) {
if(data.status) {
cb();
}
});
}
Il costruttore init
I metodi register
, pushEvent
e part
notificano al server eventuali nuovi eventi mentre il metodo hold
inizializza la chiamata asincrona e ne gestisce la risposta identificando l'id dell'ultimo evento (che viene salvato nella property lastEventId
) e per ogni evento ricevuto invoca il rispettivo handler impostato dall'utente nel costruttore.
Il file index.html
L'unico aspetto interessante legato all'implementazione client-side della chat è rappresentata dal costruttore init in quanto tutti gli altri aspetti riguardano modifiche all'interfaccia e non verranno approfondite in questo articolo.
polly.init('/register', '/hold', '/push', '/part', {
"message": function(event) {
var message = "<b>"+event.user.username+"</b>: "+event.data.message;
writeMessage(message, "black");
},
"user_join": function(event) {
var message = event.user.username+" join the chat...";
writeMessage(message, "green");
appendUser(event.user);
},
"user_part": function(event) {
var message = event.user.username+" parts...";
writeMessage(message, "grey");
removeUser(event.user);
}
});
Una volta impostate le url dei vari entry point, viene creato un eventHandler per la gestione dei vari eventi. Un eventHandler non è nient'altro che una mappa di funzioni identificate dal nome dell'evento che le deve scatenare.
Abbiamo approfondito ulteriormente il pattern di comunicazione long-polling
Polly nonostante per l'esempio funzioni egregiamente non è ancora pronto per applicazioni reali soprattutto per la mancanza di alcuni comportamenti, come ad esempio la possibilità di notificare eventi solo a determinati utenti (ora tutti gli eventi scatenati vengono inviati a tutti gli utenti collegati) oppure una gestione maggiormente oculata sulla sicurezza o sull'affidabilità (per esempio manca un meccanismo di try/catch
Tutti i sorgenti sono allegati all'articolo. Nella prossima puntanta introdurremo il pattern per applicazioni in real-time basato su WebSocket.