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 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.
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.
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:
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 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.
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