2. On transforme le serveur TCP simple en un vrai serveur concourant
On va écrire une version serv.c du serveur TCP qui soit vraiment concourante.
En effet, en (1), pour simplifier l'écriture du premier serveur TCP en ne mélangeant pas tous les problèmes, on a mis le serveur en attente pendant que le fils gère la connexion d'un client.
Ceci ne permet pas de recevoir une nouvelle connexion pendant le traitement de la première.
Un vrai serveur TCP concourant doit, aussitôt après le fork() aller se remettre en attente sur le accept() pour créer un nouveau fils pour une nouvelle connexion qui pourrait arriver juste après la première, alors que le premier fils n'a pas encore terminé son dialogue avec son client.
Se pose alors le problème de la gestion de la terminaison de tous ces fils. Quand un sous-process fils meurt, il envoi un signal SIGCHLD à son process parent et celui-ci récupère le status de fin du fils dans le paramètre ad-hoc de l'appel wait() ou waitpid()
Que se passe-t-il si le serveur (le père) ne fait pas de wait() pour récupérer la fin des fils ? On peut écrire une version sernow.c de serv.c sans le waitpid() et observer le résultat :
1929 pts/2 00:00:00 sernow
1931 pts/2 00:00:00 sernow <defunct>
1933 pts/2 00:00:00 sernow <defunct>
Comme le père ne partage pas l'espace virtuel du fils, et ne fait pas de wait, il n'est pas informé de la fin de chaque fils. Ceux-ci restent <defunct> (zombie) dans le système jusqu'à la fin du serveur.
Si on construit un serveur de ce type, après la création d'un certain nombre de fils, le fork() renverra une erreur : il y a une limite système au nombre de sous-process qu'un même process peut créer (il y aussi une limite au nombre total de process dans la machine).
Pour un serveur destiné à rester actif très longtemps (même parfois en permanence), ce comportement est inacceptable.
Il va donc falloir traiter la terminaison des fils tout en allant AUSSI le plus vite possible dans le accept().
Une façon (ce n'est pas la seule : on peut en inventer d'autres à base de données partagées avec les fils et testées par le serveur), sera de mettre en place un "handler" de traitement du signal SIGCHLD envoyé au parent quand un fils se termine.
Dans le handler, on fera un waitpid() pour récupérer le status du fils et permettre ainsi à celui-ci de disparaitre du système en libérant ses ressources.
Nota :
On ajoutera une instruction sleep(1); dans la routine "traiterclient" du serveur fils, afin de simuler un dialogue un peu long entre client et serveur. Ceci est nécessaire pour vérifier que le traitement du signal SIGCHLD a été correctement programmée. Sinon, le fils termine avant que le serveur père ai repris la main et se soit remis dans le accept(), ce qui fait que l'on peut passer à côté du problème et avoir un programme qui "à l'air" de marcher, mais qui se plantera dans une situation réelle.
On voit donc que, si l'utilisation de TCP simplifie d'une certaine façon la gestion du dialogue client<-->serveur, elle amène d'autres contraintes.
avantages de TCP :
-
garantit la fiabilité des échanges client<-->serveur
-
établit une connexion client<-->serveur pendant laquelle le fils traitant sait qu'il dialogue toujours avec le même client
-
gère de façon naturelle "n" dialogues simultanés avec des clients différents.
inconvénients de TCP :
-
l'échange par "flux d'octet" impose l'implantation dans ce flux d'un protocole applicatif (reconstituer des messages pour construire un dialogue)
-
la gestion du serveur "maitre" est plus complexe