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

1. Programmation

Dans cette section nous allons ajouter des fonctionnalités au client et au serveur.

1.1. Ajout d'une notification du client

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 RemoteClient pour ajouter une méthode clientChangeNotification() qui permet au client de recevoir la notification. Modifier ensuite RemoteConnection pour ajouter une méthode String[] who() qui retourne un tableau des noms de tous les clients. Implémenter ces deux méthodes dans les implémentations respectives.

On notera que :
  • ServerImpl implémente déjà une méthode String[] getClientsName() qui peut être utilisée depuis RemoteConnectionImpl.
  • ServerImpl possède déjà le code pour notifier les clients. Dans le fichier ServerImpl.java vous pouvez constater que les methodes addClient et removeClient font un appel à notifyAllClients(). La seule chose a faire pour activer la notification des client et d'enlever le commentaire de la ligne 142 :
    private class ClientChangeNotificationCall implements ClientCall {
      public void doCall(RemoteClient client, String name) 
                                    throws java.rmi.RemoteException {
        client.clientChangeNotification();
      }
    }
  • Dans l'implémentation de clientChangeNotification() vous devez appeler la methode String[] who() sur RemoteConnection pour afficher les clients connectés.

Tester vos modifications. Que ce passe-t-il ? Si vous obtenez des exceptions comment résoudre le problème ?

1.2. Ajout de messages privés

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) texte
Pour signifier qu'il veut envoyer le texte au client nom.

Q

Ajoutez une méthode void say(String who, String msg) sur l'interface RemoteConnection qui permet d'envoyer le message à un client donne. Implémentez cette méthode et ajoutez dans le client le code nécessaire pour supporter cette nouvelle fonctionnalité. On notera que Server implémente déjà une méthode boolean targetMessage(String msg, String from, String to) qui peut être utilisée depuis RemoteConnectionImpl.

Dans l'implementation de Client on pourra utiliser une methode pour extraire le nom du client depuis le texte tapez sur la console :

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

2. Distributed Garbage Collector (DGC)

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=tmstms 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
  1. Modifier la classe RemoteConnectionImpl pour implémenter l'interface java.rmi.server.Unreferenced qui permet d'être notifié quand le DGC a déterminé que l'objet pouvait être garbage collected. Dans l'implémentation de unreferenced on écrira simplement un message sur la console pour signaler l'événement. Ne pas oublier de préciser le nom du client dans le message.
  2. Toujours dans la classe RemoteConnectionImpl redéfinissez la méthode finalize() pour afficher un message quand l'objet est effectivement détruit par le garbage collector. Ne pas oublie de préciser le nom du client dans le message.
  3. Recompilez la classe RemoteConnectionImpl et relancez le chat serveur. Important : quand vous lancez le RemoteServerImpl spécifiez une valeur de lease de 10000ms (10 secondes) en utilisant la propriété -Djava.rmi.dgc.leaseValue=10000. Note : on changera aussi la propriété checkInterval interne au DGC pour faciliter les observations. Cette propriété définie à quelle frequence le DGC vérifie les leases expirés. Pour cela ajoutez -Dsun.rmi.dgc.checkInterval=20000 à la suite des autres propriétés quand vous lancez le serveur.
  4. Lancez plusieurs clients, stoppez les, relancez en d'autres successivement pour observer ce qui se passe au niveau du serveur.

3. Smart proxy

3.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.

3.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 RemoteServerImpl 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 ?

3.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.

3.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.

3.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.

3.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.

4. 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.

4.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.

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

}
		

4.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 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 sans paramètre et 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.