Sun Cluster: Guía del desarrollador de los servicios de datos del sistema operativo Solaris

Capítulo 12 CRNP

Este capítulo proporciona información sobre el Protocolo de notificación de reconfiguración del clúster (CRNP, Cluster Reconfiguration Notification Protocol) que permite que las aplicaciones a prueba de fallos y escalables tengan un modo de funcionamiento “en cluster”; más concretamente, proporciona un mecanismo que permite a las aplicaciones registrarse y recibir posteriores notificaciones asíncronas de los eventos de reconfiguración de Sun Cluster. Los servicios de datos que se ejecutan en el clúster y las aplicaciones que se ejecutan fuera de él se pueden registrar para recibir notificación de los eventos. Éstos se generan cuando cambian los miembros de un clúster y cuando cambia el estado de un grupo de recursos o de un recurso.

Información general sobre CRNP

CRNP proporciona mecanismos y daemons que generan eventos de reconfiguración del clúster, los encamina a través del clúster y los envía a los clientes interesados.

El daemon cl_apid interactúa con los clientes. El Gestor de grupos de recursos (RGM) de Sun Cluster genera eventos de reconfiguración del clúster. Estos daemons utilizan syseventd(1M) para transmitir eventos en todos los nodos locales. El daemon cl_apid utiliza Lenguaje de marcas extensible (XML) en TCP/IP para comunicarse con los clientes interesados.

El diagrama siguiente presenta información general sobre el flujo de eventos entre los componentes de CRNP. En este diagrama, un cliente se está ejecutando en el nodo del clúster 2 y el otro en un ordenador que no forma parte del clúster.

Figura 12–1 Cómo funciona CRNP

Diagrama de flujo que muestra el funcionamiento de CRNP

Información general sobre el protocolo de CRNP

CRNP define las capas de aplicación, presentación y sesión de la pila estándar del protocolo de interconexión de sistema abierto (OSI) de siete capas. La capa de transporte debe ser TCP y la de red, IP. CRNP es independiente de las capas física y de enlace de datos. Todos los mensajes de capa de aplicación intercambiados en CRNP se basan en XML 1.0.

Semántica del protocolo de CRNP

Los clientes inician una comunicación mediante el envío de un mensaje de registro (SC_CALLBACK_RG) que especifica los tipos de evento para los que los clientes quieren recibir notificaciones, así como un puerto al cual pueden entregarse los eventos. La IP origen de la conexión de registro y el puerto especificado forman conjuntamente la dirección de retrollamada.

Siempre que se genera un evento que pueda interesar a un cliente en el clúster, el servidor se pone en contacto con el cliente en su dirección de rellamada (IP y puerto) y le envía el evento (SC_EVENT). El servidor tiene una alta disponibilidad y se ejecuta en el propio clúster. El servidor almacena los registros del cliente en un almacenamiento que se conserva incluso después de rearrancar el clúster.

Los clientes anulan sus registros mediante el envío de un mensaje de registro (SC_CALLBACK_RG que contiene un mensaje REMOVE_CLIENT) al servidor. Cuando el cliente recibe un mensaje SC_REPLY del servidor, cierra la conexión.

El diagrama siguiente muestra el flujo de comunicación entre un cliente y un servidor.

Figura 12–2 Flujo de comunicación entre un cliente y un servidor

Diagrama de flujo que muestra el flujo de comunicaciones entre el cliente y el servidor

Tipos de mensaje que emplea CRNP

CRNP emplea tres tipos de mensajes, todos basados en XML, como se detalla en la tabla siguiente. Estos tipos de mensaje y su sintaxis se explican con más detalle en este mismo capítulo.

Tipo de mensaje  

Descripción 

SC_CALLBACK_REG

Este mensaje adopta cuatro formas: ADD_CLIENT, REMOVE_CLIENT, ADD_EVENTS y REMOVE_EVENTS. Cada una de ellas contiene la información siguiente:

  • Versión del protocolo

  • Puerto de rellamada en formato ASCII (no formato binario)

Las formas ADD_CLIENT, ADD_EVENTS y REMOVE_EVENTS contienen también una lista ilimitada de tipos de eventos, cada uno de los cuales incluye la información siguiente:

  • Clase de evento

  • Subclase de evento (opcional)

  • Lista de los pares de nombre y valor (opcional)

La clase y la subclase de evento definen conjuntamente un “tipo de evento” exclusivo. La definición de tipo de documento desde la que se generan las clases de SC_CALLBACK_REG es SC_CALLBACK_REG. Esta DTD se describe con más detalle en el Apéndice F.

SC_EVENT

Este mensaje contiene la información siguiente:

  • Versión del protocolo

  • Clase de evento

  • Subclase de evento

  • Fabricante

  • Editor

  • Lista de pares de nombres y valores (estructuras de datos de pares de 0 o más nombres y valores)

    • Nombre (cadena)

    • Valor (cadena o matriz de cadenas)

Los valores en SC_EVENT no tienen un tipo. La definición de tipo de documento desde la cual se generan las clases de SC_EVENT es SC_EVENT. Esta DTD se describe con más detalle en el Apéndice F.

SC_REPLY

Este mensaje contiene la información siguiente:

  • Versión del protocolo

  • Código de error

  • Mensaje de error

La definición de tipo de documento desde la cual se generan las clases de SC_REPLY es SC_REPLY. Esta DTD se describe con más detalle en el Apéndice F.

Cómo se registra un cliente en un servidor

Esta sección describe cómo un administrador debe configurar el servidor, cómo se identifican los clientes, cómo se envía información por las capas de aplicación y sesión y las condiciones de error.

Supuestos sobre cómo los administradores configuran el servidor

El administrador del sistema debe configurar el servidor con una dirección IP de alta disponibilidad (es decir, una dirección IP que no esté vinculada a una máquina concreta del clúster) y un número de puerto. El administrador debe publicar esta dirección de red para los clientes potenciales. CRNP no define cómo se pone este nombre de servidor a disposición de los clientes. Los administradores deberán utilizar un servicio de nombres, que permitirá a los clientes buscar la dirección de red del servidor de forma dinámica, o agregarán el nombre de red a un archivo de configuración que podrá leer el cliente. El servidor se ejecutará dentro del clúster como un tipo de recurso a prueba de fallos.

Cómo identifica el servidor a un cliente

Cada cliente está identificado de forma exclusiva por una dirección de rellamada, es decir, su dirección IP y número de puerto. El puerto se especifica en los mensajes SC_CALLBACK_REG y la dirección IP se obtiene de la conexión de registro TCP. CRNP asume que los siguientes mensajes SC_CALLBACK_REG con la misma dirección de rellamada provienen del mismo cliente, aunque el puerto de origen desde el que se envíen los mensajes sea diferente.

Cómo se pasan los mensajes SC_CALLBACK_REG entre el cliente y el servidor

Un cliente inicia un registro, abriendo una conexión TCP con la dirección IP y el número de puerto del servidor. Una vez establecida la conexión TCP y lista para escribir, el cliente debe enviar su mensaje de registro. Éste debe ser un mensaje SC_CALLBACK_REG de formato correcto que no contenga bytes adicionales antes ni después del mensaje.

Una vez escritos todos los bytes en el canal, el cliente debe mantener la conexión abierta para recibir la respuesta del servidor. Si el cliente no le da el formato correcto al mensaje, el servidor no registrará el cliente y le enviará una respuesta de error. Si el cliente cierra la conexión de zócalo antes de que el servidor envíe una respuesta, éste registra el cliente como normal.

Un cliente puede ponerse en contacto con el servidor en cualquier momento. Cada vez que un cliente se pone en contacto con el servidor, el cliente debe enviar un mensaje SC_CALLBACK_REG. Si el servidor recibe un mensaje con formato erróneo, fuera de servicio o no válido, envía una respuesta de error al cliente.

Un cliente no puede enviar un mensaje ADD_EVENTS, REMOVE_EVENTS o REMOVE_CLIENT antes de enviar el mensaje ADD_CLIENT. Un cliente no puede enviar un mensaje REMOVE_CLIENT antes de haber enviado un mensaje ADD_CLIENT.

Si un cliente envía un mensaje ADD_CLIENT y el cliente ya está registrado, es posible que el servidor tolere el mensaje. En este caso, el servidor sustituye silenciosamente el registro anterior del cliente por el nuevo, especificado en el segundo mensaje ADD_CLIENT.

En la mayoría de los casos, un cliente se registra sólo una vez en el servidor, al comenzar, con un mensaje ADD_CLIENT. El cliente sólo cancela su registro una vez, enviando un mensaje REMOVE_CLIENT al servidor. Sin embargo, CRNP proporciona una mayor flexibilidad a aquellos clientes que tengan que modificar dinámicamente la lista de tipos de eventos.

Contenido de un mensaje SC_CALLBACK_REG

Cada mensaje ADD_CLIENT, ADD_EVENTS y REMOVE_EVENTS contiene una lista de eventos. La tabla siguiente describe los tipos de eventos que acepta CRNP, incluidos los valores requeridos de nombre y valor.

Si un cliente:

En ambos casos el servidor ignora discretamente estos mensajes.

Clase y subclase  

Pares nombre-valor 

Descripción  

EC_Cluster

ESC_cluster_membership

Necesario: ninguno 

Opcional: ninguno  

Registra todos los eventos de cambio en los miembros del clúster (terminación de nodo o unión) 

EC_Cluster

ESC_cluster_rg_state

Uno necesario, como sigue: 

rg_name

Tipo de valor: cadena 

Opcional: ninguno  

Registra todos los eventos de cambio de estado del grupo de recursos nombre

EC_Cluster

ESC_cluster_r_state

Uno necesario, como sigue: 

r_name

Tipo de valor: cadena 

Opcional: ninguno  

Registra todos los eventos de cambio de estado del recurso nombre

EC_Cluster

Ninguno 

Necesario: ninguno  

Opcional: ninguno 

Registra todos los eventos de Sun Cluster 

Cómo responde el servidor a un cliente

Después de procesar el registro, el servidor envía el mensaje SC_REPLY sólo en la conexión TCP abierta desde el cliente en el que el servidor ha recibido la solicitud de registro; a continuación cierra la conexión. El cliente debe mantener abierta la conexión TCP hasta que reciba el mensaje SC_REPLY del servidor.

Por ejemplo, el cliente realiza las siguientes acciones:

  1. Abre una conexión TCP al servidor

  2. Espera que la conexión se pueda “escribir”

  3. Envía un mensaje SC_CALLBACK_REG (que contiene un mensaje ADD_CLIENT)

  4. Espera un mensaje SC_REPLY

  5. Recibe un mensaje SC_REPLY

  6. Recibe un indicador de que el servidor ha cerrado la conexión (lee 0 bytes del zócalo)

  7. Cierra la conexión

Más tarde, el cliente realiza las acciones siguientes:
  1. Abre una conexión TCP al servidor

  2. Espera que la conexión se pueda “escribir”

  3. Envía un mensaje SC_CALLBACK_REG (que contiene un mensaje REMOVE_CLIENT)

  4. Espera un mensaje SC_REPLY

  5. Recibe un mensaje SC_REPLY

  6. Recibe un indicador de que el servidor ha cerrado la conexión (lee 0 bytes del zócalo)

  7. Cierra la conexión

Cada vez que el servidor recibe un mensaje SC_CALLBACK_REG de un cliente, le envía un mensaje SC_REPLY en la misma conexión abierta. Este mensaje especifica si la operación se ha realizado de modo satisfactorio o no. En DTD de XML de SC_REPLY se explica la definición de tipo de documento XML de un mensaje SC_REPLY y los posibles mensajes de error que pueda contener.

Contenido de un mensaje SC_REPLY

Un mensaje SC_REPLY especifica si la operación se ha realizado de modo satisfactorio o no. Este mensaje contiene la versión del mensaje del protocolo CRNP, un código y un mensaje de estado que describe con más detalle el código de estado. La tabla siguiente describe los valores posibles del código de estado.

Código de estado  

Descripción 

OK

El mensaje se ha procesado satisfactoriamente. 

RETRY

El servidor ha rechazado el registro del cliente debido a un error temporal (el cliente debería volver a intentar el registro con parámetros diferentes). 

LOW_RESOURCE

Los recursos del clúster están bajos y el cliente sólo puede volver a intentarlo más tarde (el administrador del sistema del clúster también podría ampliar los recursos del clúster). 

SYSTEM_ERROR

Se ha producido un error grave. Póngase en contacto con el administrador del sistema del clúster. 

FAIL

La autorización ha fallado o se ha producido otro problema que ha provocado el fallo de registro. 

MALFORMED

La solicitud XML tenía un formato erróneo y no se ha podido analizar. 

INVALID

La solicitud XML no era válida (no cumple la especificación XML). 

VERSION_TOO_HIGH

La versión del mensaje era demasiado elevada para procesar satisfactoriamente el mensaje. 

VERSION_TOO_LOW

La versión del mensaje era demasiado baja para procesarlo satisfactoriamente. 

Cómo debe resolver el cliente las condiciones de error

En condiciones normales, un cliente que envía un mensaje SC_CALLBACK_REG recibe una respuesta que indica que el registro ha sido satisfactorio o no.

Sin embargo, el servidor puede experimentar una condición de error, durante el registro de un cliente, que le impida enviarle un mensaje SC_REPLY de vuelta. En este caso, el registro se podría haber realizado satisfactoriamente antes de que se produjera la condición de error, podría haber fallado o podría no haber sido procesado.

Dado que el servidor debe funcionar como un servidor de tipo a prueba de fallos o de alta disponibilidad en el clúster, esta condición de error no supondrá el fin del servicio. De hecho, es posible que el servidor pueda empezar muy pronto a enviar eventos al cliente recién registrado.

Para remediar estas condiciones, el cliente debería:

Cómo envía eventos el servidor al cliente

Dado que los eventos se generan dentro del clúster, el servidor CRNP los envía a todos los clientes que solicitaron eventos de este tipo. El envío consiste en dirigir un mensaje SC_EVENT a la dirección de rellamada del cliente. El envío de cada evento se produce en una conexión TCP nueva.

Inmediatamente después de que un cliente se registre para un tipo de evento, a través de un mensaje SC_CALLBACK_REG que contenga un mensaje ADD_CLIENT o ADD_EVENT, el servidor enviará el evento más reciente de ese tipo al cliente. Éste podrá así descubrir el estado actual del sistema desde el que se le enviarán los siguientes eventos.

Cuando el servidor inicie una conexión TCP con el cliente, enviará exactamente un mensaje SC_EVENT por la conexión. El servidor después enviará un cierre full-duplex.

Por ejemplo, el cliente realiza las siguientes acciones:

  1. Espera que el servidor inicie una conexión TCP

  2. Acepta la conexión entrante del servidor

  3. Espera un mensaje SC_EVENT

  4. Lee un mensaje SC_EVENT

  5. Recibe un indicador de que el servidor ha cerrado la conexión (lee 0 bytes del zócalo)

  6. Cierra la conexión

Cuando todos los clientes estén registrados, deberán recibir en todas sus direcciones de rellamada (dirección IP y número de puerto) en todo momento, por si hubiera una conexión entrante para el envío de un evento.

Si el servidor no logra contactar con el cliente para enviarle un evento, vuelve a intentar enviarlo un cierto número de veces, en el intervalo que se defina. Si los intentos fallan, el cliente es eliminado de la lista de clientes del servidor. El cliente también debe volver a registrarse mediante el envío de otro mensaje SC_CALLBACK_REG que contenga un mensaje ADD_CLIENT antes de que el cliente pueda recibir más eventos.

Cómo se garantiza el envío de eventos

Hay un orden total de generación de eventos dentro del clúster, que se mantiene en el orden de envío a cada cliente. En otras palabras, si el evento A se genera en el clúster antes que el evento B, el cliente X recibe el evento A antes que el B. Sin embargo, el orden total de envío de eventos a todos los clientes no se mantiene. Es decir, el cliente Y podría recibir los eventos A y B antes de que el cliente X recibiera el evento A. Así, los clientes más lentos no retrasan el envío a los demás clientes.

Todos los eventos que envía el servidor (salvo el primero de una subclase y los eventos que siguen a los errores de servidor) se producen como respuesta a eventos reales generados por el clúster, salvo que el servidor sufra un error que haga que pierda eventos generados por el clúster. En ese caso, el servidor genera un evento para cada tipo de evento que representa el estado actual del sistema para ese tipo. Cada evento se envía a los clientes que registraron su interés en ese tipo de evento.

El envío de eventos sigue la semántica de “una vez como mínimo”. Es decir, el servidor puede enviar el mismo evento a un cliente más de una vez. Este margen es necesario en los casos en los que el servidor se desconecta temporalmente y, cuando se vuelve a conectar, no puede determinar si el cliente ha recibido la última información.

Contenido de un mensaje SC_EVENT

El mensaje SC_EVENT contiene el mensaje real generado en el clúster, traducido para encajar en el formato de mensaje XML SC_EVENT. La tabla siguiente describe los tipos de evento que envía CRNP, incluidos los pares nombre-valor, editor y fabricante.

Clase y subclase  

Editor y fabricante 

Pares nombre-valor 

Notas 

EC_Cluster

ESC_cluster_membership

Editor: rgm 

Fabricante: SUNW  

Nombre: node_list

Tipo de valor: matriz de cadenas  

Nombre: state_list

Tipo de valor: matriz de cadenas 

Las posiciones de los elementos de la matriz de state_list están sincronizadas con las de node_list. Es decir, el estado del nodo listado en primer lugar en la matriz node_list es el primero en la matriz state_list.

state_list contiene sólo números representados en ASCII, cada uno de los cuales representa el número que encarna actualmente ese nodo en el clúster. Si el número es el mismo que el recibido en un mensaje anterior, el nodo no ha modificado su relación con el clúster (se ha ido, se ha unido o se ha vuelto a unir). Si el número de representación es –1, el nodo no es miembro del clúster. Si el número de representación es un número positivo, el nodo es miembro del clúster.

Otros nombres que empiezan por ev_ y sus valores asociados pueden estar también presentes, pero no son para el uso del cliente.

EC_Cluster

ESC_cluster_rg_state

Editor: rgm 

Fabricante: SUNW  

Nombre: rg_name

Tipo de valor: cadena  

Nombre: node_list

Tipo de valor: matriz de cadenas  

Nombre: state_list

Tipo de valor: matriz de cadenas 

Las posiciones de los elementos de la matriz de state_list están sincronizadas con las de node_list. Es decir, el estado del nodo listado en primer lugar en la matriz node_list es el primero en la matriz state_list.

state_list contiene representaciones de cadena del estado del grupo de recursos. Los valores válidos son los que se pueden recuperar con las órdenes scha_cmds(1HA).

Otros nombres que empiezan por ev_ y sus valores asociados pueden estar también presentes, pero no son para el uso del cliente.

EC_Cluster

ESC_cluster_r_state

Editor: rgm 

Fabricante: SUNW  

Tres necesarios, como sigue:  

Nombre: r_name

Tipo de valor: cadena 

Nombre: node_list

Tipo de valor: matriz de cadenas 

Nombre: state_list

Tipo de valor: matriz de cadenas 

Las posiciones de los elementos de la matriz de state_list están sincronizadas con las de node_list. Es decir, el estado del nodo que se lista en primer lugar en la matriz node_list es el primero en la matriz state_list.

state_list contiene representaciones de cadenas del estado del recurso. Los valores válidos son los que se pueden recuperar con las órdenes scha_cmds(1HA).

Otros nombres que empiezan por ev_ y sus valores asociados pueden estar también presentes, pero no son para el uso del cliente.

Cómo autentica CRNP los clientes y el servidor

El servidor autentica un cliente con una forma de envoltorio de TCP. La dirección IP de origen del mensaje de registro (que se usa también como dirección IP de rellamada a la que se deben enviar los eventos) debe estar en la lista de clientes admitidos en el servidor. La dirección IP de origen y el mensaje de registro no pueden estar en la lista de clientes rechazados. Si la dirección IP de origen y el registro no están en la lista, el servidor rechaza la solicitud y emite una respuesta de error para el cliente.

Cuando el servidor recibe un mensaje SC_CALLBACK_REG ADD_CLIENT, los mensajes SC_CALLBACK_REG posteriores para ese cliente deben contener una dirección IP de origen que es la misma dirección IP de origen del primer mensaje. Si el servidor CRNP recibe un mensaje SC_CALLBACK_REG que no cumple este requisito, el servidor:

Este mecanismo de seguridad ayuda a evitar ataques de denegación de servicio, en los que alguien intenta anular el registro de un cliente legítimo.

Los clientes deberían autenticar del mismo modo al servidor y sólo deberían aceptar los envíos de un servidor cuya dirección IP de origen y número de puerto coincidieran con la dirección IP y número de puerto de registro empleados por el cliente.

Dado que se espera que los clientes del servicio CRNP se encuentren dentro de un cortafuegos que proteja el clúster, CRNP no incluye otros mecanismos de seguridad adicional.

Creación de una aplicación Java que utilice CRNP

El ejemplo siguiente ilustra cómo se debe desarrollar una aplicación sencilla de Java, llamada CrnpClient, que utiliza CRNP. La aplicación se registra para rellamadas de eventos en el servidor CRNP del clúster, recibe las rellamadas de eventos y procesa éstos mediante la impresión del contenido. Antes de finalizar, la aplicación anula el registro de su solicitud de rellamadas de eventos.

Tenga los siguientes puntos presentes al revisar este ejemplo.

Configuración del entorno

En primer lugar hay que configurar el entorno.

  1. Descargue e instale JAXP y la versión correcta del compilador de Java y la máquina virtual Java.

    En http://java.sun.com/xml/jaxp/index.html encontrará instrucciones.


    Nota –

    Este ejemplo requiere la utilización de Java 1.3.1 o posterior.


  2. Asegúrese de que especifique una classpath en la línea de órdenes de compilación para que el compilador pueda encontrar las clases de JAXP. Desde el directorio en el que se encuentra el archivo de origen, escriba:


    % javac -classpath RAÍZ_JAXP/dom.jar:RAÍZ_JAXPjaxp-api. \
    jar:RAÍZ_JAXPsax.jar:RAÍZ_JAXPxalan.jar:RAÍZ_JAXP/xercesImpl \
    .jar:RAÍZ_JAXP/xsltc.jar -ruta_origen . NOMBRE_ARCHIVO_ORIGEN.java
    

    donde RAÍZ_JAXP es la ruta absoluta o relativa al directorio en el que se encuentran los archivos jar de JAXP y NOMBRE_ARCHIVO_ORIGEN es el nombre de su archivo de origen Java.

  3. Al ejecutar la aplicación, se debe especificar la classpath para que la aplicación pueda cargar los archivos de clase de JAXP adecuados (tenga presente que la primera ruta de classpath es el directorio actual):


    java -cp .:RAÍZ_JAXP/dom.jar:RAÍZ_JAXPjaxp-api. \
    jar:RAÍZ_JAXPsax.jar:RAÍZ_JAXPxalan.jar:RAÍZ_JAXP/xercesImpl \
    .jar:RAÍZ_JAXP/xsltc.jar NOMBRE_ARCHIVO_ORIGEN ARGUMENTOS
    

    Ahora que se ha configurado el entorno se puede desarrollar la aplicación.

Procedimientos iniciales

En esta parte del ejemplo, se crea una clase básica denominada CrnpClient, con un método principal que analiza los argumentos de la línea de órdenes y construye un objeto CrnpClient. Este objeto pasa los argumentos de línea de órdenes a la clase, espera a que el usuario termine la aplicación, invoca shutdown en la clase CrnpClient y sale.

El constructor de la clase CrnpClient debe ejecutar las siguientes tareas:

    Crear el código Java que aplica la lógica anterior.

    El ejemplo siguiente muestra el código de la estructura básica de la clase CrnpClient. Las implementaciones de los cuatro métodos propios a los que se hace referencia en los métodos de cierre y el constructor aparecen más adelante. Observe que se muestra el código que importa todos los paquetes necesarios.


    import javax.xml.parsers.*;
    import javax.xml.transform.*;
    import javax.xml.transform.dom.*;
    import javax.xml.transform.stream.*;
    import org.xml.sax.*;
    import org.xml.sax.helpers.*;
    import org.w3c.dom.*;
    
    import java.net.*;
    import java.io.*;
    import java.util.*;
    
    
    class CrnpClient
    {
            public static void main(String []args)
            {
                    InetAddress regIp = null;
                    int regPort = 0, localPort = 0;
    
                    try {
                            regIp = InetAddress.getByName(args[0]);
                            regPort = (new Integer(args[1])).intValue();
                            localPort = (new Integer(args[2])).intValue();
                    } catch (UnknownHostException e) {
                            System.out.println(e);
                            System.exit(1);
                    }
    
                    CrnpClient client = new CrnpClient(regIp, regPort, localPort,
                        args);
                    System.out.println("Pulsar Retorno para terminar la demostración...");
                    try {
                            System.in.read();
                    } catch (IOException e) {
                            System.out.println(e.toString());
                    }
                    client.shutdown();
                    System.exit(0);
            }
    
            public CrnpClient(InetAddress regIpIn, int regPortIn, int localPortIn,
                String []clArgs)
            {
                    try {
                            regIp = regIpIn;
                            regPort = regPortIn;
                            localPort = localPortIn;
                            regs = clArgs;
    
                            setupXmlProcessing();
                            createEvtRecepThr();
                            registerCallbacks();
    
                    } catch (Exception e) {
                            System.out.println(e.toString());
                            System.exit(1);
                    }
            }
    
            public void shutdown()
            {
                    try {
                            unregister();
                    } catch (Exception e) {
                            System.out.println(e);
                            System.exit(1);
                    }
            }
    
            private InetAddress regIp;
            private int regPort;
            private EventReceptionThread evtThr;
            private String regs[];
    
            public int localPort;
            public DocumentBuilderFactory dbf;
    }

    Las variables de los miembros se explican con más detalle más adelante.

Análisis de los argumentos de la línea de órdenes

    Para ver cómo analizar los argumentos de línea de órdenes, consulte el código del Apéndice G.

Definición del subproceso de recepción de eventos

En el código, hay que comprobar que la recepción de eventos se realice en un subproceso aparte de forma que la aplicación pueda seguir realizando el otro trabajo, mientras el subproceso de evento se bloquea y espera rellamadas de eventos.


Nota –

La configuración del XML se detalla más adelante.


  1. En el código, defina una subclase de Thread llamada EventReceptionThread que cree ServerSocket y espere que lleguen eventos al zócalo.

    En esta parte del código de ejemplo, los eventos no se leen ni se procesan. La lectura y el procesamiento de eventos se explican más adelante. EventReceptionThread crea ServerSocket en una dirección comodín de protocolo de interred. EventReceptionThread mantiene también una referencia al objeto CrnpClient para que EventReceptionThread pueda enviar eventos al objeto CrnpClient para que los procese.


    class EventReceptionThread extends Thread
    {
            public EventReceptionThread(CrnpClient clientIn) throws IOException
            {
                    client = clientIn;
                    listeningSock = new ServerSocket(client.localPort, 50,
                        InetAddress.getLocalHost());
            }
    
            public void run()
            {
                    try {
                            DocumentBuilder db = client.dbf.newDocumentBuilder();
                            db.setErrorHandler(new DefaultHandler());
    
                            while(true) {
                                    Socket sock = listeningSock.accept();
                                    // Construir un evento desde el flujo del zócalo y procesarlo
                                    sock.close();
                            }
                            // INACCESIBLE
    
                    } catch (Exception e) {
                            System.out.println(e);
                            System.exit(1);
                    }
            }
    
            /* variables privadas de miembro */
            private ServerSocket listeningSock;
            private CrnpClient client;
    }

  2. Ahora que sabe cómo funciona la clase EventReceptionThread, cree un objeto createEvtRecepThr:


    private void createEvtRecepThr() throws Exception
    {
            evtThr = new EventReceptionThread(this);
            evtThr.start();
    }

Registro y anulación del registro de rellamadas

La tarea de registro consiste en:

  1. Crear el código Java que aplica la lógica anterior.

    El ejemplo siguiente muestra la aplicación del método registerCallbacks de la clase CrnpClient (que invoca el constructor de CrnpClient). Las llamadas a createRegistrationString() y readRegistrationReply() se describen con más detalle más adelante.

    regIp y regPort son miembros objeto configurados por el constructor.


    private void registerCallbacks() throws Exception
    {
            Socket sock = new Socket(regIp, regPort);
            String xmlStr = createRegistrationString();
            PrintStream ps = new
                    PrintStream(sock.getOutputStream());
            ps.print(xmlStr);
            readRegistrationReply(sock.getInputStream();
            sock.close();
    }

  2. Aplique el método unregister. Este método lo invoca el método shutdown de CrnpClient. La implementación de createUnregistrationString se describe más adelante con más detalle.


    private void unregister() throws Exception
    {
            Socket sock = new Socket(regIp, regPort);
            String xmlStr = createUnregistrationString();
            PrintStream ps = new PrintStream(sock.getOutputStream());
            ps.print(xmlStr);
            readRegistrationReply(sock.getInputStream());
            sock.close();
    }

Generación de XML

Ahora que se ha configurado la estructura de la aplicación y ha escrito todo el código de conexión a red, es necesario escribir el código que genera y analiza el XML. Empiece escribiendo el código que genera el mensaje de registro de XML SC_CALLBACK_REG.

Un mensaje SC_CALLBACK_REG consiste en un tipo de registro (ADD_CLIENT, REMOVE_CLIENT, ADD_EVENTS o REMOVE_EVENTS), un puerto de rellamada y una lista de eventos de interés. Cada evento consta de una clase y una subclase, seguidos de una lista de pares de nombres y valores.

En esta parte del ejemplo, se escribe una clase CallbackReg que guarda el tipo de registro, el puerto de rellamada y la lista de eventos de registro. Esta clase también puede serializarse en un mensaje de XML SC_CALLBACK_REG.

Un método interesante de esta clase es el convertToXml, que crea una cadena de mensaje de XML SC_CALLBACK_REG a partir de los miembros de la clase. La documentación de JAXP de http://java.sun.com/xml/jaxp/index.html describe el código de este método con más detalle.

La aplicación de la clase Event se muestra a continuación. Observe que la clase CallbackReg utiliza una clase Event que almacena un evento y puede convertirlo en un Element de XML.

  1. Crear el código Java que aplica la lógica anterior.


    class CallbackReg
    {
            public static final int ADD_CLIENT = 0;
            public static final int ADD_EVENTS = 1;
            public static final int REMOVE_EVENTS = 2;
            public static final int REMOVE_CLIENT = 3;
    
            public CallbackReg()
            {
                    port = null;
                    regType = null;
                    regEvents = new Vector();
            }
    
            public void setPort(String portIn)
            {
                    port = portIn;
            }
    
            public void setRegType(int regTypeIn)
            {
                    switch (regTypeIn) {
                    case ADD_CLIENT:
                            regType = "ADD_CLIENT";
                            break;
                    case ADD_EVENTS:
                            regType = "ADD_EVENTS";
                            break;
                    case REMOVE_CLIENT:
                            regType = "REMOVE_CLIENT";
                            break;
                    case REMOVE_EVENTS:
                            regType = "REMOVE_EVENTS";
                            break;
                    default:
                            System.out.println("Error, regType no válido" +
                                regTypeIn);
                            regType = "ADD_CLIENT";
                            break;
                    }
            }
    
            public void addRegEvent(Event regEvent)
            {
                    regEvents.add(regEvent);
            }
    
            public String convertToXml()
            {
                    Document document = null;
                    DocumentBuilderFactory factory =
                        DocumentBuilderFactory.newInstance();
                    try {
                            DocumentBuilder builder = factory.newDocumentBuilder();
                            document = builder.newDocument();
                    } catch (ParserConfigurationException pce) {
                            // No se puede crear el analizador con las opciones
                            // especificadas
                            pce.printStackTrace();
                            System.exit(1);
                    }
    
                    // Crear el elemento raíz
                    Element root = (Element) document.createElement(
                        "SC_CALLBACK_REG");
    
                    // Añadir los atributos
                    root.setAttribute("VERSION", "1.0");
                    root.setAttribute("PORT", port);
                    root.setAttribute("regType", regType);
    
                    // Añadir los eventos
                    for (int i = 0; i < regEvents.size(); i++) {
                            Event tempEvent = (Event)
                                (regEvents.elementAt(i));
                            root.appendChild(tempEvent.createXmlElement(
                                document));
                    }
                    document.appendChild(root);
    
                    // Ahora, convertir el documento en cadena.
                    DOMSource domSource = new DOMSource(document);
                    StringWriter strWrite = new StringWriter();
                    StreamResult streamResult = new StreamResult(strWrite);
                    TransformerFactory tf = TransformerFactory.newInstance();
                    try {
                            Transformer transformer = tf.newTransformer();
                            transformer.transform(domSource, streamResult);
                    } catch (TransformerException e) {
                            System.out.println(e.toString());
                            return ("");
                    }
                    return (strWrite.toString());
            }
    
            private String port;
            private String regType;
            private Vector regEvents;
    }

  2. Implementar las clases Event y NVPair.

    Observe que la clase CallbackReg utiliza una clase Event, que usa a su vez una clase NVPair.


    class Event
    {
    
            public Event()
            {
                    regClass = regSubclass = null;
                    nvpairs = new Vector();
            }
    
            public void setClass(String classIn)
            {
                    regClass = classIn;
            }
    
            public void setSubclass(String subclassIn)
            {
                    regSubclass = subclassIn;
            }
    
            public void addNvpair(NVPair nvpair)
            {
                    nvpairs.add(nvpair);
            }
    
            public Element createXmlElement(Document doc)
            {
                    Element event = (Element)
                        doc.createElement("SC_EVENT_REG");
                    event.setAttribute("CLASS", regClass);
                    if (regSubclass != null) {
                            event.setAttribute("SUBCLASS", regSubclass);
                    }
                    for (int i = 0; i < nvpairs.size(); i++) {
                              NVPair tempNv = (NVPair)
                                  (nvpairs.elementAt(i));
                              event.appendChild(tempNv.createXmlElement(
                                  doc));
                    }
                    return (event);
            }
    
            private String regClass, regSubclass;
            private Vector nvpairs;
    }
    
    class NVPair
    {
            public NVPair()
            {
                    name = value = null;
            }
    
            public void setName(String nameIn)
            {
                    name = nameIn;
            }
    
            public void setValue(String valueIn)
            {
                    value = valueIn;
            }
    
            public Element createXmlElement(Document doc)
            {
                    Element nvpair = (Element)
                        doc.createElement("NVPAIR");
                    Element eName = doc.createElement("NAME");
                    Node nameData = doc.createCDATASection(name);
                    eName.appendChild(nameData);
                    nvpair.appendChild(eName);
                    Element eValue = doc.createElement("VALUE");
                    Node valueData = doc.createCDATASection(value);
                    eValue.appendChild(valueData);
                    nvpair.appendChild(eValue);
    
                    return (nvpair);
            }
    
            private String name, value;
    }

Creación de los mensajes de registro y de anulación de registro

Ahora que se han creado las clases propias que generan los mensajes de XML, se puede escribir la aplicación del método createRegistrationString. Este método lo invoca el método registerCallbacks, descrito en Registro y anulación del registro de rellamadas.

createRegistrationString construye un objeto CallbackReg y establece su puerto y tipo de registro. Después, createRegistrationString construye varios eventos, mediante los métodos propios createAllEvent, createMembershipEvent, createRgEvent y createREvent. Cada evento se agrega al objeto CallbackReg después de crearlo. Finalmente, createRegistrationString invoca el método convertToXml en el objeto CallbackReg para recuperar el mensaje de XML en la forma de String.

Observe que la variable de miembro regs almacena los argumentos de la línea de órdenes que proporciona un usuario a la aplicación. El quinto argumento y siguientes especifican los eventos para los cuales debería registrarse la aplicación. El cuarto argumento especifica el tipo de registro, pero se ignora en este ejemplo. El código completo del Apéndice G muestra cómo utilizar este cuarto argumento.

  1. Crear el código Java que aplica la lógica anterior.


    private String createRegistrationString() throws Exception
    {
            CallbackReg cbReg = new CallbackReg();
            cbReg.setPort("" + localPort);
    
            cbReg.setRegType(CallbackReg.ADD_CLIENT);
    
            // añadir eventos
            for (int i = 4; i < regs.length; i++) {
                    if (regs[i].equals("M")) {
                            cbReg.addRegEvent(
                                createMembershipEvent());
                    } else if (regs[i].equals("A")) {
                            cbReg.addRegEvent(
                                createAllEvent());
                    } else if (regs[i].substring(0,2).equals("RG")) {
                            cbReg.addRegEvent(createRgEvent(
                                regs[i].substring(3)));
                    } else if (regs[i].substring(0,1).equals("R")) {
                            cbReg.addRegEvent(createREvent(
                                regs[i].substring(2)));
                    }
            }
    
            String xmlStr = cbReg.convertToXml();
            return (xmlStr);
    }
    
    private Event createAllEvent()
    {
            Event allEvent = new Event();
            allEvent.setClass("EC_Cluster");
            return (allEvent);
    }
    
    private Event createMembershipEvent()
    {
            Event membershipEvent = new Event();
            membershipEvent.setClass("EC_Cluster");
            membershipEvent.setSubclass("ESC_cluster_membership");
            return (membershipEvent);
    }
    
    private Event createRgEvent(String rgname)
    {
            Event rgStateEvent = new Event();
            rgStateEvent.setClass("EC_Cluster");
            rgStateEvent.setSubclass("ESC_cluster_rg_state");
    
            NVPair rgNvpair = new NVPair();
            rgNvpair.setName("rg_name");
            rgNvpair.setValue(rgname);
            rgStateEvent.addNvpair(rgNvpair);
    
            return (rgStateEvent);
    }
    
    private Event createREvent(String rname)
    {
            Event rStateEvent = new Event();
            rStateEvent.setClass("EC_Cluster");
            rStateEvent.setSubclass("ESC_cluster_r_state");
    
            NVPair rNvpair = new NVPair();
            rNvpair.setName("r_name");
            rNvpair.setValue(rname);
            rStateEvent.addNvpair(rNvpair);
    
            return (rStateEvent);
    }

  2. Crear la cadena de anulación de registro.

    Crear la cadena de anulación de registro es más fácil que crear la de registro, porque no es necesario incorporar eventos:


    private String createUnregistrationString() throws Exception
    {
            CallbackReg cbReg = new CallbackReg();
            cbReg.setPort("" + localPort);
            cbReg.setRegType(CallbackReg.REMOVE_CLIENT);
            String xmlStr = cbReg.convertToXml();
            return (xmlStr);
    }

Configuración del analizador de XML

Una vez creado el código de generación de XML y de red para la aplicación, el paso final es analizar y procesar la respuesta de registro y las rellamadas de eventos. El constructor CrnpClient invoca un método setupXmlProcessing que crea un objeto DocumentBuilderFactory y establece en él varias propiedades de análisis. La documentación JAXP de http://java.sun.com/xml/jaxp/index.html describe este método con más detalle.

    Crear el código Java que aplica la lógica anterior.


    private void setupXmlProcessing() throws Exception
    {
            dbf = DocumentBuilderFactory.newInstance();
    
            // No es necesario validar
            dbf.setValidating(false);
            dbf.setExpandEntityReferences(false);
    
            // Se desea ignorar comentarios y espacios en blanco
            dbf.setIgnoringComments(true);
            dbf.setIgnoringElementContentWhitespace(true);
    
            // Fusionar secciones CDATA en nodos de TEXT.
            dbf.setCoalescing(true);
    }

Analizar la respuesta de registro

Para analizar el mensaje de XML SC_REPLY que envía el servidor CRNP en respuesta a un mensaje de registro o anulación de registro, se necesita una clase propia RegReply que se puede construir a partir de un documento de XML. Esta clase proporciona accesores para el código de estado y el mensaje de estado. Para analizar el flujo de XML desde el servidor hay que crear un nuevo documento XML y utilizar el método de análisis del documento (la documentación de JAXP en http://java.sun.com/xml/jaxp/index.html describe el método con más detalle).

  1. Crear el código Java que aplica la lógica anterior.

    Observe que el método readRegistrationReply utiliza la nueva clase RegReply.


    private void readRegistrationReply(InputStream stream) throws Exception
    {
            /// Crear el constructor de documento
            DocumentBuilder db = dbf.newDocumentBuilder();
            db.setErrorHandler(new DefaultHandler());
    
           //analizar el archivo de entrada
            Document doc = db.parse(stream);
    
            RegReply reply = new RegReply(doc);
            reply.print(System.out);
    }

  2. Implementar la clase RegReply.

    Observe que el método retrieveValues recorre el árbol DOM del documento XML y extrae el código y el mensaje de estado. La documentación de JAXP de http://java.sun.com/xml/jaxp/index.html ofrece más detalles.


    class RegReply
    {
            public RegReply(Document doc)
            {
                    retrieveValues(doc);
            }
    
            public String getStatusCode()
            {
                    return (statusCode);
            }
    
            public String getStatusMsg()
            {
                    return (statusMsg);
            }
            public void print(PrintStream out)
            {
                    out.println(statusCode + ": " +
                        (statusMsg != null ? statusMsg : ""));
            }
    
            private void retrieveValues(Document doc)
            {
                    Node n;
                    NodeList nl;
                    String nodeName;
    
                    // Buscar el elemento SC_REPLY.
                    nl = doc.getElementsByTagName("SC_REPLY");
                    if (nl.getLength() != 1) {
                            System.out.println("Error al analizar: "
                             " no se puede encontrar "
                             + "nodo SC_REPLY.");
                            return;
                    }
    
                    n = nl.item(0);
    
                    // Recuperar el valor del atributo statusCode
                    statusCode = ((Element)n).getAttribute("STATUS_CODE");
    
                    // Buscar el elemento SC_STATUS_MSG
                    nl = ((Element)n).getElementsByTagName("SC_STATUS_MSG");
                    if (nl.getLength() != 1) {
                            System.out.println("Error al analizar: "
                            " no se puede encontrar "
                            + "nodo SC_STATUS_MSG.");
                            return;
                    }
                    // Obtener la sección TEXT, si la hubiera.
                    n = nl.item(0).getFirstChild();
                    if (n == null || n.getNodeType() != Node.TEXT_NODE) {
                    // Sin error, si no lo hay
                    // se retorna discretamente..
                            return;
                    }
    
                    // Recuperar el valor
                    statusMsg = n.getNodeValue();
            }
    
            private String statusCode;
            private String statusMsg;
    }

Análisis de los eventos de rellamada

El paso final consiste en analizar y procesar los eventos reales de rellamada. Para facilitar esta tarea, se debe modificar la clase Event creada en Generación de XML de modo que pueda construir un Event a partir de un documento de XML y crear un Element de XML. Este cambio requiere un constructor adicional (que acepte un documento XML), un método retrieveValues, la adición de dos variables de miembro (vendor y publisher), métodos de accesor para todos los campos y un método de impresión.

  1. Crear el código Java que aplica la lógica anterior.

    Observe que este código es similar al código de la clase RegReply descrito en Analizar la respuesta de registro.


            public Event(Document doc)
            {
                    nvpairs = new Vector();
                    retrieveValues(doc);
            }
            public void print(PrintStream out)
            {
                    out.println("\tCLASS=" + regClass);
                    out.println("\tSUBCLASS=" + regSubclass);
                    out.println("\tVENDOR=" + vendor);
                    out.println("\tPUBLISHER=" + publisher);
                    for (int i = 0; i < nvpairs.size(); i++) {
                            NVPair tempNv = (NVPair)
                                (nvpairs.elementAt(i));
                            out.print("\t\t");
                            tempNv.print(out);
                    }
            }
    
            private void retrieveValues(Document doc)
            {
                    Node n;
                    NodeList nl;
                    String nodeName;
    
                    // Buscar el elemento SC_EVENT.
                    nl = doc.getElementsByTagName("SC_EVENT");
                    if (nl.getLength() != 1) {
                       System.out.println("Error al analizar:"
                            " no se puede encontrar "
                            + "nodo SC_EVENT.");
                       return;
                    }
    
                    n = nl.item(0);
    
                    //
                    // Recuperar los valores de los atributos
                    // CLASS, SUBCLASS, VENDOR y PUBLISHER.
                    //
                    regClass = ((Element)n).getAttribute("CLASS");
                    regSubclass = ((Element)n).getAttribute("SUBCLASS");
                    publisher = ((Element)n).getAttribute("PUBLISHER");
                    vendor = ((Element)n).getAttribute("VENDOR");
    
                   // Recuperar todos los pares n-v
                    for (Node child = n.getFirstChild(); child != null;
                        child = child.getNextSibling())
                    {
                           nvpairs.add(new NVPair((Element)child));
                    }
            }
    
            public String getRegClass()
            {
                    return (regClass);
            }
    
            public String getSubclass()
            {
                    return (regSubclass);
            }
    
            public String getVendor()
            {
                    return (vendor);
            }
    
            public String getPublisher()
            {
                    return (publisher);
            }
    
            public Vector getNvpairs()
            {
                    return (nvpairs);
            }
    
            private String vendor, publisher;

  2. Implementar los métodos y constructores adicionales para la clase NVPair que admite el análisis de XML.

    Los cambios en la clase Event que se muestran en el Paso 1 requieren cambios similares en la clase NVPair.


            public NVPair(Element elem)
            {
                    retrieveValues(elem);
            }
            public void print(PrintStream out)
            {
                    out.println("NAME=" + name + " VALUE=" + value);
            }
            private void retrieveValues(Element elem)
            {
                    Node n;
                    NodeList nl;
                    String nodeName;
    
                    // Buscar el elemento NAME
                    nl = elem.getElementsByTagName("NAME");
                    if (nl.getLength() != 1) {
                       System.out.println("Error al analizar:  "
                              " no se puede encontrar "
                              + "sección TEXT.");
                       return;
                    }
                    // Obtener la sección TEXT
                    n = nl.item(0).getFirstChild();
                    if (n == null || n.getNodeType() != Node.TEXT_NODE) {
                       System.out.println("Error al analizar:  "
                              " no se puede encontrar "
                              + "sección TEXT.");
                       return;
                    }
    
                    // Recuperar el valor
                    name = n.getNodeValue();
    
                   // Obtener ahora el elemento valor
                    nl = elem.getElementsByTagName("VALUE");
                    if (nl.getLength() != 1) {
                       System.out.println("Error al analizar:  "
                              " no se puede encontrar "
                           + "nodo VALUE.");
                       return;
                    }
                    // Obtener la sección TEXT
                    n = nl.item(0).getFirstChild();
                    if (n == null || n.getNodeType() != Node.TEXT_NODE) {
                   System.out.println("Error al analizar:  "
                              " no se puede encontrar "
                              + "sección TEXT.");
                            return;
                    }
    
                    // Recuperar el valor
                    value = n.getNodeValue();
                    }
    
            public String getName()
            {
                    return (name);
            }
    
            public String getValue()
            {
                    return (value);
            }
    }

  3. Implementar el bucle while en EventReceptionThread, que espera rellamadas de eventos (EventReceptionThread se describe en Definición del subproceso de recepción de eventos).


    while(true) {
                    Socket sock = listeningSock.accept();
                    Document doc = db.parse(sock.getInputStream());
                    Event event = new Event(doc);
                    client.processEvent(event);
                    sock.close();
            }

Ejecución de la aplicación

    Ejecutar la aplicación.


    # java CrnpClient sistema_crnp puerto_crnp puerto_local ...
    

    El código completo de la aplicación CrnpClient se puede consultar en el Apéndice G.