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.
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:
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:
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.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.
3. Compilar y ejecutar el servidor
Ya tenemos definido el servidor. Para compilar sus clases seguimos los pasos:
javac MiInterfazRemoto.java jar cvf objRemotos.jar MiInterfazRemoto.classSe verán con más detalle los ficheros JAR en otro tema.
set CLASSPATH=%CLASSPATH%;.\objRemotos.jar;. javac MiClaseRemota.java rmic -d . MiClaseRemota (Windows)
export CLASSPATH=$CLASSPATH:./objRemotos.jar:. javac MiClaseRemota.java rmic -d . MiClaseRemota (Linux)Para ejecutar el servidor, seguimos los pasos:
grant { permission java.net.SocketPermission "*:1024-65535","connect, accept"; permission java.net.SocketPermission "*:80", "connect "; };
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.
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:
set CLASSPATH=%CLASSPATH%;.\objRemotos.jar;. javac MiClienteRMI.java (Windows)
export CLASSPATH=$CLASSPATH:./objRemotos.jar:. javac MiClienteRMI.java (Linux)Para ejecutar el cliente, seguimos los pasos:
java -Djava.rmi.server.codebase=http://juan/misclases -Djava.security.policy=java.policy MiClienteRMI 127.0.0.1 1200
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.
Los argumentos y los tipos de retorno que utilicemos en objetos remotos pueden ser de casi cualquier tipo Java, siempre que dicho tipo sea:
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.
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:
Debe implementar la interfaz remota (esto no cambia), y heredar de java.rmi.activation.Activatable (en lugar de UnicastRemoteObject)
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.