3. Paso de parámetros por la red

RMI depende enteramente de la capacidad para enviar objetos a y desde métodos en máquinas remotas. RMI asume una arquitectura orientada a objetos. Cualquier objeto que se envía como parámetro a un objeto remoto lo hace por valor. Esto significa que se pasa una copia del objeto, no el original o una referencia al original. La excepción a esta regla es un objeto que haya sido exportado como un objeto remoto (objeto que implementa métodos remotos). En este caso, al cliente se le envía un proxy o stub que representa un objeto que reside en el host, o máquina servidor. Cualquier interacción con el stub es enviada al original, que es quién realiza realmente cualquier operación que sea requerida por el stub.

Cuando se envían estructuras de datos desde una máquina a otra, dichos datos se deben codificar de cierta manera para que puedan ser enviados a través de la red. Esto normalmente implica implementar un protocolo de transmisión, incluyendo algún tipo de marcas para delimitar los campos y registros enviados. Las conexiones de red son, en su mayor parte, en serie, y los datos deben transformarse antes de que sean transmitidos a través de las conexiones serie.

El modelo RMI asume una plataforma homogénea, la plataforma Java. Pero incluso asumiendo un entorno homogéneo, se debe desarrollar un protocolo para manejar el desensamblado, transmisión y reconstrucción de los datos. La serialización de objetos hace referencia a los métodos usados para duplicar un objeto escribiendo los valores de sus campos en un flujo de datos y recreando una copia de dicho objeto a partir de dichos valores de campos.

Es importante destacar que la reconstrucción del objeto obtiene una copia, no el objeto original. Los objetos serializados son siempre copias del original. Cuando se necesite enviar una referencia a un objeto, en vez de una copia, se debe usar un objeto remoto, es decir, un objeto exportado de forma implícita extendiendo la clase java.rmi.server.UnicastRemoteObject, o explícitamente utilizando UnicastRemoteObject.exportObject(Remote), y en cualquier caso, debe implementar una interfaz remota.

3.1. Serialización de los datos

Para poder ejecutar un método remoto, RMI tiene que transmitir los argumentos del método desde el cliente al servidor, y transmitir los resultados en la dirección contraria. El proceso de codificar los argumentos y resultados para su transmisión se denomina marshalling; el proceso de decodificarlos cuando se reciben se denomina unmarshalling. RMI realiza los procesos de marshalling y unmarshalling via serialización Java.

Al proceso de convertir los componentes que conforman los datos en un flujo (stream) de bytes se denomina serialización. Se denomina de-serializazión a la acción de decodificar dicho flujo de bytes para reconstruir una copia del objeto original. El término serialización se usa también de forma informal para incluir ambos procesos.

Para que un objeto pueda ser marshalled y unmarshalled por RMI debe ser serializable. Para ello debe cumplir las siguientes reglas:

Si en una llamada RMI se encuentra una clase no serializable, se lanza la excepción NotSerializableException, la cual se manifiesta en el cliente como MarshalException o UnmarshalException.

Un objeto es serializado de forma implícita por RMI cuando éste es marshalled, o de forma explícita llamando a ObjectOutputStream.writeObject. Este método lanza una IOException ante cualquier error de entrada/salida, incluyendo NotSerializableException si se encuentra con un objeto no serializable.

Cuando un objeto está siendo serializado, se construye un grafo representando a dicho objeto (como raiz) y las referencias que contenga a otros objetos (ver Figura 3.1). Un grafo de objetos contiene un objeto raiz y todos los objetos alcanzables desde la raiz. Cuando un objeto es serializado, se escribe en el flujo de datos: su objeto base, si es serializable, y sus atributos (los atributos declarados como static o transient son ignorados). El proceso de serialización es recursivo: el mismo proceso se aplica al objeto base (si es serializable), y a cualquier atributo que sea un objeto (no tipos primitivos), a través del grafo de objetos del que el objeto original es la raiz. Cualquier objeto en el grafo de objetos se serializa solamente una vez, posteriores referencias a él se codifican usando un mecanismo de referencias compartidas, que es decodificada durante el proceso de de-serialización.

Figura 3.1. Ejemplos de grafos de objetos.

Un objeto es de-serializado de forma implícita por RMI cuando éste es unmarshalled, o de forma explícita llamando a ObjectIntputStream.readObject. Este método lanza una IOException ante cualquier error de entrada/salida, o ClassNotFoundException si la clase correspondiente a algún objeto en el grafo de objetos que está siendo de-serializado no puede cargarse. Cuando un objeto es de-serializado, el grafo de objetos se reconstruye con un aspecto idéntico al grafo utilizado para su serialización.

La serialización por defecto funciona bastante bien, pero quizá es demasiado lenta. Una clase serializable puede implementar su propia serialización. Para ello se deben declarar los métodos privados readObject y writeObject, que implementan cualquier tratamiento especial que requiera dicha clase. Estos métodos son opcionales.

Como ejemplo de serializazión por defecto, supongamos que queremos enviar un objeto sencillo que representa un punto en dos dimenesiones:

class MyObject implements java.io.Serializable {
  int x;
  int y;
  MyObject (int x, int y) {
    this.x = x;
    this.y = y;
  }
}

Debido a que implementa la interfaz Serializable, dicho objeto podría enviarse a un flujo de datos con el siguiente código:

MyObject point = new MyObject(10,20);
ObjectOutputStream oos = new ObjectOutputStream(SomeOutputStream);
oos.writeObject(point);

Dicho código crea un nuevo MyObject, crea un ObjectOutputStream (asumiendo que SomeOutputStream es un objeto OutputStream válido), y escribe los datos de MyObject en el flujo de datos.

El receptor puede reconstruir el objeto, asumiendo que tiene acceso local al bytecode de MyObject, con el siguiente código:

MyObject point = new MyObject(10,20);
ObjectInputStream ois = new ObjectInputStream(SomeInputStream);
point = (MyObject)ois.readObject();

A continuación mostramos el código para crear nuestros propios métodos para serializar los datos y reconstruir el objeto.

class MyObject implements java.io.Serializable {
  int x;
  int y;
  public MyObject() {  //constructor sin argumentos
  }
  public MyObject (int x, int y) {
    this.x = x;
    this.y = y;
  }
  void writeObject(ObjectOutputStream s)
     throws java.io.IOException {
    s.writeInt(x);
    s.writeInt(y);
  }

  void readObject(ObjectInputStream s)
     throws java.io.IOException {
    x= s.readInt();
    y= s.readInt();
  }
}

En este ejemplo trivial, el objeto reconstruido no es diferente del que se obtiene con la serialización por defecto, pero podían haberse incluido otras sentencias en los métodos readObject y writeObject para calcular nuevos valores de campos o manejar de forma especial los datos.

Cuando el objeto es serializado, el sistema chequea si el objeto define los métodos readObject() y writeObject(). Si es así, entonces chequea para ver si tiene un constructor sin argumentos. En ese caso, el sistema llama al método writeObject() con un ObjectStream como argumento para que el objeto pueda serializarse él mismo. Entonces, en la parte del receptor, el objeto es instanciado usando su constructor sin argumentos. A continuación se llama al método readObject() con un objeto ObjectInputStream como parámetro que es usado para reconstruir el objeto de datos.

3.2. La clase Externalizable

En lugar de implementar la clase java.io.Serializable, una clase puede implementar la interfaz java.io.Externalizable, para tener un control completo sobre el protocolo usado para codificar las instancias de esa clase. Por ejemplo, los métodos readObject y writeObject solamente tienen en cuenta la lectura y escritura de atributos declarados en su clase, pudiendo obviar campos de sus superclases. Además, la serialización via Externalizable probablemente funcionará más rápido que la serialización por defecto via Serializable.

La principal diferencia entre los objetos Externalizable y Serializable es que un objeto Externalizable debe definir métodos de lectura y escritura (readExternal() y writeExternal()). No hay serialización por defecto para un objeto Externalizable.

public void writeExternal(ObjectOutput stream) throws IOException;


public void readExternal(objectInput stream) throws IOException;

Típicamente, los métodos readExternal y writeExternal de una clase Externalizable cuya clase base sea también Externalizable comenzarán llamando a super.readExternal y super.writeExternal respectivamente.