Skip to content

9. JAVA RMI

9.1. Introducción

Hemos visto cómo crear aplicaciones de red utilizando las herramientas de comunicación denominadas sockets. En una aplicación cliente/servidor basada en estas herramientas, el vínculo entre el cliente y el servidor es el protocolo de comunicación que han adoptado para comunicarse. Ambas aplicaciones pueden estar escritas en lenguajes diferentes: Java, por ejemplo, para el cliente; Perl para el servidor; o cualquier otra combinación. De hecho, se trata de dos aplicaciones distintas conectadas por un protocolo de comunicación conocido por ambas. Por otra parte, el acceso a la red a través de sockets no es transparente para una aplicación Java: debe utilizar la clase Socket, creada especialmente para gestionar estas herramientas de comunicación que son las sockets.

JAVA RMI (Invocación de Métodos Remotos) permite crear aplicaciones de red con las siguientes características:

  1. Las aplicaciones cliente/servidor son aplicaciones Java en ambos extremos de la comunicación
  2. El cliente puede utilizar objetos ubicados en el servidor como si fueran locales
  3. La capa de red se vuelve transparente: las aplicaciones no tienen que preocuparse por la forma en que se transporta la información de un punto a otro.

El último punto es un factor de portabilidad: si la capa de red de una aplicación RMI llegara a cambiar, no sería necesario reescribir la aplicación en sí. Serán las clases RMI del lenguaje Java las que deberán adaptarse a la nueva capa de red.

El principio de una comunicación RMI es el siguiente:

  1. Se escribe una aplicación Java convencional en una máquina A. Esta desempeñará el papel de servidor. Para ello, algunos de sus objetos serán «publicados» en la máquina A en la que se ejecuta la aplicación y, de este modo, se convertirán en servicios.
  2. Se escribe una aplicación Java convencional en una máquina B. Esta desempeñará el papel de cliente. Tendrá acceso a los objetos/servicios publicados en la máquina A; es decir, a través de una referencia remota, podrá manipularlos como si fueran locales. Para ello, necesitará conocer la estructura del objeto remoto al que desea acceder (métodos y propiedades).

9.2. Aprendamos con un ejemplo

La teoría que subyace a la interfaz RMI no es sencilla. Para entenderla mejor, vamos a seguir paso a paso la creación de una aplicación cliente/servidor que utilice el paquete RMI de Java. Tomamos una aplicación que se encuentra en numerosas publicaciones sobre RMI: el cliente invoca un único método de un objeto remoto que, a su vez, le devuelve una cadena de caracteres. Aquí presentamos una ligera variante: el servidor repite lo que le envía el cliente. Ya hemos presentado en este libro una aplicación de este tipo que, a su vez, se basa en sockets.

9.2.1. La aplicación del servidor

9.2.1.1. Paso 1: la interfaz del objeto/servidor

Un objeto remoto es una instancia de clase que debe implementar la interfaz Remote definida en el paquete java.rmi. Los métodos del objeto a los que se podrá acceder de forma remota son aquellos declarados en una interfaz derivada de la interfaz Remote:

import java.rmi.*;

// la interfaz remota
public interface interEcho extends Remote{
    public String echo(String msg) throws java.rmi.RemoteException;
}

Aquí, por lo tanto, se declara una interfaz interEcho que declara un método echo como accesible de forma remota. Este método puede generar una excepción de la clase RemoteException, clase que agrupa todos los errores relacionados con la red.

9.2.1.2. Paso 2: creación del objeto servidor

En el siguiente paso, se define la clase que implementa la interfaz remota anterior. Esta clase debe derivarse de la clase UnicastRemoteObject, la cual cuenta con los métodos que permiten invocar métodos de forma remota.

import java.rmi.*;
import java.rmi.server.*;
import java.net.*;

// clase que implementa el eco remoto
public class srvEcho extends UnicastRemoteObject implements interEcho{

    // constructor
    public srvEcho() throws RemoteException{
        super();
    }// fin del constructor

    // método que implementa el eco
    public String echo(String msg) throws RemoteException{
        return  "["  + msg + "]";
    }// fin del eco
}// fin de clase

En la clase anterior, encontramos:

  1. el método que devuelve el mensaje
  2. un constructor que no hace nada más que llamar al constructor de la clase padre. Está ahí para indicar que puede generar una excepción de tipo RemoteException.

Vamos a crear una instancia de esta clase con un método main. Para que un objeto o servicio sea accesible desde el exterior, debe crearse y registrarse en el directorio de objetos accesibles desde el exterior. Un cliente que desee acceder a un objeto remoto procede, de hecho, de la siguiente manera:

  1. se dirige al servicio de directorio de la máquina en la que se encuentra el objeto que desea. Este servicio de directorio opera en un puerto que el cliente debe conocer (1099 por defecto). El cliente solicita al directorio una referencia de un objeto o servicio, indicando su nombre. Si ese nombre corresponde a un objeto o servicio del directorio, este le devuelve al cliente una referencia mediante la cual el cliente podrá comunicarse con el objeto o servicio remoto.
  2. A partir de ese momento, el cliente puede utilizar ese objeto remoto como si fuera local

Volviendo a nuestro servidor, debemos crear un objeto de tipo srvEcho y registrarlo en el directorio de objetos accesibles desde el exterior. Este registro se realiza con el método de clase rebind de la clase Naming:

Naming.rebind(String nom, Remote obj)

con

nombre: el nombre que se asociará al objeto remoto

obj: el objeto remoto

Por lo tanto, nuestra clase srvEcho queda así:

import java.rmi.*;
import java.rmi.server.*;
import java.net.*;

// clase que implementa el eco remoto
public class srvEcho extends UnicastRemoteObject implements interEcho{

    // constructor
    public srvEcho() throws RemoteException{
        super();
    }// fin del constructor

    // método que implementa el eco
    public String echo(String msg) throws RemoteException{
        return  "["  + msg + "]";
    }// fin del eco

    // creación del servicio
    public static void main (String arg[]){
        try{
            srvEcho serveurEcho=new srvEcho();
            Naming.rebind("srvEcho",serveurEcho);
            System.out.println("Serveur d’écho prêt");
        } catch (Exception e){
            System.err.println(" Erreur "  + e + "  lors du lancement du serveur d’écho ");
        }
    }// main
}// fin de clase

Al leer el programa anterior, da la impresión de que se detendrá inmediatamente después de crear y registrar el servicio de eco. No es así. Debido a que la clase srvEcho deriva de la clase UnicastRemoteObject, el objeto creado se ejecuta indefinidamente: escucha las solicitudes de los clientes en un puerto anónimo, es decir, elegido por el sistema según las circunstancias. La creación del servicio es asíncrona: en el ejemplo, el método main crea el servicio y continúa su ejecución; mostrará correctamente «Servidor de eco listo».

9.2.1.3. Paso 3: compilación de la aplicación de servidor

En este punto, ya podemos compilar nuestro servidor. Compilamos el archivo interEcho.java de la interfaz interEcho, así como el archivo srvEcho.java de la clase srvEcho. Obtenemos los archivos .class correspondientes: interEcho.class y srvEcho.class.

9.2.1.4. Paso 4: creación del cliente

Escribimos un cliente al que le pasamos como parámetro el archivo URL del servidor de eco y que

  1. lee una línea ingresada desde el teclado
  2. la envía al servidor de eco
  3. muestra la respuesta que este envía
  4. vuelve al paso 1 y se detiene cuando la línea ingresada es «fin».

Esto da como resultado el siguiente cliente:

import java.rmi.*;
import java.io.*;

public class cltEcho {

    public static void main(String arg[]){
        // sintaxis: cltEcho URLService

        // verificación de argumentos
        if(arg.length!=1){
            System.err.println("Syntaxe : pg url_service_rmi");
            System.exit(1);
        }

        // diálogo cliente-servidor
        String urlService=arg[0];
        BufferedReader in=null;
        String msg=null;
        String reponse=null;
        interEcho serveur=null;

        try{
            // apertura del flujo del teclado
            in=new BufferedReader(new InputStreamReader(System.in));
            // localización del servicio
            serveur=(interEcho) Naming.lookup(urlService);                
            // bucle de lectura de los mensajes que se enviarán al servidor de eco
            System.out.print("Message : ");
            msg=in.readLine().toLowerCase().trim();
            while(! msg.equals("fin")){
                // envío del mensaje al servidor y recepción de la respuesta
                reponse=serveur.echo(msg);
                // seguimiento
                System.out.println("Réponse serveur : " + reponse);
                // mensaje siguiente
                System.out.print("Message : ");                
                msg=in.readLine().toLowerCase().trim();
            }// while
            // se acabó
            System.exit(0);
        // manejo de errores        
        } catch (Exception e){
            System.err.println("Erreur : " + e);
            System.exit(2);
        }// try
    }// main
}// clase                

No hay nada en particular en este cliente, salvo la instrucción que solicita una referencia del servidor:

            serveur=(interEcho) Naming.lookup(urlService);

Recordemos que nuestro servicio de eco se registró en el directorio de servicios de la máquina en la que se encuentra con la instrucción:


            Naming.rebind("srvEcho",serveurEcho);

Por lo tanto, el cliente también utiliza un método de la clase Naming para obtener una referencia del servidor que desea utilizar. El método lookup utilizado admite como parámetro la URL del servicio solicitado. Esta tiene la forma de una URL convencional:

    rmi://máquina:puerto/nom_service

con

rmi: opcional — protocolo RMI

máquina: nombre o dirección IP de la máquina en la que opera el servidor de eco —opcional, por defecto localhost.

port: puerto de escucha del servicio de directorio de esta máquina —opcional, por defecto 1099

nom_service: nombre con el que se registró el servicio solicitado (srvEcho en nuestro ejemplo)

Lo que se obtiene es una instancia de la interfaz remota interEcho. Si suponemos que el cliente y el servidor no se encuentran en la misma máquina, al compilar el cliente cltEcho.java, se debe contar, en el mismo directorio, con el archivo interEcho.class, resultado de la compilación de la interfaz remota interEcho; de lo contrario, se producirá un error de compilación en las líneas que hacen referencia a dicha interfaz.

9.2.1.5. Paso 5: generación de los archivos .class necesarios para la aplicación cliente-servidor

Para entender bien qué pertenece al lado del servidor y qué al lado del cliente, colocaremos el servidor en un directorio echo\serveur y el cliente en un directorio echo\client.

El directorio del servidor contiene los siguientes archivos fuente:

E:\data\java\RMI\echo\serveur>dir *.java

INTERE~1 JAV           158  09/03/99  15:06 interEcho.java
SRVECH~1 JAV           759  09/03/99  15:07 srvEcho.java

Tras compilar estos dos archivos fuente, se obtienen los siguientes archivos .class:

E:\data\java\RMI\echo\serveur>dir *.class

SRVECH~1 CLA         1 129  09/03/99  15:58 srvEcho.class
INTERE~1 CLA           256  09/03/99  15:58 interEcho.class

En el directorio del cliente, se encuentra el siguiente archivo fuente:

E:\data\java\RMI\echo\client>dir *.java

CLTECH~1 JAV         1 427  09/03/99  16:08 cltEcho.java

así como el archivo interEcho.class, que se generó durante la compilación del servidor:

E:\data\java\RMI\echo\client>dir *.class

INTERE~1 CLA           256  09/03/99  15:59 interEcho.class

Tras la compilación del archivo fuente, se obtienen los siguientes archivos .class:

E:\data\java\RMI\echo\client>dir *.class

CLTECH~1 CLA         1 506  09/03/99  16:08 cltEcho.class
INTERE~1 CLA           256  09/03/99  15:59 interEcho.class

Si intentamos ejecutar el cliente cltEcho, aparece el siguiente error:

E:\data\java\RMI\echo\client>j:\jdk12\bin\java cltEcho rmi://localhost/srvEcho
Erreur : java.rmi.UnmarshalException: error unmarshalling return; nested exception is:
        java.lang.ClassNotFoundException: srvEcho_Stub

Si intentamos ejecutar el servidor srvEcho, aparece el siguiente error:

E:\data\java\RMI\echo\serveur>j:\jdk12\bin\java srvEcho
Erreur java.rmi.StubNotFoundException: Stub class not found: srvEcho_Stub; nested exception is:
        java.lang.ClassNotFoundException: srvEcho_Stub  lors du lancement du serveur d’écho

En ambos casos, la máquina virtual de Java indica que no ha encontrado la clase srvEcho_stub. De hecho, nunca hemos oído hablar de esta clase. En el cliente, la localización del servidor se realizó con la siguiente instrucción:

            serveur=(interEcho) Naming.lookup(urlService);                

Aquí, urlservice es la cadena rmi://localhost/srvEcho con

Rmi: protocolo RMI

Localhost: máquina en la que opera el servidor; en este caso, la misma máquina en la que se encuentra el cliente. La sintaxis suele ser máquina:puerto. Si no se especifica el puerto, se utilizará el puerto 1099 por defecto. En este puerto se encuentra el servicio de directorio del servidor.

srvEcho: es el nombre del servicio específico solicitado

Durante la compilación, no se reportó ningún error. Solo era necesario que el archivo interEcho.class de la interfaz remota estuviera disponible.

Al ejecutarse, la máquina virtual requiere la presencia de un archivo srvEcho_stub.class si el servicio solicitado es el servicio srvEcho; en general, un archivo X_stub.class para un servicio X. Este archivo solo es necesario durante la ejecución, no durante la compilación del cliente. Lo mismo ocurre con el servidor. ¿Qué es, entonces, este archivo?

En el servidor se encuentra la clase srvEcho.class, que es nuestro objeto o servicio remoto. El cliente, aunque no necesite esta clase, sí necesita una especie de imagen de ella para poder comunicarse con ella. De hecho, el cliente no envía sus solicitudes directamente al objeto remoto: las envía a su imagen local srvEcho_stub.class, ubicada en la misma máquina que él. Esta imagen local srvEcho_stub.class se comunica con una imagen de la misma naturaleza (srvEcho_stub.class) ubicada esta vez en el servidor. Esta imagen se crea a partir del archivo .class del servidor con una herramienta de Java llamada rmic. En Windows, el comando:

E:\data\java\RMI\echo\serveur>j:\jdk12\bin\rmic srvEcho

generará, a partir del archivo srvEcho.class, otros dos archivos .class:

E:\data\java\RMI\echo\serveur>dir *.class

SRVECH~2 CLA         3 264  09/03/99  16:57 srvEcho_Stub.class
SRVECH~3 CLA         1 736  09/03/99  16:57 srvEcho_Skel.class

Ahí está, efectivamente, el archivo srvEcho_stub.class que tanto el cliente como el servidor necesitan para la ejecución. También hay un archivo srvEcho_Skel.class cuya función, por el momento, se desconoce. Hacemos una copia del archivo srvEcho_stub.class en el directorio del cliente y del servidor, y eliminamos el archivo srvEcho_Skel.class. Así que tenemos los siguientes archivos:

en el lado del servidor:

E:\data\java\RMI\echo\serveur>dir *.class

SRVECH~1 CLA         1 129  09/03/99  15:58 srvEcho.class
INTERE~1 CLA           256  09/03/99  15:58 interEcho.class
SRVECH~1 CLA         3 264  09/03/99  16:01 srvEcho_Stub.class

del lado del cliente:

E:\data\java\RMI\echo\client>dir *.class

CLTECH~1 CLA         1 506  09/03/99  16:08 cltEcho.class
INTERE~1 CLA           256  09/03/99  15:59 interEcho.class
SRVECH~1 CLA         3 264  09/03/99  16:01 srvEcho_Stub.class

9.2.1.6. Paso 6: Ejecución de la aplicación cliente-servidor de eco

Estamos listos para ejecutar nuestra aplicación cliente-servidor. En un primer momento, el cliente y el servidor funcionarán en la misma máquina. Primero debemos iniciar nuestra aplicación de servidor. Recordemos que esta:

  • crea el servicio
  • lo registra en el directorio de servicios de la máquina en la que opera el servidor de eco

Este último punto requiere la presencia de un servicio de directorio. Este se inicia con el comando:

start j:\jdk12\bin\rmiregistry

rmiregistry es el servicio de directorio. Aquí se inicia en segundo plano en una ventana de DOS de Windows mediante el comando start. Una vez activo el directorio, podemos crear el servicio de eco y registrarlo en el directorio de servicios. De nuevo, se inicia en segundo plano mediante el comando start:

E:\data\java\RMI\echo\serveur>start j:\jdk12\bin\java srvEcho

El servidor de eco se ejecuta en una nueva ventana DOS y muestra, tal como se le había solicitado:

Serveur d’écho prêt

Ahora solo nos queda ejecutar y probar nuestro cliente:

E:\data\java\RMI\echo\client>j:\jdk12\bin\java cltEcho rmi://localhost/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin

9.2.1.7. El cliente y el servidor en dos máquinas diferentes

En el ejemplo anterior, el cliente y el servidor estaban en la misma máquina. Ahora los colocamos en máquinas diferentes:

  • el servidor en una máquina con Windows
  • el cliente en una máquina con Linux

El servidor se inicia como antes en la máquina con Windows. En la máquina con Linux, se han transferido los archivos .class del cliente:

shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir
total 9
drwxr-xr-x   2 serge    admin        1024 Mar 10 10:02 .
drwxr-xr-x   4 serge    admin        1024 Mar 10 10:01 ..
-rw-r--r--   1 serge    admin        1506 Mar 10 10:02 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class
-rw-r--r--   1 serge    admin        3264 Mar 10 10:02 srvEcho_Stub.class

Se inicia el cliente:

$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : msg1
Erreur : java.rmi.ServerException: RemoteException occurred in server thread; nested exception is: 
      java.rmi.UnmarshalException: error unmarshalling call header; nested exception is: 
      java.rmi.UnmarshalException: skeleton class not found but required for client version

Por lo tanto, tenemos un error: al parecer, la máquina virtual de Java requiere el archivo srvEcho_skel.class, que había sido generado por la utilidad rmic, pero que hasta ahora no se había utilizado. Lo volvemos a crear y también lo transferimos a la máquina Linux:

shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir
total 11
drwxr-xr-x   2 serge    admin        1024 Mar 10 10:17 .
drwxr-xr-x   4 serge    admin        1024 Mar 10 10:01 ..
-rw-r--r--   1 serge    admin        1506 Mar 10 10:02 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class
-rw-r--r--   1 serge    admin        1736 Mar 10 10:17 srvEcho_Skel.class
-rw-r--r--   1 serge    admin        3264 Mar 10 10:02 srvEcho_Stub.class

Nos sale el mismo error que antes... Así que pensamos un poco y volvemos a leer la documentación sobre RMI. Al final llegamos a la conclusión de que tal vez sea el servidor mismo el que necesita el famoso archivo srvEcho_Skel.class. Entonces reiniciamos, en la máquina con Windows, el servidor con los dos archivos srvEcho_Stub.class y srvEcho_Skel.class presentes :

E:\data\java\RMI\echo\serveur>dir *.class

SRVECH~1 CLA         1 129  09/03/99  15:58 srvEcho.class
INTERE~1 CLA           256  09/03/99  15:58 interEcho.class
SRVECH~2 CLA         3 264  10/03/99   9:05 srvEcho_Stub.class
SRVECH~3 CLA         1 736  10/03/99   9:05 srvEcho_Skel.class

E:\data\java\RMI\echo\serveur>start j:\jdk12\bin\java srvEcho

Luego, en la máquina Linux, volvemos a probar el cliente y esta vez sí funciona:

shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r--   1 serge    admin        1506 Mar 10 10:02 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class
-rw-r--r--   1 serge    admin        3264 Mar 10 10:02 srvEcho_Stub.class

shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin

De ello se deduce, por lo tanto, que del lado del servidor deben estar presentes los dos archivos srvEcho_Stub.class y srvEcho_Skel.class. Del lado del cliente, hasta ahora solo ha sido necesario el archivo srvEcho_Stub.class. Este resultó indispensable cuando el cliente y el servidor estaban en la misma máquina con Windows. En Linux, lo eliminamos para ver qué pasa...

shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r--   1 serge    admin        1506 Mar 10 10:02 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class

shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
*** Security Exception: No security manager, stub class loader disabled ***
java.rmi.RMISecurityException: security.No security manager, stub class loader disabled
        at sun.rmi.server.RMIClassLoader.getClassLoader(RMIClassLoader.java:84)
        at sun.rmi.server.MarshalInputStream.resolveClass(MarshalInputStream.java:88)
        at java.io.ObjectInputStream.inputClassDescriptor(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.inputObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:105)
        at java.rmi.Naming.lookup(Naming.java:60)
        at cltEcho.main(cltEcho.java:28)
Erreur : java.rmi.UnexpectedException: Unexpected exception; nested exception is: 
        java.rmi.RMISecurityException: security.No security manager, stub class loader disabled

Tenemos un error interesante que parece indicar que la máquina virtual de Java intentó cargar la famosa clase stub, pero no lo logró debido a la falta de un «administrador de seguridad». Recordamos haber visto algo al respecto en la documentación. Volvemos a revisar la documentación... y descubrimos que el servidor debe crear e instalar un administrador de seguridad (security manager) que garantice a los clientes que solicitan la carga de clases que estas sean seguras. Sin este administrador de seguridad, la carga de clases es imposible. Parece encajar: nuestro cliente Linux solicitó al servidor la clase srvEcho_stub.class que necesita y este la rechazó indicando que no se había instalado ningún administrador de seguridad. Por lo tanto, modificamos el código de la función main del servidor de la siguiente manera:

    // creación del servicio
    public static void main (String arg[]){

        // instalación de un administrador de seguridad
        System.setSecurityManager(new RMISecurityManager());

        // inicio y registro del servicio
        try{
            srvEcho serveurEcho=new srvEcho();
            Naming.rebind("srvEcho",serveurEcho);
            System.out.println("Serveur d’écho prêt");
        } catch (Exception e){
            System.err.println(" Erreur "  + e + "  lors du lancement du serveur d’écho ");
        }
    }// principal

Se compila y se generan los archivos srvEcho_stub.class y srvEcho_Skel.class con la herramienta rmic. Iniciamos el servicio de directorio (rmiregistry) y luego el servidor, ¡y nos aparece un error que antes no teníamos!

Erreur java.security.AccessControlException: access denied (java.net.SocketPermission 127.0.0.1:1099 connect,resolve)  lors du lancement du serveur d’écho

Parece que el administrador de seguridad ha sido demasiado eficaz. Volvemos a leer la documentación... Nos damos cuenta de que, cuando un administrador de seguridad está activo, hay que especificar los permisos al iniciar un programa. Esto se hace con la siguiente opción:

start j:\jdk12\bin\java -Djava.security.policy=mypolicy srvEcho

donde

java.security.policy es una palabra clave

mypolicy es un archivo de texto que define los permisos del programa. En este caso, es el siguiente:

grant {
    // Permitir todo por ahora
    permission java.security.AllPermission;
};

Aquí, el programa tiene todos los derechos.

Empecemos de nuevo. Nos colocamos en el directorio del servidor y hacemos lo siguiente:

  • iniciar el servicio de directorio: start j:\jdk12\bin\rmiregistry
  • iniciar el servidor: start j:\jdk12\bin\java -Djava.security.policy=mypolicy srvEcho

Y esta vez, el servidor de eco (el cliente aún no) se inicia correctamente. Ahora puedes hacer el siguiente experimento:

  • detenga el servidor de eco y luego el servicio de directorio
  • reinicia el servicio de directorio desde un directorio distinto al del servidor
  • regresa al directorio del servidor inicia el servidor de eco; obtendrás el siguiente error:
Erreur java.rmi.ServerException: RemoteException occurred in server thread; nes
ted exception is:
java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is:
java.lang.ClassNotFoundException: srvEcho_Stub  lors du lancement du serveur d’écho

De esto se deduce que el directorio desde el cual se inicia el servicio de directorio es importante. En este caso, Java no encontró la clase srvEcho_stub.class porque el servicio de directorio no se inició desde el directorio del servidor. Al iniciar el servidor, se puede especificar en qué directorio se encuentran las clases necesarias para el servidor:

start j:\jdk12\bin\java 
-Djava.security.policy=mypolicy 
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/ 
srvEcho

El comando se escribe en una sola línea. La palabra clave java.rmi.server.codebase sirve para indicar la ruta del directorio que contiene las clases necesarias para el servidor. En este caso, este URL especifica el protocolo «file», que es el protocolo de acceso a los archivos locales, y el directorio que contiene los archivos .class del servidor. Por lo tanto, si se procede de la siguiente manera:

  • detener el servicio de directorio
  • reiniciar el servicio de directorio desde un directorio distinto al del servidor
  • en el directorio del servidor, ejecutarlo con el comando (una sola línea):
 start j:\jdk12\bin\java 
-Djava.security.policy=mypolicy 
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/ 
srvEcho

El servidor se habrá iniciado correctamente. Ahora podemos pasar al cliente. Probémoslo:

shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r--   1 serge    admin        1506 Mar 10 14:28 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class

shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
*** Security Exception: No security manager, stub class loader disabled ***
java.rmi.RMISecurityException: security.No security manager, stub class loader disabled
        at sun.rmi.server.RMIClassLoader.getClassLoader(RMIClassLoader.java:84)
        at sun.rmi.server.MarshalInputStream.resolveClass(MarshalInputStream.java:88)
        at java.io.ObjectInputStream.inputClassDescriptor(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.inputObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:105)
        at java.rmi.Naming.lookup(Naming.java:60)
        at cltEcho.main(cltEcho.java:31)
Erreur : java.rmi.UnexpectedException: Unexpected exception; nested exception is: 
        java.rmi.RMISecurityException: security.No security manager, stub class loader disabled

Aparece el mismo error que indica la falta de un administrador de seguridad. Pensamos que tal vez nos hayamos equivocado y que sea el cliente quien deba crear su propio administrador de seguridad. Dejamos el servidor con su administrador de seguridad, pero creamos uno también para el cliente. La función main del cliente cltEcho.java queda entonces así:

public static void main(String arg[]){
        // sintaxis: cltEcho puerto de la máquina
        // máquina: máquina en la que opera el servidor de eco
        // puerto: puerto en el que opera el directorio de servicios en la máquina del servicio de eco

        // verificación de los argumentos
        if(arg.length!=1){
            System.err.println("Syntaxe : pg url_service_rmi");
            System.exit(1);
        }

        // instalación de un administrador de seguridad
        System.setSecurityManager(new RMISecurityManager());

        // interacción cliente-servidor
        String urlService=arg[0];
        BufferedReader in=null;
        String msg=null;
        String reponse=null;
        interEcho serveur=null;

        try{
            ....
        } catch (Exception e){
            ....
        }// intentar
    }// main

A continuación, se procede de la siguiente manera:

  • se recompila cltEcho.java
  • se transfieren los archivos .class a la máquina Linux
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r--   1 serge    admin        1506 Mar 10 14:28 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class
  • Se inicia el cliente
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho

java.io.FileNotFoundException: /e:/data/java/rmi/echo/serveur/srvEcho_Stub.class
        at java.io.FileInputStream.<init>(FileInputStream.java)
        at sun.net.www.protocol.file.FileURLConnection.connect(FileURLConnection.java:150)
        at sun.net.www.protocol.file.FileURLConnection.getInputStream(FileURLConnection.java:170)
        at sun.applet.AppletClassLoader.loadClass(AppletClassLoader.java:119)
        at sun.applet.AppletClassLoader.findClass(AppletClassLoader.java:496)
        at sun.applet.AppletClassLoader.loadClass(AppletClassLoader.java:199)
        at sun.rmi.server.RMIClassLoader.loadClass(RMIClassLoader.java:159)
        at sun.rmi.server.MarshalInputStream.resolveClass(MarshalInputStream.java:97)
        at java.io.ObjectInputStream.inputClassDescriptor(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.inputObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:105)
        at java.rmi.Naming.lookup(Naming.java:60)
        at cltEcho.main(cltEcho.java:31)
File not found when looking for: srvEcho_Stub
Erreur : java.rmi.UnmarshalException: Return value class not found; nested exception is: 
        java.lang.ClassNotFoundException: srvEcho_Stub

A pesar de las apariencias, estamos avanzando: el error ya no es el mismo. Se observa que el cliente pudo solicitar al servidor la clase srvEcho_Stub.class, pero este no la encontró. Por lo tanto, el cliente debe contar con un administrador de seguridad si desea poder solicitar clases al servidor.

Si observamos el error anterior, vemos que se buscó el archivo srvEcho_Stub.class en el directorio e:/data/java/rmi/echo/servidor/ y no se encontró. Sin embargo, sí se encuentra ahí. Si observamos con más detalle la lista de métodos involucrados en el error, encontramos este: sun.net.www.protocol.file.FileURLConnection.getInputStream. El cliente parece haber abierto un flujo con un objeto de tipo FileURLConnection. Nos damos cuenta de que todo esto tiene que ver con la forma en que iniciamos nuestro servidor:

start j:\jdk12\bin\java 
-Djava.security.policy=mypolicy 
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/ 
srvEcho

El mensaje de error parece referirse al valor de la palabra clave java.rmi.server.codebase. Al revisar nuevamente la documentación, se observa que el valor de esta palabra clave, en los ejemplos proporcionados, siempre es: http://.., c.a.d. El protocolo utilizado es http. No queda claro cómo el cliente solicita y obtiene sus clases del servidor. Quizás las solicita con el valor URL de la clave java.rmi.server.codebase, URL, especificada al iniciar el servidor. Por lo tanto, decidimos iniciar el servidor con el siguiente comando nuevo:

start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho

El protocolo ahora es http. Es necesario mover los archivos .class a una ubicación accesible para el servidor http de la máquina en la que se almacenarán las clases. En nuestro ejemplo, el servidor se ejecuta en una máquina con Windows que cuenta con un servidor HTTP de Microsoft denominado PWS. La raíz de este servidor es d:\Inetpub\wwwroot. Por lo tanto, procedemos de la siguiente manera:

  • se crea el directorio d:\Inetpub\wwwroot\rmi\echo
  • colocamos en él los archivos .class del servidor, así como el archivo mypolicy
  • iniciamos el servidor web, si aún no lo hemos hecho
  • se reinicia el servicio de directorio (rmiregistry)
  • reiniciamos el servidor con el comando
start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho
  • En la máquina Linux, se inicia el cliente:
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r--   1 serge    admin        1622 Mar 10 14:37 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class

shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin

¡Uf! Funciona. El cliente logró recuperar el famoso archivo srvEcho_Stub.class.

Todo esto nos ha dado algunas ideas, y nos preguntamos si el cliente que se encuentra en la máquina Windows del servidor también funcionaría sin el archivo srvEcho_Stub.class. Nos dirigimos al directorio del cliente, eliminamos el archivo srvEcho_Stub.class si se encuentra allí y ejecutamos el cliente de la misma manera que en Linux:

E:\data\java\RMI\echo\client>dir *.class

CLTECH~1 CLA         1 622  10/03/99  14:12 cltEcho.class
INTERE~1 CLA           256  09/03/99  15:59 interEcho.class

E:\data\java\RMI\echo\client>j:\jdk12\bin\java -Djava.security.policy=mypolicy cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho

Message : nouveau message
Réponse serveur : [nouveau message]
Message : fin

9.2.1.8. Résumé

En el lado del servidor Windows:

  • el servidor cuenta con un administrador de seguridad
  • se inició con las siguientes opciones: start j:\jdk12\bin\java -Djava.security.policy=mypolicy

-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho

En el lado del cliente, ya sea Linux o Windows

  • el cliente cuenta con un administrador de seguridad
  • en Linux, se inició mediante java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
  • En Windows, se ejecutó mediante j:\jdk12\bin\java -Djava.security.policy=mypolicy cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho

9.2.1.9. Servidor de eco en Linux, clientes en Windows y Linux

Ahora trasladamos el servidor a una máquina con Linux y probamos clientes de Linux y Windows. El procedimiento a seguir es el siguiente:

  • trasladamos los archivos .class del servidor a la máquina Linux
shiva[serge]:/home/admin/serge/WWW/rmi/echo/serveur#
$ dir
total 11
drwxr-xr-x   2 serge    admin        1024 Mar 10 16:15 .
drwxr-xr-x   3 serge    admin        1024 Mar 10 16:09 ..
-rw-r--r--   1 serge    admin         256 Mar 10 16:09 interEcho.class
-rw-r--r--   1 serge    admin        1245 Mar 10 16:09 srvEcho.class
-rw-r--r--   1 serge    admin        1736 Mar 10 16:09 srvEcho_Skel.class
-rw-r--r--   1 serge    admin        3264 Mar 10 16:09 srvEcho_Stub.class
  • Dado que los clientes solicitarán la clase srvEcho_Stub.class, el directorio elegido para las clases del servidor es un directorio accesible desde el servidor HTTP de la máquina Linux. En este caso, la clase URL de este directorio es http://shiva.istia.univ-angers.fr/~serge/rmi/echo/servidor
  • El servicio de directorio se inicia en segundo plano: /usr/local/bin/jdk/rmiregistry &
  • el servidor se inicia en segundo plano: /usr/local/bin/jdk/bin/java

-Djava.rmi.server.codebase=http://shiva.istia.univ-angers.fr/~serge/rmi/echo/servidor/

srvEcho &

Podemos probar los clientes. Primero el cliente de Windows.

  • Nos desplazamos al directorio del cliente en la máquina con Windows
  • ejecutamos el cliente con el comando:
E:\data\java\RMI\echo\client>j:\jdk12\bin\java -Djava.security.policy=mypolicy cltEcho rmi://shiva.istia.univ-angers.fr/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : fin

Probamos el cliente de Linux:

shiva[serge]:/home/admin/serge/java/rmi/echo/client#
$ java cltEcho srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin

Cabe señalar que, para el cliente Linux que opera en la misma máquina que el servidor de eco, no fue necesario especificar una máquina en el URL del servicio solicitado.

9.3. Segundo ejemplo: Servidor SQL en una máquina con Windows

9.3.1. El problema

En el capítulo JDBC vimos cómo administrar bases de datos relacionales. En los ejemplos presentados, las aplicaciones y la base de datos utilizada se encontraban en la misma máquina con Windows. Aquí nos proponemos escribir un servidor RMI en una máquina con Windows, lo que permitiría a los clientes remotos acceder a las bases de datos públicas ODBC de la máquina en la que se encuentra el servidor.

Image

El cliente RMI podría realizar tres operaciones:

  • conectarse a la base de datos de su elección
  • enviar consultas SQL
  • cerrar la conexión

El servidor ejecuta las consultas SQL del cliente y le envía los resultados. Esa es su función principal y por eso lo llamaremos servidor SQL.

Aplicamos los diferentes pasos vistos anteriormente con el servidor de eco.

9.3.2. Paso 1: la interfaz remota

La interfaz remota es la interfaz que enumera los métodos del servidor RMI a los que tendrán acceso los clientes RMI. Utilizaremos la siguiente interfaz:

import java.rmi.*;

// la interfaz remota
public interface interSQL extends Remote{
    public String connect(String pilote, String url, String id, String mdp)
        throws java.rmi.RemoteException;
    public String[] executeSQL(String requete, String separateur) 
        throws java.rmi.RemoteException;
    public String close()
        throws java.rmi.RemoteException;
}

La función de los distintos métodos es la siguiente:

Connect: el cliente se conecta a una base de datos remota, de la cual proporciona el pilote, el url, el JDBC, así como su identidad id y su contraseña mdp para acceder a dicha base. El servidor le devuelve una cadena de caracteres que indica el resultado de la conexión:

    200 - Connexion réussie
    500 - Echec de la connexion

executeSQL: el cliente solicita la ejecución de una consulta SQL en la base de datos a la que está conectado. Indica el carácter que debe separar los campos en los resultados que se le devuelven. El servidor devuelve una matriz de cadenas:

    100 n

para una consulta de actualización de la base de datos, donde n es el número de filas actualizadas

    500 msg d’erreur

si la consulta generó un error

    501 Pas de résultats

si la consulta no generó ningún resultado

    101 ligne1
    101 ligne2
    101 ...

si la consulta ha generado resultados. Las líneas devueltas por el servidor son los resultados de la consulta.

close: el cliente cierra su conexión con la base de datos remota. El servidor devuelve una cadena que indica el resultado de este cierre:

    200 Base fermée
    500 Erreur lors de la fermeture de la base (msg d’erreur)

9.3.3. Paso 2: Código del servidor

A continuación se presenta el código fuente Java del servidor SQL. Para comprenderlo, es necesario haber asimilado la gestión de bases de datos JDBC, así como la construcción de servidores RMI. Los comentarios del programa deberían facilitar su comprensión.

// paquetes importados
import java.rmi.*;
import java.rmi.server.*;
import java.sql.*;
import java.util.*;

// clase srvSQL
public class srvSQL extends UnicastRemoteObject implements interSQL{

    // datos globales de la clase
    private Connection DB;

    // ------------- constructor
    public srvSQL() throws RemoteException{
        super();
    }

    // --------------- conexión
    public String connect(String pilote, String url, String id,
        String mdp) throws RemoteException{

        // conexión a la base de datos url mediante el controlador
        // identificación con el ID de usuario e ID y la contraseña

        String resultat=null;            // resultado del método
        try{
            // carga del controlador
            Class.forName(pilote);
            // solicitud de conexión
            DB=DriverManager.getConnection(url,id,mdp);
            // ok
            resultat="200 Connexion réussie";
        } catch (Exception e){
            // error
            resultat="500 Echec de la connexion (" + e + ")";
        }
        // fin
        return resultat;
    }            

    // ------------- executeSQL
    public String[] executeSQL(String requete, String separateur)
        throws RemoteException{

        // ejecuta una consulta SQL en la base de datos DB
        // y coloca los resultados en una tabla de cadenas

        // datos necesarios para ejecutar la consulta
        Statement S=null;
        ResultSet RS=null;
        String[] lignes=null;
        Vector resultats=new Vector();
        String ligne=null;

        try{
            // creación del contenedor de la consulta
            S=DB.createStatement();
            // ejecución de la consulta
            if (! S.execute(requete)){
                // consulta de actualización
                // se devuelve el número de filas actualizadas
                lignes=new String[1];
                lignes[0]="100 "+S.getUpdateCount();
                return lignes;
            }
            // era una consulta
            // se recuperan los resultados
            RS=S.getResultSet();
            // número de campos del conjunto de resultados
            int nbChamps=RS.getMetaData().getColumnCount();
            // se procesan
            while(RS.next()){
                // creación de la fila de resultados
                ligne="101 ";
                for (int i=1;i<nbChamps;i++)
                    ligne+=RS.getString(i)+separateur;
                ligne+=RS.getString(nbChamps);
                // se agrega al vector de resultados
                resultats.addElement(ligne);
            }// while
            // fin del procesamiento de los resultados
            // se liberan los recursos
            RS.close();
            S.close();
            // se entregan los resultados
            int nbLignes=resultats.size();
            if (nbLignes==0){
                lignes=new String[1];
                lignes[0]="501 Pas de résultats";
            } else {
                lignes=new String[resultats.size()];
                for(int i=0;i<lignes.length;i++)
                    lignes[i]=(String) resultats.elementAt(i);
            }//si
            return lignes;
        } catch (Exception e){
            // error
            lignes=new String[1];
            lignes[0]="500 " + e;
            return lignes;
        }// try-catch
    }// executeSQL

    // --------------- cerrar
    public String close() throws RemoteException {
        // cierra la conexión con la base de datos
        String resultat=null;
        try{
            DB.close();
            resultat="200 Base fermée";
        } catch (Exception e){
            resultat="500 Erreur à la fermeture de la base ("+e+")";
        }
        // devuelve el resultado
        return resultat;
    }

    // ----------- main
    public static void main (String[] args){

        // administrador de seguridad
        System.setSecurityManager(new RMISecurityManager());

        // inicio del servicio
        srvSQL serveurSQL=null;
        try{
            // creación
            serveurSQL=new srvSQL();
            // registro
            Naming.rebind("srvSQL",serveurSQL);
            // seguimiento
            System.out.println("Serveur SQL prêt");
        } catch (Exception e){
            // error
            System.err.println("Erreur lors du lancement du serveur SQL ("+ e +")");
        }// try-catch
    }// main

}// clase

9.3.4. Codificación del cliente RMI

El cliente del servidor RMI se invoca con los siguientes parámetros:

    urlserviceAnnuaire pilote urlBase id mdp separateur

urlserviceAnnuaire: URL RMI del servicio de directorio que registró el servidor SQL

controlador: controlador que debe utilizar el servidor SQL para administrar la base de datos

urlBase: URL JDBC de la base de datos que se va a administrar

id: identidad del cliente o nulo si no hay identidad

mdp: contraseña del cliente o nula si no hay contraseña

separador: carácter que el servidor SQL debe utilizar para separar los campos de las líneas de resultados de una consulta

A continuación se muestra un ejemplo de los parámetros posibles:

    srvSQL sun.jdbc.odbc.JdbcOdbcDriver jdbc:odbc:articles null null ,

donde:

urlserviceAnnuaire    srvSQL

nombre RMI del servidor SQL

pilote     sun.jdbc.odbc.JdbcOdbcDriver

el controlador habitual para bases de datos con interfaz ODBC

urlBase    jdbc:odbc:articles

Para utilizar una base de datos de artículos declarada en la lista de bases de datos públicas ODBC de la máquina Windows

id    null

sin identidad

mdp    null

sin contraseña

separateur    , 

los campos de los resultados estarán separados por una coma

Una vez iniciado con los parámetros anteriores, el cliente sigue los siguientes pasos:

  • se conecta al servidor RMI srvSQL, es decir, un servidor RMI en la misma máquina que el cliente
  • solicita la conexión a la base de datos de artículos
connect(‘’sun.jdbc.odbc.JdbcOdbcDriver’’, ‘‘jdbc:odbc:articles’’, ’’’’, ’’’’)
  • le pide al usuario que escriba una consulta SQL en el teclado
  • la envía al servidor SQL
executeSQL(requete, ’’,’’);
  • muestra en pantalla los resultados devueltos por el servidor
  • vuelve a pedirle al usuario que escriba una consulta SQL en el teclado. Se detendrá cuando termine la consulta.

A continuación se muestra el código Java del cliente. Los comentarios deberían ser suficientes para entenderlo.

import java.rmi.*;
import java.io.*;

public class cltSQL {

    // datos globales de la clase
    private static String syntaxe =
        "syntaxe : cltSQL urlServiceAnnuaire pilote urlBase id mdp separateur";
    private static BufferedReader in=null;
    private static interSQL serveurSQL=null;

    public static void main(String arg[]){
        // sintaxis: cltSQL urlServiceAnnuaire separador, carácter especial, URL, ID, contraseña
        // urlServiceAnnuaire: URL del directorio de servicios RMI con los que hay que ponerse en contacto
        // controlador: controlador que se debe utilizar para la base de datos que se va a utilizar
        // urlBase: URL JDBC de la base de datos que se va a utilizar
        // id: identidad del usuario
        // contraseña: su contraseña
        // separador: cadena que separa los campos en los resultados de una consulta

        // verificación del número de argumentos
        if(arg.length!=6)
            erreur(syntaxe,1);

        // inicialización de los parámetros de conexión a la base de datos
        String urlService=arg[0];        
        String pilote=arg[1];
        String urlBase=arg[2];
        String id, mdp, separateur;
        if(arg[3].equals("null")) id=""; else id=arg[3];
        if(arg[4].equals("null")) mdp=""; else mdp=arg[4];        
        if(arg[5].equals("null")) separateur=" "; else separateur=arg[5];        

        // instalación de un administrador de seguridad
        System.setSecurityManager(new RMISecurityManager());

        // interacción cliente-servidor
        String requete=null;
        String reponse=null;
        String[] lignes=null;
        String codeErreur=null;

        try{
            // apertura del flujo del teclado
            in=new BufferedReader(new InputStreamReader(System.in));
            // seguimiento
            System.out.println("--> Connexion au serveur RMI en cours...");
            // localización del servicio
            serveurSQL=(interSQL) Naming.lookup(urlService);
            // seguimiento
            System.out.println("--> Connexion à la base de données en cours");
            // solicitud de conexión inicial a la base de datos
            reponse=serveurSQL.connect(pilote,urlBase,id,mdp);
            // seguimiento
            System.out.println("<-- "+reponse);
            // análisis de la respuesta
            codeErreur=reponse.substring(0,3);
            if(codeErreur.equals("500")) 
                erreur("Abandon sur erreur de connexion à la base",3);
            // bucle de lectura de las consultas que se enviarán al servidor SQL
            System.out.print("--> Requête : ");
            requete=in.readLine().toLowerCase().trim();
            while(! requete.equals("fin")){
                // envío de la consulta al servidor y recepción de la respuesta
                lignes=serveurSQL.executeSQL(requete,separateur);
                // seguimiento
                afficheLignes(lignes);
                // siguiente solicitud
                System.out.print("--> Requête : ");                
                requete=in.readLine().toLowerCase().trim();
            }// mientras
            // seguido de
            System.out.println("--> Fermeture de la connexion à la base de données distante");
            // se cierra la conexión
            reponse=serveurSQL.close();
            // seguimiento
            System.out.println("<-- " + reponse);
            // fin
            System.exit(0);
        // manejo de errores        
        } catch (Exception e){
            erreur("Abandon sur erreur : " + e,2);
        }// intentar
    }// main

    // ----------- AfficheLignes
    private static void afficheLignes(String[] lignes){
        for (int i=0;i<lignes.length;i++)
            System.out.println("<-- " + lignes[i]);
    }// afficheLignes

    // ------------ error
    private static void erreur(String msg, int exitCode){
        // visualización del mensaje de error
        System.err.println(msg);
        // posible liberación de recursos
        try{
            in.close();
            serveurSQL.close();
        } catch(Exception e){}
        // salimos
        System.exit(exitCode);
    }// error

}// clase    

9.3.5. Paso 3: creación de los archivos .class

  • El servidor está compilado
E:\data\java\RMI\sql\serveur>j:\jdk12\bin\javac interSQL.java

E:\data\java\RMI\sql\serveur>j:\jdk12\bin\javac srvSQL.java

E:\data\java\RMI\sql\serveur>dir *.class

INTERS~1 CLA           451  12/03/99  17:54 interSQL.class
SRVSQL~1 CLA         3 238  12/03/99  17:54 srvSQL.class
  • Se crean los archivos Stub y Skel
E:\data\java\RMI\sql\serveur>j:\jdk12\bin\rmic srvSQL

E:\data\java\RMI\sql\serveur>dir *.class


INTERS~1 CLA           451  12/03/99  17:54 interSQL.class
SRVSQL~1 CLA         3 238  12/03/99  17:54 srvSQL.class
SRVSQL~2 CLA         4 491  12/03/99  17:56 srvSQL_Stub.class
SRVSQL~3 CLA         2 414  12/03/99  17:56 srvSQL_Skel.class
  • Se transfieren los archivos interSQL.class, srvSQL_Stub.class y srvSQL_Skel.class al directorio del cliente
E:\data\java\RMI\sql\client>dir

CLTSQL~1 JAV         3 486  11/03/99  11:39 cltSQL.java
INTERS~1 CLA           451  11/03/99  10:55 interSQL.class
SRVSQL~1 CLA         4 491  11/03/99  13:19 srvSQL_Stub.class
SRVSQL~2 CLA         2 414  11/03/99  13:19 srvSQL_Skel.class
  • Se compila el cliente
E:\data\java\RMI\sql\client>j:\jdk12\bin\javac cltSQL.java

E:\data\java\RMI\sql\client>dir *.class

INTERS~1 CLA           451  11/03/99  10:55 interSQL.class
CLTSQL~1 CLA         2 839  12/03/99  18:00 cltSQL.class
SRVSQL~1 CLA         4 491  11/03/99  13:19 srvSQL_Stub.class
SRVSQL~2 CLA         2 414  11/03/99  13:19 srvSQL_Skel.class

9.3.6. Paso 4: Pruebas con el servidor y el cliente en la misma máquina con Windows

  • El servicio de directorio se ejecuta en un directorio distinto al del servidor y al del cliente
F:\>start j:\jdk12\bin\rmiregistry
  • Se coloca el siguiente archivo mypolicy en los directorios del cliente y del servidor
grant {
    // Permitir todo por ahora
    permission java.security.AllPermission;
};
  • Se inicia el servidor
E:\data\java\RMI\sql\serveur>start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/sql/serveur/ srvSQL
  • Se inicia el cliente
E:\data\java\RMI\sql\client>j:\jdk12\bin\java -Djava.security.policy=mypolicy cltSQL srvSQL 
sun.jdbc.odbc.JdbcOdbcDriver jdbc:odbc:articles null null ,

--> Connexion au serveur RMI en cours...
--> Connexion à la base de données en cours
<-- 200 Connexion réussie
--> Consulta: select nombre, stock_actu from artículos ordenado por stock_actu en orden descendente
<-- 101 vélo,31
<-- 101 essai3,13
<-- 101 skis nautiques,13
<-- 101 canoé,13
<-- 101 panthère,11
<-- 101 léopard,11
<-- 101 cachalot,10
<-- 101 fusil,10
<-- 101 arc,10
--> Consulta: update artículos set stock_actu=stock_actu-1 where stock_actu<=11
<-- 100 5
--> Consulta: select nombre, stock_actu from artículos ordenado por stock_actu en orden ascendente
<-- 101 cachalot,9
<-- 101 fusil,9
<-- 101 arc,9
<-- 101 panthère,10
<-- 101 léopard,10
<-- 101 essai3,13
<-- 101 skis nautiques,13
<-- 101 canoé,13
<-- 101 vélo,31
--> Requête : fin
--> Fermeture de la connexion à la base de données distante
<-- 200 Base fermée

9.3.7. Paso 5: Pruebas con el servidor en una máquina con Windows y el cliente en una máquina con Linux

  • Si es necesario, se detienen el servidor y el servicio de directorio
  • Se transfieren los archivos .class del cliente a una máquina con Linux
shiva[serge]:/home/admin/serge/java/rmi/sql/client#
$ dir *.class
-rw-r--r--   1 serge    admin        2839 Mar 11 14:37 cltSQL.class
-rw-r--r--   1 serge    admin         451 Mar 11 14:37 interSQL.class
  • Los archivos del servidor se colocan en un directorio accesible para el servidor HTTP de la máquina con Windows
D:\Inetpub\wwwroot\rmi\sql>dir

INTERS~1 CLA           451  11/03/99  10:55 interSQL.class
SRVSQL~1 CLA         3 238  11/03/99  13:19 srvSQL.class
SRVSQL~2 CLA         4 491  11/03/99  13:19 srvSQL_Stub.class
SRVSQL~3 CLA         2 414  11/03/99  13:19 srvSQL_Skel.class
MYPOLICY                81  08/06/98  15:01 mypolicy
  • Se relanza el servicio de directorio
F:\>start j:\jdk12\bin\rmiregistry
  • Se reinicia el servidor con parámetros diferentes a los utilizados en la prueba anterior
D:\Inetpub\wwwroot\rmi\sql>start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/sql/ srvSQL
  • Se inicia el cliente en la máquina Linux
/usr/local/bin/jdk/bin/java cltSQL rmi://tahe.istia.univ-angers.fr/srvSQL sun.jdbc.odbc.JdbcOdbcDriver jdbc:odbc:articles null null ,

--> Consulta: select nombre,stock_actu,stock_mini from artículos ordenado por nombre
<-- 101 arc,9,8
<-- 101 cachalot,9,6
<-- 101 canoé,13,7
<-- 101 essai3,13,9
<-- 101 fusil,9,8
<-- 101 léopard,10,7
<-- 101 panthère,10,7
<-- 101 skis nautiques,13,8
<-- 101 vélo,31,8
--> Consulta: update artículos set stock_actu=stock_mini where stock_mini<=7
<-- 100 4
--> Consulta: seleccione nombre, stock_actu, stock_mini de artículos ordenados por nombre
<-- 101 arc,9,8
<-- 101 cachalot,6,6
<-- 101 canoé,7,7
<-- 101 essai3,13,9
<-- 101 fusil,9,8
<-- 101 léopard,7,7
<-- 101 panthère,7,7
<-- 101 skis nautiques,13,8
<-- 101 vélo,31,8
--> Requête : fin
--> Fermeture de la connexion à la base de données distante
<-- 200 Base fermée

9.3.8. Conclusión

Esta es una aplicación interesante, ya que permite acceder a una base de datos desde cualquier equipo de la red. Se podría haber escrito perfectamente de manera tradicional con sockets, tal como se pide, por cierto, en un ejercicio del capítulo sobre bases de datos. Si escribiéramos esta aplicación de manera tradicional:

  • tendríamos un cliente y un servidor que podrían estar escritos en lenguajes diferentes
  • el cliente y el servidor se comunicarían mediante el intercambio de líneas de texto y tendrían un diálogo que podría ser algo así:
client : connect machine port pilote urlBase id mdp

donde los dos primeros parámetros especifican dónde encontrar el servidor, y los cuatro siguientes indican los parámetros de conexión a la base de datos que se va a utilizar

El servidor podría responder con algo como:

200 - Connexion réussie
500 - Echec de la connexion
client : executeSQL requete, separateur

para solicitarle al servidor que ejecute una consulta SQL en la base de datos conectada al cliente. «separador» es el carácter que separa los campos en las líneas de la respuesta.

El servidor podría responder algo como

    100 n

para una consulta de actualización de la base de datos, donde n es el número de filas actualizadas

    500 msg d’erreur

si la solicitud generó un error

    501 Pas de résultats

si la consulta no ha generado ningún resultado

    101 ligne1
    101 ligne2
    101 ...

si la consulta ha generado resultados. Las líneas devueltas por el servidor son los resultados de la consulta.

client : close

para cerrar la conexión con la base de datos remota. El servidor podría devolver una cadena que indique el resultado de este cierre:

    200 Base fermée
    500 Erreur lors de la fermeture de la base (msg d’erreur)

Aquí vemos que, si somos capaces de desarrollar una aplicación tradicional con un protocolo como el anterior, podemos deducir una posible estructura del servidor RMI. En el protocolo, cuando tenemos una frase del cliente al servidor del tipo:

    commande param1 param2 ... paramq

podríamos tener dentro del servidor RMI un método

    String commande(param1,param2,..., paramq)

y este método, accesible para el cliente, deberá formar parte de la interfaz publicada del servidor.

Para concluir, cabe señalar que nuestro servidor actualmente solo maneja un cliente: no puede manejar varios, tal como está escrito actualmente. De hecho, si un cliente se conecta a una base de datos B1, el servidor crea un objeto Connection DB=DB1. Si un segundo cliente solicita conectarse a una base de datos B2, el servidor lo registra como Connection DB=DB2, interrumpiendo así la conexión del primer cliente a la base B1.

9.4. Ejercicios

9.4.1. Ejercicio 1

Amplíe el servidor SQL anterior para que pueda gestionar varios clientes.

9.4.2. Ejercicio 2

Escriba el applet de Java de comercio electrónico presentado en los ejercicios del capítulo JDBC para que funcione con el servidor RMI del ejercicio anterior.