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.
El diseño del servidor es relativamente simple. El servidor inicia un servicio registry y comienza por registrarse él mismo. El servidor exporta un método 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 4.1.
Figura 4.1. Arquitectura del servidor de chat.
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 via 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.
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 4. 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 participatnes, 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 con un 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:
Socket
o ServerSocket
,
por ejemplo para fijar un timeout para el cliente, o para usar una
interfaz de escucha de red en el lado del servidor en hosts "multi-homed".
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 atravé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 mensjaes 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; 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ó 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).
Después de definir la interfaz remota y la clase que la implementa,
se deben compilar utilizando el compilador estándar de Java. Para crear
los correspondientes stubs y skeletons se utilizará el
compilador rmic
utilizando el nombre de la clase que implementa
la interfaz como argumento.
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 tatno, el servidor no puede construirse sin tener en cuenta la especificación
de las interfaces remotas del cliente con el que interactúa.
La interface 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; }
Finalmente se deben 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 ChatServerImpl //generamos las clases stub y skeleton java ChatServerImpl & //arrancamos el servidor