Remarques préliminaires :
ClientSockInStream()Serveur daytime TCP itératif et son client
exo_01c1 : ppal() d'un client daytime
ClientConnectSockIn() et ClientSockInStream() : seconde version
RecvAll() : seconde version
exo_01c2 : ppal() d'un client daytime sécurisé
exo_01c3 : ppal() d'un client multiservices (daytime et echo)
ServeurSockInStream()Serveur echo TCP itératif et son client
ServeurAccept()
exo_02s : ppal() du serveur
exo_02c : ppal() du client
Recv() : deuxième versionMultiplexage des E/S
exo_03c : ppal() du client
exo_03s : ppal() du serveur
exo_04s : ppal() du serveurServeur TCP concurrent multi-processus mono-protocole mono-service
exo_04c : ppal() du client multiplexé
exo_05s : ppal() du serveurUtilisation d'un processus esclave pour rendre un service
exo_05c : ppal() du client
exo_06e : ppal() de l'esclaveServeur TCP concurrent mono-processus mono-protocole mono-service
exo_06s : ppal() du serveur
exo_07s : ppal() du serveur
exo_01
int ClientSockInStream (in_addr Addr, unsigned short Port) throw (CExcFctSyst); |
qui obtient un descripteur de socket Internet de type SOCK_STREAM en appelant la fonction Socket() écrite précédemment, la connecte à l'adresse qui lui est passée en paramètre au moyen de la fonction ClientConnectSockIn() écrite au TP 2, puis renvoie le descripteur de socket.
Remarques :
Tester sur plusieurs machines, comme par exemple cisco, cnam.fr, wanadoo.fr, etc. Attention : si vous n'avez pas de réponse au-delà du délai de lecture que vous avez indiqué, ne vous jetez pas (encore) par la fenêtre. Tuez simplement le processus (Ctrl C) et essayez une autre machine. Ce problème devrait être résolu ci-dessous.
Corrigés : nsNet.h - nsNet.cxx - exo_01c1.cxx
Pour résoudre ce problème, deux solutions s'offrent à nous :
Outre le fait que la seconde solution est beaucoup plus simple à mettre en oeuvre pour l'utilisateur, qui n'aura pas à répéter l'ensemble des opérations à chaque demande de connexion, elle permet un traitement plus sophistiqué, que nous allons détailler.
La fonction ClientConnectSockIn(), lorsqu'elle est utilisée pour effectuer une pseudo-connexion, ne nécessite pas de délai. Cependant, elle est aussi appelée par la fonction ClientSockInStream(). Pour cette raison, un paramètre indiquant le délai d'attente de connexion doit leur être ajouté.
Lorsque le délai est expiré, une exception non système et non réseau (ni errno, ni h_errno ne sont positionnés) doit être levée. Nous avons le choix entre les classes CException et CExcFct. La même exception pouvant être levée par les deux fonctions, il est préférable de choisir la classe CException. Ces fonctions peuvent aussi lever une exception CExcFctNet si connect() échoue. La seconde étant une descendante de la première, il suffit que le profil indique la plus haute dans la hiérarchie, soit ici CException.
Dans le fichier nsNet.h, modifier les profils des deux fonctions de la façon suivante :
void ClientConnectSockIn (int sd, const ::in_addr & Adresse,
unsigned short Port, unsigned TimeOut = 0) throw (CException); int ClientSockInStream (const ::in_addr & Addr,
|
On remarquera que, le paramètre supplémentaire pouvant avoir une valeur par défaut, le profil précédemment employé est totalement compatible, et ne remet pas en cause les exercices faits dans les TPs précédents.
Dans le fichier nsNet.cxx, le corps de la fonction ClientConnectSockIn() doit être profondément modifié. Le principe est le suivant (attendre les précisions plus loin avant d'implémenter) :
Avant toutes ces modifications, il faut ajouter dans l'espace de noms anonyme du fichier nsNet.cxx :
Il est possible que le signal SIGALRM ait déjà été dérouté avant l'appel de ClientConnectSockIn(). Il faut donc récupérer l'ancien traitant, afin de le restaurer avant de ressortir de la fonction [1].
De même, il est possible que l'alarme ait été armée avant l'appel de ClientConnectSockIn(). Deux possibilités doivent donc être envisagées :
Si la fonction a pris en charge la gestion de l'alarme, il faut qu'elle restaure le traitant de signal et éventuellement qu'elle réarme l'alarme au temps qui restait, diminué du temps qui s'est écoulé quel que soit le mode de sortie ("normal" ou par une exception).
Attention : Il est possible que l'utilisateur ait dérouté plusieurs signaux avant d'appeler la fonction de connexion. Dans ce cas, connect() peut être interrompu, sans pour cela que le délai d'attente ait expiré. Il faut donc vérifier aussi que l'événement qui a fait échouer connect() est bien l'arrivée de SIGALRM.
Cette fonction présente quelques différences par rapport à ClientConnectSockIn() :
Contrairement à la fonction ClientConnectSockIn(), nous ne pouvons donc pas faire évoluer la précédente version du wrapper RecvAll() en conservant la compatibilité ascendante (c'est-à-dire en ne remettant pas en cause les exercices déjà écrits).
Dans les fichiers nsNet.h et nsNet.cxx, ajouter la fonction RecvAll() de profil :
size_t RecvAll (int socket, void * buffer, size_t length,
unsigned & TimeOut, int flags = 0) throw (CExcFctSystFile); |
Compiler et tester.
Corrigés : nsNet.h - nsNet.cxx - exo_01c2.cxx
Modifier ensuite dans ce dernier fichier le client de la façon suivante : il reçoit au lancement un argument supplémentaire : le nom du service qui doit être soit daytime, soit echo (à vérifier).
La boucle se termine par la "fin de fichier" clavier (Ctrl+D).
On considèrera que la connexion est en principe fermée par le serveur daytime lorsqu'il a envoyé la date (ce qui n'est pas le cas de tous les serveurs, comme nous l'avons vu précédemment). Le client, qui ne sait pas a priori la taille du message envoyé par le serveur, attend donc de sa part que la socket de communication soit fermée pour considérer le service comme rendu.
Au contraire, le serveur echo ne peut pas savoir quand le client décide que l'échange est terminé. C'est donc à ce dernier que revient l'initiative de signaler que la communication est finie, en appelant la fonction shutdown(), dont différents wrappers Shutdown...() ont été étudiés au cours des TPs de système (modèle Producteurs/Consommateurs) : en principe, lorsque le client décide de ne plus envoyer de messages au serveur, il ferme la socket en écriture, se réservant de finir de lire les réponses du serveur jusqu'à ce que celui-ci ferme à son tour la socket. Cependant, dans le cas présent, le client a forcément reçu la totalité de ce qu'il a envoyé, puisque c'est la fonction RecvAlll() qui est utilisée. Il suffit donc, pour terminer le client, d'appeler le wrapper ShutdownRDWR().
Remarque : en cas de timeout lors de la lecture, on peut considérer que la communication a lieu d'être terminée et terminer la boucle.
Tester sur plusieurs machines.
Corrigés : exo_01c3.cxx
exo_02
int ServeurSockInStream (unsigned short & NumPort,
int backlog = SOMAXCONN) throw (CExcFctSyst); |
qui :
Les deux premières opérations sont réalisées au moyen de la fonction ServeurSockIn() écrite dans le TP "Sockets Internet en mode non connecté".
Si NumPort est nul en entrée, il contient en sortie la valeur attribuée par le système, sinon il est inchangé.
int ServeurAccept (int sd, sockaddr_in & Adresse) throw (CExcFctSystFile); |
Remarque : il est toujours possible d'appeler la fonction ServeurAccept(sd) si nécessaire (si l'adresse de l'expéditeur n'est pas recherchée).
Tester.
Corrigés : nsNet.h - nsNet.hxx - nsNet.cxx - exo_02s.cxx - exo_02c.cxx
exo_03
Un serveur qui échange des informations en mode connecté est confronté aux mêmes problèmes qu'un client : il peut être bloqué dans une lecture si le client ne lui envoie rien. Malheureusement, même pour un service aussi simple qu'echo, il ne peut jamais savoir quel est le nombre d'octets qu'il doit lire. Il ne peut donc pas utiliser le wrapper RecvAll() étudié plus haut. Cependant, encore moins qu'un client, un serveur ne peut se faire bloquer dans une lecture, il serait trop vulnérable à l'attaque d'un client mal intentionné. Plusieurs solutions sont possibles, qui ne s'excluent d'ailleurs pas :
Contrairement à la fonction RecvAll() vue plus haut, nous ne pouvons pas positionner le flag MSG_WAITALL. La fonction recv() qui va être appelée est alors susceptible d'être interrompue par SIGALRM (ou tout autre signal) en renvoyant la valeur -1 et en positionnant errno = EINTR. Nous nous retrouvons alors dans le même cas que pour la fonction connect() (voir plus haut), et la fonction Recv() peut alors lever une exception CException de code d'erreur CstTimeOut.
Le paramètre TimeOut peut redevenir un paramètre-donnée, et avoir une valeur par défaut (= 0). Il peut donc être repassé en dernière position, et le nouveau profil de la fonction est complètement compatible avec l'ancien. Cependant, la fonction devient trop importante pour rester en inline et doit donc être déplacée dans le fichier nsNet.cxx.
Dans le fichier nsNet.h, ajouter le profil de la fonction Recv() suivante :
size_t Recv (int socket, void * buffer, size_t length,
int flags = 0, unsigned TimeOut = 0) throw (CException); |
Transférer le corps de la fonction du fichier nsNet.h dans le fichier nsNet.cxx. Faire les modifications nécessaires.
Le corps du programme du client est constitué d'une boucle comportant, outre une lecture au clavier :
On peut supposer ici que les délais d'écriture peuvent être négligés. En effet, ils ne prennent en compte que le temps de passer les données de l'application à TCP, mais en aucun cas l'acheminement, et surtout la délivrance des données au serveur. En revanche, il faut prendre en compte le délai d'attente de la réponse, car il inclut le délai d'acheminement des données et de la réponse, ainsi et surtout que le temps de réponse du serveur, d'où l'utilisation d'un timeout avec lecture par RecvAll().
Plusieurs événements sont susceptibles de perturber le bon fonctionnement du client :
Dans tous les cas, la boucle for doit être interrompue, mais les données éventuellement déjà reçues doivent être affichées.
La commande man 7 tcp indique de plus :
BUGS
Not all errors are documented.
En conséquence :
Faites plusieurs essais de votre client sur le port 61002. Le serveur correspondant est un peu fantaisiste et impatient : il fragmente de façon aléatoire les messages qu'il renvoie, et a un délai d'attente des messages de 10 secondes après qu'une connexion a été réalisée. Votre client tient-il le choc ?
Recopier le fichier exo_02s.cxx dans le fichier exo_03s.cxx. Soit le numéro de port est passé en argument au serveur lors de son lancement, soit il est attribué arbitrairement par le système.
Symétriquement au client, le corps du programme du serveur est constitué, après la connexion, d'une boucle comportant :
La terminaison normale du service intervient lorsque le client ferme la socket de communication (la fonction Read() renvoie 0).
Comme pour le client, la lecture est protégée par une alarme, et les causes de dysfonctionnement sont les mêmes. La totalité du service doit donc être placée dans un bloc try, et les erreurs centralisées dans le bloc catch correspondant.
Lorsque le service est terminé (normalement ou pas), le serveur doit retourner en attente d'une nouvelle demande de connexion.
Le déroulement du serveur peut lui aussi être affecté par l'arrivé du signal SIGPIPE. En particulier, la fonction accept() sur laquelle le serveur attend une nouvelle connexion peut être interrompue par le signal. Il y a donc lieu par exemple de bloquer le signal pendant cet appel.
Tester en essayant plusieurs délais de timeout, pour le client ou pour le serveur, et en lançant successivement plusieurs clients pour le même serveur.
Corrigés : exo_03s.cxx - exo_03c.cxx
exo_04
Le premier objectif de cet exercice est d'illustrer la notion de flux : les échanges se font de façon continue entre le client et le serveur, sans qu'aucun ne sâche s'il a reçu tout ce que son interlocuteur lui a envoyé. La notion de message n'a plus de sens, et la communication ne s'interrompt que lorsque chaque interlocuteur a signifié à son partenaire qu'il n'a plus rien à envoyer. Cela ne signifie cependant pas que les processus se terminent : le client doit finir de recevoir le reste des informations que devait lui renvoyer le serveur avant sa décison d'interrompre l'échange. Il peut alors se terminer.
Le second objectif est de rappeler l'utlisation de la fonction select() pour multiplexer des E/S. Cette fonction sera largement utilisée ultérierement pour développer des serveurs concurrents mono-processus.
Modifier le serveur pour qu'il renvoie la suite de caractères qu'il reçoit après avoir supprimé les espaces (mais pas les tabulations ou les passages à la ligne suivante).
Dès lors, le client ne connaît plus le nombre de caractères qu'il doit recevoir et ne peut plus utiliser la fonction ReadAll(). Il faut donc modifier profondément le client de la façon suivante : il est susceptible de recevoir des caractères au clavier et dans la socket. Il doit se mettre en attente de lecture sur ces deux descripteurs (appel à Select()). Lorsqu'il reçoit un ou deux événements, il les analyse (appel à FD_ISSET()) :
Tester. Vous pouvez aussi tester votre client avec le port 61001.
Corrigés : exo_04s.cxx - exo_04c.cxx
exo_05
Le père peut être simplifié car il ne risque plus d'être interrompu par SIGPIPE et n'est plus concerné par le timeout. Le numéro de port est soit passé en argument, soit donné arbitrairement par le système (il faut alors l'afficher).
Le fils rend le service echo puis se termine lorsque le client ferme sa socket en écriture.
Recopier le fichier exo_03c.cxx dans le fichier exo_05c.cxx. Le modifier pour qu'il puisse tester le serveur : le client doit émettre plusieurs fois (par exemple 5 fois) le même message comme "Emission toutes les xxx secondes" (voir par exemple : dirsidgram/exo_10c.cxx).
L'intervalle de temps entre chaque émission est passé en argument.
Tester avec plusieurs clients lancés simultanément à partir de plusieurs fenêtres.
Corrigés : exo_05s.cxx - exo_05c.cxx
exo_06
Compiler et tester.
Compiler et tester en utilisant par exemple le client exo_05c (qui peut être lancé plusieurs fois à partir de fenêtres différentes. Donner des délais différents pour constater tous les cas d'erreur (timeout ou déconnexion de l'un des deux interlocuteurs).
Corrigés : exo_06e.cxx - exo_06s.cxx
exo_07
Compte tenu de la taille du code pour un seul échange (plusieurs dizaines de lignes), il est plus élégant de le placer dans une fonction qui sera écrite dans l'espace de noms anonyme. Cependant, il importe de préserver la lisibilité de l'algorithme de haut niveau - la fonction ppal() - en particulier la partie concernant la gestion des socket descriptors, donc des masques.
La fonction du service doit se charger des échanges (lectures/écritures), afficher éventuellement des messages d'erreurs, mais en aucun cas modifier les masques. Elle doit donc fournir à la procédure appelante (ppal()) un "compte-rendu" de son déroulement, afin que cette dernière puisse agir en conséquence. C'est pourquoi il est conseillé d'en faire un prédicat (fonction renvoyant un booléen), qui pourrait être appelée EchoOK().
Le signal SIGPIPE peut être dérouté ou ignoré, les fonctions read()/write() renvoient -1 et positionnent errno à EPIPE si la socket a été fermée. Il est donc plus simple d'ignorer le signal.
Tester avec plusieurs clients exo_05c lancés à partir de fenêtres différentes.
Corrigés : exo_07s.cxx
[1] Il faudrait en réalité sauvegarder et restaurer non seulement le traitant de signal mais aussi le masque et les flags.
Cela nécessiterait une fonction analogue à SaveSig(), pour un seul signal.
[2] Un simple appel à Close() suffirait dans cet exercice, mais, pour préparer les exercices suivants, il est préférable de fermer dès que possible par Shutdown() les différents canaux de communication (lecture, écriture) avant de fermer la socket elle-même par Close().
© D. Mathieu
mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique