Sockets connectées - Domaine Internet

© D. Mathieu    mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique
Créé le 01/10/1999 - Dernière mise à jour : 29/11/2001

Remarques préliminaires :

Sommaire

Client multiservices TCP
ClientSockInStream()
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)
Serveur daytime TCP itératif et son client
ServeurSockInStream()
ServeurAccept()
exo_02s : ppal() du serveur
exo_02c : ppal() du client
Serveur echo TCP itératif et son client
Recv() : deuxième version
exo_03c : ppal() du client
exo_03s : ppal() du serveur
Multiplexage des E/S
exo_04s : ppal() du serveur
exo_04c : ppal() du client multiplexé
Serveur TCP concurrent multi-processus mono-protocole mono-service
exo_05s : ppal() du serveur
exo_05c : ppal() du client
Utilisation d'un processus esclave pour rendre un service
exo_06e : ppal() de l'esclave
exo_06s : ppal() du serveur
Serveur TCP concurrent mono-processus mono-protocole mono-service
exo_07s : ppal() du serveur


Client multiservices TCP

exo_01

ClientSockInStream()

    On rappelle qu'en mode connecté, un client doit toujours obtenir un descripteur de socket (appel de socket()) puis le connecter à une socket avant de pouvoir l'utiliser (appel de connect()). Comme cela a été fait pour les sockets Unix connectées (fonction ClientSockUnStream()), ajouter dans l'espace des noms nsNet (fichiers nsNet.h et nsNet.cxx), la fonction :
 
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.

exo_01c1 : ppal() d'un client daytime

    Recopier le fichier dirsidgram/exo_07c.cxx dans le fichier exo_01c1.cxx. Interroger le service daytime/tcp sur une machine passée en argument de la commande.

    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


ClientConnectSockIn() et ClientSockInStream() : seconde version

    La fonction connect() n'est bloquante pour une application que le temps que TCP établisse la connexion (dont on rappelle qu'elle se déroule en trois temps (voir : Etablissement de la connexion TCP). Il est fréquent que les serveurs ne ferment pas un service, mais qu'ils n'acceptent pas la connexion, probablement en ne renvoyant pas l'acquittement ACK du segment SYN. A l'autre extrémité, le module TCP arme un timer et considère que la connexion a échoué à expiration de ce temps. La fonction connect() renvoie alors la valeur -1, et errno est positionné à ETIMEOUT. Le seul problème est que la valeur du délai d'attente est très élevé sur notre machine (probablement plus de 10 minutes) et qu'il est impossible de le modifier (c'est un paramètre certainement modifiable dans le noyau, mais qui ne nous est pas accessible). En conséquence, le client est irrémédiablement bloqué dans l'appel de la fonction connect().

    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,
                          unsigned short NumPort,
                          unsigned TimeOut = 0) throw (CException);

    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.

RecvAll() : seconde version

    La fonction RecvAll() est bloquante si le producteur ne referme pas la socket de communication et n'envoie pas la totalité de ce qu'attend le consommateur. Ce dernier cas est fréquent si le consommateur ne connaît pas a priori la longueur du message qu'il attend. C'est pour cela qu'une troisième possibilité de sortie "normale" est offerte par la fonction recv() : on rappelle que, contrairement à la plupart des fonctions systèmes, elle ne sort pas en erreur quand elle est interrompue par un signal (sous réserve que le flag MSG_WAITALL ait été positionné), mais renvoie le nombre d'octets qu'elle avait lus avant la réception du signal. Comme précédemment, on peut laisser à l'utilisateur le soin de positionner une alarme, et de gérer le signal SIGALRM, ou au contraire incorporer ce mécanisme à l'intérieur du wrapper RecvAll(). Nous allons mettre en place cette possibilité.

    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);

exo_01c2 : ppal() d'un client daytime sécurisé

    Recopier le fichier exo_01c1.cxx dans le fichier exo_01c2.cxx. Passer en argument de la commande le délai d'attente de connexion avant le délai d'attente de lecture. Ajouter un paramètre effectif à l'appel de la fonction ClientSockInStream(). Faire de même à l'appel de la fonction RecvAll(). Mettre en place les tests pour s'assurer qu'une éventuelle alarme préalablement armée est correctement restaurée si nécessaire, ainsi que le traitant du signal.

    Compiler et tester.

Corrigés : nsNet.h    -    nsNet.cxx    -    exo_01c2.cxx


exo_01c3 : ppal() d'un client multiservices (daytime et echo)

    Recopier le fichier exo_01c2.cxx dans le fichier exo_01c3.cxx. Supprimer tout ce qui concerne la gestion des alarmes par l'utilisateur, pour ne garder que les timeout passés en paramètres.

    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).

    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

Sommaire


Serveur daytime TCP itératif et son client

exo_02

ServeurSockInStream()

     A l'espace de noms nsNet (fichiers nsNet.h et nsNet.cxx), ajouter la fonction ServeurSockInStream(), analogue à la fonction ServeurSockUnStream() écrite précédemment, de profil :
 
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é.

ServeurAccept()

    A l'espace de noms nsNet (fichiers nsNet.h et nsNet.hxx), ajouter la fonction ServeurAccept(), de profil :
 
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).

exo_02s : ppal() du serveur

    Copier le fichier dirsidgram/exo_09s.cxx dans le fichier exo_02s.cxx. Faire les modifications nécessaires pour l'adapter aux sockets "streams". Si le numéro de port n'est pas passé en argument, il est donné arbitrairement par le système.

exo_02c : ppal() du client

    Recopier exo_01c3.cxx dans exo_02c.cxx. Eliminer tout ce qui concerne le service echo et le modifier pour qu'il lise le numéro de port en argument.

    Tester.

Corrigés : nsNet.h    -    nsNet.hxx    -    nsNet.cxx    -    exo_02s.cxx    -    exo_02c.cxx

Sommaire


Serveur echo TCP itératif et son client

exo_03

Recv() : deuxième version

    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.

exo_03c : ppal() du client

    Recopier exo_01c3.cxx dans exo_03c.cxx et le simplifier en ne conservant que l'interrogation du service echo (celui du serveur exo_03s) dont il récupérera le numéro de port en argument de la commande.

    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 ?

exo_03s : ppal() du serveur

    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

Sommaire


Multiplexage des E/S

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.

exo_04s : ppal() du serveur

    Recopier le fichier exo_03s.cxx dans le fichier exo_04s.cxx.

    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).

exo_04c : ppal() du client multiplexé

    Recopier le fichier exo_03c.cxx dans le fichier exo_04c.cxx.

    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()) :

    N'utilisant plus la fonction RecvAll(), le client n'a plus à armer une alarme pour la lecture. Une alarme pourrait être utilisée dans la fonction Select(), mais il ne correspondrait qu'à une attente au clavier. En effet, il est impossible de savoir si le client est en attente du serveur, même s'il lui a envoyé quelque chose : il peut avoir envoyé une suite d'espaces ! Le seul cas de timeout détectable serait celui où le client a fermé la socket en écriture, et où il attend la fermeture de la socket par le serveur.

    Tester. Vous pouvez aussi tester votre client avec le port 61001.

Corrigés : exo_04s.cxx    -    exo_04c.cxx

Sommaire


Serveur TCP concurrent multi-processus mono-protocole mono-service

exo_05

exo_05s : ppal() du serveur

    Recopier le fichier exo_03s.cxx dans le fichier exo_05s.cxx. Modifier le serveur pour qu'à chaque connexion, il crée un processus fils (Fork()) puis retourne en attente de connexion. Pour alléger la programmation, on ne mettra pas de nettoyeur de zombies en rappelant qu'il est inutile sous Linux, à condition que le signal SIGCHLD soit ignoré. Cette solution peut ne pas être portable sur d'autres plateformes Unix ou Unix-like.

    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.

exo_05c : ppal() du client

    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

Sommaire


Utilisation d'un processus esclave pour rendre un service

exo_06

exo_06e : ppal() de l'esclave

     Dans le fichier exo_06e.cxx, le corps du processus esclave est très simple : il écrit à l'écran caractère/caractère (cout.put(c)) ce qu'il lit au clavier caractère/caractère (cin.get(c)). Ajouter une alarme sur la lecture, dont la valeur est passée en argument de la commande. En cas de dépassement de délai, afficher un message dans le flux cerr.

     Compiler et tester.

exo_06s : ppal() du serveur

    Recopier le fichier exo_05s.cxx dans le fichier exo_06s.cxx. Remplacer le corps du processus fils par un appel à une fonction système exec...(), après avoir redirigé les E/S standard sur la socket de communication et fermé tous les descripteurs inutiles.

     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

Sommaire


Serveur TCP concurrent mono-processus mono-protocole mono-service

exo_07

exo_07s : ppal() du serveur

    Le service echo étant un service rapide, il est inutile de mobiliser un processus pour assurer les échanges avec un client donné. Recopier le fichier exo_05s.cxx dans le fichier exo_07s.cxx. Modifier le serveur d'echo de la façon suivante : il crée une socket de connexion, l'ajoute à un masque de lecture et se met en attente d'un événement sur ce masque (select()). Lorsqu'un événement le fait sortir du select(), il l'analyse. Si c'est une demande de connexion, il l'accepte et ajoute la socket de communication dans le masque de lecture. Si c'est un événement sur l'une des sockets de communication déjà positionnées dans le masque, il rend le service. Puis il retourne en attente d'événement par l'appel à la fonction select().

     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

Sommaire


[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