Maîtrise InformatiqueProgrammation Répartie et Architecture 3 Tiers
 

TD-TP - no 5 : Smart Proxies / Activation

 
Denis Caromel, Christian Delbé
Université Nice-Sophia Antipolis, Département Informatique

1. Smart proxy

1.1. Principes

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.

1.2. Illustration

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
  • Dans l'interface RemoteClient ajouter une méthode getName() qui retourne le nom du client.
  • Dans l'interface RemoteServer changer la signature de la methode logon pour ne plus passer le nom du client en paramètre mais juste le client.
  • Dans la classe ClientImpl changez l'appel à logon pour ne plus passer le nom du client et implémentez la nouvelle méthode getName(). Note important : dans l'implémentation de getName(), faite un System.out.println pour afficher le nom du thread (Thread.currentThread().getName()) réalisant l'appel.
  • Dans la classe ServerImpl modifier l'implémentation de logon pour ne plus prendre le nom du client en paramètre et utiliser la nouvelle méthode getName() de l'interface du client.
Q

Recompilez et vérifiez que tout fonctionne bien. Qu'observez-vous du côté du client quand la méthode getName() est appelée ?

1.3. Pourquoi un Smart Proxy ?

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.

1.4. Changement de l'interface Client

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
  • Dans le répertoire src/chat/remote/, copiez RemoteClient.java en Client.java, puis éditez ce nouveau fichier pour renommer l'interface et enlever le extends java.rmi.Remote.
  • Changez ensuite RemoteClient pour qu'elle étende la nouvelle interface Client.java en plus de extends java.rmi.Remote. L'interface RemoteClient ne doit plus maintenant comporter une seule méthode puisse qu'elles sont toutes définies dans Client.

1.5. Implementation du Smart Proxy

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 ClientProxy dans le package chat.client à l'aide du code donné ci-dessus. Compléter l'implémentation de Client. Dans l'implémentation de la méthode getName() on écrira un message pour afficher le nom du thread (Thread.currentThread().getName()) réalisant l'appel.

1.6. Utilisation du Smart Proxy

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
  • Modifier le fichier ClientImpl pour passer dans l'appel de la méthode logon une nouvelle instance de ClientProxy au lieu de this.

  • Dans le répertoire serverSide, dans le package chat.remote supprimez complètement l'interface RemoteClient. Dans ce même package copier la nouvelle interface Client que vous avez définie dans le module client.

  • Dans le module serveur il faut maintenant remplacer toutes les dépendances à RemoteClient par une dépendance sur Client. Pour cela un simple chercher-remplacer est nécessaire puisque juste le nom change (les méthodes sont les mêmes). Vous pouvez faire cela à la main : dans le module server il faut remplacer toutes les occurrences de RemoteClient par Client. Vous pouvez aussi faire ça avec un script shell :

    cd ~/td03/src/chat/server
    find . -name "*.java" -exec rename.sh {} \;
    
    où le fichier rename.sh est le script suivant :
    cp $1 ${1}.bak  ; sed 's/RemoteClient/Client/g' ${1}.bak > $1 ; rm ${1}.bak
    

    Faîtes un backup de vos fichiers avant d'utiliser le script au cas où ...

Q

Recompilez le client et le serveur et executez l'ensemble. Observez.

2. Activation

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.

2.1. Création d'un Serveur activable

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 ActivableServerImpl qui étend java.rmi.activation.Activatable . Cette classe est similaire à RemoteServerImpl à la différence qu'elle ne comporte pas de méthode main, et qu'elle étend la classe java.rmi.activation.Activatable et qu'elle comporte un constructeur à deux paramètres au lieu du constructeur vide :

  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.

2.2. Activation setup

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 :

  1. créer un groupe d'activation pour l'objet à activer
  2. enregistrer ce nouveau groupe auprès du Runtime RMI afin d'obtenir un unique identifiant
  3. créer un descripteur d'activation basé sur la classe activable et sur cet identifiant
  4. enregistrer ce descripteur auprès du runtime afin d'obtenir une instance de stub sur l'objet activable
  5. enregistrer cette instance dans le registre RMI afin que les clients puissent l'utiliser.

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 ActivableServerSetup basée sur le code ci-dessous :

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";
    }
  }

}
		

2.3. Test de l'activation

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 -C-Djava.security.policy=java.policy -J-Dsun.rmi.activation.execPolicy=none 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 :

  1. lancez le RMIRegistry
  2. lancez le RMI démon (commande rmid -C-Djava.security.policy=java.policy -J-Dsun.rmi.activation.execPolicy=none sans classpath)
  3. lancez le classServer du Serveur
  4. lancez le classServer des clients
  5. lancez le setup d'activation, c'est à dire la classe 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
    		
  6. lancez plusieurs clients

Q

Observez les différentes consoles, en particulier celles des deux classServers et celle du rmid.