PROGRAMMATION DISTRIBUÉE TUTORIEL RMI

Dimension: px
Commencer à balayer dès la page:

Download "PROGRAMMATION DISTRIBUÉE TUTORIEL RMI"

Transcription

1 PROGRAMMATION DISTRIBUÉE TUTORIEL RMI PUBLIC CONCERNÉ : formation initiale, 2 e année. NOM DE L AUTEUR : V. Thomas DATE 2012/2013 UNIVERSITÉ DE LORRAINE IUT NANCY CHARLEMAGNE 2 ter boulevard Charlemagne CS NANCY cedex Tél : Fax :

2 2

3 Contents 1 Programmation distribuée et RMI Application distribuée Approches pour développer des applications distribuées Programmation Réseau Programmation avec RMI Principes de RMI Notion de référence distante Schéma général du lancement d une application RMI Problèmes auxquels RMI doit répondre Construire une application distribuée avec RMI Schéma de développement d une application distribuée avec RMI Exemple: gestionnaire de tickets Gestion des packages Répertoire source Répertoire bin Scripts Structure générale Définition du service Écriture de l objet serveur Écriture du main du serveur Écriture du main du client Compilation Exécution des applications lancement du rmiregistry Lancement du serveur Lancement du client Mettre en place l application Erreurs fréquemment rencontrées illegal remote method encountered java.security.accesscontrolexception UnmarshalException MalformedURLException Compléments Autre manière de voir RMI Passage de paramètres Codebase Sécurité CallBack

4 L objectif de ce tutoriel est de présenter à l aide d un exemple simple la manière de développer une application distribuée en utilisant RMI. L exemple utilisé sera un distributeur de tickets: un serveur peut fournir un ticket représenté par un numéro aux clients qui s y connectent; à chaque fois qu un ticket est donné, le serveur incrémente ce numéro pour fournir le numéro suivant au prochain client qui se connecte. 1

5 1 Programmation distribuée et RMI 1.1 Application distribuée On parlera d application distribuée, lorsqu une application est décomposée en plusieurs modules séparés s exécutant sur des machines distantes. Ainsi, un jeu multi-joueur est bien une application distribuée dans le sens où chaque joueur possède une application qui communique avec les autres applications (un serveur ou alors les autres applications clients selon une architecture en pair-à-pair). Par simplicité, la suite du tutoriel se concentre sur une application client-serveur mais bien d autres choses sont envisageables. 1.2 Approches pour développer des applications distribuées Deux approches 1 peuvent être envisagées pour développer une application distribuée en JAVA: en utilisant des Socket (cf cours Réseau) en utilisant des solutions de plus haut niveau comme RMI (cf ce cours) Programmation Réseau Une première idée consiste à mettre en place de la communication UDP ou TCP entre les machines clients et la machine serveur. Cela consiste à créer des socket entre la machine client et la machine serveur (sockerserver, accept,...), définir un protocole d échange permettant de représenter une demande sous la forme d une chaîne de caractères (par exemple utiliser les noms des méthodes dans une chaîne de caractère comme méthode getnumero Durand ). créer la demande de la part du client en générant et en envoyant la chaîne correspondant à la requête attendue. récupérer cette chaîne dans la socket du serveur, l interpréter, appeler la bonne méthode sur l objet correspondant, récupérer le résultat et le retransmettre via la socket. Ce type d approche fonctionne parfaitement, mais cela devient assez vite laborieux. Á chaque fois que l on souhaite faire communiquer des objets, il est nécessaire de refaire les mêmes opérations: créer des sockets, définir un protocole à partir des noms de méthodes, faire les écoutes au niveau du serveur, faire le lien entre les chaînes de caractères et les bonnes méthodes de l objet, etc... De le même manière que le processus de sérialisation, on peut facilement imaginer que ce processus est automatisable, c est à dire qu on peut construire des classes qui permettent de gérer cette mécanique de manière automatique en encapsulant dans des classes bien définies ce processus. C est quelque chose de difficile à faire (car il faut pouvoir correctement généraliser ce qui se passe), mais cela est envisageable. 1 parmi d autres, c est bien sûr non exhaustif C est justement l objectif de RMI. 2

6 1.2.2 Programmation avec RMI RMI (acronyme de Remote Method Invocation) est un middleware JAVA destiné à aider la création d applications distribuées. Un middleware est une architecture logicielle qui se charge de fournir les services pour faire interagir différents composants logiciels. Dans notre cas, RMI est un middleware permettant de faire interagir des applications JAVA. RMI propose en effet des classes permettant de faire communiquer des objets distants. Le coeur de RMI est la notion de référence distante qui va être développée dans la partie suivante. 1.3 Principes de RMI Notion de référence distante Dans un programme java classique, les objets sont représentés par des références désignant l emplacement mémoire (dans le tas) au sein duquel on peut trouver le descriptif des attributs de l objet. Chaque application java possède son propre espace d adressage, ce qui fait qu il n est pas possible pour une application Java d utiliser un objet référencé dans une autre application. Cela est d autant plus vrai lorsque les applications sont lancées sur des machines différentes (les espaces de stockage étant alors dans des lieux différents). La notion de référence distante a pour objectif de répondre à ce problème. Une référence distante est un objet capable de communiquer avec un objet distant (situé dans une autre application java et potentiellement sur une autre machine). Pour caricaturer, la référence distante possède en interne l adresse IP de la machine où trouver l objet distant à appeler et le port permettant d accéder à cet objet. De plus, une référence distante implémente une interface qui décrit les méthodes qu il est possible d utiliser sur l objet distant. Seules ces méthodes sont appelables à distance. Dans une application classique, un appel de méthode à un objet consiste simplement à utiliser la référence de l objet sur lequel la méthode doit s appliquer. Dans une application distribuée, le schéma est le même sauf que c est désormais une référence distante qu il faut utiliser pour contacter l objet situé dans un autre espace d adressage Schéma général du lancement d une application RMI Création de l objet serveur La première étape consiste à créer l objet qui va être amené à répondre au service 2 sur la machine serveur. Si on souhaite faire un compteur, il faut créer l objet compteur sur le serveur. C est cet objet qui sera ensuite appelé par le client. Création d une référence distante Le serveur doit ensuite créer une référence distante. Cette référence distante permet aux applications qui la possèdent d effectuer des appels sur l objet serveur. Seules les méthodes de la référence distante seront visibles à distance. 2 autrement dit qui contient le code qui sera exécuté 3

7 Une fois que le serveur a construit une référence distante permettant d appeler des méthodes sur l objet qu il a créé, il va diffuser cette référence (en utilisant un gestionnaire de référence - rmiregistry qui sera décrit plus tard). Récupération de la référence distante gestionnaire de références). Le client récupère cette référence distante (par le Utilisation de la référence distante Grâce à la référence distante, le client peut appeler l objet situé sur le serveur en passant par le réseau. L appel parvient jusqu à la machine serveur qui applique la méthode sur l objet et obtient un résultat. Ce résultat est retourné à l application client par l intermédiaire de la référence distante Problèmes auxquels RMI doit répondre Pour pouvoir effectuer toutes ces opérations, il faut que RMI propose les bases pour 1. définir un service proposé par un serveur et utilisable par un client: pour cela, RMI fournit l interface Remote permettant de définir une interface distante(cf partie 2.2). 2. créer des références distantes à partir des objets sur le serveur qu on souhaite appeler à distance: pour cela, RMI fournit la classe UnicastRemoteObject permettant d exporter un objet en référence distante (cf partie 2.4) 3. permettre à un serveur de diffuser cette référence et à un client de pouvoir télécharger une référence distante: pour cela, RMI fournit un annuaire / gestionnaire de références nommé rmiregistry capable de stocker et de distribuer des références distantes 4. proposer une mécanique transparente pour que le client puisse, de manière simple, faire des demandes à l objet situé sur le serveur grâce à la référence distante: pour cela RMI utilise les références distantes masquées par l interface distante Construire une application distribuée avec RMI Définir le service distant Pour que l objet distant et le proxy puissent répondre aux mêmes méthodes (les méthodes qu on souhaite appeler à distance), il faut que ces deux objets implémentent la même interface : c est l interface distante qui explicite les méthodes qui sont fournies par le serveur (et qu il doit donc implémenter) et qui doivent être reconnues par le proxy (pour que le client puisse faire les appels). La première étape consiste donc à définir le service distant proposé par le serveur. Ce service correspond aux méthodes que les clients pourront appeler sur l objet serveur. Ce service doit être implémente au niveau du serveur pour que le serveur puisse répondre aux sollicitations du client être connu par le client pour savoir quel service est accessible, ou autrement dit, quelle méthode est appelable sur l objet serveur. En RMI, ce service est décrit par une interface JAVA héritant de Remote. L interface distante doit respecter plusieurs contraintes elle doit hériter de Remote toutes ses méthodes doivent lever une RemoteException 3 3 qui peut se déclencher lorsque l objet distant ne répond plus 4

8 Comme l appel passe par le réseau, il est nécessaire que les paramètres de l appel et le type de retour soient serializable 4 Construire un objet serveur Une fois l interface distante déclarée, il faut que le serveur puisse répondre aux sollicitations du client. Le serveur doit donc possèder le code capable de répondre à l interface distante. Pour cela, on construit un objet serveur implémentant l interface distante. Ainsi, les méthodes de cette interface doivent être définies au niveau du serveur. Cela assure que le service soit effectivement implémenté. Construire la référence distante La construction de la référence distante se fait dans le main du serveur grâce à la méthode statique exportobject de la classe UnicastRemoteObject. Cette méthode prend en paramètre un objet implémentant Remote et retourne une référence distante implémentant le service de l objet serveur. Cette construction de référence distante se fait automatiquement en utilisant la réflexivité 5. La classe réelle de l objet retourné est une classe anonyme générée automatiquement par JAVA. La référence distante est donc un objet implémentant l interface distante, mais n est pas du type du serveur. Elle possède les méthodes de l interface distante mais fonctionne différemment: la référence distante ne répond pas directement à la méthode mais transmet l appel à l objet serveur qui se charge de transmettre sa réponse. La construction de la référence distante effectué sur le serveur génère un Thread qui écoute les appels vers l objet serveur, se charge de transmettre la demande à l objet serveur et se charge de récupérer le résultat qu il retransmet à la référence distante à l origine de l appel. Ce Thread correspond à ce qu on appelle le Skeleton. De manière simple, la référence distante se fera passer au niveau du client pour l objet serveur en lui déléguant les méthodes du service distant. Enregistrer et récupérer la référence distante Enfin, le serveur doit communiquer au client la référence distante qu il a créée pour que le client puisse appeler l objet à l origine de la référence. Pour cela, RMI se base sur une application intermédiaire fournie dans java: le rmiregistry. Le rmiregistry est un annuaire chargé de stocker les références distantes. pour lancer un rmiregistry, il suffit de lancer la commande rmiregistry en console (dans le jdk) pour accéder au rmiregistry à partir d une application Java, il suffit d utiliser la classe LocateRegistry et les méthode statiques getregistry ou createregistry. Ces méthodes renvoient un objet de type Registry qui se charge de communiquer avec le rmiregistry pour accéder à la liste des références enregistrées, on utilisera la méthode list() sur un objet Registry 4 Les objets sont sérialisés pour pour pouvoir être transmis à travers le réseau. Ils sont reconstitués sur le serveur si ce sont des paramètres lors de l appel de méthode ou sur le client si l objet correspond à une valeur de retour, il s agit donc d un passage de paramètre par copie entre le client et le serveur 5 La notion de réflexivité fait référence au fait que java peut interroger un objet sur les méthodes et les attributs qu il possède 5

9 pour sauver une référence distante, méthode bind() ou rebind() sur un objet Registry pour récupérer une référence distante, utiliser la méthode lookup() sur un objet Registry Le serveur doit donc accéder au rmiregistry et y enregistrer (grâce à la méthode bind()) la référence distante qu il vient de créer. Le serveur est désormais prêt l objet serveur qui contient le code à exécuter a été créé une référence distante a bien été construite cette référence distante est enregistrée au niveau du rmiregistry Il ne reste plus au client qu à récupérer la référence et à l utiliser pour faire des appels aux méthodes implémentées par l objet serveur. Construction du client Ce main doit Le client est relativement simple et n est composé que d un main. récupérer la référence distante qui a été déposée par le serveur sur le rmiregistry utiliser la référence distante pour faire un appel de méthode sur l objet distant situé sur le serveur 1.4 Schéma de développement d une application distribuée avec RMI Pour mettre en place une application distribuée avec RMI, le schéma est habituellement le suivant 1. Définir le service distant et écrire l interface décrivant les méthodes appelables à distance 2. Écrire un objet serveur implémentant cette interface 3. Écrire le main du serveur créant l objet serveur, générant la référence distante et l enregistrant sur le rmiregistry 4. Écrire le main du client chargé de se connecter sur le rmiregistry du serveur, de récupérer la référence distante et de faire des appels Un schéma équivalent est le suivant 1. Définir l objet serveur chargé de répondre aux appels du client. Il s agit à ce niveau d un objet classique en JAVA 2. En déduire la liste des méthodes appelables à distance et écrire en conséquence l interface distante que devra implémenter ce serveur 3. Écrire le main du serveur créant l objet serveur, générant la référence distante et l enregistrant sur le rmiregistry 4. Écrire le main du client chargé de se connecter sur le rmiregistry du serveur, de récupérer la référence distante et de faire des appels 6

10 2 Exemple: gestionnaire de tickets Afin de mettre en application les éléments présentés dans ce tutoriel, cette partie propose de développer un serveur de tickets. Il s agit simplement d une application permettant de distribuer des tickets différents aux applications client qui le demandent (par exemple pour gérer une file d attente). Pour faire les choses correctement, nous allons en outre utiliser des packages afin de générer deux applications différentes 1. une application serveur chargée de distribuer des tickets 2. une application client qui demandera des tickets à l application serveur 2.1 Gestion des packages Comme le service distant est partagé par le client et le serveur, les deux applications doivent connaître l interface distante. Il faut de plus que cette interface distante soit strictement identique 6. Ainsi, afin d éviter d avoir des versions non cohérentes entre le client et le serveur, le plus simple consiste à mettre toutes les sources dans le même répertoire et à écrire un script compilant simultanément les applications client et serveur dans deux répertoires dédiés. Cela évite les problèmes consistant à compiler le serveur, modifier l interface puis compiler le client (ce qui ne fonctionnera pas puisque l interface distante du client et du serveur ne sont pas les mêmes). Pour simplifier les choses, on propose une structure constituée de plusieurs répertoires décrite ci-dessous Répertoire source Le répertoire source contient toutes les sources du projet (et uniquement les sources). Comme l application utilisera des packages, quelques rappels s avèrent nécessaires. Rappel sur les packages En java, les packages ont pour objectif de regrouper du code. Il s agit habituellement de regrouper du code par fonctionnalité (affichage,...). En java, il y a un lien fort entre structure logique et structure physique. la structure logique désigne l organisation des classes en package. Cette organisation est décidée par le programmeur qui peut vouloir regrouper des classes ensemble pour des raisons diverses (fonctionnalité identique, contraintes d accès,...) la structure physique désigne les fichiers sur le disque et la manière dont ils sont organisés (nom du fichier, répertoire,...) En java, la structure physique est fortement liée à la structure logique: un package correspond forcément à un sous-répertoire de même nom 7. Le classpath est le troisième élément qui se charge de faire le lien entre la structure logique et la structure physique. Il désigne les répertoires de départ à partir desquels, java va chercher 6 A un espace prés car cette vérification est faite par un checksum - à savoir les résultat d un calcul fait sur les octets du bytecode 7 Pour éviter d oublier ces considérations, il est conseillé de penser le véritable nom d une classe comme nomdu- Package.nomClasse 7

11 les packages et donc les sous-répertoires correspondant. Le classpath est modifiable soit en passant une option -cp à la JVM soit en modifiant la variable classpath dans la console avant d exécuter ou de compiler l application 8 Le plus simple consiste à structurer l application en trois pack- Organisation des packages ages un package service contenant le descriptif du service de distribution de ticket (interface distante) un package distributeur contenant le distributeur de ticket (classe implémentée au niveau du serveur) un package client contenant la classe Client demandeuse de ticket Répertoire bin L objectif va consister à produire deux applications une application AppliServeur qui tournera sur le serveur, qui contiendra les packages service et distributeur et qui distribuera des tickets une application AppliClient qui correspondra aux clients, qui contiendra les packages service et client et qui demandera des tickets au serveur Chacune de ces applications aura un sous-repertoire dédié dans le répertoire bin. Ainsi, l application serveur sera dans le sous-repertoire AppliServeur avec les sous-répertoires service et distributeur pour les packages utilisés l application client dans le sous-repertoire AppliClient avec les sous-répertoires service et client pour les packages utilisés Scripts L ensemble des scripts sera situé dans le répertoire parent à bin et src qu on appellera répertoire racine du projet. Ces scripts seront un script de compilation chargé de générer les deux applications client et serveur à partir des mêmes sources un script permettant de lancer le serveur un script permettant de lancer les clients Structure générale Pour résumer, la structure de répertoire sera donc la suivante répertoire racine du projet répertoire src répertoire client (sources package client) répertoire distributeur (sources package distributeur) 8 dans une console DOS cela peut se faire avec la commande set classpath=%classpath%;nouveauchemin pour ajouter nouveauchemin au classpath existant 8

12 répertoire service (sources package service - ref distante) répertoire bin répertoire AppliServeur (contient le bytecode du serveur) répertoire distributeur (class package distributeur) répertoire service (class package service - ref distante) répertoire AppliClient (contient le bytecode du client) répertoire client (class package client) répertoire service (class package service - ref distante) script de compilation scripts de lancement 2.2 Définition du service L étape suivante consiste à définir le service proposé par le serveur et utilisable par le client. Ce service sera défini dans l interface distante héritant de Remote. Nous appellerons cette interface ServiceDistribution. On souhaite que le client puisse demander un ticket et que le serveur puisse lui fournir en retour un numéro de ticket qu il incrémentera à chaque demande. Dans une application classique l objet client appellera l objet serveur grâce à la méthode int donneticket() 9. C est donc cette méthode qui fait le lien entre le client et le serveur et qui devra donc être définie dans l interface distante. De plus, il est nécessaire que (cf partie précédente) l interface distante hérite de l interface Remote pour tirer parti de RMI chaque méthode de l interface distante retourne une exception de type RemoteException qui peut se produire à chaque appel distant 10 Enfin, nous avons fait le choix d inclure cette interface distante dans un package service, cette interface ServiceDistribution doit être déclaré dans le package service et le fichier ServiceDistribution.java doit donc se trouver dans le répertoire src\service. Le fichier ServiceDistribution.java contiendra donc le code suivant import java.rmi.*; //declaration du package d appartenance package service; // etendre interface à remote public interface ServiceDistribution extends Remote { //penser à lever les exceptions RemoteException public int donneticket() throws RemoteException; } 9 sous la forme int num=serveur.donneticket() 10 par exemple, lorsque le serveur ne répond plus 9

13 2.3 Écriture de l objet serveur Maintenant que le service a été décrit, il est possible d écrire l objet qui sera amené à répondre à ce service. Pour rappel, cet objet sera construit et exécuté sur le serveur cet objet doit naturellement implémenter le service défini La classe à construire est identique à une classe classique: cette classe possède des attributs, un(des) constructeur,... La seule différence réside dans le fait que cette classe implémente une interface héritant de Remote et qu on pourra, de ce fait, exporter des références distantes à partir de ses instances. Dans notre cas, la classe des objets devant répondre au service sera nommée Distributeur. Cette classe possédera un attribut entier num correspondant au dernier numéro de ticket distribué un constructeur chargé d initialiser num à 0 la méthode int donneticket() qui implémente le service De plus, cette classe Distributeur implémente l interface ServiceDistribution situé dans le package service appartient au package distributeur Les sources de Distributeur seront donc situées dans le répertoire source\distributeur du répertoire source et s écriront de la manière suivante //declaration du package d appartenance package distributeur; //importation du service distant import service.servicedistribution; //implementation du service distant public class Distributeur implements ServiceDistribution { //attributs classiques private int num; } //constructeur classique public Distributeur() { num=0; } //implementation de la méthode distante public int donneticket() { num++; return num; } 10

14 2.4 Écriture du main du serveur Pour finir le serveur, il reste à écrire un main qui se chargera de mettre ne place les différents elements, à savoir 1. créer l objet distributeur qui pourra distribuer les tickets; 2. construire une référence distante qui permettra aux clients de demander des tickets à distance; 3. diffuser cette référence distante en la stockant dans un rmiregistry local; 4. se mettre en attente de la réception d un appel distant. Ces différentes parties vont être détaillées dans les paragraphes suivants. 1. Créer objet distant Cette partie consiste simplement à créer un objet de type Distributeur dont la classe est accessible au niveau du serveur. Distributeur d=new Distributeur(); 2. Construire une référence distante La construction de la référence distante se fait grâce à la méthode statique export de la classe UnicastRemoteObject. Quelques remarques sont à ajouter la méthode à appeler est la méthode exportobject(remote obj, int port) qui prend deux paramètres 11 et pas la méthode exportobject(remote obj) qui fonctionne différemment; cette méthode retourne un objet de type Remote. Plus précisément, comme l export se fait à partir d un objet Distributeur, l objet retourné implémente l interface ServiceDistribution 12 mais N est PAS un objet de type Distributeur 13. Il faut donc faire un cast de cette référence en ServiceDistribution pour pouvoir ensuite appeler les méthodes décrites dans l interface distante. ServiceDistribution refdistante; refdistante=(servicedistribution)unicastremoteobject.export(d,0); 3. Diffuser la référence distante La diffusion de la référence distante ainsi construite se fait par le rmiregistry qui fait office d annuaire: il stocke cette référence et la fournit aux clients sur demande. Pour identifier cette référence, il faut lui attribuer un nom au moment de cet enregistrement. C est le même nom que le client utilisera pour la récupérer. Dans notre cas, nous appellerons cette référence LeDistributeur pour bien distinguer ce nom du nom de la classe. Pour faire cet enregistrement, on supposera qu un service rmiregistry est déjà présent sur la machine du serveur 14. Il suffit alors de récupérer un objet Registry lié au rmiregistry grâce à la classe LocateRegistry d enregistrer la référence distante grâce à la méthode bind() 15 de la classe Registry Registry reg=locateregistry.getregistry(); reg.bind(refdistante, "LeDistributeur"); 11 On utilisera un port par défaut en passant 0 au paramètre port 12 qui est bien un sous-type de Remote 13 il s agit d une classe frère qui se fera passer pour le distributeur au niveau du client 14 dans le cas contraire, il est possible d un créer un à partir de l application Java avec la méthode statique createregistry(int port) de la classe LocateRegistry 15 ou rebind s il s agit d écraser les références déja présentes 11

15 4. Mettre le serveur en attente La création de la référence distante crée un Thread d écoute, ainsi le Thread est déja crée et l application ne ferme pas malgré le fait que la JVM arrive à la fin du main. Code résultant En conséquence, la classe LanceServeur chargée de lancer le serveur se trouve dans le package distributeur et s écrira de la manière suivante //declaration du package d appartenance package distributeur; //importation du service distant import service.servicedistribution; //Cette classe est juste un main et n implémente rien public class LanceServeur { public static void main(string args[]) { //creation de l objet Distributeur d=new Distributeur(); //création de la référence distante ServiceDistribution refdistante; refdistante=(servicedistribution)unicastremoteobject.export(d,0); //enregistrement de la référence Registry reg=locateregistry.getregistry(); reg.bind(refdistante, "LeDistributeur"); } } //le serveur est pret System.out.println("serveur en attente"); 2.5 Écriture du main du client On suppose désormais que la classe Distributeur fonctionne sur la machine serveur pour se concentrer sur la partie client. à Dans notre exemple, la partie client est très simple et se limite à un simple main qui consiste récupérer la référence distante vers le distributeur que le serveur a stocké dans le rmiregistry utiliser cette référence distante pour utiliser le distributeur de ticket Récupérer la référence distante La récupération de la référence distante se fait grâce aux services proposés par le rmiregistry. Il faut pour cela localiser le rmiregistry avec la méthode getregistry(string host) de la classe LocateRegistry. Cela nécessite de posséder l adresse du serveur sur lequel tourne le rmiregistry Cette adresse peut être stockée en dur dans le code de l application, mais bien entendu, il vaut mieux la passer 12

16 récupérer la référence distante grâce à la méthode lookup(string name) de la classe Registry. Cette méthode nécessite aussi que le client connaisse le nom sous lequel la référence a été enregistrée. Nous avions opté pour LeDistributeur à l écriture du serveur. La méthode lookup retourne un objet implémentant Remote mais nous SAVONS par construction 17 que cet objet implémente bien l interface ServiceDistribution et nous pouvons donc faire un cast sans crainte. En supposant que le nom de l hôte est passé comme premier paramètre au main (à savoir args[0]), cela revient aux lignes suivantes Registre reg=locateregistry.get(args[0]); ServiceDistribution ref=reg.lookup("ledistributeur"); Utiliser la référence distante La référence distante implémente l interface ServiceDistribution, elle connaît donc les méthodes définies dans l interface distante 18. Il suffit donc simplement d appeler ces méthodes sur la référence. En d autres termes, la référence se fait passer pour l objet en répondant aux mêmes méthodes. Simplement, au lieu de répondre directement, l objet a redéfini les méthodes de l interface distante. Ces méthodes consistent désormais à contacter via le réseau l objet distant en lui fournissant la demande du client, à attendre la réponse de l objet distant et à reconstruire le résultat pour le retourner au client. Il faut juste penser à correctement récupérer les exceptions possibles lorsque le serveur ne répond pas. int num=ref.donneticket(); Code de la classe Client la manière suivante La classe Client déclarée dans le package client s écrira donc de en paramètre dans le main pour pouvoir changer l adresse du serveur au lancement du client sans recompilation 17 en écrivant le serveur 18 C est justement l intérêt de définir une interface distante chargée de déclarer les méthodes utilisables à distance 13

17 //declaration du package d appartenance package client; //importation du service distant import service.servicedistribution; //Cette classe est juste un main et n implémente rien public class Client { public static void main(string args[]) { //récuperation de la référence Registry reg=locateregistry.getregistry(args[0]); ServiceDistribution ref=(servicedistribution)reg.lookup("ledistributeur"); //le client est pret System.out.println("client pret, demande de ticket"); System.out.println("ticket n "+ref.donneticket()); } } 2.6 Compilation La partie compilation n est censée poser aucun problème, la seule difficulté réside dans la gestion des classpath et des répertoires (cf cours Cpt JAVA). Dans cet exemple, le script de compilation se trouve dans le répertoire racine alors que les packages (c est à dire les répertoires ayant pour noms les packages et contenant les sources correspondantes) se situent dans le sous-répertoire src. Il est donc nécessaire de préciser que le point d accés aux sources est dans ce répertoire src. La solution la plus simple 19, consiste à modifier le classpath avec l option -cp. Le classpath doit correspondre au répertoire src à partir duquel javac pourra trouver les package ainsi que les bonnes classes 20. De plus, on souhaite générer deux applications à partir des mêmes sources. Il va donc falloir effectuer deux compilations la première compilation se chargera de compiler le serveur à savoir la classe LanceServeur et toutes les classes liées pour alimenter le répertoire bin\appliserveur la seconde compilation se chargera de compiler le client à savoir la classe Client et toutes les classes liées pour alimenter le répertoire bin\appliclient Il est possible de spécifier le répertoire de destination de la compilation grâce à l option -d. Le script de compilation s écrira donc de cette manière javac -cp "src" -d "bin\appliserveur" distributeur.lanceserveur javac -cp "src" -d "bin\appliclient" client.client 19 Pour ne pas avoir à changer de répertoire 20 Pour rappel, le vrai identifiant d une classe est nompackage.nomclasse et javac ira chercher le source de la classe dans le fichier nompackage\nomclasse.java 14

18 Vous noterez que la compilation des classes LanceServeur et Client va impliquer la compilation des classes liées (exemple l interface distante dans le package service) et que javac reconstitue automatiquement la bonne hiérarchie de répertoire dans les fichiers.class pour bin\appliserveur et bin\appliclient 2.7 Exécution des applications Pour exécuter l application distribuée, il est nécessaire de lancer dans l ordre 1. le rmiregistry 2. l application serveur 3. l application client lancement du rmiregistry La solution la plus simple pour lancer les applications consiste à ne pas utiliser de codebase (la notion de codebase est détaillée dans les parties suivantes). Pour cela il suffit de lancer le rmiregistry dans le répertoire contenant les packages décrivant l interface distante. En d autre termes, le script à partir du répertoire racine lançant le rmiregistry s écrira cd bin/appliserveur rmiregistry la commande rmiregistry lance l annuaire de référence distante qui se met en attente. Il est possible d utiliser des options au lancement de rmiregistry pour afficher les logs de l annuaire rmiregistry -J-Dsun.rmi.log.debug=true -J-Dsun.rmi.server.exceptionTrace=true -J-Dsun.rmi.loader.logLevel=verbose -J-Dsun.rmi.dgc.logLevel=verbose -J-Dsun.rmi.transport.logLevel=verbose -J-Dsun.rmi.transport.tcp.logLevel=verbose Lancement du serveur Pour lancer le serveur, il faut lancer la classe distributeur.lanceserveur située dans le repertoire bin\appliserveur. Il faut noter que bin\appliserveur N est PAS un package mais simplement un répertoire qui contient les.class de l application serveur, les packages sont déclarés dans le code sources des classes au moment de leur écriture (structure logique). Il faut donc 1. se placer dans le bon répertoire bin\appliserveur en modifiant le classpath 2. lancer l application distributeur.lanceserveur à partir de ce répertoire 21 En supposant que le script d exécution du serveur se trouve dans le répertoire racine, il s écrit donc de la manière suivante java -cp "bin$\backslash$appliserveur" distributeur.lanceserveur 21 et non pas à partir du répertoire bin\appliserveur\distributeur à partir duquel la JVM ne trouvera pas les packages 15

19 Normalement, tout devrait correctement s exécuter sans lever d exception. En cas d exception levée, il y a de fortes chances que cela provienne du fait que le rmiregistry n est pas lancé du bon endroit (ou que le serveur ait un mauvais codebase - cf partie 3) Lancement du client Il ne reste plus qu à lancer le client en suivant le même raisonnement que dans la partie précédente et en n oubliant pas de donner le nom du serveur (pour les tests, tout se fait en local) java -cp "bin$\backslash$appliclient" client.client localhost 2.8 Mettre en place l application Une fois que les applications fonctionnent bien en local, il faut penser à les tester en situation réelle (serveur et client sur des machines différentes). Cela réserve parfois des surprises (liées à la notion de codebase et de telechargement dynamique de classe - cf partie 4.3). 16

20 3 Erreurs fréquemment rencontrées Les premiers TPs sur RMI génèrent de nombreuses erreurs, voici un guide succinct pour vous aider (en faisant référent aux chapitres vous fournissant les explications complémentaires) 3.1 illegal remote method encountered Exception: java.lang.illegalargumentexception: illegal remote method encountered: public abs Ce problème ne pose pas de problème à la compilation mais conduit à des erreur à l exécution au moment de exportobject. Origine Ce problème arrive lorsque l interface distante n est pas valide, en particulier, l erreur classique consiste à oublier de lever les RemoteExceptions dans l interface. Ajouter throws RemoteException dans les méthodes de l interface distante (cf par- Solution tie 2.2). 3.2 java.security.accesscontrolexception Exception in thread "main" java.security.accesscontrolexception: access denied (java.net.soc Ce problème ne pose pas de problème à la compilation, mais conduit à des erreurs à l exécution au moment du bind(). Origine Ce problème arrive lorsqu un codebase est passé pour transmettre dynamiquement du code(cf partie 4.3) mais que l utilisateur n a pas accordé l autorisation à l application de télécharger du code distant. Solution Il suffit de donner des droits à l application à son lancement. Pour cela, il faut ajouter un fichier texte décrivant ces droits (par exemple java.policy). grant { permission java.net.socketpermission "*: ","connect,accept,listen,resolve"; permission java.security.allpermission; }; et ensuite lancer l application en lui donnant ces droits dans la propriété java.security.policy java -Djava.security.policy=java.policy [...] Attention, ouvrir les droits de cette manière autorise l application à rapatrier et exécuter n importe quel code distant ce qui pose d énormes problèmes de sécurité - par exemple si un code distant cherche à effacer votre disque dur 3.3 UnmarshalException java.rmi.unmarshalexception: Error unmarshaling return; Ce problème se pose fréquemment au lancement du serveur lorsqu il essaie d enregistrer la référence distante sur le rmiregistry 17

21 Origine Ce problème provient d un mauvais codebase qui ne permet pas au rmiregistry de récupérer à distance le code de la référence distante (cf chapitre 4.3). Solution - cas simple Dans les cas simples (voire simplistes), il n y a pas besoin de charger dynamiquement du code et il suffit de lancer le rmiregistry dans le répertoire du serveur. Solution - cas complexe La solution consistant à lancer le rmiregistry au niveau du serveur ne résout pas le problème dans tous les cas car l application distribuée peut nécessiter de charger dynamiquement des classes (plus de détail dans le chapitre 4.3). Il est donc parfois nécessaire de préciser un codebase et vous DEVEZ savoir le faire pour pouvoir appréhender des solutions plus complexes. Il suffit habituellement de spécifier l URL où rmiregistry va pouvoir charger les classes nécessaires pour pouvoir exécuter la référence distante. Cela se fait en utilisant la propriété java.rmi.serveur.codebase. Cette URL doit être bien formée (cf partie 3.4) java -Djava.rmi.serveur.codebase=file:///c:/repertoire/ Serveur 3.4 MalformedURLException java.rmi.unmarshalexception: Error unmarshaling return; nested exception is: java.net.malformedurlexception: no protocol: codebase.jar L erreur apparaît à l exécution du serveur lorsqu il enregistre l objet dans le rmiregistry Origine L erreur est un problème de codebase (cf chapitre 4.3). Le codebase est bien spécifié au lancement du serveur mais l URL fournie n est pas une URL valide. Solution Vérifier la validité de l URL du codebase, en particulier une URL doit commencer par un protocole (à savoir file:/// ou une URL ne peut pas être un chemin relatif. une URL ne peut pas contenir d espace une URL contient des / et pas antislash \ (contrairement aux chemins windows) Exemple : file:///c:/... Une seconde solution dans les cas simples consiste à se passer de chargement dynamique de classe et de lancer le rmiregistry dans le répertoire du serveur (si il n y a pas d autres classes qui transitent entre le client et le serveur) 18

22 4 Compléments En cours de construction 4.1 Autre manière de voir RMI Avec RMI, il est possible de voir une application distribuée comme une simple application dont les classes sont réparties sur plusieurs machines. Une approche possible de développement 22 consisterait à développer un code classique en JAVA et à ensuite séparer les composants en rajoutant les interfaces distantes nécessaires. Dans l exemple du distributeur, cela consisterait à écrire les classes client et distributeur sans se préoccuper de l interface distante tester à partir d un seul main que tout fonctionne correctement. Le main serait chargé de créer des clients et un distributeur, et les objets clients font une simple demande en local sur l objet distributeur pour récupérer des tickets. A cette étape, l ensemble des appels se fait en LOCAL sur la même JVM. une fois les tests réalisés, couper l application en morceaux destinés à être exécutés sur des machines différentes (dans ce cas, les classes distributeur et client) chercher tous les appels de méthodes entre ces morceaux et en déduire les interfaces distantes: dans notre cas, client appelle la méthode donneticket de distributeur, cette méthode est donc le service proposé par le distributeur et devra être implémenté dans l interface distante reconstruire les classes et méthodes en utilisant les interfaces distantes et les références distantes redéployer et tester l application. 4.2 Passage de paramètres 4.3 Codebase 4.4 Sécurité 4.5 CallBack 22 qui ne semble pas efficace mais qui pourra vous donner une autre vision de RMI 19