Aller au contenu principal

Sockets et client-serveur

Ce que ce chapitre apporte

  • Décrire l'architecture client-serveur et la répartition des rôles.
  • Définir un socket et nommer les éléments qui identifient une extrémité.
  • Comparer TCP et UDP et savoir lequel choisir.
  • Expliquer pourquoi un flux d'octets impose de délimiter les messages.
  • Décrire l'enchaînement des appels côté serveur et côté client.
Tout ce qui précède suppose une seule machine. Dès que deux programmes s'exécutent sur des machines différentes, il leur faut un moyen de se parler, et ce moyen n'est pas celui qu'on imagine : il ne transporte pas des messages, mais un flux d'octets sans frontières. Ce chapitre traite du premier point : l'architecture client-serveur, le socket, et la raison pour laquelle un flux d'octets oblige à délimiter soi-même ses messages.

L'architecture client-serveur

Définition

Dans une architecture client-serveur, un programme dit serveur attend des demandes et y répond ; un ou plusieurs programmes dits clients émettent ces demandes.

La dissymétrie porte sur l'initiative. Le serveur ne connaît pas ses clients à l'avance et ne les contacte jamais spontanément : il écoute et réagit. Le client connaît l'adresse du serveur et déclenche l'échange.

Cette répartition explique l'ordre des opérations. Le serveur doit être en écoute avant qu'un client ne se connecte, et il doit pouvoir traiter plusieurs clients, ce qui ramène aux threads du chapitre précédent.

Le socket

Définition

Un socket est le point d'accès par lequel un programme émet et reçoit des données sur un réseau. Une extrémité de communication est identifiée par une adresse IP, un numéro de port et un protocole de transport.

L'adresse désigne la machine, le port désigne le programme sur cette machine. Un serveur web occupe habituellement le port 443, un serveur de base de données un autre : c'est ce qui permet à une même machine d'héberger plusieurs services.

Une connexion établie se caractérise par quatre valeurs : adresse et port de la source, adresse et port de la destination. C'est ce qui permet à un serveur de distinguer deux clients venant de la même machine.

TCP et UDP

TCPUDP
Connexionétablie avant l'échangeaucune
Livraisongarantie, retransmission en cas de pertesans garantie
Ordrepréservénon garanti
Unitéflux d'octets continudatagrammes délimités
Coûtplus élevéminimal
Emploifichiers, requêtes, tout ce qui doit arriver entierflux audio ou vidéo, découverte, mesures
TCP transporte un flux, pas des messages
C'est le point que la plupart des programmes réseau ratent au début. TCP garantit que les octets arrivent dans l'ordre et sans perte. Il ne garantit pas qu'un envoi corresponde à une réception.
Deux envois consécutifs peuvent être reçus en une seule lecture, et un envoi de mille octets peut être reçu en trois morceaux. Le protocole applicatif doit donc porter lui-même la délimitation des messages.

Délimiter les messages

Trois techniques existent, et le choix a des conséquences.

Un délimiteur, souvent un saut de ligne, marque la fin d'un message. Simple, lisible, mais interdit au contenu de comporter ce caractère sans échappement.

Une longueur en préfixe annonce le nombre d'octets à lire. C'est la solution générale, qui accepte n'importe quel contenu binaire.

Une taille fixe convient quand tous les messages ont la même structure.

main.py
Sortie
>_ Prêt à exécuter…
Le défaut qui n'apparaît qu'en production
En local, les messages sont petits et la latence nulle : chaque envoi tient dans une lecture, et le programme paraît correct. Sur un réseau réel, avec des messages plus gros et des découpages imposés par la taille des paquets, les lectures partielles deviennent la règle.
Le symptôme classique est un message tronqué ou deux messages collés, apparaissant sous charge et jamais en test.

La séquence des appels

L'ordre est imposé par la nature de la connexion, et il se retient facilement.

Le serveur commence seul : il crée son socket, l'associe à une adresse et à un port, puis se met en écoute. Rien ne circule encore. Les échanges commencent quand un client se présente.

Diagramme de séquence
ClientServeurdemande de connexionconnexion acceptéeenvoyer les donnéesréponsefermer

L'appel qui accepte une connexion est bloquant : il attend qu'un client se présente. Il rend un nouveau socket, propre à cette connexion, tandis que le socket d'écoute reste disponible pour les suivants.

Le lien avec le chapitre précédent
Un serveur qui traite un client à la fois fait attendre tous les autres pendant la durée de l'échange. Pour servir plusieurs clients, il faut soit un thread par connexion, soit des entrées-sorties asynchrones.
Le thread par connexion est simple à écrire et ne passe pas l'échelle : quelques milliers de connexions coûtent quelques milliers de piles. Les serveurs à forte charge emploient l'asynchrone, où un petit nombre de threads gère un grand nombre de connexions en attente.

Ce que ce chapitre ne traite plus

Les deux sections qui suivaient portaient sur les conteneurs et sur la chaîne d'intégration continue. Elles ont été retirées le jour où l'atelier de développement a rejoint ce parcours, parce que chacune de ces notions y a désormais son chapitre entier :

Un résumé de dix lignes à côté d'un chapitre qui traite la même chose n'aide personne : il se périme sans qu'on le relise, et il donne au lecteur l'impression d'avoir fait le tour.

Exercices type

Pourquoi ne peut-on pas supposer qu'un envoi TCP correspond à une réception ?

Parce que TCP transporte un flux d'octets et non des messages. Il garantit l'ordre et l'absence de perte, rien de plus.

Le système regroupe ou découpe les envois selon la taille des paquets, la congestion et ses propres optimisations. Deux envois peuvent arriver dans une seule lecture, un envoi peut arriver en plusieurs.

Le protocole applicatif doit donc délimiter lui-même : par un caractère de fin, par une longueur en préfixe, ou par une taille fixe.

UDP se comporte différemment : un datagramme est reçu entier ou pas du tout, ce qui règle la délimitation mais supprime les garanties de livraison et d'ordre.

Un serveur de fichiers doit-il utiliser TCP ou UDP ?

TCP, sans hésitation.

Un fichier doit arriver entier, dans l'ordre et sans altération. UDP ne garantit aucun des trois : des datagrammes peuvent se perdre, arriver dans le désordre ou être dupliqués.

Employer UDP obligerait à réimplémenter les accusés de réception, la retransmission et le réordonnancement, c'est-à-dire à réécrire TCP, moins bien.

UDP se justifie quand la perte est préférable au retard : un flux vidéo continue en sautant une image, alors qu'attendre la retransmission produirait un gel.

Que se passe-t-il si un serveur traite les clients un par un ?

Chaque client attend que tous les précédents soient servis. Le débit s'effondre dès que les échanges durent, et un client lent bloque tous les autres.

La solution simple est un thread par connexion. Elle fonctionne jusqu'à quelques centaines de clients, puis le coût des piles et des commutations devient dominant.

Au-delà, on emploie des entrées-sorties asynchrones : un petit nombre de threads gère un grand nombre de connexions, chacune ne consommant un thread que pendant le traitement effectif, pas pendant l'attente.

C'est le même arbitrage que celui du chapitre sur les processus, entre simplicité d'écriture et passage à l'échelle.

Vérification

Vérification rapideon peut se reprendre

1.Un client envoie deux messages TCP à la suite. Que reçoit le serveur ?

2.Quelle technique de délimitation accepte n'importe quel contenu binaire ?

3.Qu'identifie le numéro de port d'une extrémité de communication ?

4.Que rend l'appel qui accepte une connexion côté serveur ?

La méthode

  1. Identifier qui prend l'initiative pour répartir les rôles client et serveur.
  2. Choisir TCP dès que le contenu doit arriver entier, UDP quand le retard coûte plus cher que la perte.
  3. Délimiter les messages dans le protocole applicatif, sans supposer qu'une lecture corresponde à un envoi.
  4. Prévoir la concurrence côté serveur avant d'écrire la boucle d'acceptation.
  5. Fermer les sockets, y compris sur le chemin d'erreur : un socket oublié retient un port.

Synthèse

  • Dans une architecture client-serveur, le serveur écoute et le client prend l'initiative.
  • Une extrémité se caractérise par adresse, port et protocole ; une connexion par les quatre valeurs des deux extrémités.
  • TCP : connexion, livraison garantie, ordre préservé, flux d'octets. UDP : sans connexion ni garantie, datagrammes délimités.
  • Un flux impose de délimiter les messages applicativement, par délimiteur, longueur en préfixe ou taille fixe.
  • L'appel qui accepte une connexion est bloquant et rend un socket propre à cette connexion.
  • Servir plusieurs clients suppose un thread par connexion ou des entrées-sorties asynchrones.
  • Le déploiement de ces serveurs et leur mise en ligne automatique sont traités par Conteneurs et images et L'intégration continue.

Mettre en pratique