Corrigé - Coucou, c`est en construction
Transcription
Corrigé - Coucou, c`est en construction
IN328 : RMI - Corrigé SI http://personnel.supaero.fr/garion-christophe/IN328 Ce TP va vous permettre de manipuler RMI, l’API d’appel distant fournie par Sun. 1 Contenu Ce corrigé succinct contient l’essentiel des explications nécessaires pour réaliser le TP sur RMI. Les sources des classes sont disponibles sur le site. Vous trouverez un fichier de construction build.xml pour Ant qui permet de construire et de lancer les applications facilement. Il faudra bien sûr personnaliser les variables que j’ai utilisées (chemin d’accès, machines utilisées etc.). Les exécutables que j’ai créés prennent tous en paramètre le nom d’une ou deux machines suivant les questions (serveur RMI, serveur de fichiers). J’ai choisi d’avoir quatres répertoires distincts contenant le bytecode : un pour le client, un pour le serveur, un pour le registre et un pour le serveur de fichiers quand on l’utilise. Pour chaque question du TP, j’ai créé un paquetage spécifique. Je n’ai pas utilisé Ant pour les « exécutions » du serveur, car on « forke » la machine virtuelle et on n’a plus l’affichage. Vous remarquerez également que le fichier Ant est construit maladroitement : je compile les interfaces distantes et je les copie dans le bon répertoire pour le client. Dans un vrai projet, il vaut mieux créer toutes les interfaces distantes, les mettre dans un JAR et distribuer ce JAR. Enfin, vous trouverez une archive contenant toutes les politiques de sécurité utilisées. Par défaut, j’ai utilisé clervoy comme machine sur laquelle tourne le registre et guerin comme machine sur laquelle tourne le serveur de fichiers. Vous pouvez avoir quelques problèmes si vous utilisez localhost, dans ce cas, mettez l’adresse de la machine à la place. 2 Création d’un objet distant et appel d’une méthode Nous allons dans un premier temps créer un objet qui fournit des services distants. Un client va appeler ces services. Dans un premier temps, ce client se trouvera sur la même machine que l’objet distant, mais même dans ce cas, RMI utilise des sockets. On a donc le même comportement que pour un objet se situant physiquement sur une machine distante. Dans un second temps, nous utiliserons deux machines différentes (vous vous connecterez via ssh sur une machine Sun du CI, comme clervoy par exemple). Important : il faudra placer les bytecodes des applications client et serveur (et pour le registre) dans des répertoires différents pour bien comprendre ce qu’il est nécessaire d’avoir de chaque côté (classes, stubs, interfaces, positionnement du classpath). 1. définir une interface distante InterfaceBonjour qui définit une méthode afficher(String s) ; 2. définir une classe BonjourDistant qui réalise cette interface et qui étend la classe java.rmi.server.UnicastRemoteObject ; 3. créer une classe Enregistrement qui possède une méthode main qui enregistre l’objet dans un registre ; Ces trois questions permettaient de réaliser la partie serveur de l’application. Rien de bien particulier, il suffisait de créer une interface étendant java.rmi.Remote et une classe réalisant cette interface. L’« application » serveur liant l’objet dans le registre utilisait bien sûr la méthode Naming.rebind(). 1 Cette méthode est pratique en développement1 , mais ne doit pas être utilisée systématiquement dans le cadre d’un développement final. Le nom de l’objet dans la base de registre est un nom long, comme par exemple rmi://localhost/ objetDistant. On aurait très bien pu l’appeler simplement objetDistant. On peut remarquer qu’ici on a omis le numéro de port, donc par défaut on considère le port 1099. On peut également définir le numéro de port du serveur en utilisant l’argument en ligne de commande de java. L’interface développée fait partie du paquetage fr.supaero.rmi.serveur. Les classes développées font partie du paquetage fr.supaero.rmi.serveur.base et les bytecodes correspondants sont stockés dans un répertoire classesServeur. 4. du côté client, implanter une classe AppelDistant qui possède une méthode main qui cherche une référence sur l’objet distant et appelle sa méthode afficher ; Rien de particulier. Il ne fallait pas oublier de transtyper la référence obtenue après le lookup, car on obtient une référence de type java.rmi.Remote. De même, il faut posséder l’interface InterfaceBonjour dans son classpath pour pouvoir compiler correctement le client. La classe fait partie du paquetage fr.supaero.rmi.client.base et le bytecode correspondant est stocké dans le répertoire classesClient. NB : vous remarquerez que j’ai détaillé les exceptions qui peuvent être lancées pour que vous voyez bien quelles sont ces exceptions. On aurait bien sûr pu rattraper toutes les exceptions avec un catch (Exception e). 5. générer le stub de la classe BonjourDistant en utilisant l’option -v1.2 de rmic pour ne pas générer de skeleton ; 6. lancer une base de registre grâce à rmiregistry et le serveur. On lancera le registre en prenant de garde de bien positionner le classpath pour que le stub soit visible. 7. lancer le client. Où s’exécute la méthode de l’objet distant ? Rien de bien particulier pour ces trois questions. Voici le détail des opérations utilisées : (a) lancement du registre. J’ai utilisé la commande suivante : CLASSPATH=/chemin_TP/classesRegistre/ rmiregistry Je positionne le classpath pour que le registre puisse « voir » l’interface et le stub du serveur. (b) « exécution » de la classe fr.supaero.rmi.serveur.base.Enregistrement. Rien de bien particulier, une erreur peut se produire si on n’a pas généré le stub. (c) « exécution » de la classe fr.supaero.rmi.client.base.AppelDistant. Comme on n’utilise pas de SecurityManager, on n’a pas accès au chargement dynamique des classes. Il faut donc que le stub se trouve dans le classpath du client, sinon une exception est levée (ce qui n’est pas nécessaire si on peut le charger dynamiquement, cf. section 4). On voit bien ici que la méthode afficher de l’objet distant s’exécute du côté serveur. A partir de la version 5.0 du JDK, lorsque l’on exporte un stub dans le registre, si le bytecode du stub n’est pas disponible, on génére un objet de type java.lang.reflect.Proxy à la place du stub. Le client et le registre n’ont donc plus besoin d’avoir le bytecode du stub dans le classpath (il faut bien sûr que le client utilise du code et une JVM compatibles avec le JDK 5.0). J’ai affiché dans AppelDistant l’objet récupéré par l’appel à Naming.lookup. Si les stubs ont été utilisés par le serveur, le registre et le client, on obtiendra à l’affichage : Objet distant recupere : BonjourDistant_Stub[UnicastRef [liveRef: 1 Elle est même nécessaire si on ne veut pas relancer le registre à chaque fois. 2 [endpoint:[134.212.136.180:35046](remote),objID:[705be52f:117e92e1b19:-8000, 0]]]] ce qui montre bien que l’on utilise le stub (on voit également quel est le port utilisé par le stub pour communiquer, ici le port 35046 de la machine dont l’adresse IP est 134.212.136.180). Si on ne dispose pas des stubs (il suffit de ne pas l’avoir dans le CLASSPATH du serveur !), on obtient alors : Objet distant recupere : Proxy[InterfaceBonjour,RemoteObjectInvocationHandler [UnicastRef [liveRef: [endpoint:[134.212.136.180:35058](remote),objID: ce qui montre bien que l’on utilise un objet de type Proxy et non pas un stub. Attention toutefois, cette solution suppose que le serveur et le client utilisent une version du JDK supérieure à la version 5.0. Dans toute la suite du TP, j’utiliserai les stubs et non pas la classe Proxy. On devra donc avoir le bytecode des stubs nécessaires dans le classpath. 3 Passage d’un objet en paramètre Dans cette section, nous allons utiliser une méthode d’un objet distant qui prend en paramètre un objet se situant chez le client. On créera un nouveau paquetage pour pouvoir réutiliser le code écrit précédemment facilement. Important : comme précédemment, nous n’utiliserons pas le chargement dynamique des classes nécessaires. Il faudra donc s’assurer que les classes et stubs nécessaires sont bien dans les classpaths (en particulier, le serveur va avoir besoin de classes dont se sert le client). 1. créer une interface InterfaceMessage dans le paquetage du serveur qui définit une méthode getTexte qui renvoie une chaı̂ne de caractères ; 2. créer une classe Message dans le paquetage du client qui réalise cette interface et qui a pour attribut la chaı̂ne de caractères à renvoyer ; Rien de bien particulier ici. J’ai choisi d’utiliser deux nouveaux paquetages, fr.supaero.rmi.client.objet et fr.supaero.rmi.serveur.objet. 3. modifier les classes précédentes pour que la méthode afficher de InterfaceBonjour prenne un objet de type InterfaceMessage en paramètre. Que se passe-t-il ? La modification de InterfaceBonjour et de BonjourDistant était triviale. Lorsque l’on essaye d’exécuter l’appel client à afficher, une erreur de marshalling est trouvée : la JVM nous indique que la classe Message n’est pas sérialisable. En effet, nous avons vu en cours qu’un objet passé en paramètre d’une méthode distante devait soit être sérialisable, soit lui-même distant. 4. écrire une classe MessageSerialisable sérialisable et réalisant InterfaceMessage et l’utiliser dans AppelDistant. Que se passe-t-il maintenant ? Cette fois-ci tout fonctionne, à condition que le serveur puisse reconstruire l’objet de type MessageSerialisable2 . Comme nous n’utilisons pas le chargement dynamique, cela revient à copier le bytecode de MessageSerialisable dans le CLASSPATH du serveur. Vous remarquerez que j’ai fait afficher un petit texte dans la méthode getTexte de MessageSerialisable. Ce texte « apparaı̂t » du côté serveur, ce qui est normal car l’objet est sérialisé et envoyé au serveur. 2 Sinon une exception d’unmarshalling est levée dans le serveur. 3 Si on avait utilisé un objet distant, ce texte serait apparu du côté client. J’ai implanté cette solution, les classes correspondantes sont dans les paquetages fr.supaero.rmi.client.distant et fr.supaero.rmi.serveur.distant. On voit alors qu’il n’y a pas besoin d’enregistrer l’objet distant de type Message dans un registre, tout se fait « automatiquement » (il faut bien sûr que le serveur possède le bytecode du stub de Message). 4 Chargement dynamique des classes Dans les sections précédentes, nous avons supposé que le serveur, le client et le registre disposaient des stubs et des bytecodes nécessaires à leur bon fonctionnement. Nous allons maintenant utiliser le chargement dynamique des classes. De cette façon, le serveur et le client ne disposeront que des interfaces distantes nécessaires à leur compilation. 1. récupérer les classes développées dans la section 2 dans deux nouveaux paquetages, fr.supaero.rmi.client.dyn et fr.supaero.rmi.serveur.dyn. Modifier la classe cliente pour que celle-ci puisse utiliser le chargement dynamique des classes ; Il n’y avait pas grand chose à faire. Comme la classe client ne disposera pas du stub, mais devra le charger dynamiquement, il ne fallait pas oublier de mettre en place un RMISecurityManager dans l’application cliente. J’ai également changé l’application serveur pour utiliser le constructeur de UnicastRemoteObject permettant de préciser le numéro de port sur lequel le stub attend les connexions. Ceci me permet de définir précisement la politique de sécurité dont j’ai besoin. J’ai choisi ici le port 1200. 2. lancer un registre sans classpath. On a besoin d’un serveur Web pour servir les fichiers. On va utiliser un serveur de fichier léger, disponible sur le site sous l’onglet ressources, la classe ClassFileServer. Pour l’utiliser : java ClassFileServer numPort CHEMIN_VERS_FICHIERS. Lancer ensuite le serveur en précisant comme codebase "http://nomMachineServeurFichier:numPort/" (ne pas oublier le « / » final !). Enfin, lancer le client avec un fichier de politique de sécurité adéquat ; Là encore rien de particulier si on effectuait bien les opérations demandées (ne pas mettre de classpath pour le registre, lancer le serveur de fichier etc.). Voici le fichier de politique de sécurité que j’ai utilisé personnellement pour le client : grant { // connexions vers le serveur de fichiers permission java.net.SocketPermission "guerin.supaero.fr:2000", "connect"; // connexions vers le registre permission java.net.SocketPermission "clervoy.supaero.fr:1099", "connect"; // connexions vers le stub permission java.net.SocketPermission "clervoy.supaero.fr:1200", "connect"; }; Il ne fallait pas oublié que l”on a également besoin de l’interface InterfaceDistant sur le serveur de fichiers pour pouvoir reconstruire le stub. On remarquera que l’on voit bien les appels au serveur de fichier dans les traces de ce dernier lorsque le registre et le client vont charger les interfaces et les classes dont ils ont besoin. 4 On remarquera enfin que si le client utilise des objets en paramètre de la méthode distante, il peut également préciser que le bytecode des classes et interfaces correspondantes se trouvent sur le serveur de fichier (pour que l’objet serveur puisse les reconstruire) via java.rmi.server.codebase. 3. nous allons essayer de « bootstrapper » le client et le serveur. Pour cela, créer un répertoire qui contiendra les classes de l’application (même celles du client. Il faudra donc modifier la classe AppelDistant pour qu’elle n’ait plus de méthode main, mais qu’elle appelle la méthode afficher à sa création), et deux applications qui chargent dynamiquement la classe applicative du serveur et la classe applicative du client. C’est le plus gros morceau du TP. Il faut rester méthodique et ne pas se précipiter. Les classes sont disponibles sur le site dans les paquetages fr.supaero.rmi.client.boot et fr.supaero.rmi.serveur.boot pour les classes applicatives et dans fr.supaero.rmi.boot pour les classes de démarrage. J’ai choisi les machines suivantes : – le serveur de fichiers tourne sur guerin:2000 – le serveur et le registre tournent sur clervoy – le client tourne sur dortie Les classes « applicatives » ne nécessitaient pas de modifications importantes. Seule la classe AppelDistant devait être modifiée : le traitement qu’elle faisait (appel de la méthode afficher) devait être encapsulé dans son constructeur qui ne devait pas posséder d’argument (appel à newInstance). Ceci peut poser problème si l’on veut paramétrer le nom du serveur où se trouve le registre par exemple. On pouvait alors lancer un serveur de fichier pointant sur le répertoire contenant ces classes et lancer un registre sans classpath sur la machine servant de serveur. Le serveur « bootstrappé » lui se contente de récupérer l’objet serveur dynamiquement via la classe RMIClassLoader et crée un objet. On remarquera l’utilisation de la classe java.util.Property qui permet de récupérer les propriétés du système (codebase ici). Il faut préciser dans le fichier de politique de sécurité que l’on est autorisé à le faire : grant { // on autorise les connexions par socket sur le port 2000 du serveur de // fichiers permission java.net.SocketPermission "guerin:2000", "connect"; // on autorise les connexions par socket sur le port 1099 de la machine // pour le registre permission java.net.SocketPermission "clervoy:1099", "connect"; // on autorise les connexions par socket sur le stub permission java.net.SocketPermission "clervoy:1200", "accept"; // on autorise les connexions par socket sur la machine appelante // pour le retour permission java.net.SocketPermission "dortie:1024-", "accept"; // on autorise la lecture de la propriete du systeme java.rmi.server.codebase permission java.util.PropertyPermission "java.rmi.server.codebase", "read"; }; On remarquera que je suis obligé d’autoriser les connexions sur dortie (sur laquelle tourne le client) sur tous les ports utilisateurs, car la socket factory utilisé (ici RMISocketFactory, la factory par 5 défaut) choisit un port anonyme sur le client. Il faut créer sa propre SocketFactory pour choisir un port spécifique sur le client. On le lance avec java -Djava.security.policy=/CHEMIN_POLITIQUE/politique_serveur_boot.txt -Djava.rmi.server.codebase=http://machineContenantClasses:numPort/ BootServer. Il faut bien sûr préciser où se trouvent les classes à charger. On lance de la même façon le client boostrappé. J’ai utilisé dans le constructeur de BonjourDistant un numéro de port pour pouvoir écrire précisement le fichier de politique de sécurité. Celui-ci est le suivant : grant { // on autorise les connexions par socket sur le port 2000 du serveur de // fichiers permission java.net.SocketPermission "guerin:2000", "connect"; // on autorise les connexions par socket sur le registre permission java.net.SocketPermission "clervoy:1099", "connect"; // on autorise les connexions par socket sur le stub permission java.net.SocketPermission "clervoy:1200", "connect"; // on autorise la lecture de java.rmi.server.codebase permission java.util.PropertyPermission "java.rmi.server.codebase", "read"; }; 5 Utiliser le mécanisme d’activation Dans cette section, il faut essayer d’étendre non pas UnicastRemoteServer pour la classe représentant l’objet distant, mais Activatable. 1. modifier la classe BonjourDistant pour qu’elle étende correctement Activatable ; Toutes les classes sont disponibles sur le site. Pas de problème particulier pour celle là, il fallait juste faire attention au constructeur. 2. modifier la classe Enregistrement pour qu’elle enregistre l’objet Activatable. Ne pas oublier de mettre en place un RMISecurityManager ; Ne pas oublier de changer les chemins d’accès dans la classe disponible sur le corrigé ! Sinon, il fallait suivre « tranquillement » les transparents : créer un groupe d’activation, créer le descripteur de l’objet, l’enregister auprès du service d’activation, et enfin le lier à un nom dans le registre RMI. J’ai utilisé System.getProperties() pour récupérer les propriétés du système et utiliser l’argument java.security.policy que je passe à la JVM. De même, je récupére le codebase du serveur de fichier grâce à la propriété java.rmi.server.codebase. 3. lancer l’application en utilisant un « client de base » précédemment développé. Attention à la politique de sécurité pour rmid. J’ai choisi ici de réutiliser la solution utilisant le chargement dynamique des classes. J’ai par contre réécrit les classes dans des paquetages différents, java.rmi.client.activ et java.rmi.serveur.activ. Il ne fallait pas oublier de prendre un fichier de politique de sécurité suffisant. Celui-ci permettait à l’utilitaire rmid d’exécuter certaines commandes sur le système. J’ai utilisé le fichier suivant (très laxiste !) : 6 grant { // on autorise les activation groups a utiliser certaines // proprietes permission com.sun.rmi.rmid.ExecOptionPermission "*"; }; J’ai lancé rmid comme suit : rmid -J-Djava.security.policy=politique_rmid.txt -port 1098 Il fallait également positionner le codebase en lançant l’enregistrement de telle sorte que celui-ci pointe correctement sur le répertoire contenant la classe de l’objet distant. Par exemple, j’ai lancé : java -Djava.security.policy=politique_server_activ.txt -Djava.rmi.server.codebase=http://guerin:2000/ fr.supaero.rmi.serveur.activ.Enregistrement. Je disposais évidemment d’un serveur de fichiers via la classe ClassFileServer qui attendait des connexions sur le port 2000 de la machine guerin. J’ai utilisé le fichier de politique suivant pour l’enregistrement : grant { // on autorise les connexions par socket sur le port 2000 du serveur de // fichiers permission java.net.SocketPermission "guerin:2000", "connect"; // on autorise les connexions par socket sur le port 1099 de la machine // pour le registre permission java.net.SocketPermission "clervoy:1099", "connect"; // on autorise les connexions par socket sur la machine // pour rmid permission java.net.SocketPermission "clervoy:1098", "connect"; // on autorise les connexions par socket sur la machine // pour les activations permission java.net.SocketPermission "clervoy:1024-", "accept"; // on autorise les connexions par socket sur la machine // pour le stub permission java.net.SocketPermission "clervoy:1200", "connect"; // on autorise les connexions par socket sur la machine appelante // pour le retour permission java.net.SocketPermission "dortie:1024-", "accept"; // on autorise la lecture et l’ecriture des permissions permission java.util.PropertyPermission "*", "read,write"; // on autorise l’utilisation du runtime permission java.lang.RuntimePermission "*"; // on autorise les connexions par socket sur le stub permission java.net.SocketPermission "clervoy:1200", "accept"; }; 7 Pour le client, on peut utiliser le fichier de politique suivant (identique à celui de la section 4 sauf que l’on rajoute l’autorisation de se connecter à rmid sur le port 1098) : grant { // connexions vers le serveur de fichiers permission java.net.SocketPermission "guerin.supaero.fr:2000", "connect"; // connexions vers le registre permission java.net.SocketPermission "clervoy.supaero.fr:1099", "connect"; // connexions vers rmid permission java.net.SocketPermission "clervoy.supaero.fr:1098", "connect"; // connexions vers le stub permission java.net.SocketPermission "clervoy.supaero.fr:1200", "connect"; }; On s’aperçoit à l’exécution que si l’on arrête la JVM lançant l’enregistrement, lors de l’appel du client, une machine virtuelle est bien redémarrée. L’affichage se fait dans les traces de rmid. 8