Réalisation d'une application réseau de télédiscussion

basée sur des sockets en mode connecté

T.D. "Systèmes distribués" 

Patrick.Itey@sophia.inria.fr


Eléments d'implémentation :

Objectif  du T.D. :

L'objectif de ce T.D est d'étudier des protocoles de synchronisation dans les systèmes distribués.
 

Présentation:

Il s'agit de programmer  un service de Télédiscussion : des personnes (situées sur des machines différentes) dialoguent entre elles par l'intermédiaire de leur clavier.

Cet exemple est basé sur une relation client-serveur avec des clients communiquant par l'intermédiaire du serveur, celui-ci maintenant une liste des connexions courantes.

Le fonctionnement de ce serveur est simple. Il envoie à tous les clients (enregistrés dans la liste de connexions) le message qu'il a reçu d'un client.

Tous les messages sont préfixés par le nom du client afin d'identifier les propos des différents interlocuteurs. Un client peut intervenir dans la discussion à tout instant, rester seulement "à l'écoute" ou terminer la discussion .

Cette application constitue une communication de groupes par télédiscussion.
 

Etude du modèle et des protocoles :

Le modèle et les protocoles à envisager  (voir cours de Le Than "systèmes distribués")  sont :


Travail à effectuer :

Il s'agit d'implémenter en Java cette application client-serveur de type télédiscussion.
Avec cet exemple complet,  outre la mise en pratique des mécanismes de synchronisation et de communication de groupes des systèmes distribués, vous aborderez un grand nombre d'aspects intéressants de la programmation réseau avec Java :


Notions à connaître :
Vous pourrez allez voir cette page de rappels rapides sur l'utilisation des sockets en Java.
Vous pourrez aussi vous aider des TDs Numero 2 et 3 sur  mon cours "programmation réseau en Java" (.ps.gz ou .pdf).
Pour l'utilisation des threads Java, vous pouvez jeter un coup d'oeil sur mon cours "multithreading en Java" (.ps.gz ou .pdf).
 
 

Eléments d'implémentation :
Identification et rôle des processus nécessaire à la télédiscussion
Un certain nombre de processus (threads) ont été identifiés pour mettre en oeuvre simplement cette application de télédiscussion (voir schéma ci-après).
Tous les processus (sauf l'application client) sont des instances de la classe java.lang.Thread.


Identification des données à manipuler


Les Processus à implanter :

Le constructeur de ce processus crée une instance de la classe java.net.ServerSocket pour gérer les demandes de connexions, une instance de la classe java.util.Vector pour stocker la liste des connexions et une instance du processus Nettoyeur. Le processus Serveur se met ensuite en attente d'une nouvelle connexion (méthode accept() de ServerSocket). A chaque nouvelle demande de connexion, il crée une instance du processus Connexion à partir du socket (java.net.Socket) que lui a renvoyé la méthode accept() et ajoute ce nouveau processus dans la liste de connexions, cet ajout étant protégé de l'exécution simultanée d'autres processus (utiliser synchronized). public class Server extends Thread {
    public Serveur() {
        try {... // Enregistrement du service }
        catch(IOException e) {...}
            ... // Création de la liste de connexions
            ... // Création du processus Nettoyeur
            this.start() ;
    }

    public void run() {
       try {
            while(true) {
                ... // Attente d'une demande
                ... // Création du processus Connexion
                ... // Ajout de ce processus dans la liste de connexions
                    // (en exclusion mutuelle)
            }
        } catch(IOException e) {...}
    }
}

  Le constructeur de cette classe reçoit plusieurs références en paramètres : Le constructeur récupère les canaux de lecture et d'écriture sur le socket (méthodes getInputStream() et getOutputStream() de java.net.Socket).

Ce processus (dans sa méthode run()) attend un message du client sur son socket. Lorsqu'elle en reçoit un, elle demande l'accès exclusif (synchronized) à la liste de connexions, puis écrit sur le socket de chaque connexion de cette liste le message qu'elle a reçu.

 
public class Connexion extends Thread {

    public Connexion(Socket client_sock, Serveur serveur, Nettoyeur nettoyeur) {
        ... // Récupération des canaux de lecture et d'écriture du socket
        this.start() ;
    }

    public void run() {
        try {
            while(true){
                ... // Attente d'une lecture d'un message sur le canal de lecture du socket
                ... // Envoyer le message à tous les clients de la liste (accès exclusif)
                ... // Si message "FIN" alors terminer ce processus
            }
        }
        catch(IOException e){...}
        finally {
            ... // Fermer le socket
            ... // Réveiller le processus Nettoyeur (accès exclusif)
        }
    }
}

Le nettoyeur est un processus dont le constructeur requiert une référence au processus Serveur pour l'accès à la liste des connexions. La méthode run() de ce processus est une boucle sans fin (l'arrêt du serveur entraînera l'arrêt du nettoyeur). Cette boucle fait attendre le nettoyeur 5 secondes (méthode wait() de java.lang.Thread), puis demande l'accès exclusif (synchronized) à la liste des connexions pour en vérifier l'activité (méthode isAlive() de java.lang.Thread). Une connexion inactivée est retirée de la liste (méthode elementAt() et removeElementAt() de java.util.Vector; attention au sens de parcours de cette liste). Une connexion est inactive si sa méthode run() a terminé son exécution.

La veille du processus Nettoyeur peut-être interrompue par une notification provenant d'un processus Connexion.

 
public class Nettoyeur extends Thread {

    public Nettoyeur(Serveur serveur){
        ...
        this.start();
    }

    public void run() {
        while(true) {
            ... // Attente de 5 secondes
            ... // Pour tous les processus Connexion de la liste des connexions
            ... // Tester si le processus est actif
            ... // Si oui, le retirer de la liste
        }
    }
}

Ce processus simple attend les messages provenant du socket et les affiche au fur et à mesure dans le champs de texte adéquat de la fenêtre graphique.

Son constructeur reçoit comme paramètres la référence du socket (pour récupérer son canal de lecture) et celle du champs de texte de l'application client pour y afficher le message en provenance de la connexion.

 
public class Ecouteur extends Thread {

    public Ecouteur(Socket s, TextArea out) {
        ... // Récupération du canal de lecture du socket
        this.start();
    }

    public void run() {
        try {
            while(true){
                ... // Lecture du message en provenance de la connexion
                ... // Si la connexion est terminée : break
                ... // Sinon, on affiche le message dans le champs de texte
            }
        }
        catch(IOException e) {...}
        finally {...}
    }
}

 

L'interface graphique (voir ci-dessus) de cette application est une fenêtre composée de 2 champs de texte : un en bas pour la saisie et un en haut pour l'affichage des messages. Cette fenêtre contient également 2 boutons Send et Fin. La classe AppliClient est une sous-classe de la classe java.awt.Frame. Le constructeur de l'application client demande la création d'un socket sur la machine sur laquelle fonctionne le serveur. Le processus Ecouteur est crée pour permettre la lecture sur le socket de manière asynchrone avec la saisie des messages de l'utilisateur.

L'appui sur le bouton Send provoque l'écriture sur le socket du texte que l'utilisateur a tapé (préfixé par le nom du client).

L'appui sur le bouton Fin signale la demande de fin de connexion de la part d'un client (message "FIN") puis arrête l'application client.

Ceci constitue naturellement un exemple de petite interface graphique destinée aux clients. Mais vous êtes libre de faire differemment ...

Bon courage !