Jérôme Hugues (hugues@enst.fr)
avec l'aide de B. Dupouy.
BSD sockets C et JavaL'objectif de ce TP est de vous familiariser avec l'API sockets des
systèmes Unix d'une part, et du langage Java d'autre part.
Le modéle client/serveur est le canevas sur lequel sont construites de nombreuses applications distribuées.
Dans ce modéle, le serveur se contente d'attendre les demandes de connexion des clients distants sur un port qualifié de port d'écoute. La gestion de la communication avec chaque client est prise en charge par un nouveau processus fils.
Le serveur, libéré de toutes les tâches de communication, se remet en attente de demande de connexions émanant d'autres clients.
Schéma de principe.
Après fin de l'attente sur accept, le serveur créé un processus fils, ce dernier fermera le socket d'écoute, tandis que le serveur fermera celui de communications :
Le serveur se libère ainsi de la gestion des communications pour se consacrer à l'écoute des demandes de connexions. Le nombre de ces demandes en attente est borné par listen , le nombre de communications en parallèle étant, lui, limité par le nombre de processus que le serveur peut créer :
Remarque: la fonction socket ne fait qu'allouer des structures de données, mais n'affecte pas de numéro de port. Ce numéro de port est attribué de façon explicite lors du bind, ou de façon transparente lors du connect.
Ce serveur affiche à l'écran les messages émis par les clients en indiquant avec lequel de ses fils ils communiquent. A faire pour le serveur :
Voici le programme serveur: Serv_TCP.c.
Si le serveur est arrêté et relancé peu de temps après, il ne peut pas
reprendre le même port d'écoute. Pour pouvoir réutiliser immédiatement
ce port il faut utiliser la fonction setsockopt() (après création
de la socket et avant l'appel à bind()) : setsockopt(...,
SOL_SOCKET, SO_REUSEADDR, ... , ... ).
Note: Sur une machine "System V" (par exemple, sur Solaris), ajouter les options suivantes lors de la compilation :
-lsocket pour charger la bibliothèque des sockets
-lnsl pour charger les utilitaires htons...
Voici le code du client: Client_TCP.c. Vérifier que ce client transmet bien au serveur les informations qu'on lui a données en utilisant le clavier.
On peut aussi rediriger la sortie standard stdin (on suppose que
le fichier exécutable s'appelle Client_TCP) pour simuler une entrée
ls -il | Client_TCP ... ...
cat *.c | Client_TCP ... ...
man ls | Client_TCP ... ...
SIGCHLD
n'est pas géré. La commande ps permettra de voir les zombies résultant
de la terminaison des processus fils du serveur.
La fonction select() permet à une application de se mettre à
l'écoute sur plusieurs sources simultanément, c'est à dire de
multiplexer les entrées-sorties.
Nous allons étudier le fonctionnement d'un serveur utilisant
select(). Il attendra des entrées depuis le clavier et depuis
quelques ports UDP.
Pour avoir les détails du mécanisme de select, vous pouvez vous référer
à la page du manuel correspondant en utilisant la commande : man
-s 3c select
Pour comprendre le mécanisme de cet appel système, on va compléter le programme du client et du serveur dont on donne les squelettes ci-dessous.
Récuperez le programme serveur: Serv_UDP.c et le client Client_UDP.c.
Comme le serveur TCP pécédent, ce serveur UDP se contente d'afficher à l'écran les messages émis par des clients UDP.
Le client envoie périodiquement des datagrammes vers un destinataire dont on lui a passé l'adresse IP et le numéro de port sur la ligne de commande.
Le serveur que nous allons construire attend des entrées depuis : le clavier, un ou plusieurs ports TCP. Voici son fonctionnement :
L'utilisation des threads s'impose pour gérer les attentes simultanées sur ces différentes sources d'information.
Récupérer les trois fichiers : Serv_Mux.java, Serv_Clav.java, Serv_TCP.java.
Serv_Mux.java permet de lancer plusieurs threads :
Thread_Standard se terminer les uns
après les autres.
Serv_TCP pour qu'il réponde à un client
telnet. (Note: les lignes à modifier sont repérées par : // MODIF
A FAIRE)
On va maintenant ajouter une écoute sur un port UDP. Pour ce faire, on utilisera ce serveur UDP qui attend des messages sur le port 6666 et renvoie la date à l'émetteur.
Serv_Mux modifié. Il écoute ainsi sur le clavier, des
ports TCP et un port UDP.
Il renvoie toujours le datagramme reçu sur la machine émettrice, mais sur le port 12000.
Exemple de traces:
Trace produite par le client UDP-C :
Envoi de datagramme (Message emis par pid 7592 sur bajazet.enst.fr) vers ribouldingue
Trace produite par le serveur UDP-Java :
Serv_UDP-6666 va emettre : Tue Oct 02 16:45:29 GMT+0:00 2001 (taille : 33)
Trace produite par le serveur UDP-C :
(33) Tue Oct 02 16:45:39 GMT+0:00 2001
Remarque: Le programme Serv_Mux.java réalise l'équivalent de la
commande select.
Note: cet exercice est facultatif.
Dans le serveur TCP écrit en Java, on souhaite être averti de la terminaison d'un thread standard dès qu'elle se produit. Or, ce n'est pas le cas puisque join impose un ordre sur les réeceptions d'événement de fin des threads : vous avez sans doute constaté que main n'imprime pas de message lors de la fin d'un thread standard.
Pour résoudre le problème, on va créer un outil de synchronisation qui propose les méthodes Attendre et Debloquer (qui utilisent respectivement wait et notify : Attendre est appelée par main pour attendre la fin d'un thread standard quelconque;
Debloquer est appelée par un thread standard pour réveiller main.