Portail / Sujets autour de la programmation / Programmation C/C++ sous Linux / Serveur d'application avec Qt 6 / Concepts généraux(Sommaire)

Infrastructure pour serveur d'application.

Mise à jour du 15/06/2024.

Avec l'abandon de Java (liée à la politique d'Oracle en matière de licence) nous perdons aussi l'excellent serveur d'application Tomcat produit sous licence "libre". Il a donc été décidé de créer une infrastructure C++ sur la base des librairies QT pour créer un serveur offrant les fonctionnalités d'un serveur d'application. Ce serveur n'a pas pour seule destination de retourner des pages Web construites à la demande. Il peut être utilisé comme fournisseur de services divers (téléchargements, interrogation d'une base de données, etc.). Le protocole HTTP ne sert ici qu'au transport des requêtes et des réponses. Au final, la réponse peut être d'une nature quelconque elle est simplement encapsulée par le protocole HTTP pour son transport.

Organisation et définitions.

Qu'est-ce qu'un serveur d'application ?

Nous simplifierons ici la définition d'un serveur d'application en posant qu'il s'agit d'un serveur HTTP dédié à la mise en ligne d'applications. La grande différence avec un site Web traditionnel est double :

  1. le contenu des pages dépend des interactions avec l'utilisateur et n'est donc quasilent jamais connu à l'avance 
  2. il faut mettre en oeuvre un mécanisme de suivi de session. Les RFC et le standard HTML ne définissent pas ce mécanisme. De façon basique, le serveur envoi une réponse en réaction à l'URL (requête) reçu mais ne conserve pas de trace en mémoire de cet échange. Ce fonctionnement de base interdit la notion de "dialogue" qui repose, par définition, sur un jeu de questions/réponse à condition d'identifier de façon non univoque l'utilisateur final. C'est ce mécanisme d'identification de l'utilisateur final qui est appelé "suivi de session". Netscape fut l'une des premières sociétés à vouloir transformer un serveur HTTP en serveur d'application. Pour résoudre le problème du suivi de session, elle a imaginé et introduit dans ses navigateurs les cookies standardisés et améliorés depuis (ils sont gérés désormais par absolument tous les navigateurs existants). Le cookie est un jeu de propriétés dont au moins l'une d'entre elles contient une valeur unique attribuée par le serveur et qui sert à identfier l'utilisateur final. Le cookie peut être conservé dans la mémoire du navigateur et perdu lorsque ce dernier est fermé ou conserver dans un peit fichier texte qui permet de reprendre toute ou partie de la session lors de la réouverture du navigateur avec interrogtaion du site qui a produit le cookie. Il existe cependant d'autres mécanismes permettant d'assurer le suivi de session.

Principe de la commutation d'applications.

Un serveur d'application peut supporter plusieurs applications. Pour cela, il a besoin de distinguer les différentes applications invoquées par l'utilisateur. Or la seule chose qu'il reçoit est un URL. C'est donc via l'URL que la distinction entre deux applications doit être faite.

Chaque URL dispose d'un chemin et c'est ce dernier qui joue le rôle de commutateur d'application. Dans le vocabulaire Qt, cette commutation est appelée "routage". Un article est dédié sur la manière dont notre infrastructure met en oeuvre ce routage. Un chemin ou partie de chemin qui joue le rôle de commutateur est appelé "contexte". On peut par exemple associer un contexte différent à chaque application supportée par le serveur.

Analyse de l'environnement actuel.

Tout ce qui a été écrit jusqu'à présent se trouve être la base de la plupart des serveurs d'appliction. Le serveur reçoit un URL et doit envoyer une réponse, soit sous forme d'un document HTML, soit sous forme de l'envoi d'un document d'un type MIME donné. Ce sont les en-têtes HTTP reçues par le navigateur qui lui permettent d'identifier le type MIME du document reçu. Chaque éditeur de serveur d'application est alors libre d'imaginer l'infrastructure sous-jacente pour élaborer puis émettre cette réponse. Pour notre part, nous avons constater plusieurs points :

  1. l'interface graphique doit évoluer pour répondre aux effets de mode et conserver ainsi son attractivité ;
  2. les fonctionnalités évoluent nettement plus lentement et portent généralement sur l'ajout de nouvelles fonctionnalités plutôt que sur la modification de celles existantantes. Ce sont plutôt les interactions des nouvelles fonctionnalités avec les existantes qui imposent la modification de ces dernières ;
  3. Il est nettement plus difficile de modéliser une application web qu'une application graphique traditionnelle. Cela est lié pour une bonne part à l'absence du support offert par le système d'exploitation. Via la gestion de sa boucle évènementielle, une application graphique traditionnelle "cadre" le domaine d'action de l'utilisateur. Cette boucle n'existe pas avec le navigateur : on ne peut pas associer à chaque action potentielle de l'utilisateur un "évènement" et son gestionnaire associé. La meilleure preuve en est la volonté du langage JavaScript d'introduire cette notion au sein des navigateurs modernes. Cette capacité est forcément limitée dans la mesure où chaque éditeur de navigateur est libre dans sa façon de réaliser sa pseudo boucle évènementielle et que le code JavaScript est transporté par la réponse. Ce transport rend difficile un volume de code de script trop important qui aurait un impact sur les temps de réponse. En outre, la lecture aisée de ces scripts pourrait dévoiler des informations de traitement "sensible". Tout ceci interdit, dans la pratique, la réalisation de code complexe ou faisant appel à des ressources distantes (une des raisons de la création des websocket).

La volonté des éditeurs à transformer leurs navigateurs en pseudo système d'exploitation est louable mais dangereuse aux plans de l'économie et de la sécurité.

Du point de vue de la sécurité, comme il n'existe qu'une dizaine de navigateurs répandus, trouver leurs failles, c'est mettre en danger des milliers d'applications (cf. analyse des journaux des serveurs HTTP pour s'en convaincre).

D'un point de vue économique c'est aller à contre-courant de la reprise par le navigateur et son serveur de l'architecture centralisée de type mainframe concentrant et donc diminuant le coût des "experts" (cf. coûts comparés entre client-serveur multi-niveau et mainframe dans les années 80). De plus, la mise au point et en sécurité des documents lourdement chargés en scripts ainsi que leur reprise pour évolution et nettement plus coûteuse que celle du code côté serveur. En gros, on déploie des efforts conséquents côté infrastructure pour rationaliser le code en vue de son évolution et de sa réutilisation mais on fait pratiquement l'inverse côté navigateur : il y a là une forme d'incohérence.

Concepts de base.

Fort de ces constats nous tirons les règles suivantes :

  1. le découplage entre la présentation et le fonctionnement doit être le plus important possible. l'idée est de conserver à chaque composant son efficience maximum : au navigateur une présentation soignée, une ergonomie agréable et un suivi de la mode web. Cela n'interdit aucunement l'usage de script et de fonctionnalités spécifiques, bien au contraire, tant qu'elles n'entrent pas ou n'interfèrent pas dans le processus de réalisation de la réponse par le serveur. Au serveur un découplage optimal des fonctionnalités avec un maximum de réutilisation de code ce qui suppose une modélisation avancée des processus élaborant les réponses. Pour faciliter les choses, il faut imaginer devoir remplacer le navigateur par un terminal. Pour interagir avec l'utilisateur, au lieu de produire un document HTML, on produit un script interprétable dans un terminal (Python par exemple), script lui-même capable de saisies et d'envoi d'URL.
  2. Chaque réponse est produite par un "service" dédié. Autrement dit, à chaque URL correspond un "service". Comme le chemin est déjà utilisé pour commuter les applications, on doit considérer que chaque URL d'une même application dispose du même chemin (avec Qt et sa notion de routage, on pourrait envisager les choses différemment mais ce n'est pas notre choix). A partir de là, la commutation vers un service ne peut se faire que via un paramètre HTTP. Comme il y aura possibilité de chiffrer la partie "paramètres" de l'RUL, nous utiiserons le nom de paramètre srv pour indiquer le service à invoquer. Il faudra donc fournir au serveur un objet de commutation qui lui permette de savoir quel service invoqué. Pour reprendre l'image d'un terminal distant au lieu du navigateur, changer de type d'interface, c'est simplement changer le "service" qui produit la réponse.
  3. Il faut appliquer au développement d'une application le modèle de conception MVC (Modèle-Vue-Contrôleur). La "vue" est le document HTML produit par le service dédié. Le "modèle" est une tour à 3 étages : les données de session, les données de contexte (chemin de l'application) et les données globales (ou de configuration). Par "données", il faut entendre les données elles-mêmes et également les ressources partagées. Le "contrôleur" est le service qui lui même peut s'appuyer sur d'autres services en fonction de la requête et du contexte.

Le modèle de conception MVC facilite l'évolution d'une application. Pour aller plus loin, il est recommandé de distinguer les services de traitement non lié à une IHM des services de visualisation destiné à produire une IHM avec laquelle l'utilisateur interagit. Cela permet de concevoir et tester les service non IHM en premier. Ces services fournissent alors les spécifications à respecter par les services IHM. Comme l'évolution des applications portent prncipalement sur la présentation des IHM, il sera nettement plus facile de se concentrer sur ces derniers par la suite pour faire du "beau" et du "pratique".

ce paragraphe montre que toute application déportée via un serveur d'application répond à un schéma du type :

Concept d'un serveur d'application

Retour sur le modèle.

Pour permettre le fonctionnement d'une ou plusieurs applications, il est indispensable de pouvoir lire, ajouter, mettre à jour ou supprimer des données. Le schéma ci-dessus montre que ces données ne sont pas toutes du même niveau et qu'elles peuvent être regroupées selon leur niveau de visibilité par un service donné d'une application donnée.

Comme vu auparavant, le "modèle" est constitué de 3 niveaux et à chacun correspond un niveau de visibilité :

Retour sur les "services"

Dans les serveurs traditionnels, il existe la notion de service avec état (statefull) et celle de service sans état (stateless). Dans notre cas, la délégation de service à service rend délicate la gestion de services avec état. De plus, un service avec état doit être dupliqué en fonction de son environnement d'utilisation ce qui peut amener à consommer de la mémoire de façon excessive. Pour cette raison, nous avons fait le choix de services sans état. Les données nécessaires au service seront soit fournies par le "modèle" (généralement les données de session). Un autre avantage des services sans état et qu'ils ont une et une seule instance en mémoire. En cas de très fortes montées en charge, on pourrait envisager un pool par services dont le nombre d'instances dépendrait de la fréquence d'appel du service. Chaque service du pool s'exécuterait alors sur un thread dédié. Ce type de fonctionnment impose une infrastructure matérielle conséquente et n'est donc pas envisagé ici. Un service demandé simultanément mettre les demandes successives à la première en attente. Il est donc important que le temps d'exécution du service soit le plus bref possible.

Cycle de vie d'une session.

Chaque utilisateur débute une session au premier URL invoqué. La création d'une session et donc implicite. Il ne faut pas confondre authentification de l'utilisateur et session : la session permet de repérer l'utilisateur en ligne qu'il soit connecté ou non.

En revanche, une session peut être achevée explicitement (déconnexion explicite de l'utilisateur). Elle doit toujours pouvoir être fermée implicitement (expiration de la durée d'inactivité de la session). La session dispose d'une durée d'expiration car chacune d'elle consomme de la mémoire. Sans ce mécanisme, il arriverait forcément un moment où la mémoire s'avèrerait insuffisante. Certains serveurs proposent de "sérialiser" la session sur disque en ne laissant en mémoire qu'une amorce pour son rechargement. Ce mécanisme présente plusieurs avantages dont la résilience aux pannes et le transfert à un autre serveur en cas de forte montée en charge du serveur initial ou encore mécanisme de distribution d'un pool de serveur (type round robin par exemple). Mais ce mécanisme complexifie sérieusement le serveur d'application et peut avoir un impact significatif sur les temps de réponse. Il n'est dons pas mis en oeuvre ici.

A la place, nous préférons limiter le nombre de sessions actives pour une application donnée. Cette valeur est une donnée de configuration de l'application et une donnée de configuration du serveur. Dès qu'une nouvelle session à créer excède un des ses paramètres, la session est refusée. C'est généralement la pratique sur une infrastructure donnée qui permet d'ajuster au mieux ces paramètres.

Conclusion

Voici posées les bases de la réalisation de notre serveur d'application. Des choix structurants ont été faits et on peut voir qu'il s'agit pas de développer un serveur aisément paramétrable et utilisable.

Un point important non encore abordé est celui de la modélisation des applications web. Ce processus est complexe car le plus souvent itératif. Il est pratiquement impossible et certainement peu souhaitable de "penser" à l'avance toutes les fonctions d'une application. Il est sans doute plus judicieux d'accepter d'emblée qu'elle évolue avce le besoin et les capacités de réalisation. La notion de "service" le facilite grandement. Elle permet de modéliser la cinématique de départ et de dresser l'inventaire des URL d'appel des services. Si le serveur est bien réaliser, produire une application web revinet à 3 tâches :

De plus, cette façon de procéder permet de raccourcir le délai conception-test. Or les tests amènent non seulement à corriger les bugs mais aussi à rétro-concevoir le contenu des services voir certains srevices eux-mêmes. C'est pouquoi imaginer pouvoir "penser" à l'avance l'application dans son ensemble est une illusion... (qui a fait beaucoup de dégâts dans le passé). De même, imaginer que l'on peut se lancer dans le codage d'une application un peu complexe sans un minimum de recul via la modélisation est tout aussi illusoire... (dégâts actuels). Comme toujours, il s'agit d'une question d'équilibre.

Rédaction par Jean-Marie Piatte (1983-2021)