Sun Cluster 3.1 10/03 Data Services Developer's Guide

Chapter 12 CRNP

This chapter provides information about the Cluster Reconfiguration Notification Protocol (CRNP). CRNP enables failover and scalable applications to be “cluster aware.” More specifically, CRNP provides a mechanism that enables applications to register for, and receive subsequent asynchronous notification of, Sun Cluster reconfiguration events. Data services that run within the cluster and applications that run outside the cluster can register for notification of events. Events are generated when membership in a cluster changes and when the state of a resource group or a resource changes.

Overview of CRNP

CRNP provides mechanisms and daemons that generate cluster reconfiguration events, route them through the cluster, and send them to interested clients.

The cl_apid daemon interacts with the clients. The Sun Cluster Resource Group Manager (RGM) generates cluster reconfiguration events. These daemons use syseventd(1M) to transmit events on each local node. The cl_apid daemon uses Extensible Markup Language (XML) over TCP/IP to communicate with interested clients.

The following diagram presents an overview of the flow of events between the CRNP components. In this diagram, one client is running on cluster node 2, and the other client is running on a computer that is not part of the cluster.

Figure 12–1 How CRNP Works

Flow diagram showing how CRNP works

Overview of the CRNP Protocol

The CRNP defines the Application, Presentation, and Session layers of the standard seven layer Open System Interconnect (OSI) protocol stack. The Transport layer must be TCP and the Network layer must be IP. The CRNP is independent of the Data Link and Physical layers. All Application layer messages that are exchanged in the CRNP are based on XML 1.0.

Semantics of the CRNP Protocol

Clients initiate communication by sending a registration message (SC_CALLBACK_RG) to the server. This registration message specifies the event types for which the clients want to receive notification as well as a port to which the events can be delivered. The source IP of the registration connection and the specified port, taken together, form the callback address.

Whenever an event of interest to a client is generated within the cluster, the server contacts the client on its callback address (IP and port) and delivers the event (SC_EVENT) to the client. The server is highly available, running within the cluster itself. The server stores client registrations in storage that persists even after the cluster is rebooted.

Clients unregister by sending a registration message (SC_CALLBACK_RG, which contains a REMOVE_CLIENT message) to the server. After the client receives an SC_REPLY message from the server, the client closes the connection.

The following diagram shows the flow of communication between a client and a server.

Figure 12–2 Flow of Communication Between a Client and a Server

Flow diagram showing flow of communication between client and server

Message Types That the CRNP Uses

The CRNP uses three types of messages, all of which are XML-based, as described in the following table. These message types are described in more detail later in this chapter. Usage is also described in more detail later in this chapter.

Type of Message 

Description 

SC_CALLBACK_REG

This message takes four forms: ADD_CLIENT, REMOVE_CLIENT, ADD_EVENTS, and REMOVE_EVENTS. Each of these forms contains the following information:

  • Protocol version

  • Callback port in ASCII format (not binary format)

The ADD_CLIENT, ADD_EVENTS, and REMOVE_EVENTS forms also contain an unbounded list of event types, each of which includes the following information:

  • Event class

  • Event subclass (optional)

  • List of the name and value pairs (optional)

Together, the event class and event subclass define a unique “event type.” The DTD (document type definition) from which the classes of SC_CALLBACK_REG are generated is SC_CALLBACK_REG. This DTD is described in more detail in Appendix F, Document Type Definitions for CRNP.

SC_EVENT

This message contains the following information:

  • Protocol version

  • Event class

  • Event subclass

  • Vendor

  • Publisher

  • Name and value pairs list (0 or more name and value pair data structures)

    • Name (string)

    • Value (string or string array)

The values in an SC_EVENT are not typed. The DTD (document type definition) from which the classes of SC_EVENT are generated is SC_EVENT. This DTD is described in more detail in Appendix F, Document Type Definitions for CRNP.

SC_REPLY

This message contains the following information:

  • Protocol version

  • Error code

  • Error message

The DTD (document type definition) from which the classes of SC_REPLY are generated is SC_REPLY. This DTD is described in more detail in Appendix F, Document Type Definitions for CRNP.

How a Client Registers With the Server

This section describes how an administrator will set up the server, how clients are identified, how information is sent over the Application and Session layers, and error conditions.

Assumptions About How Administrators Will Set Up the Server

The system administrator must configure the server with a highly available IP address (that is, an IP address that is not tied to a particular machine in the cluster) and a port number. The administrator must publish this network address to prospective clients. The CRNP does not define how this server name is made available to clients. Administrators will either use a naming service, which will enable clients to find the network address of the server dynamically, or will add the network name to a configuration file for the client to read. The server will run within the cluster as a failover resource type.

How the Server Identifies a Client

Each client is uniquely identified by its callback address, that is, its IP address and port number. The port is specified in the SC_CALLBACK_REG messages, and the IP address is obtained from the TCP registration connection. CRNP assumes that subsequent SC_CALLBACK_REG messages with the same callback address come from the same client, even if the source port from which the messages are sent is different.

How SC_CALLBACK_REG Messages Are Passed Between a Client and the Server

A client initiates a registration by opening a TCP connection to the server's IP address and port number. After the TCP connection is established and ready for writing, the client must send its registration message. The registration message must be one correctly formatted SC_CALLBACK_REG message that does not contain extra bytes either before or after the message.

After all the bytes have been written to the stream, the client must keep its connection open to receive the reply from the server. If the client does not format the message correctly, the server does not register the client, and sends an error reply to the client. If the client closes the socket connection before the server sends a reply, the server registers the client as normal.

A client can contact the server at any time. Every time a client contacts the server, the client must send an SC_CALLBACK_REG message. If the server receives a message that is malformed, out of order, or invalid, the server sends an error reply to the client.

A client cannot send an ADD_EVENTS, REMOVE_EVENTS, or REMOVE_CLIENT message before that client sends an ADD_CLIENT message. A client cannot send a REMOVE_CLIENT message before that client sends an ADD_CLIENT message.

If a client sends an ADD_CLIENT message and the client is already registered, the server might tolerate this message. In this situation, the server silently replaces the old client registration with the new client registration that is specified in the second ADD_CLIENT message.

In most situations, a client registers with the server once, when the client starts, by sending an ADD_CLIENT message. And a client unregisters once by sending a REMOVE_CLIENT message to the server. However, the CRNP provides more flexibility for those clients that need to modify their event type list dynamically.

Contents of an SC_CALLBACK_REG Message

Each ADD_CLIENT, ADD_EVENTS, and REMOVE_EVENTS message contains a list of events. The following table describes the event types that the CRNP accepts, including the required name and value pairs.

If a client either:

the server silently ignores these messages.

Class and Subclass 

Name and Value Pairs 

Description 

EC_Cluster

ESC_cluster_membership

Required: none 

Optional: none 

Registers for all cluster membership change events (node death or join) 

EC_Cluster

ESC_cluster_rg_state

One required, as follows: 

rg_name

Value type: string 

Optional: none 

Registers for all state change events for resource group name

EC_Cluster

ESC_cluster_r_state

One required, as follows: 

r_name

Value type: string 

Optional: none 

Registers for all state change events for resource name

EC_Cluster

None 

Required: none 

Optional: none 

Registers for all Sun Cluster events 

How the Server Replies to a Client

After processing the registration, the server sends the SC_REPLY message. The server sends this message on the open TCP connection from the client on which the server received the registration request. The server then closes the connection. The client must keep the TCP connection open until it receives the SC_REPLY message from the server.

For example, the client carries out the following actions:

  1. Opens a TCP connection to the server

  2. Waits for a connection to be “writeable”

  3. Sends an SC_CALLBACK_REG message (which contains an ADD_CLIENT message)

  4. Waits for an SC_REPLY message

  5. Receives an SC_REPLY message

  6. Receives an indicator that the server has closed the connection (reads 0 bytes from the socket)

  7. Closes the connection

At a later point in time, the client then carries out the following actions:
  1. Opens a TCP connection to the server

  2. Waits for a connection to be “writeable”

  3. Sends an SC_CALLBACK_REG message (which contains a REMOVE_CLIENT message)

  4. Waits for an SC_REPLY message

  5. Receives an SC_REPLY message

  6. Receives an indicator that the server has closed the connection (reads 0 bytes from the socket)

  7. Closes the connection

Each time that the server receives an SC_CALLBACK_REG message from a client, the server sends an SC_REPLY message on the same open connection. This message specifies whether the operation succeeded or failed. SC_REPLY XML DTD contains the XML document type definition of an SC_REPLY message, and the possible error messages that this message can include.

Contents of an SC_REPLY Message

An SC_REPLY message specifies whether an operation succeeded or failed. This message contains the version of the CRNP protocol message, a status code, and a status message, which describes the status code in more detail. The following table describes the possible values for the status code.

Status Code 

Description 

OK

The message was processed successfully. 

RETRY

The registration of the client was rejected by the server due to a transient error (the client should try to register again, with different parameters). 

LOW_RESOURCE

Cluster resources are low, and the client can only try again at a later time (the system administrator for the cluster can also increase the resources on the cluster). 

SYSTEM_ERROR

A serious problem occurred. Contact the system administrator for the cluster. 

FAIL

Authorization failed or another problem caused the registration to fail. 

MALFORMED

The XML request was malformed and could not be parsed. 

INVALID

The XML request was invalid (does not meet the XML specification). 

VERSION_TOO_HIGH

The version of the message was too high to process the message successfully. 

VERSION_TOO_LOW

The version of the message was too low to process the message successfully. 

How a Client Is to Handle Error Conditions

Under normal conditions, a client that sends an SC_CALLBACK_REG message receives a reply that indicates that the registration succeeded or failed.

However, the server can experience an error condition when a client is registering that prohibits the server from sending an SC_REPLY message to the client. In this case, the registration could either have succeeded before the error condition occurred, could have failed, or could not yet have been processed.

Because the server must function as a failover, or highly available, server on the cluster, this error condition does not mean an end to the service. In fact, the server could soon begin sending events to the newly registered client.

To remedy these conditions, your client should both:

How the Server Delivers Events to a Client

As events are generated within the cluster, the CRNP server delivers them to all clients who requested events of those types. The delivery consists of sending an SC_EVENT message to the client's callback address. The delivery of each event occurs on a new TCP connection.

Immediately after a client registers for an event type, through an SC_CALLBACK_REG message that contains an ADD_CLIENT message or an ADD_EVENT message, the server sends the most recent event of that type to the client. The client can then discover the current state of the system from which the subsequent events come.

When the server initiates a TCP connection to the client, the server sends exactly one SC_EVENT message on the connection. The server then issues a full-duplex close.

For example, the client carries out the following actions:

  1. Waits for the server to initiate a TCP connection

  2. Accepts the incoming connection from the server

  3. Waits for an SC_EVENT message

  4. Reads an SC_EVENT message

  5. Receives an indicator that the server has closed the connection (reads 0 bytes from the socket)

  6. Closes the connection

When all clients have registered, they must listen at their callback address (the IP address and port number) at all times for an incoming event delivery connection.

If the server fails to contact the client to deliver an event, the server tries again to deliver the event the number of times and at the interval that you define. If all attempts fail, the client is removed from the server's list of clients. The client also needs to re-register by sending another SC_CALLBACK_REG message that contains an ADD_CLIENT message before the client can receive more events.

How the Delivery of Events Is Guaranteed

There is a total ordering of event generation within the cluster that is preserved in the order of delivery to each client. In other words, if event A is generated within the cluster before event B, then client X receives event A before that client receives event B. However, the total ordering of event delivery to all clients is not preserved. That is, client Y could receive both events A and B before client X receives event A. In this way, slow clients do not hold up delivery to all clients.

All events that the server delivers (except the first event for a subclass and events that follow server errors) occur in response to the actual events that the cluster generates, except if the server experiences an error that causes it to miss cluster-generated events. In this case, the server generates an event for each event type that represents the current state of the system for that type. Each event is sent to clients that registered interest in that event type.

Event delivery follows the “at least once” semantics. That is, the server is allowed to send the same event to a client more than once. This allowance is necessary in cases in which the server goes down temporarily, and when it comes back up, cannot determine if the client has received the latest information.

Contents of an SC_EVENT Message

The SC_EVENT message contains the actual message that is generated within the cluster, translated to fit into the SC_EVENT XML message format. The following table describes the event types that the CRNP delivers, including the name and value pairs, publisher, and vendor.

Class and Subclass 

Publisher and Vendor 

Name and Value Pairs 

Notes 

EC_Cluster

ESC_cluster_membership

Publisher: rgm 

Vendor: SUNW 

Name: node_list

Value type: string array 

Name: state_list

Value type: string array 

The positions of the array elements for state_list are synchronized with those of the node_list. That is, the state for the node listed first in the node_list array is first in the state_list array.

The state_list contains only numbers represented in ASCII. Each number represents the current incarnation number for that node in the cluster. If the number is the same as the number that was received in a previous message, the node has not changed its relationship to the cluster (departed, joined, or rejoined). If the incarnation number is –1, the node is not a member of the cluster. If the incarnation number is a number other than a negative number, the node is a member of the cluster.

Additional names starting with ev_ and their associated values might be present, but are not intended for client use.

EC_Cluster

ESC_cluster_rg_state

Publisher: rgm 

Vendor: SUNW 

Name: rg_name

Value type: string 

Name: node_list

Value type: string array 

Name: state_list

Value type: string array 

The positions of the array elements for state_list are synchronized with those of the node_list. That is, the state for the node listed first in the node_list array is first in the state_list array.

The state_list contains string representations of the state of the resource group. Valid values are those values that you can retrieve with the scha_cmds(1HA) commands.

Additional names starting with ev_ and their associated values might be present, but are not intended for client use.

EC_Cluster

ESC_cluster_r_state

Publisher: rgm 

Vendor: SUNW 

Three required, as follows:  

Name: r_name

Value type: string 

Name: node_list

Value type: string array 

Name: state_list

Value type: string array 

The positions of the array elements for state_list are synchronized with those of the node_list. That is, the state for the node listed first in the node_list array is first in the state_list array.

The state_list contains string representations of the state of the resource. Valid values are those values that you can retrieve with the scha_cmds(1HA) commands.

Additional names starting with ev_ and their associated values might be present, but are not intended for client use.

How the CRNP Authenticates Clients and the Server

The server authenticates a client by using a form of TCP wrappers. The source IP address of the registration message (which is also used as the callback IP address on which events are delivered) must be in the list of allowed clients on the server. The source IP address and registration message cannot be in the denied clients list. If the source IP address and registration are not in the list, the server rejects the request and issues an error reply to the client.

When the server receives an SC_CALLBACK_REG ADD_CLIENT message, subsequent SC_CALLBACK_REG messages for that client must contain a source IP address that is the same as the source IP address in the first message. If the CRNP server receives an SC_CALLBACK_REG that does not meet this requirement, the server either:

This security mechanism helps to prevent denial of service attacks, where someone attempts to unregister a legitimate client.

Clients should also similarly authenticate the server. Clients need only accept event deliveries from a server whose source IP address and port number are the same as the registration IP address and port number that the client used.

Because it is expected that clients of the CRNP service are located inside a firewall that protects the cluster, CRNP does not include additional security mechanisms.

Creating a Java Application That Uses CRNP

The following example illustrates how to develop a simple Java application named CrnpClient that uses the CRNP. The application registers for event callbacks with the CRNP server on the cluster, listens for the event callbacks, and processes the events by printing their contents. Before terminating, the application unregisters its request for event callbacks.

Keep the following points in mind when reviewing this example.

Set Up Your Environment

First, you need to set up your environment.

  1. Download and install JAXP and the correct version of the Java compiler and virtual machine.

    You can find instructions at http://java.sun.com/xml/jaxp/index.html.


    Note –

    This example requires Java 1.3.1 or a later version of Java.


  2. Ensure that you specify a classpath in your compilation command line so that the compiler can find the JAXP classes. From the directory in which your source file is located, type:


    % javac -classpath JAXP_ROOT/dom.jar:JAXP_ROOTjaxp-api. \
    jar:JAXP_ROOTsax.jar:JAXP_ROOTxalan.jar:JAXP_ROOT/xercesImpl \
    .jar:JAXP_ROOT/xsltc.jar -sourcepath . SOURCE_FILENAME.java
    

    where JAXP_ROOT is the absolute or relative path to the directory in which the JAXP jar files are located and SOURCE_FILENAME is the name of your Java source file.

  3. When you run the application, specify the classpath so that the application can load the proper JAXP class files (note that the first path in the classpath is the current directory):


    java -cp .:JAXP_ROOT/dom.jar:JAXP_ROOTjaxp-api. \
    jar:JAXP_ROOTsax.jar:JAXP_ROOTxalan.jar:JAXP_ROOT/xercesImpl \
    .jar:JAXP_ROOT/xsltc.jar SOURCE_FILENAME ARGUMENTS
    

    Now that your environment is configured, you can develop your application.

Get Started

In this part of the example, you create a basic class called CrnpClient, with a main method that parses the command line arguments and constructs a CrnpClient object. This object passes the command line arguments to the class), waits for the user to terminate the application, calls shutdown on the CrnpClient, and then exits.

The constructor of the CrnpClient class needs to execute the following tasks:

    Create the Java code that implements the preceding logic.

    The following example shows the skeleton code for the CrnpClient class. The implementations of the four helper methods that are referenced in the constructor and shutdown methods are shown later. Note that the code that imports all the packages you need is shown.


    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("Hit return to terminate demo...");
                    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;
    }

    Member variables are discussed in more detail later.

Parse the Command Line Arguments

    To see how to parse command line arguments, refer to the code in Appendix G, CrnpClient.java Application.

Define the Event Reception Thread

In the code, you need to ensure that event reception is performed in a separate thread so that your application can continue to do other work while the event thread blocks and waits for event callbacks.


Note –

Setting up the XML is discussed later.


  1. In your code, define a Thread subclass called EventReceptionThread that creates a ServerSocket and waits for events to arrive on the socket.

    In this part of the example code, events are neither read nor processed. Reading and processing events are discussed later. The EventReceptionThread creates a ServerSocket on a wildcard internetworking protocol address. EventReceptionThread also keeps a reference to the CrnpClient object so that EventReceptionThread can send events to the CrnpClient object to process.


    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();
                                    // Construct event from the sock stream and process it
                                    sock.close();
                            }
                            // UNREACHABLE
    
                    } catch (Exception e) {
                            System.out.println(e);
                            System.exit(1);
                    }
            }
    
            /* private member variables */
            private ServerSocket listeningSock;
            private CrnpClient client;
    }

  2. Now that you see how the EventReceptionThread class works, construct an createEvtRecepThr object:


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

Register and Unregister Callbacks

The registration task consists of:

  1. Create the Java code that implements the preceding logic.

    The following example shows the implementation of the registerCallbacks method of the CrnpClient class (which is called by the CrnpClient constructor). The calls to createRegistrationString() and readRegistrationReply() are described in more detail later.

    regIp and regPort are object members that are set up by the 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. Implement the unregister method. This method is called by the shutdown method of CrnpClient. The implementation of createUnregistrationString is described in more detail later.


    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();
    }

Generate the XML

Now that you have set up the structure of the application and have written all the networking code, you write the code that generates and parses the XML. Start by writing the code that generates the SC_CALLBACK_REG XML registration message.

An SC_CALLBACK_REG message consists of a registration type (ADD_CLIENT, REMOVE_CLIENT, ADD_EVENTS, or REMOVE_EVENTS), a callback port, and a list of events of interest. Each event consists of a class and a subclass, followed by a list of name and value pairs.

In this part of the example, you write a CallbackReg class that stores the registration type, callback port, and list of registration events. This class also can serialize itself to an SC_CALLBACK_REG XML message.

An interesting method of this class is the convertToXml method, which creates an SC_CALLBACK_REG XML message string from the class members. The JAXP documentation at http://java.sun.com/xml/jaxp/index.html describes the code in this method in more detail.

The implementation of the Event class is shown below. Note that the CallbackReg class uses an Event class that stores one event and can convert that event to an XML Element.

  1. Create the Java code that implements the preceding logic.


    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, invalid regType " +
                                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) {
                            // Parser with specified options can't be built
                            pce.printStackTrace();
                            System.exit(1);
                    }
    
                    // Create the root element
                    Element root = (Element) document.createElement(
                        "SC_CALLBACK_REG");
    
                    // Add the attributes
                    root.setAttribute("VERSION", "1.0");
                    root.setAttribute("PORT", port);
                    root.setAttribute("regType", regType);
    
                    // Add the events
                    for (int i = 0; i < regEvents.size(); i++) {
                            Event tempEvent = (Event)
                                (regEvents.elementAt(i));
                            root.appendChild(tempEvent.createXmlElement(
                                document));
                    }
                    document.appendChild(root);
    
                    // Convert the whole thing to a string
                    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. Implement the Event and NVPair classes.

    Note that the CallbackReg class uses an Event class, which itself uses an NVPair class.


    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;
    }

Create the Registration and Unregistration Messages

Now that you have created the helper classes that generate the XML messages, you can write the implementation of the createRegistrationString method. This method is called by the registerCallbacks method, which is described in Register and Unregister Callbacks.

createRegistrationString constructs a CallbackReg object and sets its registration type and port. Then createRegistrationString constructs various events, using the createAllEvent, createMembershipEvent, createRgEvent, and createREvent helper methods. Each event is added to the CallbackReg object after this object is created. Finally, createRegistrationString calls the convertToXml method on the CallbackReg object to retrieve the XML message in String form.

Note that the regs member variable stores the command line arguments that a user provides to the application. The fifth and subsequent arguments specify the events for which the application should register. The fourth argument specifies the type of registration, but is ignored in this example. The complete code in Appendix G, CrnpClient.java Application shows how to use this fourth argument.

  1. Create the Java code that implements the preceding logic.


    private String createRegistrationString() throws Exception
    {
            CallbackReg cbReg = new CallbackReg();
            cbReg.setPort("" + localPort);
    
            cbReg.setRegType(CallbackReg.ADD_CLIENT);
    
            // add the events
            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. Create the unregistration string.

    Creating the unregistration string is easier than creating the registration string because you don't need to accommodate events:


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

Set Up the XML Parser

You have now created the networking and XML generation code for the application. The final step is to parse and process the registration reply and event callbacks. The CrnpClient constructor calls a setupXmlProcessing method. This method creates a DocumentBuilderFactory object and sets various parsing properties on that object. The JAXP documentation at http://java.sun.com/xml/jaxp/index.html describes this method in more detail.

    Create the Java code that implements the preceding logic.


    private void setupXmlProcessing() throws Exception
    {
            dbf = DocumentBuilderFactory.newInstance();
    
            // We don't need to bother validating
            dbf.setValidating(false);
            dbf.setExpandEntityReferences(false);
    
            // We want to ignore comments and whitespace
            dbf.setIgnoringComments(true);
            dbf.setIgnoringElementContentWhitespace(true);
    
            // Coalesce CDATA sections into TEXT nodes.
            dbf.setCoalescing(true);
    }

Parse the Registration Reply

To parse the SC_REPLY XML message that the CRNP server sends in response to a registration or unregistration message, you need a RegReply helper class. You can construct this class from an XML document. This class provides accessors for the status code and status message. To parse the XML stream from the server, you need to create a new XML document and use that document's parse method (the JAXP documentation at http://java.sun.com/xml/jaxp/index.html describes this method in more detail).

  1. Create the Java code that implements the preceding logic.

    Note that the readRegistrationReply method uses the new RegReply class.


    private void readRegistrationReply(InputStream stream) throws Exception
    {
            // Create the document builder
            DocumentBuilder db = dbf.newDocumentBuilder();
            db.setErrorHandler(new DefaultHandler());
    
            //parse the input file
            Document doc = db.parse(stream);
    
            RegReply reply = new RegReply(doc);
            reply.print(System.out);
    }

  2. Implement the RegReply class.

    Note that the retrieveValues method walks the DOM tree in the XML document and pulls out the status code and status message. The JAXP documentation at http://java.sun.com/xml/jaxp/index.html contains more detail.


    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;
    
                    // Find the SC_REPLY element.
                    nl = doc.getElementsByTagName("SC_REPLY");
                    if (nl.getLength() != 1) {
                            System.out.println("Error in parsing: can't find "
                                + "SC_REPLY node.");
                            return;
                    }
    
                    n = nl.item(0);
    
                    // Retrieve the value of the statusCode attribute
                    statusCode = ((Element)n).getAttribute("STATUS_CODE");
    
                    // Find the SC_STATUS_MSG element
                    nl = ((Element)n).getElementsByTagName("SC_STATUS_MSG");
                    if (nl.getLength() != 1) {
                            System.out.println("Error in parsing: can't find "
                                + "SC_STATUS_MSG node.");
                            return;
                    }
                    // Get the TEXT section, if there is one.
                    n = nl.item(0).getFirstChild();
                    if (n == null || n.getNodeType() != Node.TEXT_NODE) {
                            // Not an error if there isn't one, so we just silently return.
                            return;
                    }
    
                    // Retrieve the value
                    statusMsg = n.getNodeValue();
            }
    
            private String statusCode;
            private String statusMsg;
    }

Parse the Callback Events

The final step is to parse and process the actual callback events. To aid in this task, you modify the Event class that you created in Generate the XML so that this class can construct an Event from an XML document and create an XML Element. This change requires an additional constructor (that takes an XML document), a retrieveValues method, the addition of two member variables (vendor and publisher), accessor methods for all fields, and finally, a print method.

  1. Create the Java code that implements the preceding logic.

    Note that this code is similar to the code for the RegReply class that is described in Parse the Registration Reply.


            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;
    
                    // Find the SC_EVENT element.
                    nl = doc.getElementsByTagName("SC_EVENT");
                    if (nl.getLength() != 1) {
                            System.out.println("Error in parsing: can't find "
                                + "SC_EVENT node.");
                            return;
                    }
    
                    n = nl.item(0);
    
                    //
                    // Retrieve the values of the CLASS, SUBCLASS,
                    // VENDOR and PUBLISHER attributes.
                    //
                    regClass = ((Element)n).getAttribute("CLASS");
                    regSubclass = ((Element)n).getAttribute("SUBCLASS");
                    publisher = ((Element)n).getAttribute("PUBLISHER");
                    vendor = ((Element)n).getAttribute("VENDOR");
    
                    // Retrieve all the nv pairs
                    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. Implement the additional constructors and methods for the NVPair class that support the XML parsing.

    The changes to the Event class that are shown in Step 1 require similar changes to the NVPair class.


            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;
    
                    // Find the NAME element
                    nl = elem.getElementsByTagName("NAME");
                    if (nl.getLength() != 1) {
                            System.out.println("Error in parsing: can't find "
                                + "NAME node.");
                            return;
                    }
                    // Get the TEXT section
                    n = nl.item(0).getFirstChild();
                    if (n == null || n.getNodeType() != Node.TEXT_NODE) {
                            System.out.println("Error in parsing: can't find "
                                + "TEXT section.");
                            return;
                    }
    
                    // Retrieve the value
                    name = n.getNodeValue();
    
                    // Now get the value element
                    nl = elem.getElementsByTagName("VALUE");
                    if (nl.getLength() != 1) {
                            System.out.println("Error in parsing: can't find "
                                + "VALUE node.");
                            return;
                    }
                    // Get the TEXT section
                    n = nl.item(0).getFirstChild();
                    if (n == null || n.getNodeType() != Node.TEXT_NODE) {
                    System.out.println("Error in parsing: can't find "
                                + "TEXT section.");
                            return;
                    }
    
                    // Retrieve the value
                    value = n.getNodeValue();
                    }
    
            public String getName()
            {
                    return (name);
            }
    
            public String getValue()
            {
                    return (value);
            }
    }

  3. Implement the while loop in EventReceptionThread, which waits for event callbacks (EventReceptionThread is described in Define the Event Reception Thread).


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

Run the Application

    Run your application.


    # java CrnpClient crnpHost crnpPort localPort ...
    

    The complete code for the CrnpClient application is listed in Appendix G, CrnpClient.java Application.