Couches 7 : Application.
Protocoles de la couche 7
Nous pouvons désormais acheminer des messages de l'expéditeur au service destinataire. Cependant, il faut que les logiciels clients et serveurs parlent le même langage afin de pouvoir se comprendre. Par exemple, dans le cadre d'un serveur Web, définir comment un client demande la ressource à un URL donnée, et comment le serveur lui répondra. Il convient alors d'utiliser ou de définir un protocole de couche 7 : Application.
Il existe de très nombreux protocoles :
- FTP : transfers de fichiers ;
- VoIP : communications orales ;
- HTTP : pour les sites Web ;
- NFS : synchronisation de dossiers ;
- RDP : accès à distance ;
- ...
Le choix du protocole dépendra donc des besoins de l'application. Vous pouvez aussi facilement créer votre propre protocole ad hoc. Cependant, il convient de ne pas anticiper des besoins que vous n'avez pas, notamment de performances.
En effet, il vaut mieux un protocol clair, facile à utiliser et à déboguer, qu'un protocol optimisé qui vous fera perdre du temps de développement pour des performances dont vous n'avez pas besoin. En ce, il est préférable d'avoir des messages au format texte, faciles à lire par un humain, que des messages binaires plus optimisés, mais difficiles à traduire.
Définir un protocole de communication est assez simple en soit, il suffit de définir l'ensemble des types de messages qui peuvent être reçus ou envoyés ainsi que leur format. Comme en programmation, il convient d'adopter des conventions et de s'y tenir afin d'avoir une interface uniforme et prévisible. Par exemple, pour obtenir la taille d'un élément en programmation, vous pouvez avoir différents noms de propriétés/méthodes. Il convient d'en choisir une et de s'y tenir.
De même, il est préférable de factoriser les messages et d'en encourager la réutilisation. Notamment, en distinguant bien la transmission de l'information et sa présentation. La responsabilité du protocole de communication est la transmission des informations. La présentation, quant à elle, est la responsabilité du logiciel. Ainsi il est préférable de transmettre des données formatées, que le logiciel pourra utiliser pour divers usages. Aussi, cela rendra le protocole de communication plus stable, ne nécessitant pas d'évolutions lorsque l'usage et/ou la présentation change.
Idéalement, le serveur ne devrait pas avoir à maintenir un état de la communication, i.e. les messages doivent contenir l'ensemble des informations nécessaires à leur réponse. Cela a plusieurs avantages :
- reproductibilité: un même message produit la même réponse (facilite tests/debugs).
- cache: possibilité de précalculer ou de mettre en cache des réponses.
- DOS: protège de certaines attaques par déni de service (e.g. ouverture de connexions sans les fermer).
Plusieurs contraintes peuvent aussi se poser en fonction de l'application :
- asynchronisme : les messages ne suivent pas un modèle requête-réponse.
- latence : les messages doivent être transmis rapidement (e.g. VoIP).
- débit (throughput) : transmettre un large volume de données rapidement (e.g. fichiers, vidéos).
- synchronisation : envoyer l'ensemble de l'état (lourd) ou les changements (problèmes de désynchronisation) ?
Sauf cas particuliers, nous vous recommanderons d'utiliser par défaut une API REST HTTP (cf semestre 4).
Implémenter un serveur
En pratique, on peut dire que :
- Les couches 1&2 sont gérées par le matériel (carte réseau, divers).
- Les couches 3&4 sont gérées par l'OS.
- Les couches 5 à 7 sont gérées par l'application.
Ainsi, une application utilisera des fonctions fournies par l'OS afin d'envoyer et de recevoir des paquets TCP/IP ou UDP/IP. Sous Linux, tout est fichier, ainsi l'application lit/écrit dans un "fichier" appelé socket pour recevoir/envoyer des données.
💡 Pour des communications inter-processus (IPC), i.e. sur un même ordinateur, il est possible d'utiliser un socket unix (bidirectionnel) ou un pipe (unidirectionnel). Un named pipe (tube nommé ou FIFO), est un pipe auquel un chemin a été associé.
Bien évidemment, on utilise usuellement des bibliothèques afin d'exploiter les protocoles de couches 5 à 7 existant. Certains frameworks encapsulent la communication et permettent ainsi d'effectuer des opérations à distance de manière transparente. D'autres frameworks permettent de simplifier l'écriture d'un serveur en assurant certaines fonctionnalités avancées. Par exemple en associant un type de requête reçue à un gestionnaire (handler) qui se chargera de le traiter et d'y répondre.
Gestion de plusieurs clients
Une des principales difficultés des serveurs est de pouvoir traiter plusieurs clients en parallèle. On peut envisager de créer un processus par client, qui se chargera d'en lire et traiter les requêtes. Cependant cela est en pratique très peu efficace du fait des changements de contextes fréquents à chaque fois que le processus en cours d'exécution change.
Une meilleure manière de faire est d'utiliser un nombre fixe de processus. Dès lors, lorsqu'un message est reçu, il est mis sur une liste d'attente (queue) pour être ensuite traité lorsqu'un des processus devient disponible. On parle alors de tâches (work), et de workers.
Cependant, certaines opérations sont bloquantes, comme la lecture/écriture de fichiers. C'est à dire que le processus ne fera rien en attendant que la lecture/écriture s'effectue. Il est ainsi préférable de favoriser des opérations non-bloquantes (généralement asynchrones), c'est à dire qu'au lieu d'attendre que l'opération d'E/S se finisse, le processus traite une autre tâche.
Il est aussi possible de spécialiser les processus, i.e. au lieu d'avoir N processus généralistes qui font tout, d'avoir des processus dédiés à certaines opérations précises. Cela permet notamment de réduire les duplications de données en mémoire, et d'éventuels problèmes de synchronisations/accès concurrents.
Autres optimisations
Load balancing + reverse proxy
cache réponse / cache RAM
Dans tous ces cas, sans état + facile :
- cache
- load balancing