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 enlace 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. Se trata, efectivamente, de dos aplicaciones distintas conectadas mediante 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 específicamente para gestionar estas herramientas de comunicación que son las sockets.

JAVA y RMI (Remote Method Invocation) permiten 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 cómo se transporta la información de un punto a otro.

Este ú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 propia aplicación. Serían las clases RMI del lenguaje Java las que tendrían que adaptarse a la nueva capa de red.

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

  1. Se escribe una aplicación Java clásica en una máquina A. Esta desempeñará el papel de servidor. Para ello, algunos de sus objetos se «publicarán» en la máquina A en la que se ejecuta la aplicación y se convertirán así en servicios.
  2. Se escribe una aplicación Java clásica 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, que, 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 aparece en numerosas obras 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 basada 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 los 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í se declara, por tanto, 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, que dispone de los métodos que permiten la invocación de 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 valor
  2. un constructor que no hace nada más que llamar al constructor de la clase padre. Está ahí para declarar 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 devuelve al cliente una referencia a través de 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 mediante 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í. Dado 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

Llegados a 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 introducida con 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 introducida 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 van a enviar 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 ha terminado
            System.exit(0);
        // gestión de errores        
        } catch (Exception e){
            System.err.println("Erreur : " + e);
            System.exit(2);
        }// try
    }// main
}// clase                

No hay nada especialmente destacable 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 ha registrado 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 el formato de una URL clásica:

    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 ha registrado 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, debemos disponer, en el mismo directorio, del 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 comprender 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 se intenta 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 habíamos 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 concreto solicitado

Durante la compilación no se había señalado ningún error. Simplemente 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 srvEcho; por lo 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, pues, 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 la misma para poder comunicarse con ella. De hecho, el cliente no envía sus solicitudes directamente al objeto remoto, sino que las envía a su imagen local srvEcho_stub.class, situada 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) situada, en este caso, 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í se encuentra, efectivamente, el archivo srvEcho_stub.class que tanto el cliente como el servidor necesitan para su 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í pues, 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

Ya estamos listos para ejecutar nuestra aplicación cliente-servidor. En un primer momento, el cliente y el servidor funcionarán en el mismo equipo. Lo primero que hay que hacer es iniciar nuestra aplicación de servidor. Recordemos que esta:

  • crea el servicio
  • lo registra en el directorio de servicios del equipo en el que opera el servidor de eco

Este último punto requiere la presencia de un servicio de directorio. Este se inicia mediante 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 y 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 se encontraban en el mismo equipo. Ahora los colocamos en equipos diferentes:

  • el servidor en un equipo con Windows
  • el cliente en un equipo con Linux

El servidor se inicia, como antes, en el equipo con Windows. En el equipo 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 solicita 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 nos lo pensamos y volvemos a leer la documentación sobre RMI. Al final llegamos a la conclusión de que quizá sea el propio servidor el que necesita el famoso archivo srvEcho_Skel.class. Así que reiniciamos, en el equipo con Windows, el servidor con los dos archivos srvEcho_Stub.class y srvEcho_Skel.class presentes, además de :

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

A continuación, en el equipo Linux, volvemos a probar el cliente y, esta vez, 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 tanto, que en el lado del servidor deben estar presentes los dos archivos srvEcho_Stub.class y srvEcho_Skel.class. En el lado del cliente, hasta ahora solo ha sido necesario el archivo srvEcho_Stub.class. Resultó imprescindible cuando el cliente y el servidor estaban en el mismo equipo 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 que no lo consiguió al no disponer de un «gestor de seguridad». Recordamos haber visto algo al respecto en la documentación. Volvemos a consultarla… y descubrimos que el servidor debe crear e instalar un gestor de seguridad (security manager) que garantice a los clientes que solicitan la carga de clases que estas son seguras. A falta de este gestor de seguridad, la carga de clases es imposible. Parece que encaja: nuestro cliente Linux solicitó al servidor la clase srvEcho_stub.class que necesitaba y este se negó alegando que no se había instalado ningún gestor 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 gestor 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 compilan y se generan los archivos srvEcho_stub.class y srvEcho_Skel.class con la herramienta rmic. Se inicia el servicio de directorio (rmiregistry) y, a continuación, el servidor, ¡y aparece un error que antes no se producía!

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 gestor de seguridad ha sido demasiado eficaz. Volvemos a consultar la documentación… Nos damos cuenta de que, cuando hay un gestor de seguridad 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.

Volvemos a empezar. Nos situamos en el directorio del servidor y realizamos sucesivamente 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 realizar el siguiente experimento:

  • detén el servidor de eco y, a continuación, el servicio de directorio
  • reinicia el servicio de directorio desde un directorio distinto al del servidor
  • vuelve al directorio del servidor inicia el servidor de eco; aparecerá 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 ello se deduce que el directorio desde el que se inicia el servicio de directorio es importante. En este caso, Java no ha encontrado la clase srvEcho_stub.class porque el servicio de directorio no se ha iniciado 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, iniciar este último 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. Lo probamos:

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 ausencia de un gestor de seguridad. Pensamos que quizá nos hayamos equivocado y que sea el cliente quien deba crearse su propio gestor de seguridad. Dejamos el servidor con su gestor 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 gestor 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 vuelve a compilar cltEcho.java
  • se transfieren los archivos .class al equipo 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 ha podido solicitar al servidor la clase srvEcho_Stub.class, pero que este no la ha encontrado. Por lo tanto, el cliente debe disponer de un gestor de seguridad si quiere poder solicitar clases al servidor.

Si nos fijamos en 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í que está ahí. Si observamos con más detalle la lista de métodos implicados 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. Pensamos que todo esto tiene que ver con la forma en que hemos iniciado 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 hacer referencia al valor de la palabra clave java.rmi.server.codebase. Al volver a consultar la documentación, se observa que el valor de esta palabra clave, en los ejemplos proporcionados, es siempre: http://.., c.a.d, y que el protocolo utilizado es http. No queda claro cómo el cliente solicita y obtiene sus clases del servidor. Quizá las solicite utilizando el valor URL de la clave java.rmi.server.codebase, URL, especificado 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 es ahora http. Es necesario mover los archivos .class a una ubicación accesible para el servidor http del equipo en el que se almacenarán las clases. En nuestro ejemplo, el servidor se ejecuta en un equipo Windows 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)
  • se reinicia 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 el equipo 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 ha conseguido recuperar el famoso archivo srvEcho_Stub.class.

Todo esto nos ha dado algunas ideas, y nos preguntamos si el cliente que se encuentra en el servidor con Windows también funcionaría sin el archivo srvEcho_Stub.class. Nos desplazamos al directorio del cliente, eliminamos el archivo srvEcho_Stub.class si lo encontramos allí y ejecutamos el cliente de la misma forma 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 servidor Windows:

  • el servidor cuenta con un gestor de seguridad
  • se ha iniciado 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 dispone de un gestor de seguridad
  • en Linux, se ha iniciado mediante java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
  • En Windows, se ha iniciado 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 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 van a solicitar 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/serveur
  • El servicio de directorio se ejecuta 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 el equipo 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 funciona en la misma máquina que el servidor de eco, no ha sido necesario especificar ninguna máquina en el URL del servicio solicitado.

9.3. Segundo ejemplo: servidor SQL en un equipo Windows

9.3.1. El problema

En el capítulo JDBC hemos visto cómo gestionar bases de datos relacionales. En los ejemplos presentados, las aplicaciones y la base de datos utilizada se encontraban en el mismo equipo Windows. En este caso, nos proponemos crear un servidor RMI en un equipo Windows, que permita a los clientes remotos acceder a las bases de datos públicas ODBC del equipo en el que se encuentra el servidor.

Image

El cliente RMI podría realizar tres operaciones:

  • conectarse a la base de datos que elija
  • 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 distintos 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 podrán acceder 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 que proporciona el pilote, el url, el JDBC, así como su identidad id y su contraseña mdp para acceder a dicha base de datos. 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, siendo n el número de filas actualizadas

    500 msg d’erreur

si la consulta ha generado 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.

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 muestra 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 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 almacena los resultados en una matriz de cadenas

        // datos necesarios para la ejecución de 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 líneas 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 línea de resultados
                ligne="101 ";
                for (int i=1;i<nbChamps;i++)
                    ligne+=RS.getString(i)+separateur;
                ligne+=RS.getString(nbChamps);
                // se añade al vector de resultados
                resultats.addElement(ligne);
            }// while
            // fin del procesamiento de los resultados
            // se liberan los recursos
            RS.close();
            S.close();
            // se devuelven 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){

        // gestor 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 ha registrado el servidor SQL

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

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

id: identidad del cliente o «null» si no hay identidad

mdp: contraseña del cliente o «null» si no hay contraseña

separador: carácter que debe utilizar el servidor SQL 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 las 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 del equipo 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 el mismo equipo que el cliente
  • solicita la conexión a la base de datos de artículos
connect(‘’sun.jdbc.odbc.JdbcOdbcDriver’’, ‘‘jdbc:odbc:articles’’, ’’’’, ’’’’)
  • solicita al usuario que introduzca una consulta SQL mediante el teclado
  • la envía al servidor SQL
executeSQL(requete, ’’,’’);
  • Muestra en pantalla los resultados devueltos por el servidor
  • vuelve a pedir al usuario que introduzca una consulta SQL con el teclado. Se detendrá cuando la consulta haya finalizado.

A continuación se muestra el código Java del cliente. Los comentarios deberían bastar 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 el que hay que ponerse en contacto
        // controlador: controlador que se debe utilizar para la base de datos que se va a explotar
        // urlBase: URL JDBC de la base de datos que se va a utilizar
        // id: identificador del usuario
        // mdp: 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 gestor 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 van a enviar 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);
        // gestión 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){}
        // salida
        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 el mismo equipo 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 order by stock_actu desc
<-- 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 order by stock_actu asc
<-- 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 un equipo Windows y el cliente en un equipo Linux

  • Si es necesario, se detienen el servidor y el servicio de directorio
  • Se transfieren los archivos .class del cliente a un equipo 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 del equipo 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 reanuda 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 el equipo 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 order by 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: SELECT nombre, stock_actu, stock_mini FROM artículos ORDER BY 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

Se trata de 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 forma tradicional con sockets, que es, por cierto, lo que se pide en un ejercicio del capítulo sobre bases de datos. Si escribiéramos esta aplicación de forma 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 mantendrían un diálogo que podría ser del tipo:
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 así:

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

para solicitar 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 así como

    100 n

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

    500 msg d’erreur

si la consulta ha generado 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 crear una aplicación tradicional con un protocolo como el anterior, podemos deducir una posible estructura del servidor RMI. En el protocolo, cuando hay una frase del cliente al servidor del tipo:

    commande param1 param2 ... paramq

podremos tener dentro del servidor RMI un método

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

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

Para terminar, cabe señalar que nuestro servidor solo gestiona actualmente un cliente: tal y como está escrito ahora, no puede gestionar varios. 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 una conexión 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ía el servidor SQL anterior para que pueda gestionar varios clientes.

9.4.2. Ejercicio 2

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