Crear un Programa Cliente

El motor de cálculo es un bonito y sencillo programa - ejecuta las tareas que le son enviadas. Los clientes del motor de cálculo son más complejos. Un cliente necesita llamar al motor de cálculo, pero también tiene que definir la tarea que éste va a realizar.

Nuestro ejemplo está compuesto por dos clases separadas. la primera clase ComputePi, busca y llama a un objeto Compute. La segunda clase Pi, implementa el interface Task y define el trabajo que va a hacer el motor de cálcilo. El trabajo de la clase Pi es calcular el valor del número pi, con algún número de posiciones decimales.

Como recordaremos, el interface no-remoto Task se define de esta forma:

package compute; 
public interface Task extends java.io.Serializable {
    Object execute(); 
}
El interface Task extiende java.io.Serializable por lo que cualquier objeto que lo implemente puede ser serializado por el sistema RMI y enviado a una máquina virtual remota como parte de una llamada a un método remoto. Podríamos haber elegido hacer que la implementación de nuestra clase implementara los interfaces Task y Serializable, y hubiera tenido el mismo efecto. Sin embargo, el único proposito del interface Task es permitir que las implementaciones de este interface sean pasadas a objetos Compute, por eso, una clase que implemente el interface Task no tiene sentido que también implemente el interface Serializable. Dado esto, hemos asociado explícitamente los dos interfaces en el tipo system, asegurando que todos los objetos Task sean serializables.

El código que llama a los métodos del objeto Compute debe obtener una referencia a ese objeto, crear un objeto Task, y luego pedir que se ejecute la tarea. Más adelante veremos la definición de la tarea Pi. Un objeto Pi se construye con un sólo argumento, la precisión deseada en el resultado. El resultado de la ejecución de la tarea es un java.math.BigDecimal que representa el número pi calculado con la precisión especificada.

La clase cliente ComputePi:

 
package client;

import java.rmi.*;
import java.math.*;
import compute.*;

public class ComputePi {
    public static void main(String args[]) {
        if (System.getSecurityManager() == null) {
            System.setSecurityManager(new RMISecurityManager());
        }
        try {
            String name = "//" + args[0] + "/Compute";
            Compute comp = (Compute) Naming.lookup(name);
            Pi task = new Pi(Integer.parseInt(args[1]));
            BigDecimal pi = (BigDecimal) (comp.executeTask(task));
            System.out.println(pi);
        } catch (Exception e) { 	
            System.err.println("ComputePi exception: " + e.getMessage());
            e.printStackTrace();
        }
    } 
}
Al igual que el servidor ComputeEngine, el cliente empieza instalando un controlador de seguridad. Esto es necesario porque RMI podría descargar código en el cliente. En este ejemplo, el stub ComputeEngine es descargado al cliente. Siempre que el RMI descargue código, debe presentarse un controlador de seguridad. Al igual que el servidor, el cliente utiliza el controlador de seguridad proporcionado por el sistema RMI para este propósito.

Después de llamar al controlador de seguridad, el cliente construye un nombre utilizado para buscar un objeto remoto Compute. El valor del primer argumento de la línea de comandos args[0], es el nombre del host remoto, en el que se están ejecutando los objetos Compute. Usando el método Naming.lookup, el cliente busca el objeto remoto por su nombre en el registro del host remoto. Cuando se hace la búsqueda del nombre, el código crea una URL que específica el host donde se está ejecutando el servidor. El nombre pasado en la llamada a Naming.lookup tiene la misma síntaxis URL que el nombre pasado a la llamada Naming.rebind que explícamos en páginas anteriores.

Luego, el cliente crea un objeto Pi pasando al constructor de Pi el segundo argumento de la línea de comandos, args[1], que indica el número de decimales utilizados en el cálculo. Finalmente, el cliente llama al método executeTask del objeto remoto Compute. El objeto pasado en la llamada a executeTask devuelve un objeto del tipo java.math.BigDecimal, por eso el programa fuerza el resultado a ese tipo y almacena en resultado en la variable result. Finalmente el programa imprime el resultado.


Flujo de mensajes entre el cliente ComputePi, el rmiregistry, y el ComputeEngine.

Finalmente, echemos un vistazo a la clase Pi. Esta clase implementa el interface Task y cálcula el valor del número pi con un número de decimales especificado. Desde el punto de vista de este ejemplo, el algoritmo real no es importante (excepto, por supuesto, para la fiabilidad del cálculo). Todo lo importante es que el cálculo consume numéricamene muchos recursos (y por eso es el tipo que cosa que querríamos hacer en un servidor potente).

Aquí tenemos el código de la clase Pi, que implementa Task:

 
package client;
import compute.*;
import java.math.*;

public class Pi implements Task {

    /** constantes utilizadas en el cálculo de pi*/
    private static final BigDecimal ZERO = 
        BigDecimal.valueOf(0);
    private static final BigDecimal  ONE = 
        BigDecimal.valueOf(1);
    private static final BigDecimal FOUR = 
        BigDecimal.valueOf(4);

    /** modo de redondeo utilizado durante el cálculo*/
    private static final int roundingMode = 
        BigDecimal.ROUND_HALF_EVEN;

    /** número de dígitos tras el punto decimal*/
    private int digits;
    
    /**
     * Construye una tarea para calcular el núemro pi
     * con la precisión específicada.
     */
    public Pi(int digits) {
        this.digits = digits;
    }

    /**
     * Calcula pi.
     */
    public Object execute() {
        return computePi(digits);
    }

    /**
     * Calcula el valor de Pi con el número de decimales especificados.
     * El valor se calcula utilizando la fórmula de Machin:
     *
     *          pi/4 = 4*arctan(1/5) - arctan(1/239)
     *
     * y una poderoas serie de expansiones de arctan(x) 
     * para una precisión suficiente.
     */
    public static BigDecimal computePi(int digits) {
        int scale = digits + 5;
        BigDecimal arctan1_5 = arctan(5, scale);
        BigDecimal arctan1_239 = arctan(239, scale);
        BigDecimal pi = arctan1_5.multiply(FOUR).subtract(arctan1_239).multiply(FOUR);
         return pi.setScale(digits, 
                          BigDecimal.ROUND_HALF_UP);
    }

    /**
     * Calcula el valor, en radianes, de la arcotangente de la 
     * inversa del entero suministrado para el número de decimales. 
     * El valor se calcula utilizando la poderosa serie de 
     * expansiones de arcotangente:
     *
     * arctan(x) = x - (x^3)/3 + (x^5)/5 - (x^7)/7 + 
     *     (x^9)/9 ...
     */   
    public static BigDecimal arctan(int inverseX, 
                                  int scale) 
    {
        BigDecimal result, numer, term;
        BigDecimal invX = BigDecimal.valueOf(inverseX);
        BigDecimal invX2 = 
            BigDecimal.valueOf(inverseX * inverseX);

        numer = ONE.divide(invX, scale, roundingMode);

        result = numer;
        int i = 1;
        do {
            numer = 
                numer.divide(invX2, scale, roundingMode);
            int denom = 2 * i + 1;
            term = 
                numer.divide(BigDecimal.valueOf(denom),
                             scale, roundingMode);
            if ((i % 2) != 0) {
                result = result.subtract(term);
            } else {
                result = result.add(term);
            }
            i++;
        } while (term.compareTo(ZERO) != 0);
        return result;
    }
}
La característica más interesante de este ejemplo es que el objeto Compute no necesita una definición de la clase Pi hasta que se le pasa un objeto Pi como un argumento del método executeTask. Hasta este punto, el código de la clase se ha cargado por el RMI dentro de la máquina virtual del objeto Compute, se ha llamado al método execute, y se ha ejecutado el código de la tarea. El Object resultante (que en el caso de la tarea Pi es realmente un objeto java.math.BigDecimal) es enviado de vuelta al cliente, donde se utiliza para imprimir el resultado.

El hecho de que el objeto Task suministrado calcule el valor de Pi es irrelevante para el objeto ComputeEngine. Por ejemplo, también podríamos implementar una tarea que generara un número primo aleatorio utilizando un algoritmo probabilistico. (Esto también consume muchos recursos y por tanto es un candidato para ser enviado al ComputeEngine). Este código también podría ser descargado cuando el objeto Task fuera pasado al objeto Compute. Todo lo que el objeto Compute sabe es que cada objeto que recibe implementa el método execute, no sabe (y tampoco le interesa) qué hace la implementación.


Ozito