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 |
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 :
Hello.java
HelloClient.java
HelloServer.java
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
Regarder et comprendre le code. Quel est le rôle de chaque fichier, comparer aux différents fichiers de l'exemple rdict
en RPC.
Quel est le rôle de la chaîne private final static String REGISTRY_ENTRY_ID = "HelloServer";
dans le fichier HelloServer.java
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
rmiregistry
Que se passe-t-il si plusieurs d'entre vous lancent le serveur sur la même machine ? Comment résoudre le problème ?
Quelle est la difference entre Naming.bind()
et
Naming.rebind()
? Quel probleme peut-on rencontrer avec Naming.bind()
Est-il possible d'enregister un serveur sur un registry distant? pourquoi?
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 ?
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).
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.
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
.
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 ?
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.
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.
Nous allons partir d'une application simple de dialogue en direct (chat en Anglais). Cette application est composée de trois packages :
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
|
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.
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
|
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
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 |
|
---|
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
rmiregistryLancement du serveur
cd ~/td02/2-chatServer java -Djava.security.policy=java.policy \ chat.server.RemoteServerImpl MonNomServerLancement d'un client
cd ~/td02/2-chatServer java -Djava.security.policy=java.policy \ chat.client.ClientImpl rmi://machine.unice.fr/MonNomServer Zorglub
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.
|
---|
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.
Dans un premier Shell assurez-vous que le classpath ne contient rien puis lancez le rmiregistry :
export CLASSPATH= rmiregistry
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
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
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
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
Lancez un autre client et essayez de communiquer en tapant des messages sur la console.
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.
ant.jar
xml-apis.jar
, pour le parsing du buildfile. Regardez rapidement la page http://ant.apache.org/manual/using.html
et récuperez le fichier HelloWorld.xml
et placez le dans ./compile/.
La ligne d'appel de ANT pouvant etre longue, il est utile de
fournir des scripts unix et windows qui le font pour nous. Récuperez le script build.sh
et placez le dans ./compile/.
Notez que grâce à la commande dirname(1)
, ce script peut être appelé de n'importe quel répertoire.