Communiquer avec le serveur depuis un navigateur Web : XHR, SSE et WebSockets

105
18
avr.
2021
Internet

Dans cette dépêche, nous allons faire un tour d’horizon de différentes manières de communiquer avec un serveur depuis une application Web, avec un petit peu d’histoire, avant de rentrer plus profondément dans le fonctionnement des WebSockets, que nous allons démystifier. Nous digresserons ensuite à propos de la gestion (problématique) des requêtes longues et de HTTP 2 avec Apache, et nous discuterons d’une manière de limiter la casse. La dépêche contient quelques morceaux raisonnables mais l’absurdité est latente.

Supposons que nous ayons une application Web qui a besoin de recevoir des évènements du serveur pour voir si quelque chose s’est passé. À tout hasard, un jeu de société en ligne. Ce jeu a besoin d’envoyer les coups des joueurs et joueuses, et de recevoir les coups des autres.

Le serveur ne peut pas contacter le navigateur. Celui-ci est peut-être derrière un pare-feu, et de toute façon il n’y a pas de méthode pour cela. Le modèle du web, c’est une requête HTTP de la part du navigateur, et le serveur sert cette requête. Et puis, à la base, une requête = un chargement de page.

Mais des techniques sont apparues pour abuser de ce modèle, puis les standards se sont mis à intégrer des méthodes pour mener ces abus en toute sérénité.

Sommaire

AJAX avec XMLHttpRequest

Préambule

Au commencement, les utilisateurs saisissaient des adresses, cliquaient sur des liens ou validaient des formulaires, cela engendrait des requêtes HTTP, un (re)chargement de la page et tout le monde était content, les choses étaient claires et simples. Les pages étaient essentiellement statiques.

Puis des gens ont ressenti le besoin de rendre les pages un peu plus dynamiques. Chacun son truc ! Peut-être que ces personnes aimaient les journaux qui bougent dans Harry Potter et se sont pris pour des sorciers. Du coup, JavaScript est apparu.

Mais comme animer des formes, changer des couleurs et faire clignoter des trucs c’est rigolo mais ça va bien deux secondes, il leur a fallu plus et ils se sont mis à développer diverses bidouilles pour communiquer avec le serveur.

Tout d’abord, simple, vous créiez une balise script ou une image vers une adresse spécifique contenant des données à envoyer au serveur et c’est réglé : le navigateur va faire une requête.

Pour recevoir les évènements du serveur, vous pouviez créer des balises <script> à intervalle régulier, avec une adresse source spécifique. Cette adresse renvoie un code JavaScript qui appelle une fonction avec un nom prédéterminé et des données en paramètre de cette fonction. C’est la méthode JSONP. Avouez que ce n’est pas fou : on fait plein de requêtes inutiles, et paie la fluidité de ton jeu en ligne.

Certaines personnes, insatisfaites par la méthode précédente, se sont alors mises à détourner l’élément <iframe>, qui, à la base, servait à afficher une page dans une page (quelle drôle d’idée !). Le serveur pouvait envoyer des éléments <script> dans ces <iframe> pour exécuter du code au gré des évènements. Ça marche, mais bon, il n’y avait pas de manière simple et fiable de gérer les erreurs.

Des solutions s'appuyant sur les plugins Java ou Flash existaient également.

Naissance d’XMLHttpRequest (XHR)

Et là, l’équipe en charge de faire le Webmail d’Outlook chez Microsoft a eu besoin de ce genre de fonctionnalité. Un weekend, Alex Hopmann, de cette équipe, lança Visual Studio, écrivit un composant qui permettait de communiquer avec le serveur. Et là, il fallait trouver un moyen de ne pas devoir installer ce composant pour pouvoir utiliser la version Web Outlook : tout l’intérêt était de pouvoir consulter ses mails sur n’importe quel ordinateur équipé d’un navigateur et d’une connexion internet sans rien installer. Il était proche de l’équipe développant MSXML, une bibliothèque de chez Microsoft pour traiter le XML. Rien à voir avec les communications avec le serveur, mais Alex Hopmann réalisa que MSXML allait être livré avec Internet Explorer, un navigateur de Microsoft un peu omniprésent à l’époque. Un peu comme Google Chrome aujourd’hui si vous voulez. Mais je digresse.

Et donc, il alla toquer à la porte de l’équipe en charge de MSXML et leur demanda s’ils pouvaient embarquer le composant, alors que la bêta 2 de la version d’Internet Explorer d’alors était sur le point d’être livrée (YOLO). « OK, c’est cool », lui répondit-on, « mais nomme-le XML-machin, ou un truc comme ça, sinon ça ne passera jamais les revues ». Ce qu’Alex fit. De toute façon, XML c’était vachement à la mode donc ça faisait bien d’avoir XML dans le nom même si ça n’avait rien à voir. Et XMLHttpRequest est né comme une partie d’un composant ActiveX dans Internet Explorer.

Par la suite, il a été adopté par les autres navigateurs qui ont décidé de garder le nom. Puis, c’est devenu un standard, tel quel. Et si en plus du fait que XML est hors sujet, le fait qu’il soit en majuscules alors qu’Http, le concept quand même central de cet objet, ne le soit pas, vous titille, eh bien c’est comme ça et vous feriez mieux de vous y faire, il y a quand même des problèmes plus importants dans la vie.

Et puisqu’il faut un terme branché pour désigner tout ce mécanisme de mettre à jour la page avec des nouvelles données sans recharger la page en entier (truc de fou à l’époque !), le concept d’AJAX est né (Asynchronous JavaScript And XML - hé hé oui, on ne se débarrasse pas de XML comme ça, même dans le terme répandu, même quand XML n’est pas du tout impliqué. Parce que si des gens font bien transiter du XML de temps en temps, bien souvent, on utilise du JSON. Bien joué, Alex !).

De toute façon, maintenant il ne faut plus parler d’AJAX, il vaut mieux parler d’API REST et de PWA ou de SPA (non, ça n’a rien avoir avec les bains à remous ou les obstacles de saut équestre) ; ça sonne quand même mieux ces derniers temps. XML c’est moyen, tout le monde trouve ça verbeux et pénible un peu comme cette dépêche, ce n’est bon que pour faire du XMPP et pour les développeurs Java qui utilisent encore Maven. Mettez-vous au goût du jour et utilisez JSON et YAML. Utilisez les bons mots-clés de votre époque, vous n’êtes plus au début des années 2000 par Toutatis. (Note : il serait intéressant de présenter REST mais la notion mériterait un article dédié.)

Enfin bon, revenons à notre pote XMLHttpRequest avec son nom doucement rétro (bon, OK, j’en fais des tonnes).

XMLHttpRequest, comment ça marche ?

On crée un objet XMLHttpRequest, on lui indique l’URL de la page à télécharger, la méthode HTTP à utiliser pour la requête (GET ou POST), on paramètre les en-têtes HTTP qu’on veut, puis on envoie la requête avec éventuellement les données POST qu’on veut.

On peut alors suivre l’état de la requête en suivant l’évènement readystatechange : la connexion a été ouverte, réception en cours et réponse complètement reçue. On peut récupérer les données pendant la réception et/ou une fois la réponse reçue, vérifier le statut HTTP. On a un contrôle très fin.

On peut lancer des requêtes pour envoyer des données au serveur, mais on peut aussi envoyer une requête de longue durée (long polling) et lire les données qui arrivent au fur et à mesure. C’est tricky mais possible (vous pouvez passer sans problème le code, il est là pour les gens qui aiment les détails techniques) :

const xhr = new XMLHttpRequest();

xhr.open("POST", Conf.API_ENTRY_POINT, true);
xhr.setRequestHeader("Content-Type", "text/plain");
xhr.send(JSON.stringify(data));

let currentIndex = 0;
let expectedLength = 0;

xhr.onreadystatechange =  function () {
    if (xhr.readyState === 3) {
        // des données arrivent

        while (true) {
            if (!expectedLength) {
                let i = currentIndex;
                // On récupère la taille du message qui arrive
                while (i < xhr.responseText.length) {
                    if ("0123456789".indexOf(xhr.responseText.charAt(i)) === -1) {
                        expectedLength = parseInt(xhr.responseText.substring(currentIndex, i));
                        currentIndex = i;
                        break;
                    }
                    ++i;
                }
            }

            if (expectedLength && (xhr.responseText.length >= currentIndex + expectedLength)) {
                const end = currentIndex + expectedLength;
                let msgs;

                try {
                    msgs = JSON.parse(
                        xhr.responseText.substring(
                            currentIndex,
                            end
                        )
                    );
                    currentIndex = end;
                    expectedLength = 0;
                } catch (e) {
                    // Gérer l'erreur json
                    // on ne doit pas continuer à chercher à lire des messages sur
                    // cette connexion
                    xhr.abort();
                    // se reconnecter ou demander à l'utilisateur de rafraichir
                    // sa page
                    return;
                }

                handleReceivedMessages(msgs);
            } else {
                break;
            }
        }
    } else if (xhr.readyState === 4) {
        // La connexion est fermée, il faut gérer la reconnexion
    }
};

Un peu tricky oui, et peut-être difficile à suivre, mais globalement ça marche (c’est un extrait un peu adapté du code de Trivabble, ça tournait comme ça jusqu’au début de l’année dernière et c’est toujours dispo en solution de repli si tout échoue).

Aujourd’hui, pour les requêtes courtes, on pourrait utiliser la nouvelle API fetch, qui permet de faire des requêtes de manière simple, moins verbeuse, avec des belles promesses JavaScript à la mode. Mais le principe reste le même et tout ce que peut faire fetch peut être fait avec XMLHttpRequest et quand on utilise déjà XMLHttpRequest dans un code pour les requêtes longues, l’intérêt d’utiliser fetch pour les requêtes courtes n’est pas toujours si clair selon l’organisation du code, d’autant que cela casse la compatibilité avec les navigateurs anciens1.

Revenons à notre jeu de société : XMLHttpRequest permet d’arriver à nos fins : pour envoyer des évènements aux serveurs, on fait des requêtes classiques avec XMLHttpRequest, et pour en recevoir, on lance une requête de longue durée, et on reçoit les messages du serveur au fur et à mesure, plus ou moins en temps réel parce que l’évènement readystatechange est généré à chaque fois qu’on reçoit des données2.

Par contre, il faut se payer la gestion de la séparation des messages. Rien ne garantit qu’on va recevoir chaque message en un coup. Il « suffit » donc d’utiliser un séparateur (par exemple un ou deux retours à la ligne) ou de préfixer chaque message par sa taille, et de garder trace de l’indice du prochain message à lire en mémoire. Un peu pénible, sujet à erreur, mais faisable.

Les Server-Sent Events à la rescousse

Maintenant que le monde tourne dans le navigateur et que tout le monde écrit son application avec les technologies du Web, les requêtes longue durée sont bien évidemment devenues très répandues. Alors les développeurs de navigateurs et les organismes de standardisation du monde du Web, qui sont en fait essentiellement les mêmes personnes, se sont dit que ça serait bien de proposer une solution propre qui permettrait que les gens ne réinventent pas la roue à chaque fois.

Naissent donc les Server-Sent Events (SSE). Et comme le monde passe par les ports 80 et 443 et emballe tout dans de la requête HTTP pour faire plaisir aux différents NAT, pare-feux et autres serveurs mandataires d’entreprise retors, les SSE prennent la forme d’une requête HTTP longue durée avec un type de contenu (Content-Type) spécifique : text/event-stream. Ce format de données consiste en une suite d’évènements séparés par deux retours à la ligne. Grosso modo, chaque évènement est composé de plusieurs lignes clé:valeur, dont deux qui nous intéressent : la clé event, qui donne un nom à l’évènement (optionnel), et la clé data dans laquelle vous placez les données de l’évènement.

Dans les navigateurs, un bel objet EventSource permet de se connecter à un tel flux d’évènements, de s’y reconnecter de façon transparente en cas de perte de connexion et de récupérer chaque message facilement en surveillant l’évènement message de cet objet. Plus besoin de gérer soi-même la séparation des messages et de garder en mémoire tout le contenu de la requête. Ce sont ces deux points qui font tout l’intérêt de cette nouvelle méthode de communication par rapport à une requête longue classique. Un petit exemple d’utilisation :

const connection = new EventSource("/api/sse/");

connection.onmessage = function (e) {
    let msg;

    try {
        msg = JSON.parse(e.data);
    } catch (e) {
        // penser à gérer les échecs de parsing JSON et couper la connexion
        // si ça arrive
        connection.close();
        return;
    }

    handleReceivedMessage(msg);
};

connection.onerror = function (e) {
    // gérer l'erreur, demander à l'utilisateur de rafraîchir la page par exemple
};

Un peu plus clair que le code qui fait du long polling avec XmlHttpRequest, n’est-ce pas ?

Pour les quelques navigateurs perdus encore en circulation qui n’implémenteraient pas EventSource (ahem IE et l’ancien Edge ahem), il est tout à fait possible de l'implémenter soi-même à base de XMLHttpRequest. Il faudra juste garder à l’esprit que la requête entière sera gardée en mémoire.

Les WebSockets

Introduction parce qu’il en faut bien une

En même temps que les SSE, les WebSockets font leur apparition dans les navigateurs. Petite frise chronologique :

  • mars 1999 : XMLHttpRequest apparait accidentellement dans Internet Explorer 5. Début de la pente glissante que nous dégringolons encore. Ne blâmons pas Microsoft, s’ils n’avaient pas commencé, d’autres l’auraient fait. Ils nous ont peut-être épargné des solutions à base d’iframe, ou pire, de Java et/ou Flash pendant la première décennie de ce millénaire ;
  • 2010-2011 : apparition des WebSockets et des SSE dans les versions stables des différents navigateurs. En tout cas, dans Firefox, Safari et Chrome. Internet Explorer n’implémente jamais les SSE, et une implémentation des WebSockets arrive doucement en 2012 dans une version qui sera adoptée bien plus tard de toute façon ;
  • 2006 : apparition des SSE dans Opera de manière expérimentale. Pourquoi l’avoir mis après 2011 ? Parce que les développeurs d’Opera ont utilisé une machine à voyager dans le temps pour l’implémentation de cette fonctionnalité. Si vous vouliez lire de l’histoire correcte, vous auriez dû ouvrir un livre d’histoire. Si quoi que ce soit d’écrit dans cette dépêche ressemblait plus ou moins à des évènements qui se sont vraiment passés, ce serait purement fortuit. Vous êtes prévenu·e.

WebSocket est un acronyme récursif pour a WebSocket is not a socket, at all3. Alors, quel est le point commun avec un socket classique, s’il y en a un ? C’est la communication dans les deux sens, ce que ne permettent pas les autres techniques.

Intérêt

Les quatre points intéressants des WebSockets que je vois sont les suivants :

  1. on ne se paie plus le coût d’une connexion HTTP (établissement de la connexion TCP, puis TLS (de nos jours), envoi et réceptions des en-têtes HTTP) à chaque fois que l’on veut envoyer une donnée. Cela réduit les délais et les quantités de données transmises ;
  2. ordonnancement des messages : on peut avoir un dialogue entre le navigateur et le serveur et les messages arrivent, partent et sont traités dans un ordre prévisible, dans un même canal de communication ;
  3. transferts de message binaires faciles (même si finalement, avec XHR, ça se fait aussi) ;
  4. Comme avec les SSE et contrairement à XHR, le découpage des communications se fait par messages de taille arbitraire.

Pour le deuxième point, considérez le cas suivant : dans notre jeu de société, un joueur pose une pièce sur un plateau. Sans les WebSockets :

  1. Une requête HTTP est envoyée pour avertir le serveur.
  2. Le serveur répond OK à la requête, puis,
  3. transmet le déplacement à tous les joueurs, y compris celui qui a joué, par simplicité. Ce déplacement est reçu par le joueur via la requête de longue durée.

Sauf que dans la vraie vie, le déplacement peut être reçu avant la réponse OK par le joueur. Si le jeu est codé avec les pieds (ça arrive, en cas de tendinites aux deux bras par exemple), cela peut entraîner des bugs intéressants et difficiles à reproduire surtout en local pendant le développement, comme une disparition inopinée de la pièce. Je dis ça, c’est totalement théorique, hein, je n'aurais jamais codé moi-même un truc pareil ! (Ah, bah si. Oups)

Avec les WebSockets :

  1. Le joueur envoie le coup par la WebSocket
  2. Le serveur répond ok dans la WebSocket
  3. Le serveur envoie le déplacement à tous les joueurs, dont à ce joueur à travers cette même WebSocket.

Dans ces conditions, il faut vraiment une gestion horrible des évènements côté client pour recevoir le OK après le déplacement.

Donc pour résumer : pourquoi les WebSocket c’est intéressant ? Communication à deux sens plus efficace et plus prévisible. Mais alors, y a-t-il des inconvénients ? Oui. Déjà, certains proxys peuvent ne pas être compatibles (on verra pourquoi dans la partie suivante), et les WebSockets peuvent demander des configurations spécifiques côté serveur et c’est facile de se planter.

Du coup, si vous n’avez pas besoin d’une communication à deux sens, utilisez plutôt AJAX (SSE, XHR, fetch, comme vous voulez), votre vie sera probablement plus simple.

Comment ça marche ?

L’idée des WebSockets a certainement démarré avec une conversation de ce style.

  • Ce serait quand même bien d’avoir un mécanisme de communication dans les deux sens sur le web, sans se payer le coup des requêtes HTTP, et de pouvoir aussi balancer du binaire.
  • Un peu comme des sockets, mais pour le Web ?
  • Oui, exactement !
  • T’es fou, avec tous ces proxys, ces NAT et ces pare-feux dans tous les sens ça ne va jamais marcher, non ?
  • Bah, tu connais déjà la solution à ce problème ! On fait comme d’habitude…
  • On emballe dans une requête HTTP ? Mais c’est horrible, non ?
  • Pas le choix ! Hé hé.
  • Mais et les ports ? Et la communication dans les deux sens ? HTTP, c’est un aller, puis un retour, je te rappelle !
  • Les ports, on s’en fiche ! On peut utiliser des URLs, c’est bien plus pratique que des nombres qui n’ont pas de sens. Pour la communication dans les deux sens, j’ai ma petite idée…
  • J’ai peur d’avance…

Vous l’aurez deviné, une connexion par WebSocket commence comme une connexion HTTP. Ensuite, un mécanisme de négociation permet de se débarrasser d’HTTP et de commencer une communication à deux sens.

Note : ce qui suit est technique. Vous pouvez passer la section sans problème. Vous n’avez pas besoin de connaître ces mécanismes en détails pour utiliser les WebSockets : il existe des bibliothèques pour cela, et l’objet WebSocket des navigateurs fait tout le travail pour vous.

Début des négociations

La connexion WebSocket est toujours démarrée par le client / navigateur. Le serveur peut accepter les connexions. Le client se connecte au serveur et envoie des en-têtes HTTP classiques, et ceux-ci :

GET /api/websocket
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

Le serveur, reconnaissant les entêtes Connection: Upgrade et Upgrade: websocket, répond par le statut 101 indiquant qu’il veut bien changer de protocole pour gérer la WebSocket, en passant également les entêtes Connection et Upgrade. Il construit aussi une chaîne à partir clé de l’entête Sec-WebSocket-Key en appliquant des opérations définies par le protocole et met cette chaîne dans l’entête Sec-WebSocket-Accept :

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

À quoi sert cet échange de clé dans les entêtes Sec-WebSocket-Key et Sec-WebSocket-Accept ? Ça a l’air un peu superflu, mais c’est une mesure de sécurité qui permet de montrer au client que le serveur comprend bien qu’il a affaire à une WebSocket.

Note : on remarque qu’ici, la requête se fait en HTTP 1.1. Il existe également une norme pour faire des WebSocket à l’aide d’une requête HTTP 2, la RFC 8441. Il n’existe pas (encore), à ma connaissance, la possibilité de faire des WebSocket sur HTTP/3, qui ne s’appuie d’ailleurs pas sur TCP.

Une fois cet échange initial effectué, le serveur et le client vont pouvoir s’échanger des messages dans des frames WebSocket, le canal est bidirectionnel. À charge du client et du serveur de n’envoyer que des messages que l’autre attend, comme pour n’importe quelle connexion par socket finalement.

Il est important de réaliser que les WebSocket sont bien implémentées au-dessus de TCP (cela pourrait changer à l’avenir). L’implication la plus évidente pour moi c’est que ça permet de faire des trames (frames) de taille variable. Ces trames sont composées d’un entête indiquant leur type et leur taille, et du message lui-même.

On ne rentrera pas dans le détail ici (la doc est là pour ça ☺), mais notons qu’il existe différent types de frames :

  • les trames pong, qui ne contiennent pas de message mais qui permettent d’éviter que la connexion soit coupée par les intermédiaires pour cause d’inactivité (une sorte de keep-alive, quoi - et non, je ne sais pas pourquoi le keep-alive de TCP ne suffisait pas) ;
  • les trames ping, qui permettent de demander à l’autre d’envoyer un pong (d’ailleurs, un pong peut-être envoyé sans que l’autre ait envoyé de ping)
  • les trames contenant la fin d’un message (et ça peut être le message entier s’il n’y avait pas de bout de message avant) ;
  • les trames contenant le bout d’un message, dans ce cas le serveur doit s’attendre à une suite ;
  • les trames de fin de connexion, demandant à celui qui reçoit le message de fermer la connexion et d’ignorer quoi que ce soit qui pourrait suivre.

Les messages pas trop longs pourront tenir en une seule trame. Pour les trames contenant un (bout de) message, un code indique si c’est du texte (en UTF-8) ou du binaire quelconque (et donc oui, il faut implémenter l’UTF-8 si on implémente soi-même les WebSockets. Mais ça va, UTF-8 a été conçu en un soir à une table de resto en 1992).

Donc en résumé, ça se passe comme ça :

  • Navigateur : Salut, une petite WebSocket, ça te dit ?
  • Serveur : Ouais ouais, carrément !
  • … et c’est parti pour un échange bidirectionnel endiablé

Je vous passe le code côté serveur, vous utiliserez probablement une bibliothèque répandue pour votre langage de programmation préféré, mais si la curiosité vous pique, il y a la page Wikipedia anglaise qui est intéressante, la doc MDN assez complète et une implémentation simplissime en JavaScript dans Trivabble largement inspirée de cet article.

Côté navigateur, ce n’est pas si différent de EventSource, on sent une certaine cohérence dans la conception des API (attention cependant, pas de reconnexion automatique contrairement à EventSource !) :

const connection = new WebSocket("/api/ws/");

connection.onmessage = function (e) {
    let msg;

    try {
        msg = JSON.parse(e.data);
    } catch (e) {
        // penser à gérer les échecs de parsing JSON et couper la connexion
        // si ça arrive
        connection.close();
        return;
    }

    handleReceivedMessage(msg);
};

connection.onerror = function (e) {
    // gérer l’erreur, demander à l’utilisateur de rafraîchir la page par exemple
};

// Pour envoyer un message :
connection.send('{"command": "hello"}');

Et donc, oui, il est assez simple de partager le code si vous voulez gérer les deux types de connexion, en utilisant l’un comme une solution de repli quand l’autre n’est pas gérée ou échoue pour une raison x ou y. D’ailleurs, il est facile de casser le fonctionnement des WebSockets en configurant mal son serveur mandataire (proxy), qui doit lui-même prendre en charge les WebSockets. En effet, il faut généralement une configuration explicite sur les chemins qui impliquent des WebSockets, par exemple pour Nginx, même s’il y a probablement moyen d’avoir une configuration qui gère ça automatiquement :

location /api/ws/ {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1; # Bien demander du HTTP 1.1
    proxy_set_header Upgrade $http_upgrade; # Bien passer l’entête Upgrade 
    proxy_set_header Connection "Upgrade";  # Bien configurer l’entête Connection
    proxy_set_header Host $host; # on garde l’hôte aussi, au cas où l’applicatif derrière s’en serve 
}

Digression sur les connexions longues, HTTP2 et Apache

Le mélange de ces trois ingrédients peut mal se passer, de manière assez spectaculaire (la raison 3 va vous étonner et… allez, j’arrête, ça suffit). Apache est un serveur conçu dans les années 90, quand les navigateurs faisaient rarement beaucoup de connexions à la fois, et ne faisaient pas de connexions longues, et quand il y avait également moins souvent beaucoup de connexions simultanées à un même serveur. Les usages ont un peu changé depuis, comme vous avez certainement pu le constater en observant le monde trois secondes. De plus, il s’agit surtout d’un serveur HTTP auquel on a ajouté la possibilité de faire serveur mandataire. À contraster avec NGINX, plus récent (et donc architecturé avec les usages de maintenant en tête), qui est, pour ne presque pas caricaturer, un serveur mandataire qui peut également faire serveur HTTP (à tel point qu’il est fréquent de mettre Apache derrière NGINX pour avoir un serveur HTTP avec des fonctions avancées et un truc qui dépote pour gérer les connexions – le combo gagnant !).

Et du coup, si NGINX gère sans problème vos requêtes longues, avec Apache, ce n’est pas forcément la même histoire. Pour caricaturer, quand NGINX est capable, avec un processus, de surveiller un paquet de connexions et de se réveiller quand il se passe un truc sur l’une d’elles, Apache, historiquement, dédie un processus par connexion. Il est maintenant possible d’approcher le fonctionnement de NGINX en configurant la manière dont les connexions sont gérées, et notamment adopter le module multi-processus event et être très attentif à ses différents paramètres de configuration, gérant notamment le nombre maximal de processus et de requêtes traitées par processus.

Mais voilà, avec HTTP 2, qui permet de multiplexer complètement plusieurs requêtes (au lieu d’avoir le truc complètement séquentiel qu’on a avec HTTP 1.1), l’histoire ne s’arrête pas là. La documentation d’Apache appelle à la prudence :

Activer HTTP/2 sur votre serveur Apache a un impact sur la consommation de ressources, et si votre site est très actif, il est conseillé d’en prendre sérieusement en compte les implications.

HTTP/2 attribue à chaque requête qu’il reçoit son propre thread de travail pour son traitement, la collecte des résultats et l’envoi de ces derniers au client. Pour y parvenir, il lui faut lancer des threads supplémentaires, et ceci constituera le premier effet notable de l’activation de HTTP/2.

Dans l’implémentation actuelle, ces threads de travail font partie d’un jeu de threads distinct de celui des threads de travail du MPM avec lequel vous êtes familier.

Donc on a les paramètres du module multi-processus, et en fait il y a encore d’autres paramètres qui règlent le fonctionnement d’HTTP/2 ! Bref, difficile de configurer tout ça correctement.

Alors, que se passe-t-il si on n’a pas une configuration adaptée ? HTTP 2 ou pas, et requêtes longues ou pas si on atteint un maximum quelque part, Apache va simplement mettre les requêtes en attente. Pour les utilisateurs et les utilisatrices, c’est une interface qui ne répond plus, ou une page blanche qui n’en finit pas de se charger. Avec aucune possibilité pour l’application de l’avertir, puisque… le serveur ne veut plus rien savoir.

Avec des requêtes longues, c’est facile d’atteindre les maxima, bien sûr, puisqu’un fil d’exécution leur sont dédiées, et avec HTTP 2, c’est encore pire à cause de ces problèmes de paramétrage compliqués et de ces fils d’exécutions dédiés pour le multiplexage. De plus, les navigateurs, en HTTP 1.1, se limitent avec leur configuration par défaut à 6 requêtes simultanées vers chaque hôte. Ce n’est pas le cas en HTTP 2, et donc peuvent surcharger plus facilement un serveur si celui-ci ne gère pas bien les requêtes simultanées. Du coup, si votre application a beaucoup de ressources à charger au démarrage et qu’elle fait des requêtes vers l’API à la pelle, c’est vite la mort.

En plus, c’est pernicieux, parce que pendant le développement, quand vous êtes seul·e sur votre machine, il est probable que tout semble fonctionner correctement et les problèmes n’arrivent même pas forcément immédiatement lors de la mise en production, mais un peu après quand les visiteurs arrivent en masse et — incroyable mais vrai — utilisent votre application.

Damage control: le BroadcastChannel

Pour moi, la première mesure à appliquer si on peut le faire, c’est de passer à NGINX si on doit gérer des requêtes longues : l’outil est fait pour gérer ça. Si on doit rester sur Apache, bien gérer ses paramètres et éventuellement éviter HTTP 2 (ce qui est dommage, parce que ça permet des communications plus efficaces sinon, mais on préfère vite pas idéal à échec spectaculaire).

Par ailleurs, si votre application est susceptible d’être ouverte dans plusieurs onglets en même temps (parce qu’elle permet d’afficher des documents, par exemple), maintenir une connexion par onglet n’est pas idéal : vous pouvez facilement réduire la charge que vous imposez à votre serveur et à vos utilisateurs en vous limitant à une connexion pour tous les onglets. Ça utilise moins de bande passante et ça monopolise moins de fils d’exécution sur le serveur, surtout si Apache est utilisé.

Les onglets peuvent communiquer entre eux en utilisant BroadcastChannel (et, oui, les onglets peuvent communiquer entre eux, j’ai été surpris de découvrir ça l’année dernière). Un message envoyé dans un canal sera reçu par tous les onglets écoutant ce même canal. Un onglet est choisi (par élection de leader) pour se connecter au serveur et vous pouvez alors dispatcher les messages reçus du serveur à tous les onglets, ou aux onglets concernés.

C’est ce qu’on utilise dans Tracim, à travers la bibliothèque broadcast-channel, qui fournit une rustine (polyfill) pour les navigateurs ne fournissant pas une implémentation native et qui fournit également un mécanisme d’élection de leader super simple à utiliser.

Conclusion

Vous êtes toujours là ? Bon, d’accord, concluons. Si vous lisez encore, c’est que vous pouvez lire n’importe quoi à ce stade alors allons-y gaiement.

Pour résumer, des gens ont conçu un super système pour partager des documents inter-liés. Ensuite, des gens ont trouvé que faire des trucs qui clignotent dans des documents c’est rigolo. De fil en aiguille, on est arrivé avec des problèmes compliqués de pages blanches qui n’en finissent plus de charger, tout ça parce que quelqu’un qui voulait, dans un monde de plus en plus instantané, lire ses mails sans rafraîchir sa page, a soudoyé l’équipe voisine pour ajouter des trucs qui n’avaient rien à faire dans une visionneuse de documents, dans celle qui était à l’époque tristement en situation de monopole. Du coup, pour rester « pertinent », les potes concurrents ont adopté ces trucs. Bien plus tard, des gens ont cherché (et trouvé, c’est ça le pire !) des solutions sur-sophistiquées pour faire communiquer des documents magiques entre eux sans tout faire péter. Aujourd’hui, on fait un tas de trucs avec ces visionneuses de documents devenues des systèmes d’exploitation entiers, et de temps en temps on lit des documents avec. Désactiver certaines fonctionnalités de ces visionneuses et bloquer les trois quarts de ces documents permettent de retrouver la fluidité et le zen d’antan. Ou des pages blanches, quand ces documents sont devenus, par inadvertance, des applications.

Et le pire dans tout ça, c’est qu’il y a des gens pour raconter tout ça dans des dépêches impossiblement longues.


  1. oui, si vous voulez faire fonctionner votre jeu sur Safari 9, vous pouvez dire au revoir à fetch à moins d’utiliser un de ces fameux polyfills. Mais est-ce vraiment bien la peine d’alourdir votre projet avec encore une dépendance tout ça pour éviter trois pauvres lignes de code ? 

  2. Le fait que la propriété readyState de l’objet XMLHttpRequest indiquant l’état de la réponse ne change pas de valeur malgré le nom de l’évènement est purement accidentel. Vous ne voudriez pas que les API standardisées et présentes dans chaque navigateur de la planète soient complètement logiques non plus ? La vie des développeurs Web serait un peu morose si tout fonctionnait tout le temps logiquement. 

  3. Bon, d’accord, WebSocket veut dire « Socket pour le Web », mais ce n’est pas comme un socket quand même. Je tiens également à préciser que socket ne veut pas du tout dire socquette, qui se dit ankle sock. Parenthèse fermée. 

Aller plus loin

  • # Mercure

    Posté par  . Évalué à 9 (+8/-1). Dernière modification le 18/04/21 à 12:08.

    J’ai décliné la demande d’ajout d’un lien ou d’une section sur Mercure dans cette dépêche déjà très longue, parce que je ne connais pas trop et aussi parce que c’est plus haut niveau que ce qui y est présenté. Du coup, je lui dédie un commentaire, comme ça, si cela pique votre curiosité, vous pouvez y jeter un œil et ça permet aussi de lancer la discussion.

    Si j’ai bien compris, c’est un protocole qui cherche à standardiser les échanges entre le client et le serveur en évitant les WebSockets et qui propose d’utiliser SSE pour récupérer des données du serveur depuis le client et faire des appels « classiques » sur une API HTTP pour l’envoi de données, comptant sur HTTP2 pour en fait un mécanisme de communication efficace à travers une seule connexion grâce au multiplexage complet.

  • # Onglets et échanges avec le serveur, le cas LinuxFr.org...

    Posté par  (site Web personnel) . Évalué à 7 (+4/-0).

    Super dépêche, merci.

    Étrangement cela m'a rappelé cette entrée de suivi sur les blocages quand tu multiplies les onglets LinuxFr.org, va savoir pourquoi… https://linuxfr.org/suivi/inter-locks

    • [^] # Re: Onglets et échanges avec le serveur, le cas LinuxFr.org...

      Posté par  . Évalué à 5 (+3/-0). Dernière modification le 18/04/21 à 16:34.

      Ah oui, vous aviez peut-être le problème de la limite des 6 connexions simultanées à un même hôte. On a rencontré le problème dans Tracim l’année dernière. Depuis un an, on a un système de communication temps réel entre le serveur et le client, à l’aide d’un objet EventSource (et donc connexion permanente). Comme on est en HTTP 1.1, au-delà de 6 onglets ouverts, plus rien ne chargeait, impossible d’ouvrir un onglet de plus : page blanche, chargement infini. Forcément, le navigateur refusait de faire une connexion de plus et les autres ne se finissaient pas. On a brièvement essayé HTTP 2 et ça réglait le problème mais on a rencontré des problèmes de blocages serveur comme ceux décrits dans la dépêche.

      Donc on a implémenté presque ce que propose Mouns dans ce rapport, sauf qu’on n’utilise pas le localStorage, mais broadcast-channel (qui fait effectivement une élection de leader, relancée automatiquement quand l’onglet leader disparait). Pour les curieux c’est géré ici.

      Mais LinuxFr est en HTTP2, vous observez toujours ce problème ?

  • # le web devient monstrueux

    Posté par  (site Web personnel) . Évalué à 10 (+11/-0).

    Merci pour cette dépêche !

    Plus je fais du web plus j'ai l'impression que cet ensemble de techniques ne tient encore debout que grâce à la couche de rustines qu'on a collées autour.

    • [^] # Re: le web devient monstrueux

      Posté par  (site Web personnel) . Évalué à 5 (+3/-0).

      C'est vrai, mais c'est malheureusement le cas pour à peu près n'importe quelle techno qui vit assez longtemps.

      Parmi les exemples qui me viennent en tête vite fait:
      - IPV4 - IPV6 - IPV10
      - FTP - FTPS - SFTP
      - OAuth2 - PKCE - introspection/révocation - OIDC - une tétrachiée d'autres trucs

      Ca part d'un bon sentiment, le but c'est de limiter la réinvention de la roue puisque tu te bases sur quelque chose d'existant, connu, et déjà déployé.

      Par contre sans vraiment t'en apercevoir, tu te retrouves dans un gloubiboulga plus ou moins complexe, ou chacun implémente certaines rustines et pas d'autres, et où tu passes plus de temps à gérer l'intégration que le développement.

    • [^] # Re: le web devient monstrueux

      Posté par  (site Web personnel) . Évalué à 8 (+7/-0).

      Et de gens talentueux pour faire tenir un château de carte…

  • # Suggestion de corrections…

    Posté par  (site Web personnel) . Évalué à 4 (+2/-0).

    Conclusion

    […] Si vous lisez encore, c’est vous que pouvez lire n’importe quoi à ce stade , alors allons-y gaiement.

    et dans le paragraphe juste avant :

    […] à travers la bibliothèque broadcast-channel, qui fournit une rustine (polyfill) pour les navigateurs […]

    Eh ouais, il y en a qui ont tout lu :-) (j'ai une vidéo en cours de montage…) !

    Accessoirement, j'ai implémenté il n'y a pas longtemps les WebSockets suite à des problèmes avec XMLHttpRequest, du coup, le sujet m'a interpellé…

    Le toolkit Atlas, la bibliothèque légère et facile à utilser pour débuter avec la programmation d'interfaces graphiques… (voir 'site Web personnel").

  • # Websocket et outils réseaux.

    Posté par  . Évalué à 5 (+3/-0).

    Salut,

    Juste pour dire que certaines briques réseaux (VPN, switch, …) peuvent bloquer les websocket.

    Certains admin peuvent aussi les bloquer par méconnaissance, "Les websockets c'est pour le jeux en ligne, pas de ça chez moi".
    Et comme les websockets sont pas forcément considérés comme mainstream, il sont bloqué sans subtilités et tout ça peut être fait silencieusement.

    Pour du logiciels vers le grand public dans du navigateur d'ordi perso. C'est top. Mais dès que tu dois transiter par des réseaux privés, genre grand comptes, il faut encore se méfier un peu.

    • [^] # Re: Websocket et outils réseaux.

      Posté par  . Évalué à 4 (+2/-0). Dernière modification le 18/04/21 à 16:48.

      Oui, et c’est dommage. Des outils plus ou moins répandus utilisent les WebSockets ou sont en passe de le faire, comme Etherpad, Collabora Online ou Jitsi Meet.

      Mieux vaut effectivement implémenter une solution de repli sans WebSocket. Par défaut, Trivabble essaie de se connecter en WebSocket et si ça merde, passe à du SSE + requête HTTP classique, et si ça ça foire, bascule sur XHR long polling + requête HTTP classique.

      Selon le fonctionnement de votre code, je suggère de ne pas donner de réponse autre que des codes d'erreurs dans les requêtes classiques et de tout donner dans les WebSocket / requêtes longues si l'ordre des actions est important, sinon ça peut donner des bugs intéressants (ou de ne pas traiter les réponses des requêtes HTTP classiques si on sait que les données seront retrouvées dans les requêtes longues, ça permet parfois de garder son API cohérente ou de ne pas tout changer). Ça permet d'ordonner les messages plus facilement.

    • [^] # Re: Websocket et outils réseaux.

      Posté par  . Évalué à 2 (+2/-0).

      Merci pour l'info. En quelques mots, sur quels critères se base ce genre d'équipement pour bloquer une connexion WebSocket over HTTPS ?

      • [^] # Re: Websocket et outils réseaux.

        Posté par  . Évalué à 3 (+1/-0).

        Je sais pas la technique mais en situation réelle de production j'ai vu

        • blocage complet des websocket par une brique réseau. Parce que.
        • Non utilisation du VPN par un proxy pour les websockets alors que le https utilisait le VPN, cela aboutissant à du https sécurisé et des websocket non sécurisées.
        • filtrage des websockets a certaines heures (vicieux), les dev testaient le jour c'était ok mais la nuit ça échouait 🤮.

        De ce que j'en ai compris dans certains paramétrage d'administration les websockets sont traités spécifiquement (ce qui me parait idiot sur le principe mais je suis pas expert réseau) en conséquence tu peux te trouver avec une conf différente pour les websocket des autres transactions http.

        Sur quoi cela repose techniquement, je ne le sais pas DSL.
        Ça ne m'est arrivé que dans des structures un peu grosse avec une forte inertie sur l'adoption des nouvelles technos, si tu vois ce que je veux dire.

        Il a parfois fallut abandonner les websocket car c'était plus rapide d'utiliser une lib simulant les websockets (il faut en choisir une qui fais des websockets avec fallback ajax). D'ailleurs dans les docs de ces libs tu dois trouver plus de détails (je les ai plus en tête).

        Mais le monde change et les briques réseau se change, j'imagine que chaque année il faut réévaluer la faisabilité et que les choses ne peuvent que mieux aller.

        Ce genre de lib https://socket.io/ (MIT) aide

      • [^] # Re: Websocket et outils réseaux.

        Posté par  (site Web personnel) . Évalué à 2 (+0/-0).

        En quelques mots, sur quels critères se base ce genre d'équipement pour bloquer une connexion WebSocket over HTTPS ?

        Ya des équipements qui « simplement » mettent https over. Du coup c'est plus facile.

        Adhérer à l'April, ça vous tente ?

  • # Java, flash

    Posté par  . Évalué à 3 (+4/-3).

    Java et flash avaient leurs sandbox, potentiellement meilleures que celle du navigateur au moment de leur arrivés. C'est le passage d'une sandbox à l'autre qui posait beaucoup de problèmes.

    Je ne comprends pas bien ce premier paragraphe qui est là plus pour libérer une frustration que pour apporter de l'information. Ça se pleins de faire des choses dans un navigateur puis de faire des choses à côté du navigateur par exemple.

    C'est loin de la ligne éditoriale plus neutre que je préfère en dépêche, mais ça doit être mon opinion personnelle.

    • [^] # Re: Java, flash

      Posté par  . Évalué à 5 (+3/-0).

      Tu as raison, merci pour le retour.

      Ce paragraphe n'est pas complètement faux (pour Java, on pouvait sortir de la sandbox en signant l'applet et en demandant la permission de l'utilisateur ; Flash n'était pas reconnu pour sa fiabilité extrême), mais il n'est vraiment pas terrible.

      Si l'équipe de modération peut le remplacer par la phrase suivante, ça serait pas mal : « Des solutions s'appuyant sur les plugins Java ou Flash existaient également » (merci !)

      qui est là plus pour libérer une frustration

      Non, je te rassure ;-) Je me suis un peu laissé emporter en écrivant.

      • [^] # Re: Java, flash

        Posté par  (site Web personnel) . Évalué à 4 (+1/-0).

        Corrigé, merci.

      • [^] # Re: Java, flash

        Posté par  . Évalué à 8 (+6/-0).

        D'autres remarques :

        Et puisqu’il faut un terme branché pour désigner tout ce mécanisme de mettre à jour la page avec des nouvelles données sans recharger la page en entier (truc de fou à l’époque !), le concept d’AJAX est né (Asynchronous JavaScript And XML - hé hé oui, on ne se débarrasse pas de XML comme ça, même dans le terme répandu, même quand XML n’est pas du tout impliqué. Parce que si des gens font bien transiter du XML de temps en temps, bien souvent, on utilise du JSON. Bien joué, Alex !).

        XMLHttpRequest est anterieur de plusieurs années à json. Json a même était créé du fait de la popularité de XMLHttpRequest. Quand il a était implémenté par les navigateurs c'est bien du xml qui transité car c'était le seul format normalisé.

        On peut lancer des requêtes pour envoyer des données au serveur, mais on peut aussi envoyer une requête de longue durée (long polling) et lire les données qui arrivent au fur et à mesure.

        Le long polling c'est un peu plus subtile que ça. Il faut utiliser xhr, mais en gérant le timeout et en gérant une boucle. Tu tombe instantanément dans un callback hell avec xhr là où les promesses ou async/await vont être bien plus confortable à utiliser.

        Mais le principe reste le même et tout ce que peut faire fetch peut être fait avec XMLHttpRequest[…]

        Non, fetch peut être rerouté vers un worker par exemple, il y a un travail en cours pour pouvoir annuler des requêtes (particulièrement utile pour ton scope de requêtes longues).

        une sorte de keep-alive, quoi - et non, je ne sais pas pourquoi le keep-alive de TCP ne suffisait pas

        Le keep-alive ne remonte pas jusqu'à l'application, il est global, il est de 2h généralement,…

        De plus, les navigateurs, en HTTP 1.1, se limitent avec leur configuration par défaut à 6 connexions simultanées vers chaque hôte. Ce n’est pas le cas en HTTP 2, et donc peuvent surcharger plus facilement un serveur si celui-ci ne gère pas bien les connexions simultanées.

        Je n'ai pas les moyens de le tester, mais je ne trouve rien qui va dans ce sens et ça m'étonne beaucoup. HTTP2 est là pour réduire le nombre de connexions pas l'augmenter. Qu'ils arrêtent de limiter le nombre de requêtes, ça ne m'étonnerait pas, mais le nombre de connexion ?

        Je vois dans la doc de nginx des choses surprenantes comme :

        In HTTP/2 and SPDY, each concurrent request is considered a separate connection.

        Documentation de la directive limit_conn

        C'est pas ça qui t'a induit en erreur ?

        • [^] # Re: Java, flash

          Posté par  . Évalué à 6 (+4/-0). Dernière modification le 19/04/21 à 13:58.

          XMLHttpRequest est anterieur de plusieurs années à json

          D'accord, mais tu n'étais pas obligé de transmettre du XML avec. Tu pouvais transmettre des données plain text, des scripts javascript… bref, l'objet n'a rien de spécifique à XML et en programmation on a quand même pas mal l'habitude de séparer les probèmes…

          Ce n'est vraiment pas moi qui le dit, pour le coup, je n'ai rien inventé c'est la personne qui a conçu XmlHttpRequest (premier lien de la dépêche) !

          I got in touch with Jean Paoli who was running that team at the time and we pretty quickly struck a deal to ship the thing as part of the MSXML library. Which is the real explanation of where the name XMLHTTP comes from- the thing is mostly about HTTP and doesn't have any specific tie to XML other than that was the easiest excuse for shipping it so I needed to cram XML into the name (plus- XML was the hot technology at the time and it seemed like some good marketing for the component).

          Le long polling c'est un peu plus subtile que ça. Il faut utiliser xhr, mais en gérant le timeout et en gérant une boucle.

          C'est décrit dans la dépêche… qui reste une dépêche, pas une doc.

          Non, fetch peut être rerouté vers un worker par exemple

          Je ne sais pas précisément ce que tu entends par là mais sur cette page : https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest

          Note: This feature is available in Web Workers.

          Donc si tu dis que fetch est utilisable dans un worker, ce n'est pas un contre exemple d'un truc que fetch peut faire et pas XMLHttpRequest

          il y a un travail en cours pour pouvoir annuler des requêtes (particulièrement utile pour ton scope de requêtes longues).

          Je ne doute pas que dans le futur, les APIs vont continuer à évoluer et je ne serais pas étonné si fetch finissait par faire des choses que XmlHttpRequest ne fait pas.

          une sorte de keep-alive, quoi - et non, je ne sais pas pourquoi le keep-alive de TCP ne suffisait pas

          Le keep-alive ne remonte pas jusqu'à l'application

          À noter que le mécanisme de keep-alive ne remonte pas jusqu'au code utilisant l'objet WebSocket (ce qui ne contredit en rien ton affirmation, on est tout à fait d'accord).

          De plus, les navigateurs, en HTTP 1.1, se limitent avec leur configuration par défaut à 6 connexions simultanées vers chaque hôte. Ce n’est pas le cas en HTTP 2, et donc peuvent surcharger plus facilement un serveur si celui-ci ne gère pas bien les connexions simultanées.

          Je n'ai pas les moyens de le tester, mais je ne trouve rien qui va dans ce sens et ça m'étonne beaucoup. HTTP2 est là pour réduire le nombre de connexions pas l'augmenter. Qu'ils arrêtent de limiter le nombre de requêtes, ça ne m'étonnerait pas, mais le nombre de connexion ?

          Pour le contexte :

          Tu as raison, j'ai utilisé le mot « connexion » un peu négligemment mais ça ne change pas le sens de ce passage ni le raisonnement autour.

          Le passage de la doc de Nginx n'a rien de surprenant : il avertit simplement que le paramètre dont il est question n'a pas exactement le même sens en HTTP 1.1 qu'en HTTP 2, justement pour qu'il ait « le même effet intuitif ». Ils font le même amalgame que moi (mais avec rigueur).

          C'est pas ça qui t'a induit en erreur ?

          N'hésite pas à poser des questions si tu trouves des imprécisions, les commentaires sont justement là pour discuter des détails intéressants.

          Je tiens tout de même à rappeler que la dépêche n'est pas normative et, elle n'a pas la rigueur d'une RFC ni d'une doc, ni d'un papier de recherche. Tu va pouvoir trouver une quantité phénoménale d'imprécisions dedans.

          • [^] # Re: Java, flash

            Posté par  . Évalué à 1 (+0/-1).

            XMLHttpRequest est anterieur de plusieurs années à json

            D'accord, mais tu n'étais pas obligé de transmettre du XML avec. Tu pouvais transmettre des données plain text, des scripts javascript… bref, l'objet n'a rien de spécifique à XML et en programmation on a quand même pas mal l'habitude de séparer les probèmes…

            Ce n'est pas en opposition avec ce que je dis.

            Le long polling c'est un peu plus subtile que ça. Il faut utiliser xhr, mais en gérant le timeout et en gérant une boucle.

            C'est décrit dans la dépêche… qui reste une dépêche, pas une doc.

            Ok effectivement. Je n'avais pas bien suivi.

            Je ne sais pas précisément ce que tu entends par là mais sur cette page : https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest

            Note: This feature is available in Web Workers.

            Donc si tu dis que fetch est utilisable dans un worker, ce n'est pas un contre exemple d'un truc que fetch peut faire et pas XMLHttpRequest

            et juste en dessous ils parlent des services workers. C'est ce qui sert à gérer des applications offline entre autre.

            Le passage de la doc de Nginx n'a rien de surprenant : il avertit simplement que le paramètre dont il est question n'a pas exactement le même sens en HTTP 1.1 qu'en HTTP 2, justement pour qu'il ait « le même effet intuitif ». Ils font le même amalgame que moi (mais avec rigueur).

            C'est marrant que tu te plaigne que xmlhttprequest soit nommé ainsi alors qu'il peut servir pour autre chose et que tu ne vois pas de problème que limit_conn limite les requêtes et pas les connexions… Pour le coup perso j'ai des problématiques où une connexion et une requête ça n'est pas du tout la même chose, je suis bien content de ne pas reposer sur lui pour le coup parce que du coup je vois pas comment distinguer les 2.

            • [^] # Re: Java, flash

              Posté par  . Évalué à 6 (+4/-0). Dernière modification le 19/04/21 à 18:18.

              C'est marrant que tu te plaigne que xmlhttprequest soit nommé ainsi alors qu'il peut servir pour autre chose

              J'ai l'impression que tu perçois le ton se voulant humoristique de cette dépêche comme de la plainte, et ça doit franchement rendre sa lecture frustrante ;-)

              Tu sais, en vrai, je m'en fiche un peu. Je trouve l'histoire de ce nom et de cet objet très intéressante, et c'est en partie ce qui a motivé l'écriture de cette dépêche. Non, ce n'est pas idéal, mais non, ça ne m'empêche pas de dormir la nuit.

              et que tu ne vois pas de problème que limit_conn limite les requêtes et pas les connexions

              Je comprends pourquoi c'est nommé comme ça et que ça se comporte comme ça. Nginx a été conçu au début des années 2000, on n'était loin d'imaginer HTTP2 à ce moment là et bien sûr qu'il y a des défauts comme ça.

              Bien sûr que ce n'est pas idéal, mais c'est la vie !

              Bref, dans le passage suivant :

              De plus, les navigateurs, en HTTP 1.1, se limitent avec leur configuration par défaut à 6 connexions simultanées vers chaque hôte. Ce n’est pas le cas en HTTP 2, et donc peuvent surcharger plus facilement un serveur si celui-ci ne gère pas bien les connexions simultanées

              Il serait plus correct de parler de « requêtes simultanées » que de connexion simultanées. Une personne dans l'équipe de modération pourrait-elle remplacer les deux occurrences ? (décidément, j'en demande beaucoup pour cette dépêche !! Un grand merci pour tout ce travail !)

              • [^] # Re: Java, flash

                Posté par  (site Web personnel) . Évalué à 3 (+0/-0).

                Corrigé, merci.

              • [^] # Re: Java, flash

                Posté par  . Évalué à -5 (+0/-7).

                J'ai l'impression que tu perçois le ton se voulant humoristique de cette dépêche comme de la plainte, et ça doit franchement rendre sa lecture frustrante ;-)

                Non c'est plutôt insupportable. D'une je trouve que la forme rend le fond cryptique et d'autres part oui j'y vois beaucoup de plaintes. Le fait qu'il y en ai des tartines et que ça n'est jamais véritablement désamorcé en fait pour moi une série de critiques sous un verni de moquerie.

                Mais je ne l'aborde que parce que tu l'aborde, c'est mon ressenti et mon point de vu. Je ne te le reproche pas.

                Nginx a été conçu au début des années 2000, on n'était loin d'imaginer HTTP2 à ce moment là et bien sûr qu'il y a des défauts comme ça.

                Alors non ça n'est pas un argument recevable. Ils ont modifié l'implémentation de cette directive pour qu'elle s'applique en ayant conscience de la couche application ce qui n'était pas nécessaire avant. Ça n'est pas lié à la dette technique. C'est un choix tout à fait assumé où ils ont voulu simplifié la vie de leurs utilisateurs au détriment de la cohérence. C'est leur choix, je le trouve regrettable et je suis content de ne pas avoir eu affaire à lui quand j'ai eu des vrais contraintes de connexions sans contraintes de requêtes.

                • [^] # Re: Java, flash

                  Posté par  . Évalué à 7 (+5/-0). Dernière modification le 19/04/21 à 18:54.

                  Non c'est plutôt insupportable. D'une je trouve que la forme rend le fond cryptique et d'autres part oui j'y vois beaucoup de plaintes. Le fait qu'il y en ai des tartines et que ça n'est jamais véritablement désamorcé en fait pour moi une série de critiques sous un verni de moquerie.

                  Mais je ne l'aborde que parce que tu l'aborde, c'est mon ressenti et mon point de vu. Je ne te le reproche pas.

                  C'est bon à savoir, le retour tout à fait honnête est bon à prendre et je l'aurai en tête pour les prochaines choses que j'écrirai. Merci ! Le style marqué est assumé, je me doutais que je prenais le risque que ça ne plaise pas à tout le monde. Ça semble avoir bien plu à d'autres alors je ne peux pas promettre que je ne recommencerai pas, mais il y aura clairement des différences, ne serait-ce que parce que la moitié de la dépêche a été rédigée il y a près d'un an et qu'on a le temps de changer entre temps, je l'ai senti en la reprenant la semaine dernière.

                  Bien sûr, il y a un fond de vérité derrière certains (la plupart ?) traits humoristiques (sinon ça ne serait pas drôle), mais en tout cas, je ne me moque de personne, ça c'est important que ce soit bien clair. Je n'ai probablement pas le dixième des compétences des personnes qui ont pu être impliquées de près où de loin dans l'élaboration de toutes ces technologies dont je ne suis qu'utilisateur et une bonne dose d'humilité s'impose bien entendu.

                  C'est un choix tout à fait assumé où ils ont voulu simplifié la vie de leurs utilisateurs au détriment de la cohérence

                  On est d'accord :-)

                  • [^] # Re: Java, flash

                    Posté par  . Évalué à 10 (+10/-0).

                    C'est bon à savoir, le retour tout à fait honnête est bon à prendre et je l'aurai en tête pour les prochaines choses que j'écrirai.

                    Ne te prend pas la tête avec les diplômés de l'académie du 1er degré ;)

                    Moi j'ai trouvé que ça rendait agréable la lecture d'un sujet qui d'habitude est plutôt chiant, donc continue !

  • # Merci

    Posté par  . Évalué à 10 (+20/-0).

    Juste merci pour avoir pris le temps de rédiger cet article, aussi intéressant sur le fond qu'amusant sur la forme.

  • # Merci!

    Posté par  . Évalué à 6 (+6/-0).

    Idem que Swirly: merci beaucoup pour cette dépêche aussi intéressante que chouette à lire.

  • # Besoin de quelques éclaircissements

    Posté par  . Évalué à 2 (+2/-0).

    Hello

    J'ai apprécié la lecture de l'article. Le ton léger sans trop en faire permet d'absorber plus facilement des concepts un peu rébarbatifs.

    Cependant, je pense qu'il gagnerait en qualité s'il était un peu plus clair sur certains points. Voici quelques remarques et questions, qui ne sont que constructives, car je sais à quel point il est difficile d'écrire un texte de cette taille. Il s'agit du point de vue de quelqu'un qui n'y connaît pas grand chose en dév Web, mais c'est le public concerné je crois.

    On crée un objet XMLHttpRequest, on lui indique l’URL de la page à télécharger

    Qui ? Le client ?

    Pour en recevoir, on lance une requête de longue durée, et on reçoit les messages du serveur au fur et à mesure (…) Par contre, il faut se payer la gestion de la séparation des messages. Rien ne garantit qu’on va recevoir chaque message en un coup. Il « suffit » donc d’utiliser un séparateur (par exemple un ou deux retours à la ligne).

    Un gars plutôt réseau comme moi ne comprend pas pourquoi il faut séparer les messages. On reçoit les messages au fur et à mesure, donc des paquets de données (plus précisément des PDU de niveau 7). C'est le paquet qui sépare les messages non ?

    Sans les WebSockets :
    Une requête HTTP est envoyée pour avertir le serveur.
    Le serveur répond OK à la requête, puis,
    transmet le déplacement à tous les joueurs, y compris celui qui a joué, par simplicité. Ce déplacement est reçu par le joueur via la requête de longue durée.

    Avec les WebSockets :
    Le joueur envoie le coup par la WebSocket
    Le serveur répond ok dans la WebSocket
    Le serveur envoie le déplacement à tous les joueurs, dont à ce joueur à travers cette même WebSocket.

    C'est un peu exactement la même chose, je ne vois pas la différence.

    Elle réside peut-être là :

    Sauf que dans la vraie vie, le déplacement peut être reçu avant la réponse OK par le joueur.

    Mais je ne comprends pas du tout pourquoi.

    Ce serait quand même bien d’avoir un mécanisme de communication dans les deux sens sur le web, sans se payer le coup des requêtes HTTP,
    - Navigateur : Salut, une petite WebSocket, ça te dit ?
    - Serveur : Ouais ouais, carrément !
    - … et c’est parti pour un échange bidirectionnel endiablé

    Là encore, je ne vois pas la différence avec du HTTP. Avec le HTTP, on a aussi un échange bidirectionnel : le client envoie des données au serveur, et le serveur envoie des données au client.

    Ce qu'il faudrait expliciter vraiment (si j'ai bien compris), c'est que une fois le socket "ouvert", le serveur peut être à l'initiative d'un message vers le client, ce qui est normalement réservé au client. En fait, on peut faire un vrai "push" ici : pour recevoir des données du serveurs, on ne contente pas d'attendre le retour de requêtes envoyées au serveur. Le serveur peut de lui même initier des requêtes vers le client. Je suppose que cela sous-entend une prise en charge particulière dans les proxies et NAT.

    C'est plutôt bien expliqué sur la page Wikipedia :

    This is made possible by providing a standardized way for the server to send content to the client without being first requested by the client

    La page https://websocket.org/quantum.html donnée en référence explique aussi les méthodes historiques (polling, long-polling, streaming).

    Merci

    • [^] # Re: Besoin de quelques éclaircissements

      Posté par  . Évalué à 2 (+0/-0).

      Ce qu'il faudrait expliciter vraiment (si j'ai bien compris), c'est que une fois le socket "ouvert", le serveur peut être à l'initiative d'un message vers le client, ce qui est normalement réservé au client. En fait, on peut faire un vrai "push" ici : pour recevoir des données du serveurs, on ne contente pas d'attendre le retour de requêtes envoyées au serveur. Le serveur peut de lui même initier des requêtes vers le client. Je suppose que cela sous-entend une prise en charge particulière dans les proxies et NAT.

      Je pense que tu as bien compris le but de la manœuvre. Je fais un petit retour d'xp dans un autre fil, ça répondra peut-être à tes questions.

    • [^] # Re: Besoin de quelques éclaircissements

      Posté par  . Évalué à 3 (+1/-0). Dernière modification le 22/04/21 à 19:07.

      On crée un objet XMLHttpRequest, on lui indique l’URL de la page à télécharger

      Qui ? Le client ?

      Yep. XMLHttpRequest est une classe qu'on ne trouve que dans les navigateurs web (habituellement en tout cas !).

      Par contre, il faut se payer la gestion de la séparation des messages. Rien ne garantit qu’on va recevoir chaque message en un coup. Il « suffit » donc d’utiliser un séparateur (par exemple un ou deux retours à la ligne).
      Un gars plutôt réseau comme moi ne comprend pas pourquoi il faut séparer les messages. On reçoit les messages au fur et à mesure, donc des paquets de données (plus précisément des PDU de niveau 7). C'est le paquet qui sépare les messages non ?

      Ici on est à un plus haut niveau : un « message » pour moi est un objet JSON complet (par exemple). Ou un texte dont la taille est connue en avance. Ou un texte qui se finit par une nouvelle ligne. Il peut être composé de un ou plusieurs paquets TCP, et plusieurs messages peuvent être envoyés en même temps par le serveur, et il peut même y avoir un paquet TCP qui contient la fin d'un message et le début du suivant.

      La fonction onreadystatechange de l'objet XMLHttpRequest peut être appelée n'importe quand, quand le navigateur le décide. Mettons que le serveur envoie deux « messages » en même temps. Il peut y avoir du buffering, on peut recevoir une partie du premier, puis sa fin et tout le deuxième message en entier, ou recevoir le tout en trois fois… Bref, il faut analyser le flux au fil de l'eau et trouver les messages dedans. Je ne sais pas si c'est plus clair dit comme ça.

      Ce qu'il faudrait expliciter vraiment (si j'ai bien compris), c'est que une fois le socket "ouvert", le serveur peut être à l'initiative d'un message vers le client, ce qui est normalement réservé au client

      Le serveur peut de lui même initier des requêtes vers le client. Je suppose que cela sous-entend une prise en charge particulière dans les proxies et NAT.

      Alors, non. Le serveur peut envoyer des messages sur une connexion WebSockets établie, demandée par le client. C'est une connexion TCP classique établie, le NAT n'est pas un problème. Le proxy, si c'est un proxy TCP, non, si c'est un proxy HTTP oui, il faut qu'il comprenne les WebSockets.

      Là encore, je ne vois pas la différence avec du HTTP.

      La différence, c'est la connexion établie. HTTP c'est : une requête, une réponse et on coupe la connexion. Les WebSockets laissent une connexion TCP ouverte, par laquelle le client et le serveur peuvent envoyer des données à tout moment.

      Et du coup :

      Sans les WebSockets :
      Une requête HTTP est envoyée pour avertir le serveur.
      Le serveur répond OK à la requête, puis,
      transmet le déplacement à tous les joueurs, y compris celui qui a joué, par simplicité. Ce déplacement est reçu par le joueur via la requête de longue durée.

      Avec les WebSockets :
      Le joueur envoie le coup par la WebSocket
      Le serveur répond ok dans la WebSocket
      Le serveur envoie le déplacement à tous les joueurs, dont à ce joueur à travers cette même WebSocket.

      C'est un peu exactement la même chose, je ne vois pas la différence.

      Elle réside peut-être là :

      Sauf que dans la vraie vie, le déplacement peut être reçu avant la réponse OK par le joueur.

      Mais je ne comprends pas du tout pourquoi.

      Sans WebSocket, vu que la requête et la connexion long polling son dans deux connexions différentes (sur différents ports - ou dans une seule connexion HTTP2 mais multiplexée, donc je pense que c'est le même problème), il n'y a pas de garantie d'ordre d'arrivée des messages.

      le client envoie la requête, le serveur la traite, envoie la réponse au client, et envoie un message sur l'autre connexion correspondant à cette requête, on peut recevoir la réponse et le message dans les deux sens.

      Avec un WebSocket, les messages seront bien ordonnés puisque tout passe dans le même tuyau et que TCP garantie l'ordre : la réponse à la requête arrive toujours avant le message, par exemple.

      J'espère que ça répond aux interrogations :-)

      • [^] # Re: Besoin de quelques éclaircissements

        Posté par  . Évalué à 3 (+1/-0).

        La fonction onreadystatechange de l'objet XMLHttpRequest peut être appelée n'importe quand, quand le navigateur le décide. Mettons que le serveur envoie deux « messages » en même temps. Il peut y avoir du buffering, on peut recevoir une partie du premier, puis sa fin et tout le deuxième message en entier, ou recevoir le tout en trois fois… Bref, il faut analyser le flux au fil de l'eau et trouver les messages dedans. Je ne sais pas si c'est plus clair dit comme ça.

        C'est un truc que j'ai eu du mal à comprendre : quand tu parle de requêtes longues, tu parle en fait de multiplexer des requêtes.

        Pour moi une requête longue et l'utilisation de long polling c'est surtout le fait que le serveur ne réponde pas au client tant qu'il n'a pas lui même la réponse. Il y a des méthodes pour limiter les risques de timeout en générant du trafic tcp, mais on reste sur une requête → une réponse.

        Avec ce genre de multiplexage, je pense qu'il est intéressant d'éviter de recréer la roue et d'utiliser des méthodes plus éprouvé qu'au doigt mouiller. Le SEND de stomp par exemple est fait pour ça ou à minima un format fait pour être streamé comme yaml avec la séparation en document. Je n'ai jamais trop essayé, mais je me demande même si les bibliothèques json orientée streaming ne peuvent pas faire un truc sympas pour ça (tu stream un tableau json infini dont chaque élément est un message.

        • [^] # Re: Besoin de quelques éclaircissements

          Posté par  . Évalué à 2 (+0/-0). Dernière modification le 22/04/21 à 20:16.

          C'est un truc que j'ai eu du mal à comprendre : quand tu parle de requêtes longues, tu parle en fait de multiplexer des requêtes.

          Pas du tout. C'est pour attendre des messages. Le serveur envoie des messages qu'en il y a des messages à envoyer. On n'arrête pas la requête dès qu'un message a été reçu (on pourrait).
          Ce n'est pas une requête, une réponse.

          C'est vraiment ce qu'on fait avec les Server-Sent Events.

          • [^] # Re: Besoin de quelques éclaircissements

            Posté par  . Évalué à 2 (+0/-0).

            tu stream un tableau json infini dont chaque élément est un message.

            Pourquoi pas, mais pourquoi faire ? SSE fournit un mécanisme propre, élégant et efficace à la fois côté serveur et côté client.

          • [^] # Re: Besoin de quelques éclaircissements

            Posté par  . Évalué à 2 (+0/-0).

            C'est vraiment ce qu'on fait avec les Server-Sent Events.

            C'est l'association long polling <=> multiplexage de message serveur qui n'est pas systématique. Tu peux faire du long polling pour un usage transitoire à fin de ne recevoir qu'un seul message.

            Je ne remet pas en cause ton usage. C'est juste que j'ai dû relire plusieurs fois pour comprendre alors que j'en ai déjà fais.

  • # Retour d'xp en Go

    Posté par  . Évalué à 4 (+2/-0). Dernière modification le 22/04/21 à 11:49.

    Ton article m'a donné envie d'essayer les SSE pour remplacer un système de websocket que je n'utilisais que dans un sens (serveur vers client et classique dans l'autre sens).

    Il y a quelques années pour apprendre le langage Go j'avais réécris un système de websocket en Python (pour un jeu de scrabble) qui fonctionnait mais il m'était impossible de faire évoluer ce code spaghetti. Le résultat était déjà plutôt concluant, mais plat de pâte tout de même.
    Du coup je viens de le réécrire avec SSE.
    Première chose plus besoin de dépendre d'une lib externe (gorilla.websocket) et pas mal de code de tuyauterie à supprimer autant côté serveur que client.
    Ensuite le côté sens unique de SSE correspond exactement au fonctionnement des channels en Go. Ce qui fait que mon dispatcher fonctionne aussi bien avec des clients distants (un client SSE = un channel) ou des goroutines (par exemple un robot joueur).
    En Python j'avais séparé mon appli en plusieurs services (les robots à part) qui communiquaient par websocket, et la du coup je les ais réintégrés dans la même application et ils communiquent par channel sans changer grand chose au code, ça me laisse le choix.
    Bref, les SSE sont un excellent moyen de familiariser aux concepts de channel en Go.

    A part ça, j'ai eu quelques surprises niveau Nginx.
    Dans un premier temps je me suis dit, avec http ça va aller tout seul. Effectivement ça semble fonctionner tout seul.
    Mais en regardant les logs je me suis aperçu que la connexion était coupée toutes les minutes.
    J'ai rajouté ça :

    proxy_buffering off;
    proxy_cache off;
    proxy_set_header Connection '';
    proxy_http_version 1.1;
    chunked_transfer_encoding off;

    Tout allait bien jusqu'à ce que des joueurs se plaignent que ça rame, ce que n'avais pas constaté dans mes essais. Après enquête je comprends que c'est quand on ouvre plusieurs onglets. J'essaye d'ouvrir une dizaine d'onglets, l'ordinateur se met à souffler comme un malade et les derniers onglets tournent sans rien afficher. Systématiquement à partir du sixième ! J'essaye un autre navigateur, pareil, 6 onglets max.
    C'est finalement une limitation http, je trouve enfin qu'il faut passer en http2

    server {
    listen 443 ssl http2;

    Tout semble rentrer dans l'ordre.

    Bilan, les SSE en Go c'est un régal, je garde.

    • [^] # Re: Retour d'xp en Go

      Posté par  . Évalué à 3 (+1/-0).

      Super !

      proxy_buffering off;
      proxy_cache off;
      proxy_set_header Connection '';
      proxy_http_version 1.1;
      chunked_transfer_encoding off;

      Ça m'intéresse, tu pourrais expliquer ce que ça fait / pourquoi ça évite la coupure ? Je crois que j'ai observé ça, je ne sais plus si j'ai corrigé, mais en tout cas ça ne pose pas de problème dans mon cas.

      • [^] # Re: Retour d'xp en Go

        Posté par  . Évalué à 2 (+0/-0).

        Je ne peux pas l'expliquer pour le moment, je l'ai trouvé par tâtonnement (je me suis fait un défi de passer de websocket à sse dans la journée en prod !)… Ce qui a été délicat c'est qu'en local je testais en accès direct à mon application, et en prod par Nginx. Je n'ai pas encore testé si ça passe par un load balancer type ELB…

  • # Webchaussette ou Serveur sainte évents: retour d'expérience

    Posté par  (site Web personnel) . Évalué à 4 (+1/-0).

    Sur jb3, j'avais commencé à utiliser une webchaussette, mais j'ai fini par abandonner, car:

    • maintenir la connexion ouverte est un défi, l'API javascript ne permettais pas (je ne sais pas si c'est possible maintenant) de réglage et sur de nombreux réseaux, les battements de cœur ne suffisent pas, la connexion finit toujours par être fermé de force par un serveur, un proxy ou un équipement réseau.
    • dans Spring, l'API pour gérer les webchaussettes n'étaient pas très mature et plutôt pénible à régler.

    Depuis j'utilise les serveur sainte évents et ça marche mieux:

    • j'ai moins de déconnexion MAIS attention la reconnexion n'est pas toujours automatique, il faut gérer les erreurs et la rétablir à la main ;
    • les API côté js et java sont plus simples ;
    • je trouve le modèle de programmation plus clair: un EventSource pour recevoir des infos, des GET/POST pour faire des requêtes.

    Petit défaut: ça ne fonctionne pas avec les vieux brouteurs de Microsoft, oh ben zut alors ça c'est vraiment pas de chance.

    Incubez l'excellence sur https://linuxfr.org/board/

Envoyer un commentaire

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n’en sommes pas responsables.