Applications Réparties - Partie II: approche client/serveur par objets distribués

Cours Polytech'Nice, SI4, 2011-2012, 2012-2013, 2014, 2015, 2016

Sujet de TP1

Objectif/Contenu :


1. Objet Distant

La première étape va consister à écrire une interface distante et l'objet l'implémentant

  1. Écrivez une interface distante, Distante, ayant une méthode echo sans paramètres et sans valeur de retour
  2. Implémentez cette interface dans un objet ObjetDistant (faites lui afficher un message)
  3. Ajoutez une méthode main à votre objet permettant de créer une instance et l'enregistrer dans un rmiregistry. Cherchez ou attendez quelques explications pour savoir comment faire !
  4. Compilez ces deux classes en prenant soin de mettre les fichiers compilés dans le bon répertoire
  5. Vous n'avez pas besoin de générer le stub car ce sera normalement fait automatiquement (si >= JDK 1.5)

2. Le client

Pour écrire le client il n'est nécessaire de connaitre que l'interface distante. Il ne devra donc y avoir dans votre code que des références à Distante et jamais à ObjetDistant

  1. Créez une classe Client contenant une méthode main qui cherche l'objet distant dont la référence est passée en paramètre
  2. Écrivez le code permettant d'appeler cette méthode
  3. Compilez votre classe

3. Exécution en local

Nous allons maintenant tester ce petit programme. Pour simplifier un peu, tout se fera sur une unique machine. Ouvrez trois terminaux différents pour effectuer les manipulations suivantes en vous positionnant chaque fois dans le répertoire contenant les .class (ou restez dans l'IDE si vous voulez) :  

  1. Démarrez un rmiregistry
  2. Démarrez votre objet distant
  3. Démarrez votre client
  4. Dans quel terminal s'affiche le message? expliquez

4. Étude de la communication

Les communications (invocations de méthodes) en RMI sont dites synchrones et ce même sans retour (donc même si void). C'est ce que nous allons mettre en évidence.

  1. Dans la méthode echo de l'objet distant, mettez une instruction permettant d'attendre 5 secondes (Thread.sleep(...)).
  2. Du côté du client, faites afficher un message avant d'appeler echo et juste après cet appel
  3. Exécutez, que constatez-vous? Pourquoi?

5. Exécution "simpliste" en distribué, et problématique de la localisation des classes

Nous allons maintenant tester ce petit programme en utilisant deux machines. On fait l'hypothèse que ces 2 machines peuvent communiquer librement (sont sur le meme réseau) et que java est disponible sur cette autre machine (meme si pas forcément dans la meme version). Cet exercice ne doit pas (encore!) utiliser de SecurityManager, car ce sera l'objet du prochain TP. Attention, dans la suite, on parle du répertoire où se trouvent les .class, mais, si vous avez des packages, il faut comprendre "le répertoire correspondant au bon CLASSPATH, qui n'est pas forcément égal au répertoire où sont les fichiers .class";=)

  1. Transformez la valeur "localhost" utilisée jusqu'à présent pour désigner la machine faisant tourner le registry, en la remplacant par le nom en dur de la machine distante que vous pouvez utiliser sur le reseau local (ce peut être celle de votre voisin!).
  2. Nous allons ensuite séparer en deux nos classes: les classes utiles coté serveur, celles utiles coté client, en prenant soin de dupliquer celles utiles des 2 cotés. Transférer vers la machine distante les codes source de ce qui correspondra au serveur.
  3. Compilez les classes transférées sur la machine serveur
  4. Placez-vous dans le dossier contenant les .class. Démarrez votre rmiregistry (son classpath "implicite" est donc ce dossier: heureusement, sinon la suite ne fonctionnera pas! Voir Exercice 6 ci-après...) puis votre serveur sur la machine serveur.
  5. Démarrez le code client (tout devrait fonctionner!): via le lookup vers le registry sur la machine distante, il aura récupéré un stub dynamiquement qui lui permet de faire l'appel via le réseau

    Si l'expérience ne réussit pas à cause d'une résolution d'adresse IP erronée, (cad, l'adresse IP indiquée dans le stub reçu par le client est localhost et non l'adresse IP sur le réseau..., il semble possible, d'après des forums, de réparer le problème en démarrant la JVM qui fabrique ce stub (la JVM du serveur) avec l'option -Djava.rmi.server.hostname=xxx.xxx.xxx.xxx

  6. Placez-vous dans un dossier ne contenant pas les .class. Démarrez votre rmiregistry. Replacez-vous dans le dossier qui contient les .class. Puis démarrez votre serveur sur la machine serveur. Analysez en détail l'exception renvoyée (en voici une trace si vous n'avez pas pu réaliser la question (dans cet exemple, le serveur implante une interface nommée HelloWorld)) et proposez une explication (cad. quelle JVM a lancé l'exception ?) :

    [fbaude@bego]~/Cours/AppRep/HelloWorld% The RMI naming service is listening at 2001
    cd bin
    [fbaude@bego]~/Cours/AppRep/HelloWorld/bin% java HelloWorldServer
    Creation de l'objet serveur
    Referencement dans le registry..
    . On a l'acces au registry ecoutant sur 2001
    java.rmi.UnexpectedException: undeclared checked exception; nested exception is:
    java.lang.ClassNotFoundException: HelloWorld not found in gnu.gcj.runtime.SystemClassLoader{urls=[file:./], parent=gnu.gcj.runtime.ExtensionClassLoader{urls=[], parent=null}}
    at gnu.java.rmi.registry.RegistryImpl_Stub.rebind(libgcj.so.8rh)
    at HelloWorldServer.main(HelloWorldServer.java:23)
    Caused by: java.lang.ClassNotFoundException: HelloWorld not found in gnu.gcj.runtime.SystemClassLoader{urls=[file:./], parent=gnu.gcj.runtime.ExtensionClassLoader{urls=[], parent=null}}

    Explication: le rmi registry râle lors du (re)bind, car, ne réussit pas à charger le .class décrivant l'interface remote à partir de laquelle il pourra générer le stub

6. Génération explicite des stubs et skeletons

Avant Java2, les classes correspondant aux stubs doivent etre pre-générées et le skeleton est obligatoire. Il peut etre utile de savoir comment les générer si une des JVMs que vous utilisez correspond à cette ancienne version de Java (en effet, vous n'avez peut-etre pas envie de reinstaller java sur chaque machine utilisée par votre déploiement). En tout cas, pédagogiquement parlant, c'est intéressant de comprendre comment et pourquoi l'utilisateur (aujourd'hui, la JVM automatiquement) génère un stub.

  1. Utilisez la commande rmic après vous etre documenté. Sur quelle classe on applique rmic ? Vous testerez au minimum l'option v1.2, et, l'option v1.1 si elle fonctionne.
  2. Du côté du client, ces nouvelles classes sont-elles connues ? Lancez le rmiregistry et le serveur comme au début de la question 5 puis le client (sur l'autre machine): pourquoi obtenez-vous une exception de type ClassNotFoundException ? Dans le cas où les stubs sont donc générés à l'avance, il faut donc les transférer manuellement par copie de fichier, vers le client, à l'endroit correspondant à son CLASSPATH. Faites ce transfert et re-testez.

    Quelques explications : Quand le rmiregistry exécute l'opération de binding, il s'aperçoit qu'il y a dans son CLASSPATH un fichier .class correspondant au stub, et s'en sert (après chargement), indiquant dans l'objet stub généré que celui-ci a été généré grâce à ce fichier. Par symétrie, quand le client désérialise l'objet stub reçu, il recherche le fichier .class correspondant dans son propre CLASSPATH (donc localement pour lui). Si il le trouve, il est capable de le charger (via le class loader).

7. Passage de paramètres

Les méthodes de l'interface Remote doivent pouvoir prendre et rendre des paramètres...

  1. Ajoutez à l'interface remote une méthode prenant un int en paramètre et renvoyant en retour une instance d'une classe de votre cru, nomméee par exemple Resultat. Pensez à lui donner une méthode toString. Compléter les autres classes et utilisez cette nouvelle méthode dans le code du client.
  2. Pour éviter d'avoir des classes manquantes, assurez vous que côté client on a aussi Resultat.class. Testez. Il est possible que vous ayez cette exception du coté du client :

    On va maintenant declencher l'appel de la methode distante sayResultat renvoyant un objet Resultat.
    java.rmi.UnmarshalException: error unmarshalling return; nested exception is:
    java.io.InvalidClassException: Resultat; Resultat; class invalid for deserialization
    at sun.rmi.server.UnicastRef.invoke(Unknown Source)
    at java.rmi.server.RemoteObjectInvocationHandler.invokeRemoteMethod(Unknown Source)
    at java.rmi.server.RemoteObjectInvocationHandler.invoke(Unknown Source)
    at $Proxy0.sayResultat(Unknown Source) // sayResultat est le nom de ma méthode Remote utilisé par mon HelloWorldClient
    at HelloWorldClient.main(HelloWorldClient.java:36)
    Caused by: java.io.InvalidClassException: Resultat; Resultat; class invalid for deserialization
    at java.io.ObjectStreamClass.checkDeserialize(Unknown Source)
    at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)

    En effet, le serveur envoie vers la JVM cliente une copie de l'objet instance de Resultat. Pour que cela fonctionne, cad pour que coté client on récupère bien une copie de cette instance, il faut que l'objet puisse avoir été mis à plat sous forme d'un flot d'octets puis reconstitué (re-instanciated) dans la JVM de réception de ce flot; et donc, la classe dont il est instance doit être sérializable. Si tel n'est pas encore le cas, rendez Resultat serializable, recompilez et re-testez.


Page maintenue par Francoise Baude @2011-