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

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

Sujet de TP2

Objectif/Contenu :


1. Serialization (rappels?)

Java RMI utilise intensivement la sérialisation. Un petit rappel s'impose !

  1. Reprenez la classe Résultat (qui est serializable) faite au TP1. Transformez un de ses attributs en transient (ce qui aura pour effet que la valeur de cet attribut ne sera pas copiée). Pour s'en convaincre, et après avoir recopié le nouveau .class sur le site du client si besoin, relancer l'appli RMI et constatez que le client n'aura pour cet attribut que la valeur par défaut correspondant au type de l'attribut (la valeur n'a donc pas été sérialisée).
  2. Créez une sous classe de Resultat. Celle ci sera sérializable par héritage. Rajoutez quelques attributs (dont des attributs private) et redéfinissez toString() (qui devra faire appel aussi à super.toString()). Faites en sorte que l'implémentation (et non l'interface Remote) de l'objet serveur renvoie en résultat un objet qui soit une instance de la sous classe de Resultat. Dans le constructeur de la sous classe, donnez une autre valeur à un des attributs hérités.
  3. Pour le moment, recopiez le .class de Resultat et de cette sous-classe sur le site du client si besoin, et relancer l'appli RMI (sans oublier le rmiregistry). Vérifiez que les valeurs recues sont celles attendues, cad, celles que vous avez envoyées depuis le serveur.
  4. Recommencez après avoir rendu Resultat non serializable. Il n'est pas certain que les valeurs recues soient toutes celles que vous aviez pensé avoir envoyé !(en particulier, celles données par la sous classe aux attributs hérités). Tentez de donner une explication; Plus précisément, si vous avez défini des constructeurs, sont-ils appelés lors de la désérialisation ?

2. Le téléchargement de classes

Pour éviter de devoir recopier certains des .class manuellement, la machine serveur utilisera un serveur de classes accessibles en HTTP pour transférer automatiquement les classes qu'on lui demande. La mise en place est l'objet de cet exercice. A réaliser, pour bien comprendre, depuis des fenêtres Terminal différentes.

Selon que vous soyez en Java6 ou 7, les options à passer aux différentes JVM vont différer voir ici . Plus précisément, en Java7, les JVMs qui doivent télécharger dynamiquement du code auprès du serveur de classes vont devoir utiliser une option de JVM ainsi: -Djava.rmi.server.useCodebaseOnly=false. Cela signifie qu'en plus de la base de code issue du classpath (système de fichiers local) ou d'autres URLs explicitement indiquées (via la propriété java.rmi.server.codebase), les URLs reçues dans les objets sérialisés transmis par RMI peuvent être contactées pour télécharger du code. Le chargement de code ne se fait donc plus uniquement dans les bases de code habituellement énumérées grâce au CLASSPATH.

Le but est de vous montrer jusqu'à quel point les différentes parties de l'application réparties (y compris le rmiregistry) peuvent récupérer les classes utiles par téléchargement, sans les avoir dans le(s) répertoire(s) indiqué(s) dans leur CLASSPATH

  1. Récupérez ici un serveur http. Il doit prendre au lancement 2 paramètres: un numéro de port (il sera lancé sur la machine serveur mais pourrait en théorie tourner sur n'importe quelle machine), et un nom de répertoire dans lequel il piochera les .class pour les envoyer (ou des .jar), suite aux futures demandes de chargement (de la part du rmiregistry, comme de la part du client comme nous allons le tester dans cet exercice). Compilez-le.
  2. Préparez un déploiement minimaliste pour le client, qui va donc devoir faire du téléchargement des classes qui lui manqueraient, automatiquement. Quelles sont ces classes minimum (sachant que le client fait d'abord un lookup pour récupérer un stub qu'il doit désérialiser, puis, fait un cast du stub pour pouvoir l'utiliser) ? En vue de ce chargement, le client doit s'assurer qu'il y a un RMISecurityManager (sinon, il doit en instancier un). Il doit donc aussi y avoir un fichier de policy Java, dont le contenu le plus basique (bien que trop permissif, voir TP3 pour plus de détails) est le suivant: grant { permission java.security.AllPermission; } ; . Créez ce fichier. Pour lancer le client, il faudra définir une property Java (-Djava.security.policy="emplacement du fichier de policy"). Ne pas lancer le client pour le moment...
  3. Pour permettre le téléchargement, tout se joue du coté du serveur: celui-ci utilisera la property Java ad hoc ainsi -Djava.rmi.server.codebase="URL du serveur web:port choisi". Très important: terminer l'URL par un /
  4. Vous conviendrez que les classes dont on pourra demander le téléchargement ne sont qu'un sous-ensemble de celles dont a besoin l'objet serveur. Pour bien comprendre, rangez (quitte à en recopier) vos classes coté machine serveur en deux sous-répertoires : celles utiles pour lancer l'objet serveur, et celles utiles seulement en téléchargement. Ce second répertoire est donc celui que le serveur HTTP indique comme second paramètre.
  5. Testez l'ensemble, mais attention, pour forcer que le rmiregistry vienne demander à télécharger les classes (c'est quand même l'objet de l'exercice!), il ne doit surtout pas avoir la variable CLASSPATH pointant vers un répertoire où elles existent Donc pensez à annuler la définition du CLASSPATH dans le terminal correspondant, ou positionnez vous dans un répertoire où il n'y a aucune des classes nécessaires. Si vous êtes en Java7, pour passer la property -Djava.rmi.server.useCodebaseOnly=false à la JVM sous-jacente lancée par rmiregistry, il faut forcer ce passage en préfixant cette définition de propriété par -J. Plus clairement, rmiregistry -J-Djava.rmi.server.useCodebaseOnly=false
  6. Analysez finement quels .class sont demandés au serveur HTTP en distinguant celles demandées par le rmiregistry et celles demandées par le client, notamment si il reçoit en réponse d'un appel distant un paramètre d'une sous-classe d'une classe déjà connue.
  7. A retenir Ce mécanisme de téléchargement permet de simplifier le déploiement de nouvelles implantations de classes (par le biais de sous-classes) sur les sites des clients: au lieu de devoir transférer à la main les nouvelles .class sur chaque site client, les clients viendront automatiquement télécharger les implantations en un point unique qui est le serveur web. Seule la base de code rendue accessible via le serveur web a besoin d'être mise à jour.

3. Paramètres de type Remote

Nous maitrisons bien le passage de paramètres par copie, même pour des objets, lorsque l'on utilise RMI. Nous allons maintenant expérimenter le passage par référence, possible via RMI, et qui va nous rapprocher de la sémantique Java habituelle de passage (qui est par référence) des objets en paramètre.

  1. Definissez une interface Remote (appelons-la  Service)
  2. Ce service est très simple: son rôle est de maintenir une variable de type int, et d'offrir 2 méthodes utilisables à distance: connaitre la valeur actuelle de cette variable, et, mettre a jour la valeur de cette variable avec en parametre un nombre qui est un facteur multiplicatif.
  3. Rajoutez à la définition de notre objet distant qui implante Distante (définie au TP1), une méthode supplémentaire que le client pourra appeler. Son rôle sera de renvoyer au client une référence vers un tel objet de type Service
  4. Modifiez en conséquence le code de l'objet qui implante l'interface Distante. Faites en sorte que l'objet implantant Service ne soit instancié qu'une seule fois lorsque l'objet distant (=le serveur) démarre. L'implémentation de la méthode de la question 3 fera donc en sorte de renvoyer une référence vers cette unique instance de Service, cad. sans en recréer une à chaque appel de cette méthode
  5. Modifiez à présent le client afin que celui-ci utilise la nouvelle méthode qui lui renvoie une référence vers un objet Service. Au passage, affichez sur l'écran la valeur de cette référence, ce qui vous donnera précisément des informations sur ce service invoquable à distance. Ensuite, le client doit invoquer une ou plusieurs fois de suite (en boucle avec une petite attente entre chaque appel) la méthode qui modifie la valeur détenue par le service.
  6. Testez votre application RMI en priorité en local, et en réparti si vous avez le temps. Dans ce cas, prenez soin de mettre à disposition du serveur http vos .class utiles au client plutôt que de les copier manuellement sur la machine du client.
    Démarrez plusieurs clients depuis plusieurs consoles. Que constatez-vous quant à la valeur récupérée après chaque appel de service sur chacun de vos clients . Que ce serait-il passé si Service n'était pas défini comme une interface Remote? Quel serait l'effet de l'utilisation de ce Service sur chaque client?

4. Multi threading et RMI

L'exercice précédent nous montre qu'il y a effectivement plusieurs threads clientes qui, à un même instant, pourraient vouloir invoquer la même méthode côté serveur, et donc, en concurrence.
Ceci peut provoquer des race conditions ("conditions de vitesse", signifiant que selon la vitesse d'exécution des différentes threads, le résultat obtenu ne sera pas forcément toujours cohérent avec ce qui se serait passé si les threads s'étaient toutes exécutées à la suite les unes des autres)

Le but de l'exercice est de mettre ceci en évidence.

  1. Côté client, affichez le nom de la thread en train d'exécuter l'appel d'une de vos méthodes distantes, et respectivement côté serveur, affichez le nom de la thread en train d'effectuer l'exécution de cette méthode.
  2.  Pour la méthode distante rajoutée dans l'exercice  3 question 2 voici une proposition d'implémentation (val est un attribut)
    public   int  setVal(int v, String cname) throws RemoteException {
            val=val*v;
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread:"+Thread.currentThread().getName()+" val renvoyée : " + val + "au Client "+cname);
            return val;
        }     
    Si on lance plusieurs clients en parallèle, chacun dans une console différente, pourquoi a-t-on une forte probabilité d'obtenir ce type d'affichage côté serveur (voir ci-dessous):

    Thread:RMI TCP Connection(3)-127.0.0.1 val renvoyee :8 au Client toto
    Thread:RMI TCP Connection(2)-127.0.0.1 val renvoyee :8 au Client bibi
    Thread:RMI TCP Connection(3)-127.0.0.1 val renvoyee :32 au Client toto
    Thread:RMI TCP Connection(2)-127.0.0.1 val renvoyee :32 au Client bibi
    Thread:RMI TCP Connection(3)-127.0.0.1 val renvoyee :128 au Client toto
    Thread:RMI TCP Connection(2)-127.0.0.1 val renvoyee :128 au Client bibi
    Thread:RMI TCP Connection(3)-127.0.0.1 val renvoyee :512 au Client toto
    Thread:RMI TCP Connection(2)-127.0.0.1 val renvoyee :512 au Client bibi

  3. Remédier à ce problème pour obtenir typiquement cet affichage:
    Thread:RMI TCP Connection(3)-127.0.0.1 val renvoyee :4au Client toto
    Thread:RMI TCP Connection(2)-127.0.0.1 val renvoyee :8au Client bibi
    Thread:RMI TCP Connection(3)-127.0.0.1 val renvoyee :16au Client toto
    Thread:RMI TCP Connection(2)-127.0.0.1 val renvoyee :32au Client bibi
    Thread:RMI TCP Connection(3)-127.0.0.1 val renvoyee :64au Client toto
    Thread:RMI TCP Connection(2)-127.0.0.1 val renvoyee :128au Client bibi
    Thread:RMI TCP Connection(3)-127.0.0.1 val renvoyee :256au Client toto
    Thread:RMI TCP Connection(2)-127.0.0.1 val renvoyee :512au Client bibi
    Thread:RMI TCP Connection(3)-127.0.0.1 val renvoyee :1024au Client toto

5. Le client est accessible à distance (il est "Remote")

A présent, faire en sorte que l'objet distant puisse également invoquer une méthode à distance sur un objet Client (par exemple, pour lui faire parvenir une information que le client n'avait pas forcément demandée). Le client offre donc aussi une interface Remote que le serveur sera en mesure d'utiliser quand bon lui semble. 

  1. Au lieu de passer en paramètre, comme plus haut, l'identification du client sous forme d'une String

    public   int  setVal(int v, String cname) throws RemoteException

    on voudra avoir ceci

    public   int  setVal(int v, Client cname) throws RemoteException
    où cname sera une référence distante sur l'objet Client qui fait cet appel à setVal. Cette référence joue le rôle d'un CallBack (cad, permet à l'appelé de lui-même déclencher une méthode sur l'objet appelant). Il est évidemment inutile d'enregistrer une référence du client dans un quelconque rmiregistry ! Expliquez pourquoi.

    Le serveur pourra par exemple facturer un montant au client, proportionnel au traitement demandé par le client (par exemple, selon la valeur du paramètre v, la facture sera plus ou moins salée!). Le but du callback est alors de renvoyer la facture.
    Le code de setVal pourrait donc devenir:
        public   synchronized int  setVal(int v, Client cname) throws RemoteException {
            val=val*v;
            System.out.println("Thread:"+Thread.currentThread().getName()+" val renvoyée : " + val + "au Client "+cname);
            Facture f= new Facture(v) // prix à payer pour cette opération
            try {
                cname.facturer(f); // délivre la facture à cname !
            } catch (java.rmi.RemoteException e) {}
            return val;
        }
    Implanter cette possibilité. Quelle précaution prendre pour implanter la méthode facturer ? Pensez au cas où le client pourrait être la cible de plusieurs facturations concurrentes, suite par exemple à plusieurs invocations de service sur plusieurs serveurs différents?  Quelles questions faut-il se poser si l'on décide néanmoins de rendre factoriser "synchronized" (pensez aux deadlocks...) ?



Page maintenue par Francoise Baude @2011-