Il “long polling” è il modo più semplice per avere una connessione persistente con il server, e contrariamente ai WebSocket o ai Server Side Events, non usa nessun tipo di protocollo specifico.
Essendo molto semplice da implementare, è anche sufficientemente valido in molti casi d’uso.
Richiesta semplice
Il modo più semplice per ottenere nuove informazioni dal server è l’interrogazione periodica. Si tratta di una normale richiesta: “Ciao, sono qui, hai nuove informazioni da darmi?”. Per esempio, una volta ogni 10 secondi.
In risposta, come prima cosa, il server prende nota del fatto che il client è online, e poi invia un pacchetto di messaggi disponibili fino a quel momento.
Funziona, ma ci sono degli svantaggi:
- I messaggi vengono trasferiti con un ritardo che può arrivare fino a 10 secondi (tra una richiesta e l’altra).
- Anche se non ci sono messaggi, il server viene bombardato di richieste ogni 10 secondi, pure se l’utente è passato ad altre attività, o è inattivo. In termini di prestazioni, si tratta di un bel carico da gestire.
Quindi, se stiamo parlando di un servizio molto piccolo, l’approccio può essere percorribile, ma generalmente necessita di miglioramenti.
Long polling
Il cosiddetto “long polling” è un modo di gran lunga migliore per interrogare il server.
Inoltre è davvero semplice da implementare, e recapita i messaggi senza alcun ritardo.
Flusso:
- Viene inviata una richiesta al server.
- Il server non chiude la connessione fino a che ha un messaggio da inviare.
- Quando compare un messaggio, il server risponde alla richiesta con quest’ultimo.
- Il browser invia immediatamente una nuova richiesta.
Con questo metodo, la situazione in cui il browser ha inviato una nuova richiesta e ha una connessione pendente con il server, è la situazione standard. La connessione viene ristabilita, solo quando viene consegnato un messaggio.
Se la connessione viene persa, poniamo il caso di un errore di rete, il browser invia una nuova richiesta.
Ecco una bozza di una funzione lato client che effettua richieste long polling:
async function subscribe() {
let response = await fetch("/subscribe");
if (response.status == 502) {
// Lo status 502 indica un errore di timeout,
// potrebbe succedere per connessioni pendenti da troppo tempo,
// che il server remoto o un proxy chiudono
// e quindi avviene una riconnessione
await subscribe();
} else if (response.status != 200) {
// Un errore che andiamo a mostrare
showMessage(response.statusText);
// Riconnessione in un secondo
await new Promise(resolve => setTimeout(resolve, 1000));
await subscribe();
} else {
// Otteniamo e mostriamo il messaggio
let message = await response.text();
showMessage(message);
// Chiamiamo subscribe() nuovamente per ottenere il prossimo messaggio
await subscribe();
}
}
subscribe();
Come potete vedere, la funzione subscribe
effettua un fetch e rimane in attesa della risposta, e dopo averla gestita, richiama nuovamente sè stessa.
L’architettura server deve essere idonea per lavorare con molte connessioni pendenti.
Certe architetture server eseguono un processo per ogni connessione, con il risultato di avere tanti processi per quante sono le connessioni, ed ognuno di questi consuma un bel po’ di memoria. Quindi, troppe connessioni la consumeranno tutta.
È spesso il caso per backends scritti in linguaggi come PHP e Ruby.
I server scritti in Node.js solitamente non hanno questo tipo di problemi.
Detto ciò, non è un problema di linguaggio di programmazione. La maggior parte dei linguaggi moderni, incluso PHP e Ruby, permettono di implementare un backend adatto. Assicuratevi solo che il vostro server lavori bene con tante connessioni simultanee.
Dimostrazione: una chat
Ecco una chat dimostrativa, che potete anche scaricare ed eseguire in locale (se avete familiarità con Node.js e potete installare moduli):
// Sending messages, a simple POST
function PublishForm(form, url) {
function sendMessage(message) {
fetch(url, {
method: 'POST',
body: message
});
}
form.onsubmit = function() {
let message = form.message.value;
if (message) {
form.message.value = '';
sendMessage(message);
}
return false;
};
}
// Receiving messages with long polling
function SubscribePane(elem, url) {
function showMessage(message) {
let messageElem = document.createElement('div');
messageElem.append(message);
elem.append(messageElem);
}
async function subscribe() {
let response = await fetch(url);
if (response.status == 502) {
// Connection timeout
// happens when the connection was pending for too long
// let's reconnect
await subscribe();
} else if (response.status != 200) {
// Show Error
showMessage(response.statusText);
// Reconnect in one second
await new Promise(resolve => setTimeout(resolve, 1000));
await subscribe();
} else {
// Got message
let message = await response.text();
showMessage(message);
await subscribe();
}
}
subscribe();
}
let http = require('http');
let url = require('url');
let querystring = require('querystring');
let static = require('node-static');
let fileServer = new static.Server('.');
let subscribers = Object.create(null);
function onSubscribe(req, res) {
let id = Math.random();
res.setHeader('Content-Type', 'text/plain;charset=utf-8');
res.setHeader("Cache-Control", "no-cache, must-revalidate");
subscribers[id] = res;
req.on('close', function() {
delete subscribers[id];
});
}
function publish(message) {
for (let id in subscribers) {
let res = subscribers[id];
res.end(message);
}
subscribers = Object.create(null);
}
function accept(req, res) {
let urlParsed = url.parse(req.url, true);
// new client wants messages
if (urlParsed.pathname == '/subscribe') {
onSubscribe(req, res);
return;
}
// sending a message
if (urlParsed.pathname == '/publish' && req.method == 'POST') {
// accept POST
req.setEncoding('utf8');
let message = '';
req.on('data', function(chunk) {
message += chunk;
}).on('end', function() {
publish(message); // publish it to everyone
res.end("ok");
});
return;
}
// the rest is static
fileServer.serve(req, res);
}
function close() {
for (let id in subscribers) {
let res = subscribers[id];
res.end();
}
}
// -----------------------------------
if (!module.parent) {
http.createServer(accept).listen(8080);
console.log('Server running on port 8080');
} else {
exports.accept = accept;
if (process.send) {
process.on('message', (msg) => {
if (msg === 'shutdown') {
close();
}
});
}
process.on('SIGINT', close);
}
<!DOCTYPE html>
<script src="browser.js"></script>
All visitors of this page will see messages of each other.
<form name="publish">
<input type="text" name="message" />
<input type="submit" value="Send" />
</form>
<div id="subscribe">
</div>
<script>
new PublishForm(document.forms.publish, 'publish');
// random url parameter to avoid any caching issues
new SubscribePane(document.getElementById('subscribe'), 'subscribe?random=' + Math.random());
</script>
Il codice per il browser si trova dentro browser.js
.
Area di utilizzo
Il long polling lavora ottimamente in situazioni in cui i messaggi sono rari.
Se i messaggi diventano molto frequenti, il grafico dei messaggi richiesta-ricezione prima descritto, assumerà una forma simile a una sega.
Ogni messaggio è una richiesta separata, fornita di intestazioni, overhead di autenticazione e cosi via.
In questo caso, quindi, sono preferibili altri metodi, è il caso dei Websocket o dei Server Sent Events.