Master 1 Informatique Programmation Répartie et Architecture N Tiers
 

TD-TP - n° 2 : Programmation Distribuée en Java avec RMI

Durée: 3H

 
Denis Caromel, Brian Amedro
Université Nice-Sophia Antipolis, Département Informatique

1. RPC en Java avec RMI

1.1. Hello world !

Pour cette séance, commencez par récupérer l'archive td02.zip. Notre Hello World se trouve dans le répertoire 1-helloworld. Trois fichiers s'y trouvent :

  1. Utilisez la commande rmic pour générer le stub client et le squelette serveur. Vous pouvez utiliser l'option -keep pour conserver le code .java généré. L'option -v1.2 permet de générer le stub client au format RMI 1.2 qui rend le squelette serveur obsolète. L'option -v1.1 permet de générer à la fois le code du stub et du skeleton

  2. Regarder et comprendre le code. Quel est le rôle de chaque fichier, comparer aux différents fichiers de l'exemple rdict en RPC.

  3. Quel est le rôle de la chaîne private final static String REGISTRY_ENTRY_ID = "HelloServer"; dans le fichier HelloServer.java

  4. Modifier le code de façon à ajouter dans la string retournée, votre nom, pour être certain de parler au bon serveur. Compiler puis exécuter le programme

  5. Que se passe-t-il si plusieurs d'entre vous lancent le serveur sur la même machine ? Comment résoudre le problème ?

  6. Quelle est la difference entre Naming.bind() et Naming.rebind()? Quel probleme peut-on rencontrer avec Naming.bind()

  7. Est-il possible d'enregister un serveur sur un registry distant? pourquoi?

  8. Que se passe-t-il si vous enlevez le commentaire de la ligne //System.setSecurityManager(new java.rmi.RMISecurityManager()); dans le fichier HelloServer.java et HelloClient.java. Comment résoudre le problème ?

1.2. La mécanique de RMI

Dans cette partie nous allons essayer de comprendre la stratégie de service des requêtes RMI par le runtime RMI. Notez que l'implémentation de RMI est liée à la JVM utilisée (elle n'est pas spécifiée).

  1. Modifier le code du client pour faire l'appel de la méthode sayHello en boucle (avec une pause entre deux appels). Afficher le nom du Thread réalisant l'appel de méthode.

  2. Modifier le code du serveur pour afficher le nom du Thread réalisant l'appel de le méthode sayHello. Ajouter aussi un compteur d'appels qui s'incremente à chaque appel de la méthode sayHello.

  3. Lancer un serveur et plusieurs clients connectés au même serveur. Observer. Que constatez-vous ? Quel problème peut se poser et comment le résoudre ?

  4. Ajouter des méthodes retournant des types complexes. Vous pouvez essayer de retourner une classe qui implémente une serialization particulière que vous écrirez.

2. Un Chat Server avec RMI

2.1. Présentation et installation

Allez dans le dossier 2-chatServer

Nous allons apprendre à écrire des interfaces remote pour rendre des objets existants accessibles à distance, utiliser les fonctions avancées de RMI et comprendre les problèmes de déploiement d'une application distribuée. Il est vivement conseillé de faire un schéma indiquant les relations entre les différents morceaux de l'application à l'éxécution.

2.1.1. Installation

Nous allons partir d'une application simple de dialogue en direct (chat en Anglais). Cette application est composée de trois packages :

Un package, qui n'est pas directement lié à l'application, sera utilisé pour permettre le téléchargement de code avec RMI.

Récupérez l'archive td02.zip et décompactez-la dans un répertoire de votre choix en utilisant la commande unzip. Vous obtenez un répertoire td02 qui contient, dans le répertoire src, les sous-répertoires :

Nom du répertoire contenu
chat/client/ Le package client de l'application de chat
chat/server/ Le package serveur de l'application de chat
chat/remote/ Le package des interfaces remote de l'application de chat
classServer/ Le package de code downloading qui implémente un serveur HTTP capable de répondre à des GET

2.1.2. Fonctionnement

Le principe est le suivant :

Le serveur de chat est une classe bien définie ServerImpl qui implémente une interface Server offrant un ensemble de services. Cette classe n'est pas accessible à distance.
Pour rendre le serveur accessible par des client distribués, on définie un objet Remote (RemoteServerImpl) qui expose l'interface RemoteServer. RemoteServerImpl instancie un ServerImpl et l'utilise directement.
Une fois le serveur RemoteServerImpl lancé sur une machine (host) donnée, il s'enregistre dans le RMIRegistry de cette machine.

Le client est aussi un objet Remote car il a besoin d'être contacté par le serveur. Il expose l'interface RemoteClient. Le client prend en paramètre de lancement l'URL du serveur auquel il doit se connecter.
Une fois lancé, il fait un lookup du serveur et obtient une référence distante sur ce serveur (un Stub) qui implémente l'interface RemoteServer. Cette interface définie une méthode logon qui permet au client de se faire connaître du serveur.

  public RemoteConnection logon(String name, RemoteClient c) throws java.rmi.RemoteException;

En regardant cette méthode de plus près on voit qu'elle prend en paramètre le nom du client et une référence Remote pour les call-back. Elle retourne une référence Remote sur une connexion qui permettra au client de parler au serveur. Cette interface Remote est RemoteConnection. Elle définie deux méthodes, l'une pour envoyer un message au serveur, l'autre pour permettre au client de se déconnecter.

Si l'explication ci-dessus n'est pas clair n'hésitez pas à poser des questions.

2.1.3. Description des fichiers disponibles

Liste des fichiers et leur utilisation

Nom du fichier rôle
Interfaces Communes
RemoteClient.java Interface remote du client (pour les call-back du serveur).
RemoteServer.java Interface remote du serveur que les clients récupèrent dans le RMIRegistry et qui permet de faire le logon
RemoteConnection.java Interface remote que le serveur retourne au client en résultat du logon et qui permet au client de communiquer avec le serveur. L'interface fournie est vide son écriture est l'objet d'une question ci-dessous.
Server
RemoteServerImpl.java Implémentation de l'interface RemoteServer basé sur un UnicastRemoteObject La méthode de logon est vide. Son implémentation est l'objet d'une question ci-dessous.
RemoteConnectionImpl.java Implémentation de l'interface RemoteConnection basé sur un UnicastRemoteObject. La classe fournie n'a que le constructeur. L'implémentation de l'interface RemoteConnection est l'objet d'une question ci-dessous.
Server.java Interface offrant des fonctionnalités d'un serveur de chat. Cette interface n'est pas remote et représente une vue locale du serveur.
ServerImpl.java Implémentation de l'interface Server. ServerImpl est le code réel du serveur qui gère les clients. Il est utilise par la class RemoteServerImpl.java
Client
ClientImpl.java Une implémentation de l'interface RemoteClient qui lit les messages sur System.in et affiche sur System.out

2.1.4. Compilation

Pour voir que tout fonctionne essayez de compiler les 3 packages puis utilisez rmic pour générer les Stub des 3 objets remote. Notez qu'il est encore impossible de tester le serveur avant d'écrire le code manquant (question suivante).

Il est important de comprendre que pour compiler, le package chat.server n'a besoin QUE des interfaces communes et des classes serveurs. De la même manière, le package chat.client n'a besoin QUE des interfaces communes et de la classe client. Au fonctionnement, le serveur aura besoin des Stub sur le client et le client aura besoin des Stub sur le serveur et la connexion. Ces classes peuvent être téléchargées automatiquement par le runtime RMI comme nous le verrons plus tard.

Chat Client

   cd ~/td02/2-chatServer
   export CLASSPATH=classes
   javac -d classes src/chat/remote/*.java
   javac -d classes src/chat/client/*.java
   rmic -d classes chat.client.ClientImpl

Chat Server

   cd ~/td02/2-chatServer
   export CLASSPATH=classes
   javac -d classes src/chat/remote/*.java
   javac -d classes src/chat/server/*.java
   rmic -d classes chat.server.RemoteServerImpl
   rmic -d classes chat.server.RemoteConnectionImpl

Class Server (notez que le serveur de classes ne sera utilisé que plus tard)

   cd ~/td02/2-chatServer
   export CLASSPATH=classes
   javac -d classes src/classserver/*.java

2.2. Ecriture d'une interface remote

2.2.1. Ecriture et implémentation de l'interface de Connexion

Comme explique ci-dessus, l'interface de connexion RemoteConnection qui permet au client de communiquer avec le serveur est vide. Le but de cette partie est d'écrire cette interface, d'écrire son implémentation et de modifier le code du client et du serveur pour l'utiliser.

Q
  1. Dans le package du serveur chat.remote, éditer l'interface vide RemoteConnection pour lui ajouter deux méthodes. Une qui envoie un message passé en paramètre au serveur, l'autre qui permet au client de se déconnecter du serveur (logoff)
  2. Dans le package chat.server, éditer la classe RemoteConnectionImpl pour implémenter les deux méthodes que vous avez définies.
  3. Toujours dans le package chat.server, éditer la classe RemoteServerImpl pour implémenter la méthode logon qui instancie une RemoteConnectionImpl et la retourne au client. On pourra aussi envoyer un message de bienvenue au client.
  4. Dans le package chat.client, éditer la classe Client et modifier la méthode startClient pour utiliser l'objet connexion afin de parler au serveur.

2.2.2. Test en partageant le classpath

Recompiler les packages client et serveur pour prendre en compte les changements effectués. Ne pas oublier d'utiliser rmic sur RemoteConnectionImpl car l'interface remote qu'il implémente a changé.

Pour chaque commande décrite ci-dessous il faut que la variable CLASSPATH pointe sur les classes du client et du serveur. Pour cela on utilisera : export CLASSPATH=classes

Lancement du rmiregistry
rmiregistry	
Lancement du serveur
cd ~/td02/2-chatServer
java -Djava.security.policy=java.policy \
  chat.server.RemoteServerImpl MonNomServer
Lancement d'un client
cd ~/td02/2-chatServer
java -Djava.security.policy=java.policy \
  chat.client.ClientImpl rmi://machine.unice.fr/MonNomServer Zorglub

2.3. Lancement de l'application en distribué

Afin de mettre en évidence les problèmes de déploiement d'une application distribuée, le serveur et le client ne doivent maintenant pas partager le même classpath (cas le plus courant dans la réalité). La conséquence est qu'il faut permettre a RMI de télécharger les classes dynamiquement. Pour cela nous devons lancer deux serveurs de classes qui sont de très simple HTTP serveurs capable de lire des fichiers classes depuis un répertoire donné. Suivez les étapes ci-dessous pour lancer l'application.

Q Observer ce qui se passe dans chaque fenêtre et expliquez. En particulier observez les deux fenêtres des ClassServer et expliquez qui télécharge quoi.
Q Passer un peut de temps à regarder le code du serveur. Essayer de bien comprendre la chaîne de responsabilité qui s'établie entre les classes RemoteServerImpl, RemoteConnectionImpl et ServerImpl. Combien d'instances de chaque classe sont présentes à un instant donné dans la JVM du serveur.

2.3.1. Séparation des fichiers class

Créez 2 répertoires (serverSide et clientSide). Déplacez dans le premier les fichiers .class nécessaires au serveur, et dans le second les fichiers .class nécessaires au client.

2.3.2. Lancement du rmiregistry

Dans un premier Shell assurez-vous que le classpath ne contient rien puis lancez le rmiregistry :

   export CLASSPATH=
   rmiregistry
	

2.3.3. Lancement du ClassServer du Server

Dans un deuxième Shell lancez le ClassServer en le pointant vers les classes du serveur. Le numéro de port choisi est arbitraire mais il doit correspondre à celui passé en paramètre au Server.

   cd ~/td02/2-chatServer
   export CLASSPATH=classes
   java classserver.ClassFileServer 3000 serverSide
	

2.3.4. Lancement du ClassServer des ChatClients

Dans un troisième Shell lancez le ClassServer en le pointant vers les classes du client. Le numéro de port choisi est arbitraire mais il doit correspondre à celui passé en paramètre au ChatClient

   cd ~/td02/2-chatServer
   export CLASSPATH=classes
   java classserver.ClassFileServer 4000 clientSide
	

2.3.5. Lancement du Server

Dans un quatrième Shell lancez le Server en lui indiquant quel fichier de police utiliser pour la sécurité et quel est le codebase, c'est a dire quel est l'URL du serveur HTTP capable de servir les classes nécessaire pour utiliser ce serveur. C'est le serveur que nous avons lance sur le port 3000. En paramètre il faut mettre le nom sous lequel le serveur va s'enregistrer dans le RMIRegistry.

   cd ~/td02/2-chatServer/serverSide
   export CLASSPATH=.
   java -Djava.security.policy=../java.policy \
        -Djava.rmi.server.codebase=http://machine.unice.fr:3000/ \
        chat.server.RemoteServerImpl MonNomServer
   

2.3.6. Lancement d'un Client

Dans un cinquième Shell lancez le ChatClient en lui indiquant quel fichier de police utiliser pour la sécurité et quel est le codebase, c'est a dire quel est l'URL du serveur HTTP capable de servir les classes nécessaire pour utiliser la partie remote de ce client. C'est le serveur que nous avons lance sur le port 4000. En paramètre il faut mettre l'URL du chatServer et le nom de logon du client.

   cd ~/td02/2-chatServer/clientSide
   export CLASSPATH=.
   java -Djava.security.policy=../java.policy \
        -Djava.rmi.server.codebase=http://machine.unice.fr:4000/ \
        chat.client.ClientImpl rmi://machine.unice.fr/MonNomServer Zorglub

2.3.7. Lancement d'un autre ChatClient

Lancez un autre client et essayez de communiquer en tapant des messages sur la console.

3. Compilation portable de programmes Java

Allez dans le dossier 3-ant

Nous allons voir un mécanisme portable pour la compilation de programmes Java. Pour cela nous allons utiliser ANT qui est l'equivalent de Make mais en pure Java.

Nous allons aussi en profiter pour organiser un peu nos projets avec des repertoires spécifiques pour chacun de nos fichiers. Créez donc quatre répertoires src, classes, lib et compile qui contiendront respectivement les sources, les classes compilées, les bibliothèques annexes et les fichiers de compilation pour votre projet.