Sun Cluster 数据服务开发者指南(适用于 Solaris OS)

第 12 章 群集重新配置通知协议

本章介绍群集重配置通知协议 (CRNP)。CRNP 使故障转移和可伸缩应用程序成为“群集可识别”的应用程序。更重要的是,CRNP 提供了一种机制,使应用程序能够注册和接收 Sun Cluster 重新配置事件的后续异步通知。群集内运行的数据服务以及群集外运行的应用程序都可以注册事件通知。当群集中的成员发生变化或者资源组或某个资源的状态发生变化时,都会生成事件。


注 –

SUNW.Event 资源类型实现在 Sun Cluster 上提供了具有高可用性的 CRNP 服务。SUNW.Event(5) 手册页中对此资源类型的实现进行了详细介绍。


本章包括以下主题:

CRNP 概念

CRNP 定义了标准的七层开放式系统互连 (OSI) 协议栈的应用层、表示层和会话层。传输层必须为 TCP,网络层必须为 IP。CRNP 独立于数据链路层和物理层上。在 CRNP 中交换的所有应用层消息都基于 XML 1.0。

CRNP 的工作原理

CRNP 提供了一些机制和守护进程,用于生成群集重新配置事件、通过群集路由事件并将其发送给感兴趣的客户机。

cl_apid 守护进程与客户机交互。Sun Cluster 资源组管理器 (RGM) 生成群集重配置事件。此守护进程使用 syseventd 来传送每个本地节点上的事件。cl_apid 守护进程使用可扩展标记语言 (XML) 通过 TCP/IP 与感兴趣的客户机进行通信。

下图显示了 CRNP 组件之间的事件流程。在该图中,一台客户机在群集节点 2 上运行,另一台客户机在不属于该群集的计算机上运行。

图 12–1 CRNP 组件之间的事件流程

显示 CRNP 工作原理的流程图

CRNP 语义学

客户机通过向服务器发送一条注册消息 (SC_CALLBACK_RG) 来启动通信。此注册消息指定客户机要接收通知的事件类型,以及接收事件的端口。注册连接的源 IP 和指定的端口结合在一起形成回调地址。

当群集中生成某台客户机感兴趣的事件时,服务器将通过回调地址(IP 地址和端口号)联系该客户机,并将事件 (SC_EVENT) 传送给该客户机。在群集中运行的服务器具有高度的可用性。服务器将客户机注册存储在存储器中,即使重新引导群集,注册信息也会保留在存储器中。

客户机通过向服务器发送一条注册消息(SC_CALLBACK_RG,它包含 REMOVE_CLIENT 消息)进行取消注册。客户机接收到来自服务器的 SC_REPLY 消息后,将关闭连接。

下图显示了客户机和服务器之间的通信流程。

图 12–2 客户机和服务器之间的通信流程

显示客户机与服务器之间的通信流的流程图

CRNP 消息类型

CRNP 使用三种基于 XML 的消息。下表介绍了这些消息的用法。本章后面将对这些消息类型进行详细介绍,

CRNP 消息类型 

说明 

SC_CALLBACK_REG

此类消息采用四种格式:ADD_CLIENTREMOVE_CLIENTADD_EVENTSREMOVE_EVENTS。每种格式都包含以下信息:

 

  • 协议版本

  • ASCII 格式(而不是二进制格式)的回调端口

ADD_CLIENTADD_EVENTSREMOVE_EVENTS 还包含无界限的事件类型列表,每个事件类型都包括以下信息:

 

  • 事件类

  • 事件子类(可选)

  • 名称和值对列表(可选)

由事件类和事件子类共同定义唯一的“事件类型”。生成 SC_CALLBACK_REG 类的文档类型定义 (DTD) 为 SC_CALLBACK_REG附录 F,CRNP 的文档类型定义 中对此 DTD 进行了详细介绍。

SC_REPLY

此类消息包含以下信息: 

  • 协议版本

  • 错误代码

  • 错误消息

生成 SC_REPLY 类的 DTD 是 SC_REPLY附录 F,CRNP 的文档类型定义 中对此 DID 进行了详细介绍。

SC_EVENT

此类消息包含以下信息: 

  • 协议版本

  • 事件类

  • 事件子类

  • 供应商

  • 发行商

  • 名称和值对列表(0 或多个名称和值对数据结构)

    • 名称(字符串)

    • 值(字符串或字符串数组)

SC_EVENT 中的值未定义类型。生成 SC_EVENT 类的 DTD 是 SC_EVENT附录 F,CRNP 的文档类型定义 中对此 DID 进行了详细介绍。

客户机如何向服务器进行注册

本节介绍群集管理员如何设置服务器、如何标识客户机以及如何通过应用层和会话层发送信息,还介绍了错误状态。

管理员设置服务器的前提

群集管理员必须使用具有高可用性的 IP 地址(尚未连接到群集中特定计算机的一个 IP 地址)和端口号来配置服务器。群集管理员必须向预期的客户机发布此网络地址。CRNP 没有定义客户机获得该服务器名称的方式。群集管理员可以使用命名服务,使 客户机能够动态查找服务器的网络地址,也可以将网络名称添加到配置文件中以供客户机读取。服务器在群集中作为故障转移资源类型运行。

服务器如何标识客户机

每台客户机均由其回调地址(即其 IP 地址和端口号)唯一标识。端口是在 SC_CALLBACK_REG 消息中指定的,IP 地址是从 TCP 注册连接中获得的。CRNP 假定使用相同回调地址的后续 SC_CALLBACK_REG 消息来自同一台客户机,即使发送消息的源端口不同。

如何在客户机和服务器之间传送 SC_CALLBACK_REG 消息

客户机通过打开与服务器的 IP 地址和端口号的 TCP 连接启动注册。建立 TCP 连接并做好写入准备后,客户机必须发送其注册消息。注册消息必须是一条格式正确的 SC_CALLBACK_REG 消息,消息前后不能包含其他字节。

所有字节均已写入数据流后,该客户机必须使连接保持打开状态,以便接收来自服务器的应答。如果客户机没有正确格式化消息,服务器将不注册客户机,并向客户机发送一条出错应答。但是,如果客户机在服务器发出回复之前关闭套接字连接,服务器将照常注册客户机。

客户机可以随时联系服务器。客户机每次联系服务器时都必须发送一条 SC_CALLBACK_REG 消息。如果服务器发送的消息格式不正确、次序有误或者无效,服务器将向该客户机发送一条出错应答。

客户机必须先发送 ADD_CLIENT 消息,再发送 ADD_EVENTSREMOVE_EVENTSREMOVE_CLIENT 消息。客户机必须先发送 ADD_CLIENT 消息,再发送 REMOVE_CLIENT 消息。

如果某台客户机发送一条 ADD_CLIENT 消息,但该客户机已经注册,那么服务器可能会接收这条消息。这种情况下,服务器将使用第二条 ADD_CLIENT 消息中指定的新客户机注册替换旧的客户机注册,替换时不会发出提示。

大多数情况下,客户机在启动时通过发送一条 ADD_CLIENT 消息向服务器注册一次,通过向服务器发送 REMOVE_CLIENT 消息可取消注册一次客户机。然而,CRNP 为那些需要动态修改其事件类型列表的客户机提供了更大的灵活性。

SC_CALLBACK_REG 消息的内容

每个 ADD_CLIENTREMOVE_CLIENTADD_EVENTSREMOVE_EVENTS 消息都包含一个事件列表。下表描述了 CRNP 接受的事件类型,包括所需的名称和值对。

如果客户机执行以下操作之一,服务器将在不提示的情况下忽略这些消息:

类和子类 

名称和值对 

说明 

EC_Cluster

ESC_cluster_membership

必需:无 

可选:无 

注册所有群集成员更改事件(节点断开或连接) 

EC_Cluster

ESC_cluster_rg_state

一个是必需的,如下所示: 

rg_name

值类型:字符串 

可选:无 

注册资源组 name 的所有状态更改事件

EC_Cluster

ESC_cluster_r_state

一个是必需的,如下所示: 

r_name

值类型:字符串 

可选:无 

注册资源 name 的所有状态更改事件

EC_Cluster

无 

必需:无 

可选:无 

注册所有 Sun Cluster 事件 

服务器如何对客户机进行应答

处理注册之后,收到注册请求的服务器将在客户机打开的 TCP 连接上发送 SC_REPLY 消息。服务器将关闭连接。客户机必须保持 TCP 连接为打开状态,直到接收到来自服务器的 SC_REPLY 消息。

例如,客户机将执行以下操作:

  1. 打开服务器 TCP 连接

  2. 等候连接进入“可写入”状态

  3. 发送 SC_CALLBACK_REG 消息(其中包含 ADD_CLIENT 消息)

  4. 等待来自服务器的 SC_REPLY 消息

  5. 收到来自服务器的 SC_REPLY 消息

  6. 接收服务器已关闭连接(从套接字读取 0 字节)的指示

  7. 关闭连接

稍后,客户机将执行以下操作:

  1. 打开服务器 TCP 连接

  2. 等候连接进入“可写入”状态

  3. 发送 SC_CALLBACK_REG 消息(其中包含 REMOVE_CLIENT 消息)

  4. 等待来自服务器的 SC_REPLY 消息

  5. 收到来自服务器的 SC_REPLY 消息

  6. 接收服务器已关闭连接(从套接字读取 0 字节)的指示

  7. 关闭连接

服务器每次接收到来自客户机的 SC_CALLBACK_REG 消息时,都会通过同一个打开的连接发送一条 SC_REPLY 消息。此消息用于指明该操作是否成功。SC_REPLY XML DTD 包含 SC_REPLY 消息的 XML 文档类型定义,以及此消息可以包括的可能错误消息。

SC_REPLY 消息的内容

SC_REPLY 消息用于指定操作是成功还是失败。此消息包含 CRNP 消息的版本、状态码和状态消息(其中详细介绍了状态码)。下表描述了状态码可能具有的值。

状态码 

说明 

OK

已成功处理消息。 

RETRY

由于瞬态错误,客户机的注册已被服务器拒绝。客户机应当使用其他参数重新尝试注册。 

LOW_RESOURCE

群集资源不足,并且客户机只能稍后重新尝试。群集的群集管理员也可以增加群集中的资源。 

SYSTEM_ERROR

发生严重问题。请与群集的群集管理员联系。 

FAIL

授权失败,或者其他问题导致注册失败。 

MALFORMED

XML 请求的格式不正确,无法进行分析。 

INVALID

XML 请求无效,即 XML 请求不符合 XML 规范。 

VERSION_TOO_HIGH

消息的版本过高,无法成功处理该消息。 

VERSION_TOO_LOW

消息的版本太低,无法成功处理该消息。 

客户机如何处理错误状态

在通常情况下,发送 SC_CALLBACK_REG 消息的客户机将收到指明注册是成功还是失败的回复。

但是,服务器可能会在客户机进行注册时遇到错误状态,该错误状态禁止服务器向客户机发送 SC_REPLY 消息。在这种情况下,注册可能已经在发生错误之前成功完成,也可能已经失败,还可能尚未进行。

由于服务器必须在群集中起故障转移或具有高可用性的服务器的作用,因此,这种错误状态并不意味着服务的结束。实际上,服务器可以很快开始向新注册的客户机发送事件。

要摆脱这些状态,客户机应执行以下操作:

服务器如何向客户机传送事件

事件在群集内生成时,CRNP 服务器会将其发送给已请求这些类型的事件的每个客户机。发送过程包括向客户机的回调地址发送一条 SC_EVENT 消息。每个事件都是通过一个新的 TCP 连接传送的。

客户机注册事件类型后,服务器立即通过一条包含 ADD_CLIENTADD_EVENT 消息的 SC_CALLBACK_REG 消息向客户机发送该类型的最新事件。客户机可以确定后续事件的源系统的当前状态。

当服务器启动客户机 TCP 连接时,服务器将通过该连接发送一条 SC_EVENT 消息,服务器将发出全双工关闭。

例如,客户机将执行以下操作:

  1. 等候服务器启动 TCP 连接

  2. 接受来自服务器的传入连接

  3. 等待来自服务器的 SC_EVENT 消息

  4. 读取来自服务器的 SC_EVENT 消息

  5. 接收服务器已关闭连接(从套接字读取 0 字节)的指示

  6. 关闭连接

所有客户机都注册后,必须始终在各自的回调地址(IP 地址和端口号)上侦听传入的事件传送连接。

如果服务器未能联系客户机以传送事件,将按照您指定的次数和时间间隔重新尝试传送事件。如果所有尝试均未成功,将从该服务器的客户机列表中删除此客户机。客户机还必须通过发送包含 ADD_CLIENT 消息的另一条 SC_CALLBACK_REG 消息重新注册之后,才能接收更多事件。

如何保障事件的传送

在群集中有一个事件生成的总排序,它是按照传送给每个客户机的顺序保留的。换言之,如果事件 A 在群集中比事件 B 生成得早,则客户机 X 将先收到事件 A,然后再收到事件 B。但是,保留传送给所有客户机的事件的总排序。也就是说,客户机 Y 可以在客户机 X 接收到事件 A 之前接收事件 A 和事件 B。这样,速度慢的客户机将无法容纳传送到所有客户机的事件。

服务器传送的所有事件(子类的第一个事件和出现服务器错误后的事件除外)都是作为响应群集实际生成的事件而发生的,除非服务器遇到错误,导致丢失群集生成的事件。这种情况下,服务器将为表示系统当前状态的每个事件类型生成一个事件。每个事件都被发送到注册为对该事件类型感兴趣的客户机。

事件传送遵循“至少一次”的规则。即,服务器可以向客户机多次发送同一个事件。当服务器暂时出现故障,然后从故障中恢复后无法确定客户机是否已收到最新信息的情况下,这种宽容的规则是比要的。

SC_EVENT 消息的内容

SC_EVENT 消息包含在群集中生成的实际消息,此消息已翻译为符合 SC_EVENT XML 消息格式的消息。下表描述了 CRNP 传送的事件类型,包括名称和值对、发行商和供应商。


注 –

state_list 中数组元素的位置与 node_list 中的位置同步。即,在 node_list 数据中先列出的节点状态将在 state_list 数组中先列出。

可能会有以 ev_ 为开头的其他名称及其关联值,但它们不供客户机使用。


类和子类 

发行商和供应商 

名称和值对 

EC_Cluster

ESC_cluster_membership

发行商:rgm 

卖主:SUNW 

名称:node_list

值类型:字符串数组 

名称:state_list

state_list 仅包含用 ASCII 表示的数字。每个数字都表示该节点在群集中的当前象征数字。如果该数字与上一个消息中接收到的数字相同,则说明该节点与群集之间的关系(脱离、连接或重新连接)尚未改变。如果象征数字是 –1,则说明该节点不是该群集的成员。如果象征数字非负,则说明该节点是该群集的成员。

值类型:字符串数组 

EC_Cluster

ESC_cluster_rg_state

发行商:rgm 

卖主:SUNW 

名称:rg_name

值类型:字符串 

名称:node_list

值类型:字符串数组 

名称:state_list

state_list 包含资源组状态的字符串表示。有效值为可用 scha_cmds(1HA) 命令检索到的值。

值类型:字符串数组 

EC_Cluster

ESC_cluster_r_state

发行商:rgm 

卖主:SUNW 

名称:r_name

值类型:字符串 

名称:node_list

值类型:字符串数组 

名称:state_list

state_list 包含资源状态的字符串表示。有效值为可用 scha_cmds(1HA) 命令检索到的值。

值类型:字符串数组 

CRNP 如何鉴别客户机和服务器

服务器将使用 TCP 封装程序的形式验证客户机。注册消息的源 IP 地址(也用作传送事件的回调 IP 地址)必须在服务器允许的客户机列表中。拒绝的客户机列表中不能包含源 IP 地址和注册消息。如果源 IP 地址和注册不在列表中,服务器将拒绝请求,并向客户机发送一个出错应答。

当服务器收到 SC_CALLBACK_REG ADD_CLIENT 消息时,该客户机的后续 SC_CALLBACK_REG 消息必须包含第一条消息中包含的同一个源 IP 地址。如果 CRNP 服务器收到不满足此要求的 SC_CALLBACK_REG,服务器将执行以下操作之一:

此安全机制有助于防止拒绝服务攻击,即防止有人尝试撤销合法客户机的注册。

客户机需要以同样的方式鉴别服务器。客户机只需接收其源 IP 地址和端口号与该客户机使用的注册 IP 地址和端口号相同的服务器上的事件传送。

由于 CRNP 服务的客户机必须位于保护群集的防火墙内,因此 CRNP 不包含其他安全机制。

创建使用 CRNP 的 Java 应用程序示例

以下示例说明了如何开发使用 CRNP 的简单 Java 应用程序 CrnpClient。应用程序将向群集中的 CRNP 服务器注册事件回调,侦听事件回调并通过打印其内容来处理事件。应用程序终止前将取消注册事件回调请求。

查看此示例时,请注意以下几点:

Procedure如何设置环境

步骤
  1. 下载并安装 JAXP 以及 Java 编译器和虚拟机的正确版本。

    可在 http://java.sun.com/xml/jaxp/index.html 中找到说明。


    注 –

    此示例至少要使用 Java 1.3.1。


  2. 在资源文件所在的目录中键入:


    % 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
    

    其中,jaxp-root 为 JAXP jar 文件所在目录的绝对路径或相对路径,source-filename 为 Java 资源文件的名称。

    编译命令行中的 classpath 用于确保编译器可以找到 JAXP 类。

  3. 运行应用程序时,请指定 classpath 以使应用程序可以加载正确的 JAXP 类文件(注意,classpath 中的第一个路径是当前目录):


    % 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
    

    现在已完成环境配置,可以开发应用程序了。

Procedure如何开始开发应用程序

在示例的这一部分中,通过使用解析命令行参数和构造 CrnpClient 对象的主方法,创建名为 CrnpClient 的基本类。此对象将命令行参数传递给类,等待用户终止应用程序,对 CrnpClient 调用 shutdown 命令,然后退出。

CrnpClient 类的构造函数需要执行以下任务:

步骤

    创建实现上述逻辑的 Java 代码。

    以下实例显示了 CrnpClient 类的骨架代码。本章稍后将列出在构造函数和关闭方法中引用的四个帮助器方法的实现。请注意,将列出导入所需的所有软件包的代码。

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

    本章稍后将详细说明成员变量。

Procedure如何解析命令行参数

步骤

    要解析命令行参数,请参见附录 G,CrnpClient.java 应用程序 中的代码。

Procedure如何定义事件接收线程

在代码中,必须确保事件接收是在单独的线程中执行,这样应用程序才能在事件线程阻塞和等待事件回调时继续执行其他工作。


注 –

本章稍后将讨论如何设置 XML。


步骤
  1. 在代码中,将定义名为 EventReceptionThreadThread 子类,它用于创建 ServerSocket 和等待事件到达套接字。

    在实例代码的这一部分,既没有读取事件也没有处理事件。本章稍后将讨论如何读取和处理事件。EventReceptionThread 用于在通配符网络互联协议地址上创建 ServerSocketEventReceptionThread 还引用了 CrnpClient 对象,以使 EventReceptionThread 能够向 CrnpClient 对象发送要处理的事件。

    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. 构造 createEvtRecepThr 对象。

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

Procedure如何注册和取消注册回调

注册任务涉及以下操作:

步骤
  1. 创建实现上述逻辑的 Java 代码。

    以下示例代码显示了 CrnpClient 类(由 CrnpClient 构造函数调用)的 registerCallbacks 方法的实现。本章稍后将对 createRegistrationString()readRegistrationReply() 的调用进行详细介绍。

    regIpregPort 是由构造函数设置的对象成员。

    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. 实现 unregister 方法。

    此方法由 CrnpClientshutdown 方法调用。本章稍后将对 createUnregistrationString 的实现进行详细介绍。

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

Procedure如何生成 XML

至此,已设置应用程序的结构并写入了所有联网代码,您需要写入生成和解析 XML 的代码。开始先写入生成 SC_CALLBACK_REG XML 注册消息的代码。

SC_CALLBACK_REG 消息包括注册类型(ADD_CLIENTREMOVE_CLIENTADD_EVENTSREMOVE_EVENTS)、回调端口和感兴趣的事件列表。每个事件都包括一个类和一个子类,后跟一个名称和值对列表。

在实例的这一部分,需要编写一个存储注册类型、回调端口和注册事件列表的 CallbackReg 类。此类还可将自身序列化为 SC_CALLBACK_REG XML 消息。

此类中有一个有趣的方法,即 convertToXml 方法,它可以从类成员中创建一个 SC_CALLBACK_REG XML 消息字符串。http://java.sun.com/xml/jaxp/index.html 中的 JAXP 文档详细说明了此方法中的代码。

以下示例代码中显示了 Event 类的实现。请注意,CallbackReg 类使用了 Event 类,该类用于存储一个事件并可将该事件转换为 XML Element

步骤
  1. 创建实现上述逻辑的 Java 代码。

    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. 实现 EventNVPair 类。

    请注意,CallbackReg 类使用了 Event 类,而该类自身使用了 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;
    }

Procedure如何创建注册消息和取消注册消息

请注意,创建生成 XML 消息的帮助器类之后,您可以向 createRegistrationString 方法的实现写入数据。此方法由 registerCallbacks 方法(如何注册和取消注册回调中介绍了该方法)调用。

createRegistrationString 构造一个 CallbackReg 对象并设置其注册类型和端口。然后,createRegistrationString 通过使用 createAllEventcreateMembershipEventcreateRgEventcreateREvent 帮助器方法构造各种事件。创建 CallbackReg 对象后,每个事件都被添加到该对象中。最后,createRegistrationStringCallbackReg 对象调用 convertToXml 方法,以便在 String 表单中检索 XML 消息。

请注意,regs 成员变量用于存储用户提供给应用程序的命令行参数。第五个参数及后续参数用于指定应用程序应该注册的事件。第四个参数用于指定注册的类型,但本实例中忽略了该参数。附录 G,CrnpClient.java 应用程序 中的完整代码显示了如何使用第四个参数。

步骤
  1. 创建实现上述逻辑的 Java 代码。

    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. 创建撤销注册字符串。

    创建取消注册字符串比创建注册字符串容易,因为您不必提供事件。

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

Procedure如何设置 XML 解析器

现在已经为应用程序创建了联网和 XML 生成代码。CrnpClient 构造函数将调用 setupXmlProcessing 方法。此方法将创建 DocumentBuilderFactory 对象,并在该对象上设置各种解析属性。JAXP 文档详细介绍了此方法。请参见 http://java.sun.com/xml/jaxp/index.html

步骤

    创建实现上述逻辑的 Java 代码。

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

Procedure如何解析注册回复

要解析 SC_REPLY XML 消息(CRNP 服务器发送的响应注册或取消注册消息),需要使用 RegReply 帮助器类。可以从 XML 文档构造此类。此类提供了状态代码和状态消息的访问程序。要解析来自服务器的 XML 流,需要创建新的 XML 文档并使用该文档的解析方法。http://java.sun.com/xml/jaxp/index.html 中的 JAXP 文档详细介绍了此方法。

步骤
  1. 创建实现上述逻辑的 Java 代码。

    请注意,readRegistrationReply 方法使用新的 RegReply 类。

    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. 实现 RegReply 类。

    请注意,retrieveValues 方法将检索 XML 文档中的 DOM 树,并提取状态代码和状态消息。http://java.sun.com/xml/jaxp/index.html 中的 JAXP 文档包含详细说明。

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

Procedure如何解析回调事件

最后一步是解析和处理实际的回调事件。要帮助完成此任务,需要修改在“如何生成 XML”中创建的 Event 类,以使该类能够从 XML 文档构建 Event 并创建 XML Element。此更改需要使用其他构造函数(获取 XML 文档)、retrieveValues 方法、添加两个成员变量(vendorpublisher)、所有字段的访问程序方法,最后,还需要打印方法。

步骤
  1. 创建实现上述逻辑的 Java 代码。

    请注意,此代码与“如何解析注册回复”中说明的 RegReply 类的代码类似。

    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. 实现用于支持 XML 解析的 NVPair 类的其他构造函数和方法。

    Event 类的更改(如步骤 1 所述)需要对 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;
    
                    // 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. 实现 EventReceptionThread 中的 while 循环,它将等待事件回调。

    如何定义事件接收线程”中介绍了 EventReceptionThread

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

Procedure如何运行应用程序

步骤
  1. 成为超级用户或作为等效角色。

  2. 运行应用程序。


    # java CrnpClient crnpHost crnpPort localPort ...
    

    附录 G,CrnpClient.java 应用程序 中列出了 CrnpClient 应用程序的完整代码。