SE205: Travaux Pratiques sur la Programmation de Sockets BSD

Index


1 TP BSD sockets C et Java


1.1 Introduction

L’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.


1.2 Communication client/serveur en C

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.


1.2.1 Le serveur TCP en C

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.

  1. Après l’avoir modifié, vous vérifierez son fonctionnement en l’interrogeant à l’aide de la commande telnet.

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 :


1.2.2 Le client TCP en C

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 ... ... 
  1. Faire interroger simultanément le serveur par plusieurs clients situés sur des machines différentes, ceci pour en vérifier le fonctionnement parallèle.
  2. Dans l’exemple proposé pour le serveur, le signal SIGCHLD n’est pas géré. La commande ps permettra de voir les zombies résultant de la terminaison des processus fils du serveur.
  3. Utiliser la commande netstat ou netstat -P tcp -f inet pour tracer les communications entre clients et serveur. Observer les autres communications entrantes et sortantes.

1.3 Utilisation de select

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.

  1. Compléter les programmes serveur et client : voir les lignes où se trouvent des .... .
  2. Lancer le serveur, vérifier qu’il fait bien l’écho de ce qui est frappé au clavier
  3. Lancer un, puis plusieurs clients et vérifier que le serveur a bien reçu les datagrammes émis par les clients,

1.4 Un serveur TCP Java multiplexé

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 :

  1. Lancer Serv_Mux, constater qu’il fait bien l’écho de ce qu’on lui entre au clavier.
  2. Voir les threads de type Thread_Standard se terminer les uns après les autres.
  3. Interroger maintenant le serveur avec un client telnet lancé depuis une autre fenêtre et ... vérifier que le serveur ne répond pas !!!
  4. Compléter le code de Serv_TCP pour qu’il réponde à un client telnet. (Note: les lignes à modifier sont repérées par : // MODIF A FAIRE)
  5. Interroger ce serveur modifié avec un client telnet : il doit répondre par un écho du texte donné à telnet.

On va maintenant ajouter une écoute sur un port UDP. Pour ce faire, on utilisera un serveur UDP qui attend des messages sur le port 6666 et renvoie la date à l’émetteur.

Récupérer le fichier : Serv_UDP.java

  1. Modifier Serv_Mux.java pour mettre ce serveur UDP dans le tableau des threads créés.
  2. Relancer Serv_Mux modifié. Il écoute ainsi sur le clavier, des ports TCP et un port UDP.
  3. Pour tester le serveur UDP, utiliser le client UDP écrit en C, lancé depuis une autre machine, on lui fait envoyer des datagrammes sur le port 6666.
  4. Arrêter Serv_Mux.
  5. Modifier ainsi le serveur UDP :

    Il renvoie toujours le datagramme reçu sur la machine émettrice, mais sur le port 12000.

  6. Vérifier le bon fonctionnement de l’application :
    1. Relancer Serv_Mux ainsi modifié
    2. Lancer alors le serveur C utilisant select, sur la même machine que le client UDP, en lui demandant d’attendre sur le port 12000.
    3. On doit constater que le message envoyé par le client UDP-C réveille le serveur UDP-Java qui envoie alors la date vers le serveur UDP-C 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.


1.5 Synchronisation

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.

  1. Créer l’objet de synchronisation et ajouter les appels à ses méthodes dans main et les threads standards. Vérifier que main est bien débloqué par la fin d’un thread standard dès qu’elle arrive.