Master 1 Informatique | Programmation Répartie et Architecture N Tiers |
TD-TP - n°3 : DGC, Smart Proxies, Activation | |
Durée: 3H | |
Denis Caromel, Brian Amedro | |
Université Nice-Sophia Antipolis, Département Informatique |
Dans cette section nous allons ajouter des fonctionnalités au client et au serveur.
On veut ajouter une méthode sur l'interface RemoteClient
qui permet au Server
de notifier les clients quand un client arrive ou part du serveur. On veut aussi ajouter une méthode sur l'interface RemoteConnection
qui permet aux clients de demander au serveur la liste de tous les clients connectés.
Q |
Modifier
Tester vos modifications. Que ce passe-t-il ? Si vous obtenez des exceptions comment résoudre le problème ? |
---|
On veut maintenant permettre à un client d'envoyer un message directement à un autre client sans que les autres ne puisse le voir. Pour cela le client pourra taper sur la console :
(nom) textePour signifier qu'il veut envoyer le texte au client nom.
Q |
Ajoutez une méthode Dans l'implementation de private String findWho(String msg) { if (msg.charAt(0) != '(') return null; int i = msg.indexOf(')'); if (i == -1) return null; return msg.substring(1, i); } |
---|
Dans cette partie on veut comprendre comment fonctionne le garbage collector dans le cas d'une application distribuée. Pour une application non distribuée, le garbage collector de Java détruit tous les objets non référencés. Au lancement d'un programme Java, un ensemble de références racines est défini. A un instant donné, un objet accessible depuis une référence racine est considéré en vie, un objet non accessible depuis une référence racine est considéré mort et prêt à être réclamé par le garbage collector.
Dans le cas de RMI les choses se compliquent quelque peut. Lorsqu'un objet Remote est instancié, il peut être référencé directement en local mais il peut aussi être indirectement référencé par des Stub distants. Par exemple, dans l'exemple du chat serveur l'objet RemoteConnectionImpl
n'est pas directement référencé dans la JVM du serveur où il existe mais il est utilisé par le Stub qui a été envoyé au client.
La solution adoptée dans RMI est que le runtime RMI garde une référence sur l'objet remote pendant une certaine durée appelée lease. A chaque communication avec l'objet distant le décompte de temps de lease est remis à zéro. S'il n'y a pas de communication c'est à la charge du client de renouveler le lease par un appel distant. Si le client ne renouvelle pas ce lease, le DGC le considère comme mort.
Pour chaque objet Remote, RMI garde un compteur pour connaître combien de clients actifs possèdent un Stub sur l'objet. Quand le lease d'un client expire et n'est pas renouvelé, le compteur est decrémente. Quand il est à zéro, l'objet peut être réclamé par le garbage collector.
Il est possible de définir la valeur du lease en utilisant la propriété -Djava.rmi.dgc.leaseValue=tms
où tms
est le temps de lease en millisecondes. Notez que plus le lease est petit, plus les performances seront dégradés par des clients qui communiquent uniquement pour renouveler le lease. Plus le lease est élevé, plus les chances d'occuper la mémoire par des objets remote obsolètes sont grandes.
Notez bien qu'il n'y a rien a faire pour que le DGC de RMI fonctionne. Le but de cette section est uniquement de montrer que le DGC est bien actif dans une application RMI donnée.
Q |
|
---|
Le but de cette partie est de montrer l'utilisation de proxies intelligents (smart proxies) en addition des Stub RMI. Cette section permettra aussi de montrer une technique de reengineering bien connue appelée refactoring qui permet de transformer un programme existant en appliquant des design patterns. Dans la suite j'ai mis les termes français entre parenthèses car leur utilisation est marginale la ou la littérature est principalement en anglais.
Un Stub RMI est, en terme de design patterns (modèles de conception), un remote proxy aussi appelé surrogate (subrogé). Le but du Proxy pattern (Procuration modèle) est de fournir un surrogate ou coquille qui représente un autre objet. Il doit être utilisé quand il est nécessaire d'avoir une référence d'objet plus sophistiquée ou versatile que la simple référence (pointeur). C'est typiquement le cas du Stub qui permet d'accéder un objet distant comme s'il était local en masquant la complexité de la mécanique RMI.
Le principe du Stub RMI est de déléguer directement tous les appels fait sur lui sur son objet cible qui se trouve dans une autre JVM. Cela signifie que chaque appel de méthode sur le Stub RMI déclenche une communication TCP/IP pour réaliser l'appel distant. Ceci est bien sûr moins efficace qu'un appel local. Si dans certains cas c'est cependant nécessaire, dans d'autres il est possible d'éviter ce coût en stockant dans le Remote proxy le résultat des appels distants. Cela est possible quand l'information retournée ne change pas ou peu.
Pour illustrer ce propos nous allons changer l'interface du client et du serveur pour ne plus passer le nom du client en paramètre.
Q |
|
---|
Q |
Recompilez et vérifiez que tout fonctionne bien. Qu'observez-vous du côté du client quand la méthode |
---|
La conséquence des modifications
ci-dessus est que dans l'implémentation du logon
le
serveur fait un appel distant sur le client pour récupérer son nom. On
peut imaginer que le serveur va utiliser cette méthode
getName()
maintenant qu'elle est disponible. Chaque appel
va être transmis au client par une communication TCP/IP qui pénalise
les performances. Or le nom du client est une donnée qui ne peut pas
changer pendant toute la durée de vie du client. Cette donnée peut
donc être transmise au serveur une fois pour toute et mémorisée au
niveau du Stub
du client.
Malheureusement le
Stub
du client est une classe générée par RMI sur
laquelle on n'a aucun contrôle. La solution est d'écrire notre propre
proxy qui transmettra les appels de méthodes au Stub
RMI
ou les traitera localement si c'est possible.
Notre proxy n'est pas un
objet Remote
mais il est Serializable
pour
pouvoir être transmis au serveur par copie. Dans les variables
d'instances de ce proxy il y aura une référence sur le client qui se
transformera en une référence sur le Stub
du client au
moment de la sérialization.
Dans un premier temps on veut créer
une interface Client
qui est identique à
RemoteClient
à la différence qu'elle n'étend pas
java.rmi.Remote
. C'est cette interface que notre proxy va
implémenter.
Q |
|
---|
Le smart proxy (ClientProxy
) implémente l'interface Client
. Il est construit a partir d'un RemoteClient
. Dans le constructeur il récupère le nom du client et le place dans une variable. On donne ci-dessous le code partiel du Proxy.
package chat.client; import chat.remote.Client; import chat.remote.RemoteClient; public class ClientProxy implements Client, java.io.Serializable { private String clientName; private RemoteClient remoteClient; // // -- CONSTRUCTORS ------------------------------------------------- // public ClientProxy(RemoteClient remoteClient) { this.remoteClient = remoteClient; try { this.clientName = remoteClient.getName(); } catch (java.rmi.RemoteException e) { this.clientName = "unknown"; } } // // -- PUBLIC METHODS ------------------------------------------------- // // // -- implements Client ------------------------------------------------- // // *********** A FAIRE ********************** // // -- PRIVATE METHODS FOR SERIALIZATION ------------------------------------------------- // // NOTE : le code ci-dessous n'est pas necessaire pour serializer. // Il est juste utile pour afficher des messages. private void writeObject(java.io.ObjectOutputStream out) throws java.io.IOException { System.out.println("Thread="+Thread.currentThread().getName()+ " is serializing the ClientProxy for "+clientName); System.out.println(" remoteClient is instanceof "+remoteClient.getClass().getName()); out.defaultWriteObject(); } private void readObject(java.io.ObjectInputStream in) throws java.io.IOException, ClassNotFoundException { in.defaultReadObject(); System.out.println("Thread="+Thread.currentThread().getName()+ " is deserializing the ClientProxy for "+clientName); System.out.println(" remoteClient is instanceof "+remoteClient.getClass().getName()); } }
Q |
Créez la classe |
---|
Du côté client il faut maintenant envoyer au serveur une instance de ClientProxy
au lieu d'une référence sur ClientImpl
. Du côté serveur il faut supprimer la dépendance sur l'interface RemoteClient
et la remplacer par une dépendance sur Client
qui implémente les mêmes méthodes mais n'est pas remote.
Q |
|
---|
Q |
Recompilez le client et le serveur et executez l'ensemble. Observez. |
---|
Le but de cette section est d'utiliser la capacité d'activation automatique de RMI. Pour cela on va toujours se baser sur le Serveur et créer un ActivatableServerImpl
qui pourra automatiquement être activé par RMI à la première requête d'un client.
Pour être activable une classe doit étendre la classe Activable
. Comparativement à un UnicastRemoteObject
, un objet qui étend la classe Activatable
est toujours un objet RMI qui fonctionne de la même manière, sinon qu'il persiste de manière permanente et peut être réactivé à la demande.
Q |
Créez une nouvelle classe appelée public ActivableServerImpl(java.rmi.activation.ActivationID id, java.rmi.MarshalledObject data) throws java.rmi.RemoteException { super(id, 0); System.out.println("ActivableServerImpl activated with id="+id); chatServer = new ServerImpl(); } |
---|
La méthode main
est inutile car cette classe ne va pas être invoquée directement mais indirectement par RMI au moment de l'activation. Notez que la méthode logon
ne change pas.
Le rôle du setup est d'enregistrer la classe activable auprès du démon d'activation de RMI. Pour cela il faut suivre un certain nombre d'étapes qui sont les suivantes :
On notera que l'instance du stub est crée sans instancier l'objet (ici le ActivableServerImpl
) sur lequel les appels seront redirigés.
Q |
Créer la classe package chat.server; import chat.remote.RemoteServer; /** Implementation of Server */ public class ActivableServerSetup { private static final String userDir = System.getProperty("user.dir"); // // -- CONSTRUCTORS ------------------------------------------------- // // // -- PUBLIC METHODS ------------------------------------------------- // public static void main(String[] args) { try { if (args.length < 1) { System.out.println("You must give the name of the server as an argument"); System.exit(1); } String serverName = args[0]; // Create and install a security manager if (System.getSecurityManager() == null) { System.setSecurityManager(new java.rmi.RMISecurityManager()); } java.rmi.activation.ActivationGroupDesc chatServerGroup = new java.rmi.activation.ActivationGroupDesc(new java.util.Properties(), null); // Once the ActivationGroupDesc has been created, register it // with the activation system to obtain its ID java.rmi.activation.ActivationGroupID agID = java.rmi.activation.ActivationGroup.getSystem().registerGroup(chatServerGroup); // The "location" String specifies a URL from where the class // definition will come when this object is requested (activated). String location = System.getProperty("java.rmi.server.codebase"); // Create the rest of the parameters that will be passed to // the ActivationDesc constructor java.rmi.MarshalledObject data = null; // The location argument to the ActivationDesc constructor will be used // to uniquely identify this class; it's location is relative to the // URL-formatted String, location. java.rmi.activation.ActivationDesc descriptor = new java.rmi.activation.ActivationDesc(agID, ActivableServerImpl.class.getName(), location, data); RemoteServer activableChatServer = (RemoteServer) java.rmi.activation.Activatable.register(descriptor); System.out.println("Got the stub for the ActivatableImplementation "+ "of RemoteServer"); java.rmi.Naming.rebind(serverName, activableChatServer); System.out.println("An activable Stub on ActivatableServerImpl has \n"+ "been bound in the RMI Registry under URL "); System.out.println(" rmi://"+getHostName()+"/"+serverName); // exit System.exit(0); } catch (Exception e) { System.out.println("ActivableServerSetup err: " + e.getMessage()); e.printStackTrace(); } } // // -- PRIVATE METHODS ------------------------------------------------- // private static String getHostName() { try { return java.net.InetAddress.getLocalHost().getHostName(); } catch (java.net.UnknownHostException e) { return "Unknown"; } } } |
---|
Avant de tester ne pas oublier de recompiler les deux classes créées et de générer le stub de la classe ActivableServerImpl
.
Pour tester l'activation il faut lancer le démon rmid
sans classpath!!! La procédure est exactement la même que pour la version non-activable à la différence qu'on ne lance pas le Serveur mais la classe de setup qui l'enregistre auprès du runtime RMI. Vous pouvez suivre les étapes suivantes :
rmid
sans paramètre et sans classpath)ActivableServerSetup
en utilisant une commande telle que :
java -Djava.security.policy=java.policy \ -Djava.rmi.server.codebase=http://machine.unice.fr:3000/ \ chat.server.ActivableServerSetup MonServer
Q |
Observez les différentes consoles, en particulier celles des deux classServers et celle du |
---|