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 :
- Les messages sont transmis avec un délai allant jusqu’à 10 secondes (entre les requêtes).
- 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 :
- Une requête est envoyée au serveur.
- Le serveur ne ferme pas la connexion tant qu’il n’a pas de message à envoyer.
- Lorsqu’un message apparaît – le serveur répond à la requête avec lui.
- 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.
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) :
// 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.