27º gennaio 2021

Long polling

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:

  1. I messaggi vengono trasferiti con un ritardo che può arrivare fino a 10 secondi (tra una richiesta e l’altra).
  2. 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:

  1. Viene inviata una richiesta al server.
  2. Il server non chiude la connessione fino a che ha un messaggio da inviare.
  3. Quando compare un messaggio, il server risponde alla richiesta con quest’ultimo.
  4. 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.

Il server dovrebbe continuare a funzionare bene, anche con molte connessioni pendenti

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):

Risultato
browser.js
server.js
index.html
// 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.

Mappa del tutorial

Commenti

leggi questo prima di lasciare un commento…
  • Per qualsiasi suggerimento - per favore, apri una issue su GitHub o una pull request, piuttosto di lasciare un commento.
  • Se non riesci a comprendere quanto scitto nell'articolo – ti preghiamo di fornire una spiegazione chiara.
  • Per inserire delle righe di codice utilizza il tag <code>, per molte righe – includile nel tag <pre>, per più di 10 righe – utilizza una sandbox (plnkr, jsbin, codepen…)