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; } }
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.
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; }
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