5. Construcción de un cliente

Los clientes RMI apenas se diferencian de objetos locales: reciben u obtienen objetos, invocan métodos de dichos objetos, usan los resultados, y capturan excepciones provocadas en los métodos. Cada método exportado por una interfaz remota puede lanzar una excepción, que debe ser capturada en el cliente. La única cuestión a tener en cuenta es la del fallo remoto.

Cuando se diseña y escribe un cliente remoto puede darse la posibilidad de que se produzca un fallo remoto. Cada cliente debe estar preparado para tratar excepciones remotas de forma adecuada. Una excepción remota puede originarse tanto localmente como de forma remota, y puede ser lanzada por diversos motivos: errores de código, errores de instalación, errores en la red, errores de Java, o errores con los recursos del sistema.

En el caso de que ocurra una exceptión remota, no está siempre claro si la invocación del método remoto ha fallado completamente o sólo parcialmente. Por ejemplo, un servidor remoto puede haber completado una transacción en una base de datos, pero puede haberse producido un fallo en la transmisión del parámetro resultado, con lo que para el servidor todo habrá funcionado bien, mientras que el cliente experimentará un error.

Una de las razones de por qué pueden ocurrir errores parciales, y la principal diferencia entre una invocación normal sobre objetos locales y otra remota es que las llamdas a métodos locales usan el mecanismo de llamada/retorno existente en la JVM (que a efectos prácticos es infalible), y RMI usa la red para llamadas y retorno de resultados. El diseño de RMI hace imposible distinguir entre errores en la fase de llamada y errores en la fase de retorno. Esto hace que sea importante el considerar la aparición de fallos parciales.

En el ejemplo que estamos utilizando del Chat, vamos a considerar que un fallo parcial no va a tener repercusiones graves, ya que si un cliente percibe un fallo como consecuencia del envío de un mensaje que en realidad se ha enviado correctamente, se puede enviar de nuevo el mensaje sin que por ello se "molesten" los interlocutores del Chat.

Como se ha comentado en el tema anterior, en realidad los clientes actúan también como servidores, ya que exportan un objeto que se usa para invocar de forma remota el método chatNotify() definido en la interfaz Chat.

Vamos a ver cómo se construye un cliente que implementa la interfaz Chat y usa el servidor del tema anterior para distribuir mensajes y dibujos entre el resto de participantes.

Antes de comenzar con el cliente, vamos a describir la clase Message, que encapsula un mensaje de texto, un posible objeto Line, y el nombre del cliente que origina el mensaje. Definimos dos constructores para que se pueda crear un mensaje de texto y un mensaje de dibujo. Tal y como se vió en el tema 3, dicha clase debe implementar la interfaz Serializable para que puedan enviarse los mensajes a través de la red.

public class Message implements java.io.Serializable {
  private String sender = null; 
  private String message = null; 
  private line l = null; 

  
  public Message (String sender, String message) {
    this.sender = sender;
    this.message = message;
  }


  public Message (String sender, Line l) {
    this.sender = sender;
    this.l = l;
  }


  public String getSender () {
    return sender;
  }


  public String getMessage () {
    return message;
  }

  
  public Line getLine() {
    return l;
  }
}

También es necesario proporcionar una representación de un segmento de línea, que será usada por el cliente del Chat para compartir líneas de dibujo.

public class Line implements java.io.Serializable {
  public int x,y,dx,dy;  
    
  public Line (int x, int y, int dx, int dy) {
    this.x = x;
    this.y = y;
    this.dx = dx;
    this.dy = dy;
  }

}

5.1. Arquitectura básica del cliente

La implementación se escribe como un applet. Puesto que Java no soporta herencia múltiple (una clase solamente puede tener una superclase), debemos exportar las interfaces remotas de forma manual, en vez de hacerlo de forma implícita extendiendo la clase UnicastRemoteObject, tal y como se ha hecho con el servidor. Después de la invocación, dicho applet primero pregunta al usuario el nombre a usar para comunicarse con el resto de clientes. Cualquier mensaje enviado incluirá el nombre proporcionado.

Una vez asignado el nombre, el applet intentará localizar un objeto Registry que se corresponda con la aplicación servidor descrita en el tema anterior. A continuación, el applet se registra en el servidor y construye su interfaz de usuario (consistente en un panel de objetos Button, un TextArea que lista los mensajes de los participantes, un objeto TextField utilizado para teclear los mensajes enviados, y un Canvas, usado como pizarra virtual compartida).

Seguidamente el applet queda a la espera de que se cree un evento a partir de la interacción con el usuario o el método chatNotify sea invocado por el servidor en el que se ha registrado dicho applet. Si se teclea un mensaje en el TextField, o se dibuja algo en el canvas, el applet añade los datos asociados a un hilo de ejecución denominado ServerTalker, que mantiene una cola de mensajes salientes y los envía hacia el servidor. Se utiliza un thread para permitir que la herramienta de dibujo y la interacción básica con el usuario se realice lo más rápidamente posible sin tener que esperar a que las llamadas remotas terminen.

Cuando un mensaje es devuelto al applet a través del método chatNotify(), cualquier mensaje de datos se añade al objeto TextArea, cualquier objeto Line se añade a un buffer de pantalla, y el applet se redibuja (se utiliza un buffer para evitar parpadeos entre refrescos sucesivos). Una vez que el hilo de ejecución haya vaciado su cola de mensajes, y todas las peticiones de refresco (como resultado de llamadas a chatNotify) se hayan realizado, el applet vuelve a quedar ocioso en espera del siguiente evento GUI o notificación chatNotify().

El applet define un conjunto sencillo de elementos visuales para la interacción con el usuario. Se crea un panel de botones para permitir al usuario desconectarse del servidor, interrogar al servidor para objener una lista de participantes, y borrar el TextArea o el Canvas de cualquier texto o dibujos acumulados. Las operaciones de borrado tienen solamente efecto local y no borran las pizarras o TextAreas de otros clientes que participan en la conversación.

5.2. Interfaz del cliente

Como ya se ha visto en el tema anterior la interfaz Chat 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;

}

Como también se ha comentado, la implementación del cliente (ChatImpl) es un applet, por lo que no puede extender la clase UnicastRemoteObject directamente. En su lugar exportaremos el objeto remoto explícitamente cuando se inicialice el applet. También especificaremos la interfaz ChatApplet, que será usada por la pizarra para notificar el dibujo de objetos Line.

public class ChatImpl extends Applet
                      implements Chat, ChatApplet {
  private TestArea ta;
  private TextField tf;
  private ChatServer cs;       //referencia al servidor
  private String name = null;  //nombre de usuario
  private DrawPad dp;
  private NameDialog nd;       //para pedir el nombre de ususario
  private ServerTalker st;     //thread para manejo de envio de mensajes
   
  public ChatImpl() throws RemoteException {
     System.out.println("Inicializando el Chat");
  }
}

Cuando el applet sea cargado por un navegador, o un visor de applets, se realiza una llamada al método init(). En dicho método crearemos los componentes GUI, los añadiremos al panel Applet, pediremos al usuario un nombre, y llamaremos a una rutina para registrar el applet en el servidor.

public void init() {
  
  ...//crear el Panel y añadir los botones
  ...//crear la ventana de mensajes, el campo de texto y la pizarra
  ...//añadir todos los componentes al applet         
  ...//crear el cuadro de diálogo para obtener el nombre de usuario "nd"
  
  registerChatter();  //registro del applet en el servidor
}

Para registrar el applet se debe hacer de forma explícita mediante una llamada al método UnicastRemoteObject.exportObject(), que espera un objeto Remote como parámetro. Debido a que el applet implementa la interfaz Chat, que a su vez extiende la interfaz Remote, simplemente utilizamos this como parámetro, y los métodos declarados en la interfaz Chat estarán accesibles para los clientes remotos (o servidores, en este caso).

Una vez que el applet ha sido exportado, se debe obtener el nombre del servidor mediante la clase Naming. Debido a que un applet solamente puede crear conexiones de red con la misma máquina que la que proviene, debemos usar la variable Codebase desde el applet para crear la URL usada para buscar el servidor del objeto remoto. Una vez registrado en el servidor, crearemos el thread ServerTalker con un enlace a los métodos del servidor remoto. Este thread mantiene una cola de mensajes y los envía al servidor invocando sus métodos remotos.

public void registerChatter() {
  name = nd.getName();
  nd.setVisible(false);
  nd = null;
  
  try {
    UnicastRemoteObject.exportObject(this);
    cs= (ChatServer)Naming.lookup("rmi://" + getCodeBas().getHost() +
                                  ":5050/ChatServerImpl");
    cs.register(this,name);
    st= new ServerTalker(cs.name);        
  }
  catch (RemoteException e) {
    System.out.println("No he podido localizar el registro");
    System.exit(0);
  }
  catch (MalFormedURLException e) {
    System.out.println("Error en URL");
    System.exit(0);
  }
  catch (NotBoundException e) {
    System.out.println("Servicio no enlazado");
    System.exit(0);
  }
}

Debemos implementar un manejador de eventos para la interacción básica con el usuario.

public boolean handleEvent(Event e) { 
    //código para manejar los eventos de interacción con el usuario 
}

El método sendMessage() declarado en el interfaz ChatApplet es usado por la pizarra para enviar líneas al applet el cual las convierte en objetos Message y las añade al thread ServerTalker.

public void sendMessage(Line l) { 
    if (!st.addMessage(new Message(name,l)))
      ta.append("***Error en el servidor***\n"); 
}

Cuando un usuario "pincha" el botón para pedir una lista de usuarios conectados, el manejador de eventos realiza una llamada al método getUserList() el cual llama al método listChatters() del servidor, y devuelve un vector de Strings que representan los nombres de los clientes registrados. Cada uno de los nombres se añade al objeto TextArea.

public void getUserList() { 
   String users[] = null;
   try {
     users = cs.listChatters();   
   }
   catch (RemoteException e) {
     System.out.println(e);
     users = new String[1];
     users[0] = "***Error");
   }
   for (int i=0; i < users.length; i++)
     ta.append("***"+user[i]+"\n);
}

Finalmente debemos implementar los métodos declarados en la interfaz Chat: getName(), y chatNotify().

public synchronized void chatNofity (Message m) {
  throws RemoteException {

  
  if(m.getMessage()!= nill)
    ta.append(m.getSender()+ ": " + m.getMessage() + "\n");
    
  if (m.getLIne()!= null && |m.getSender().equals(name))
dp.addLine(m.getLine)); } public String getName() { return name; }

5.3. Uso de threads en el cliente

La clase ServerTalker crea un hilo de ejecución (thread) y mantiene un Vector de mensajes para ser enviados al servidor. Si la cola está vacía, el thread espera hasta que es "despertado" cuando se añade algo a la cola. Esto permite añadir nuevos mensajes independientemente de la velocidad de comunicación con el servidor.

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 ServerTalker extends Thread {
  private Vector messages = new Vector();
  private ChatServer cs;

 
  public ServerTalker(ChatServer cs, String name) {
    this.cs = cs;
    messages.addElement(new
          Message("***", name + " se acaba de conectar***")); 
    this.start();
  }

  
  public boolean addMessage(Message e) {
    if(cs==null) {
      System.out.println("La referencia al servidor es nula");
    return false;
    resume();
    messages.addElement(e);
    return true;
  }

  
  public void run() {
    while (true) {
      try {
        if (messages.isEmpty()) suspend();
        cs.postMessage((Message)messages.elementAt(0));
        messages.removeElementAt(0);
      }
      catch (RemoteException e) {
        System.out.println("Error del servidor");
        cs = null;
        this.stop();
      }  
      yield();
  }
}

Puesto que el cliente es un applet, se debe definir el código HTML correspondiente:

<APPLET code="ChatImpl.class"
        codebase = "/MyClasses/"
        width= 800
        height= 400 >
</APPLET>

Finalmente se debe compilar los códigos fuente, así como las clases stub y skeleton par la clase ChatImpl. 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 ChatImpl  //generamos las clases stub y skeleton 

java ChatServerImpl &     //arrancamos el servidor