19 octobre 2023

L'interrogation longue

L’interrogation longue est le moyen le plus simple d’avoir une connexion persistante avec le serveur, qui n’utilise aucun protocole spécifique comme WebSocket ou Server Side Events.

Étant très facile à mettre en œuvre, elle est également assez bonne dans de nombreux cas.

Interrogation régulière

Le moyen le plus simple d’obtenir de nouvelles informations du serveur est l’interrogation périodique. Autrement dit, des requêtes régulières au serveur : “Bonjour, je suis là, avez-vous des informations pour moi ?”. Par exemple, une fois toutes les 10 secondes.

En réponse, le serveur se signale d’abord à lui-même que le client est en ligne, et deuxièmement – envoie un paquet de messages qu’il a reçu jusqu’à ce moment.

Cela fonctionne, mais il y a des inconvénients :

  1. Les messages sont transmis avec un délai allant jusqu’à 10 secondes (entre les requêtes).
  2. Même s’il n’y a pas de messages, le serveur est bombardé de requêtes toutes les 10 secondes, même si l’utilisateur est passé ailleurs ou est endormi. C’est une charge à gérer, en termes de performances.

Donc, si nous parlons d’un très petit service, l’approche peut être viable, mais en général, elle doit être améliorée.

Interrogation longue

“L’interrogation longue” est une bien meilleure façon d’interroger le serveur.

Elle est également très facile à mettre en œuvre et délivre des messages sans délai.

Le flux :

  1. Une requête est envoyée au serveur.
  2. Le serveur ne ferme pas la connexion tant qu’il n’a pas de message à envoyer.
  3. Lorsqu’un message apparaît – le serveur répond à la requête avec lui.
  4. Le navigateur fait immédiatement une nouvelle requête.

Cette situation, dans laquelle le navigateur a envoyé une requête et maintient une connexion en attente avec le serveur, est standard pour cette méthode. Ce n’est que lorsqu’un message est délivré que la connexion est fermée et rétablie.

Si la connexion est perdue, en raison, par exemple, d’une erreur de réseau, le navigateur envoie immédiatement une nouvelle demande.

Une esquisse de la fonction subscribe côté client qui fait de longues requêtes :

async function subscribe() {
  let response = await fetch("/subscribe");

  if (response.status == 502) {
    // Le statut 502 est une erreur de dépassement de délai de connexion,
    // peut se produire lorsque la connexion est en attente depuis trop longtemps,
    // et le serveur distant ou un proxy l'a fermé
    // reconnectons-nous
    await subscribe();
  } else if (response.status != 200) {
    // Une erreur - affichons-la
    showMessage(response.statusText);
    // Reconnexion en une seconde
    await new Promise(resolve => setTimeout(resolve, 1000));
    await subscribe();
  } else {
    // Obtenons et affichons le message
    let message = await response.text();
    showMessage(message);
    // Appelons à nouveau subscribe() pour recevoir le message suivant
    await subscribe();
  }
}

subscribe();

Comme vous pouvez le voir, la fonction subscribe effectue une extraction, puis attend la réponse, la gère et se rappelle.

Le serveur devrait être ok avec de nombreuses connexions en attente

L’architecture du serveur doit pouvoir fonctionner avec de nombreuses connexions en attente.

Certaines architectures de serveur exécutent un processus par connexion ; résultant en autant de processus que de connexions, alors que chaque processus consomme pas mal de mémoire. Donc, trop de connexions consommeront tout.

C’est souvent le cas pour les backends écrits dans des langages comme PHP et Ruby.

Les serveurs écrits avec Node.js n’ont généralement pas ce genres de problèmes.

Cela dit, ce n’est pas un problème de langage de programmation. La plupart des langages modernes, y compris PHP et Ruby, permettent d’implémenter un backend approprié. Assurez-vous simplement que l’architecture de votre serveur fonctionne correctement avec de nombreuses connexions simultanées.

Démo: un tchat

Voici un tchat de démonstration, vous pouvez également le télécharger et l’exécuter localement (si vous connaissez Node.js et pouvez installer des modules) :

Résultat
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>

Le code du navigateur est dans browser.js.

Zone d’utilisation

L’interrogation longue fonctionne très bien dans les situations où les messages sont rares.

Si les messages arrivent très souvent, alors le tableau des messages de demande de réception, schématisé ci-dessus, ressemble à une scie.

Chaque message est une requête distincte, fournie avec des en-têtes, une surcharge d’authentification, etc.

Donc, dans ce cas, une autre méthode est préférée, comme Websocket ou Événements envoyés par le serveur.

Carte du tutoriel

Commentaires

lire ceci avant de commenter…
  • Si vous avez des améliorations à suggérer, merci de soumettre une issue GitHub ou une pull request au lieu de commenter.
  • Si vous ne comprenez pas quelque chose dans l'article, merci de préciser.
  • Pour insérer quelques bouts de code, utilisez la balise <code>, pour plusieurs lignes – enveloppez-les avec la balise <pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepen…)