7. Objetos Distribuidos

 

7.1. Objetos distribuidos en Java

Para comunicar aplicaciones distribuidas en Java, se emplea la tecnología RMI (Remote Method Invocation), que permite a un objeto ejecutando en una máquina virtual Java invocar a métodos de un objeto en otra máquina virtual Java, y obtener objetos existentes en dicha máquina remota.

Toda aplicación RMI normalmente se descompone en 2 partes: un cliente y un servidor.


RMI trata de forma distinta a los objetos remotos, con respecto a cómo trata a los locales: en lugar de hacer una copia local del objeto remoto, lo que hace es obtener un Stub y un Skel. El Stub es la representación local del objeto remoto (es una referencia local en el cliente al objeto remoto). Por otra parte, el Skel es lo correspondiente en la parte del servidor, es la parte del objeto remoto en el servidor que comunica con el Stub que hay en el cliente, y entre ambos circulan los datos por la red.

7.2. Creación de un servidor RMI

Un servidor RMI consiste en definir un objeto remoto que va a ser utilizado por los clientes. Para crear un objeto remoto, se define un interfaz, y el objeto remoto será una clase que implementa dicha interfaz. Veremos ahora los pasos a seguir para crear un servidor de ejemplo.

1. Definir el interfaz remoto

Cuando se crea un interfaz remoto:

Veamos un ejemplo:
public interface MiInterfazRemoto extends java.rmi.Remote
{
  public void miMetodo1() throws java.rmi.RemoteException;
  public int miMetodo2() throws java.rmi.RemoteException;
}
2. Implementar el interfaz remoto
public class MiClaseRemota  
extends java.rmi.server.UnicastRemoteObject
implements MiInterfazRemoto
{
   public MiClaseRemota() throws java.rmi.RemoteException
   {
    ... // Codigo del constructor
   }
     
   public void miMetodo1() throws java.rmi.RemoteException
   {
    ... // Aquí pondremos el código que queramos
   }
     
   public int miMetodo2() throws java.rmi.RemoteException
   {
    ... // Aquí pondremos el código que queramos
   }
     
   public void otroMetodo()
   {
    ... // Si definieramos este método no podría llamarse 
	// remotamente al no ser del interfaz remoto
   }
     
   public static void main(String[] args)
   {
     if (System.getSecurityManager() == null)
      System.setSecurityManager(new RMISecurityManager());
     try
     {
       MiInterfazRemoto mir = new MiClaseRemota();
       Naming.rebind("//" + 
	   java.net.InetAddress.getLocalHost().getHostAddress() + 
	   ":" + args[0] + "/PruebaRMI", mir);
     } catch (Exception e) { }
   }
}
La clase implementa la interfaz que hemos definido previamente. Además, hereda de UnicastRemoteObject, que es una clase de Java que podemos utilizar (no tenemos por qué) como superclase para implementar objetos remotos, puesto que redefine muchos métodos de Object para poder utilizarlos remotamente.

Luego, dentro de la clase, definimos un constructor (que lanza la excepción RemoteException porque también la lanza la superclase UnicastRemoteObject), y los métodos de la/las interfaz/interfaces que implemente, además de los métodos adicionales que queramos definir (que no podrán llamarse remotamente, serán locales a la máquina servidor).

Finalmente, en el método main se define el código para:

Naming.rebind(nombre, objeto_remoto);

de forma que ante una petición del objeto se buscará por su nombre. Dicho nombre contiene la dirección IP (y opcionalmente el puerto) donde se colocará el servidor RMI, y el nombre que identifica al objeto remoto, quedando:

"//direccion_IP:puerto/nombre"

En este caso:

"//" + 
java.net.InetAddress.getLocalHost().getHostAddress() + 
":" + 
args[0] + 
"/PruebaRMI"

La clase InetAddress se utiliza para obtener la dirección IP de la máquina local. Luego se concatena con el puerto por el que se escucharán las peticiones de los clientes (que se pasa como parámetro del main(), y por defecto es el 1099). El final de la cadena (PruebaRMI) es el nombre que le damos al objeto RMI. Cuando un cliente quiera un objeto de este tipo, buscará un objeto con el nombre indicado, como veremos más adelante.

Notar que el método main() se coloca dentro de la misma clase servidor por comodidad. Podría definirse otra clase aparte que fuese la que registrara el objeto remoto.
 

3. Compilar y ejecutar el servidor

Ya tenemos definido el servidor. Para compilar sus clases seguimos los pasos:

Para ejecutar el servidor, seguimos los pasos:
Donde se da permiso para conectar y aceptar conexiones con cualquier puerto no reservado, y conectar por el puerto 80.
start rmiregistry <puerto>
(Windows)
rmiregistry <puerto> &		
(Linux)
java -Djava.rmi.server.hostname=127.0.0.1
     -Djava.rmi.server.codebase=file:///home/pepe/
     -Djava.security.policy=java.policy
     MiClaseRemota 1200
Donde con –Djava.rmi.server.hostname se indica el nombre del servidor (para asegurarnos de que RMI lo obtiene), con –Djava.rmi.server.codebase se indica la URL donde están las clases que se envían a otras máquinas (es opcional, podría no ser necesario; haría falta si el cliente no encuentra las clases del servidor a las que debe acceder), y finalmente –Djava.security.policy indica el fichero de política de seguridad que se tiene (el fichero creado antes). Al servidor se le pasaba como parámetro, recordemos, el puerto por el que escuchar peticiones.

 

7.3. Creación de un cliente RMI

Vamos a definir el cliente que accederá a el/los objeto/s remoto/s que creemos.

1. Definir la clase para obtener los objetos remotos que se quieran

La siguiente clase obtiene un objeto de tipo MiInterfazRemoto, implementado en el servidor:

public class MiClienteRMI
{
  public  static void main(String[] args)
  {
     if (System.getSecurityManager() == null)
        System.setSecurityManager(new RMISecurityManager());
     try
     {
        MiInterfazRemoto mir =
        (MiInterfazRemoto)Naming.lookup("//" + args[0] + ":" + 
					args[1] +"/PruebaRMI");
        mir.miMetodo1();
        int i = mir.miMetodo2();
     } catch (Exception e) { e.printStackTrace(); }
  }
}
Donde se siguen los pasos:
"//direccion_IP:puerto/nombre"


2. Compilar y ejecutar el cliente

Ya tenemos definido el cliente. Para compilarlo seguimos los pasos:

Para ejecutar el cliente, seguimos los pasos:
Donde los parámetros sirven para lo mismo que en el servidor. Al cliente se le pasan como parámetros la dirección IP del servidor y el puerto por el que estará escuchando.

 

7.4. Acceso a objetos remotos. Serialización

Los argumentos y los tipos de retorno que utilicemos en objetos remotos pueden ser de casi cualquier tipo Java, siempre que dicho tipo sea:

Hay algunos objetos Java que no son serializables (por ejemplo, imágenes), y por lo tanto no pueden ser pasados a ni devueltos por objetos remotos, es decir, no pueden circular por la conexión RMI. Pero muchas clases de utilidad sí definen dicho interfaz, y también podemos definirnos nuestras propias clases que lo implementen.

Por ejemplo, si queremos obtener un objeto de tipo MiParametro de un método de un objeto remoto:

public interface MiInterfazRemoto2 extends java.rmi.Remote
{
  public MiParametro miMetodo() throws java.rmi.RemoteException;
  ...
}
Lo que hacemos es definir ese objeto como serializable:
public class MiParametro implements java.io.Serializable
{
  ...
}
Tenemos que asegurarnos también de que los campos que contiene son objetos serializables a su vez. No podemos, por ejemplo, encapsular un campo Image en una clase serializable, puesto que el campo Image no lo es.

 

7.5. Servidores activables

Hasta Java 1.2, el objeto remoto a compartir se creaba al principio, y tenía que estar siempre a disposición de los clientes. A partir de la versión 1.2 se pueden crear objetos remotos cuando un cliente lo solicite, con el consiguiente ahorro en el consumo de recursos. Esto se consigue partiendo de la clase java.rmi.activation.Activatable . y del demonio rmid, que se encarga de gestionar el objeto remoto. Veremos ahora los pasos para construir este tipo de servidores.

1. Definir el interfaz remoto

Este paso es idéntico al explicado para el servidor normal, visto más arriba.

2. Implementar el interfaz remoto

La clase que implementa el interfaz remoto tiene algunos cambios con respecto a la definida para el servidor normal. Los pasos a seguir son:

Un ejemplo de servidor de este tipo sería:
import java.rmi.*;
import java.rmi.activation.*;
                                   
public class MiClaseRemota2 
extends java.rmi.activation.Activatable
implements MiInterfazRemoto
{
  public MiClaseRemota2(ActivationID a, MarshalledObject m) 
  throws java.rmi.RemoteException
  {
    super(a, 0);
  }
                                   
  public void  miMetodo1() throws java.rmi.RemoteException
  {
    ... // Aquí pondremos el código que queramos
  }
                                   
  public int  miMetodo2() throws java.rmi.RemoteException
  {
    ... // Aquí pondremos el código que queramos
  }
                                   
  public static void main(String[] args) throws Exception
  {
     System.setSecurityManager(new RMISecurityManager());
                                   
     Properties p = new Properties();
     p.put("java.security.policy", "/rmi/servidor2/java.policy");

     ActivationGroupDesc.CommandEnvironment ace = null;
     ActivationGroupDesc ejemplo = 
	new ActivationGroupDesc(p, ace);

     ActivationGroupID agi = 
	ActivationGroup.getSystem().registerGroup(ejemplo);
     MarshalledObject m = null;

     ActivationDesc desc = new ActivationDesc 
			   (agi, "MiClaseRemota2", 
			   "file://C:/rmi/servidor2/", m);
   
     MiInterfazRemoto mir = 
	(MiInterfazRemoto)Activatable.register(desc);
     Naming.rebind("//" + 
	java.net.InetAddress.getLocalHost().getHostAddress() + 
	":" + args[0] + "/PruebaRMI");
     System.exit(0);
  }
}
El servidor es muy parecido al anterior, salvo porque en el constructor se pasan dos parámetros, y porque el método main() tiene una forma distinta de registrarlo:

Con eso, se obtiene un objeto remoto (MiInterfazRemoto), llamando al método register() de Activatable, pasándole como parámetro el ActivationDesc creado con todos los pasos anteriores. Después ya se tiene el método rebind() visto en el servidor primero.
 

3. Compilar y ejecutar el servidor

Para compilar el servidor se siguen los mismos pasos que para el servidor normal. Para ejecutarlo también, salvo que se debe lanzar también el demonio rmid (antes o después de lanzar el rmiregistry):

start rmid -J-Djava.security.policy=java.policy	
(Windows)
rmid -J-Djava.security.policy=java.policy &	
(Linux)
Se debe indicar el fichero de política de seguridad para indicar al demonio que se permite acceder a dicho fichero como parámetro.
 

 

NOTA: tanto los parámetros -Djava... que se utilizan para ejecutar servidores y clientes como la gestión de seguridad mediante ficheros java.policy y objetos SecurityManager son elementos opcionales. Se mencionan aquí para tener la forma general de una aplicación RMI, aunque dependiendo del tipo de aplicación se pueden omitir unos elementos u otros.

 

Aquí tenéis un fichero ZIP con el ejemplo completo de la sesión:

compilar
ejecutar 1200

(Windows)
. ./compilar.sh
. ./ejecutar.sh 1200

(Linux)
ejecutar 192.168.12.1 1200		
(Windows)
. ./ejecutar.sh 192.168.12.1 1200	
(Linux)

Podéis probar a conectar un cliente con un servidor RMI colocado en una máquina distinta, y probar así la verdadera utilidad de RMI.