2. Construcción de un servidor

La forma más simple de un servidor RMI es un servidor "unicast". Tratándose de programación orientada a objetos, cualquier objeto puede considerarse un servidor. Un servidor RMI es un objeto remoto que: implementa una interfaz remota, y es exportado al sistema RMI.

Las clases base que proporciona RMI para la construcción de servidores son: RemoteObject, RemoteServer, y UnicastRemoteObject. Las tres forman una jerarquía de clases, en ese orden.

RemoteObject proporciona la semántica básica para objetos remotos adecuados para servidores y stubs. RemoteServer proporciona los métodos getClientHost y getLog. UnicastRemoteObject soporta la transmisión punto a punto desde el servidor RMI.

Un objeto remoto unicast es un objeto servidor con las siguientes características:

Para ilustrar los pasos a seguir para construir un servidor unicast, vamos a utilizar como ejemplo la implementación de un servidor de chat. El servidor permite a los clientes registrarse y mantiene una tabla activa de participantes. Cuando un participante quiere enviar un mensaje a otros participantes que están actualmente conectados, se envía un mensaje al servidor, y éste lo distribuye a los otros participantes.

2.1. Arquitectura básica del servidor

El diseño del servidor es relativamente simple. El servidor inicia un servicio registry y comienza por registrarse él mismo. El servidor exporta métodos para permitir a los clientes registrarse ellos mismos, pedir al servidor una lista de participantes, y enviar mensajes al grupo actual de participantes. Ver Figura 5.

Figura 5. Arquitectura del servidor de chat.

El servidor mantiene información sobre los clientes registrados para enviarles mensajes a través de sus métodos remotos (manejo de clientes registrados). La parte central de la arquitectura de la Figura 5 hace referencia al hecho de que los clientes pueden llamar a métodos en el servidor para registrarse, y para enviar mensajes a otros clientes. Finalmente, la arquitectura de la figura muestra los servicios de registro, consistentes en que el servidor construye su propio servicio de nombres, eliminando así la necesidad de tener un proceso por separado para dicho servicio.

Cuando un cliente se registra, envía una referencia de sí mismo que el servidor usa para enviar los mensajes de vuelta al cliente. De esta forma el cliente puede permanecer ocioso hasta que los mensajes son recibidos y no necesita realizar ninguna consulta al servidor. El cliente realiza un contacto inicial con el servidor a través del registro. A continuación el servidor pasa una referencia a uno de sus propios objetos remotos vía un método remoto que es invocado en el servidor.

El servidor utiliza un objeto Message que contiene mensajes de texto o líneas de dibujo.

Para mantener un rendimiento razonable entre clientes, se crea un hilo de ejecución para manejar los datos enviados a cada cliente individual que está recibiendo datos. De esta manera, un cliente, con una conexión particularmente lenta no causa que el resto de clientes esperen hasta que el cliente anterior sea atendido. El número de conexiones que el servidor puede mantener está limitado al número de threads que permita activar simultáneamente la plataforma en la que se ejecute el servidor.

2.2. Interfaz del servidor

Lo primero que hay que realizar es definir la interfaz para los métodos que vamos a permitir sean invocados de forma remota. La interfaz debe ser una subclase de la interfaz java.rmi.Remote y describir los métodos para registrar un cliente, enviar un mensaje al servidor y recuperar una lista de clientes actualmente conectados. Dicha interfaz se especifica a continuación:

public interface ChatServer extends Remote {
  public void register(Chat c, String name) throws RemoteException;
  public void postMessage(Message m) throws RemoteException;
  public Sring[] listChatters() throws RemoteException;

}

El siguiente paso es la creación de una clase que implemente dicha interfaz. En este caso la vamos a denominar ChatServerImpl, cuyo método main() fija un manejador de seguridad, crea una instancia de sí misma para ser exportada, crea un servicio de nombres para que los clientes pueden localizar al servidor usando direcciones URL y la clase java.rmi.Naming, y mantiene una lista de clientes registrados.

public class ChatServerImpl 
             extends UnicastRemoteObject
			 implements ChatServer {};

Cuando se construye un objeto UnicastRemoteObject, éste es automáticamente exportado, registrado en el sistema RMI y preparado para escuchar en un puerto TCP.

Las clases remotas pueden ser definidas sin "extender" ninguna de las clases base comentadas en el apartado 1.1. En este caso, los objetos remotos deben ser explícitamente exportados por uno de los métodos estáticos de UnicastRemoteObject.exportObject.

Usaremos un Vector para almacenar las conexiones individuales. Cuando un usuario se conecte, se creará un objeto Talker y se añadirá a dicho Vector. De esta forma, cuando un mensaje tenga que ser enviado a los participantes, la aplicación simplemente recorre el vector y añade el mensaje a cada objeto Talker del mismo.

private Vector chatters = new Vector(); 

El constructor de la clase simplemente llama al constructor de su super-clase (UnicastRemoteObject). Existen varios constructores para la clase UnicastRemoteObject, permitiendo a sus clases derivadas elegir entre: (a) exportar el objeto sobre un puerto por defecto elegido en tiempo de ejecución, (b) exportar el objeto sobre un puerto especificado, y (c) exportar el objeto sobre un puerto especificado utilizando socket factories específicas para el cliente y el servidor. En este caso se utiliza la opción (a).

Una socket factory es una clase que permite proporcionar nuestros propios objetos Socket o ServerSocket a RMI. Las aplicaciones que usan RMI pueden proporcionar sus propias socket factories, que RMI usará cuando construya los sockets cliente y servidor en nombre de los servidores y clientes RMI. Esta facilidad tiene dos usos principales:

public ChatServerImpl() throws RemoteException {
   super();
   Sytem.out.println("Inicializando el servidor");

}

Es necesario incluir una instancia de RMI security manager explícitamente. Si no se hace así las clases stub solamente podrán cargarse desde el sistema de ficheros local, y no a través de la red.

El nombre del registro de nombres es "ChatServerImpl", que se ejecutará en el puerto 5050 (se trata de un puerto arbitrario que debe ser conocido por los clientes que se quieran conectar al servidor). A continuación se muestra el código del procedimiento main().

public static void main(String args[]) {
  Registry reg;

  
  System.setSecurityManager(new RMISecurityManager());
  try {
    ChatServerImpl cs = new ChatServerImpl();
    reg = LocateRegistry.createRegistry(5050);
    reg.bind("ChatServerImpl",cs);
    System.out.println ("Servidor en marcha");
  }
  catch (AlreadyBoundException e){
System.out.println("El nombre ya se encuentra instanciado: " + e); System.exit(0); } catch (RemoteException e){
System.out.println("Error general del servidor: " + e); System.exit(0); } }

Seguidamente implementamos los métodos de la interfaz. El método register() simplemente obtiene un objeto Chat y un nombre como parámetro y crea un objeto Talker con dichos parámetros para que tengan su propio hilo para recibir mensajes. También añade el objeto al Vector.

synchronized public void register(Chat c, String name) {
  chatters.addElement(new Talker(c,name));

}

El método listChatters() devuelve una lista de los participantes actuales, obtenida a partir de la estructura de datos Vector.

public String[] listChatters() {
  String list[] = new String[chatters.size()];
  Talker c;

  for (int i=0; i < list.lenght; i++) {
    c=(Talker)chatters.elementAt(i);
    list[i] = c.getChatterName();
  }
  return list;
}

El método postMessage() se usa por los clientes para enviar un mensaje al servidor. El servidor distribuye el mensaje a cada uno de los clientes registrados. Como este método es llamado por muchos clientes, tomamos la precaución de declararlo synchronized (al igual que hemos hecho con register). Esto evita la posibilidad de que se pierdan mensajes debido a que un cliente llame a este método mientras el servidor está procesando una invocación previa.

synchronized public void postMessage(Message m) {
  Talker t; 
  String list[] = new String [chatters.size();
 
  for (int i=0; i < chatters.size; i++) {
    t=(Talker)chatters.elementAt(i);
    if (!t.addMessage(m))
       chatters.removeElementAt(i--);
  }
  return list;
}

Esta interfaz remota (ChatServer), junto con la clase que la implementa (ChatServerImpl) serán usadas por RMI para generar un stub cliente y un skeleton servidor, que manejarán la comunicación entre los objetos locales y los remotos.

El compilador RMI (rmic) genera de forma automática las clases stub y skeleton. Para la clase stub se genera una implementación para cada método remoto, que simplemente codifica adecuadamente los argumentos del método (marshals) y los transmite al servidor. Para la clase skeleton, el compilador RMI genera otro conjunto de implementaciones de los métodos remotos, que son diseñados para recibir los argumentos de los métodos desde la llamada al método remoto, decodificarlos, y realizar la correspondiente llamada a la implementación del objeto. El resultado (datos o una excepción) son codificados y devueltos al cliente remoto. El método stub correspondiente del cliente (que todavía está en ejecución) decodifica los resultados y los entrega al cliente como resultado de la llamada al método remoto).

Finalmente compilaremos la interfaz remota utilizando el compilador estándar de Java.

2.3. Uso de threads en el servidor

La clase Talker crea un hilo de ejecución (thread) separado para cada cliente conectado al servidor. Los hilos de ejecución forman parte de la mayoría de las aplicaciones servidor debido a la asociación natural de una comunicación con el cliente y los hilos de ejecución. El soporte de ejecución de RMI no proporciona ninguna garantía de qué invocación de objeto remoto se asigna a qué thread. Ya que las invocaciones de un método remoto sobre el mismo objeto pueden ejecutarse de forma concurrente, la implementación del objeto remoto necesita asegurarse de que es "segura" (thread-safe).

En nuestra aplicación servidor, el objeto Talker comprueba si hay mensajes en su cola. Mientras haya mensajes, intenta enviarlos al cliente. Tan pronto como la cola de mensajes esté vacía, el thread se mantiene en modo "espera" hasta que se añadan más mensajes a la cola.

public class Talker extends Thread {
  private Vector messages = new Vector();
  private Chat c;
  boolean isActive = true;
  private String name; 
 
  public Talker(Chat c, String name) {
    this.c = c;
    this.name = name; 
    this.start();
  }

  
  public boolean addMessage(Message e) {
    if(!isActive)
      return false;

    messages.addElement(e);
    resume();
    return true;
  }

  
  public void run() {
    while (true) {
      try {
        if (messages.isEmpty()) suspend();
        c.chatNotify((Message)messages.elementAt(0));
        messages.removeElementAt(0);
      }
      catch (RemoteException e) {
        System.out.println("Removing "+name);
        isActive = false;
        this.stop();
      }  
      yield();
    }
  }

  
  public String getChatterName() {
    return name;
  }

}

El constructor realiza una llamada al método start(), que origina el inicio de la ejecución del thread, esencialmente realizando un bucle con el método run(). Los parámetros de entrada del constructor son un objeto Chat, representando al cliente que se está comunicando, y un String que contiene el nombre por el que el cliente es referenciado.

Cuando un mensaje es enviado, éste se elimina de Vector. Cuando el vector está vacío pasa al estado sleep. Cuando se añade un nuevo mensaje el thread es "despertado" con una llamada a resume() y continúa intentando enviar mensajes.

El método addMessage() verifica primero que la conexión todavía permanece activa chequeando la variable isActive (que se pondrá a false si se provoca una RemoteException cuando se intente enviar un mensaje).

El servidor descrito también actúa como cliente (y el cliente como servidor), por lo que hay ciertas dependencias entre las interfaces remotas necesarias para ambas aplicaciones. Ambos, cliente y servidor, exportan objetos remotos. El servidor exporta objetos utilizados para enviar mensajes entre otros clientes, y los clientes exportan objetos usados para notificarles que hay datos para leer (envío de datos al cliente). El servidor es un cliente en el sentido de que realiza llamadas al método chatNotify() de los objetos clientes Chat que se conectan al servidor. El cliente exporta este método de la misma forma que el servidor exporta los suyos. Por lo tanto, el servidor no puede construirse sin tener en cuenta la especificación de las interfaces remotas del cliente con el que interactúa.

La interfazChat declara dos métodos: un método chatNotify(), usado para enviar mensajes a un cliente Chat, y un método getName(), que puede usarse para preguntarle al cliente su nombre.

public interface Chat extends Remote {


  public void chatNotify(Message m) throws RemoteException;

  
  public String getName() throws RemoteException;

}

Finalmente se deben compilar los códigos fuente, así como las clases stub y skeleton para la clase ChatServerImpl. Para ejecutar la aplicación debemos asegurarnos de que el servidor está en marcha.

javac -d . /usr/MyClasses/ *.java   //compilamos los fuentes
 
rmic -d . /usr/MyClasses ChatServerImpl  //generamos las clases stub y skeleton

java ChatServerImpl &     //ejecutamos el servidor