本章介绍群集重配置通知协议 (CRNP)。 CRNP 使故障切换和可伸缩应用程序能够“支持群集”。 更重要的是,CRNP 提供了一种机制,使应用程序能够登记并接收 Sun Cluster 重新配置事件的后续异步通知。 群集内运行的数据服务以及群集外运行的应用程序都可以登记事件通知。 当群集中的成员发生变化或者资源组或某个资源的状态发生变化时,都会生成事件。
CRNP 提供了相应的机制和守护程序,可以生成群集重配置事件并通过群集将它们路由到感兴趣的客户机。
cl_apid 守护程序与客户机交互。 Sun Cluster 资源组管理器 (RGM) 生成群集重配置事件。 这些守护程序使用 syseventd( 1M) 传输各个本地节点上的事件。 cl_apid 守护程序使用可扩展标记语言 (XML) 通过 TCP/IP 与感兴趣的客户机进行通信。
下图概括了 CRNP 组件之间的事件流程。 在该图中,一台客户机在群集节点 2 上运行,另一台客户机在不属于该群集的计算机上运行。
CRNP 定义了标准七层开放式系统互联 (OSI) 协议栈的应用程序层、表示层和会话层。 传输层必须是传输控制协议 (TCP),网络层必须是网间协议 (IP)。 CRNP 独立于数据链路层和物理层上。 在 CRNP 中交换的所有应用层消息都基于 XML 1.0。
客户机通过向服务器发送一条登记消息 (SC_CALLBACK_RG) 来启动通信。 此登记信息指定客户机要接收其通知的事件类型,还指定事件能够被传输到的端口。 登记连接的源 IP 和指定的端口一起形成回叫地址。
当群集中生成某台客户机感兴趣的事件时,服务器将通过回叫地址(IP 地址和端口号)联系该客户机,并将事件 (SC_EVENT) 传送给该客户机。 在群集中运行的服务器具有高度的可用性。 服务器将客户机登记存储在存储器中,即使重新引导群集,登记信息也会保留在存储器中。
客户机通过向服务器发送一条登记消息(SC_CALLBACK_RG,包含 REMOVE_CLIENT 消息)来撤销登记。 客户机接收到来自服务器的 SC_REPLY 消息后,将关闭连接。
CRNP 使用三种类型的消息(都基于 XML),如下表所示。 本章后面将对这些消息类型进行详细介绍, 同时还将详细介绍这些消息类型的使用方式。
消息类型 |
说明 |
---|---|
SC_CALLBACK_REG |
此类消息采用四种格式: ADD_CLIENT、REMOVE_CLIENT、ADD_EVENTS 和 REMOVE_EVENTS。 每种格式都包含以下信息:
ADD_CLIENT、ADD_EVENTS 和 REMOVE_EVENTS 格式还包含无界限的事件类型列表,每个类型都包括以下信息:
事件类和事件子类一同定义唯一的“事件类型”。 生成 SC_CALLBACK_REG 类的 DTD(文档类型定义)是 SC_CALLBACK_REG。 附录 F,CRNP 的文档类型定义 中详细介绍了此 DTD。 |
SC_EVENT |
SC_EVENT 中的值未定义类型。 生成 SC_EVENT 类的 DTD(文档类型定义)是 SC_EVENT。 附录 F,CRNP 的文档类型定义 中详细介绍了此 DTD。 |
SC_REPLY |
生成 SC_REPLY 类的 DTD(文档类型定义)是 SC_REPLY。 附录 F,CRNP 的文档类型定义 中详细介绍了此 DTD。 |
本节介绍管理员如何设置服务器,如何识别客户机,如何通过应用层和会话层发送信息,并介绍了错误状态。
系统管理员必须使用高度可用的 IP 地址(不依赖于群集中某台特定计算机的 IP 地址)和端口号来配置服务器。 管理员必须向目标客户机发布此网络地址。 CRNP 没有定义客户机获得该服务器名称的方式。 管理员可以使用命名服务,使客户机能够动态找到服务器网络地址,或者将网络名称添加到配置文件中,以便客户机读取。 服务器将作为故障切换资源类型在群集中运行。
每台客户机均由其回叫地址(IP 地址和端口号)唯一标识。 端口在 SC_CALLBACK_REG 消息中指定,IP 地址从 TCP 登记连接获取。 CRNP 假定具有相同回叫地址的后续 SC_CALLBACK_REG 消息来自同一台客户机,即使发送这些消息的源端口不相同。
客户机通过打开一个指向服务器的 IP 地址和端口号的 TCP 连接来启动登记。 建立 TCP 连接并做好写入准备后,客户机必须发送其登记消息。 登记消息必须是一条格式正确的 SC_CALLBACK_REG 消息,消息前后不能包含其它字节。
所有字节均已写入数据流后,该客户机必须使连接保持打开状态,以便接收来自服务器的应答。 如果客户机没有正确格式化消息,服务器将不登记客户机,并向客户机发送一条出错应答。 如果客户机在服务器发出应答之前关闭套接字连接,服务器将照常登记客户机。
客户机可以随时联系服务器。 客户机每次联系服务器时都必须发送一条 SC_CALLBACK_REG 消息。 如果服务器发送的消息格式不正确、次序有误或者无效,服务器将向该客户机发送一条出错应答。
客户机不能在发送 ADD_CLIENT 消息之前发送 ADD_EVENTS、REMOVE_EVENTS 或 REMOVE_CLIENT 消息。 客户机发送 ADD_CLIENT 消息后,才能发送 REMOVE_CLIENT 消息。
如果某台客户机发送一条 ADD_CLIENT 消息,但该客户机已经登记,那么服务器可能会接收这条消息。 这种情况下,服务器将使用第二条 ADD_CLIENT 消息中指定的新客户机登记替换旧的客户机登记,替换时不会发出提示。
大多数情况下,客户机在启动时通过发送一条 ADD_CLIENT 消息向服务器登记一次, 并通过向服务器发送一条 REMOVE_CLIENT 消息撤销登记一次。 然而,CRNP 为那些需要动态修改其事件类型列表的客户机提供了更大的灵活性。
每条 ADD_CLIENT、ADD_EVENTS 和 REMOVE_EVENTS 消息都包含一个事件列表。 下表描述了 CRNP 接受的事件类型,包括所需的名称和值对。
发送一条 REMOVE_EVENTS 消息,其中指定的一个或多个事件类型是该客户机以前所未曾登记的,或者
为同一事件类型登记了两次
类和子类 |
名称和值对 |
说明 |
---|---|---|
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 事件 |
处理登记后,服务器将发送 SC_REPLY 消息。 服务器将通过从其上接收登记请求的那台客户机上打开的 TCP 连接发送此消息, 然后关闭连接。 客户机必须保持 TCP 连接为打开状态,直到接收到来自服务器的 SC_REPLY 消息。
打开服务器 TCP 连接
等候连接进入“可写入”状态
发送 SC_CALLBACK_REG 消息(其中包含 ADD_CLIENT 消息)
等候 SC_REPLY 消息
接收 SC_REPLY 消息
接收服务器已关闭连接(从套接字读取 0 字节)的指示
关闭连接
打开服务器 TCP 连接
等候连接进入“可写入”状态
发送 SC_CALLBACK_REG 消息(其中包含 REMOVE_CLIENT 消息)
等候 SC_REPLY 消息
接收 SC_REPLY 消息
接收服务器已关闭连接(从套接字读取 0 字节)的指示
关闭连接
服务器每次接收到来自客户机的 SC_CALLBACK_REG 消息时,都会通过同一个打开的连接发送一条 SC_REPLY 消息。 此消息用于指明该操作是否成功。 SC_REPLY XML DTD 包含 SC_REPLY 消息的 XML 文档类型定义,以及此消息可能包含的错误消息。
SC_REPLY 消息用于指明某个操作是否成功。 它包含 CRNP 协议消息的版本、一个状态码和一条详细描述此状态码的状态消息。 下表描述了状态码可能具有的值。
状态码 |
说明 |
---|---|
OK |
已成功处理消息。 |
RETRY |
由于出现瞬态错误,客户机登记被服务器拒绝(客户机应使用其它参数尝试重新登记)。 |
LOW_RESOURCE |
群集资源不足,客户机只能以后再尝试(该群集的系统管理员也可以增加群集资源)。 |
SYSTEM_ERROR |
发生严重问题。 与该群集的系统管理员联系。 |
FAIL |
授权失败,或者其它问题导致登记失败。 |
MALFORMED |
XML 请求的格式不正确,无法进行分析。 |
INVALID |
XML 请求无效(不符合 XML 规范)。 |
VERSION_TOO_HIGH |
消息的版本过高,无法成功处理该消息。 |
VERSION_TOO_LOW |
消息的版本太低,无法成功处理该消息。 |
正常情况下,发送 SC_CALLBACK_REG 消息的客户机将收到一个表明登记是否成功的应答。
但是,当客户机登记时,服务器可能正处于一种错误状态,从而使服务器无法向客户机发送 SC_REPLY 消息。 在这种情况下,登记可能已经在发生错误之前成功完成,也可能已经失败,还可能尚未进行。
由于服务器必须充当故障切换或高度可用的群集服务器,所以此错误状态并不意味着服务的结束。 实际上,服务器可以很快开始向新登记的客户机发送事件。
对正在等候 SC_REPLY 消息的登记连接强制执行一个应用程序级别的超时,随后客户机需要重试登记。
在登记事件回叫之前,开始在其回叫 IP 地址和端口上侦听事件传送。 客户机应当同时等候登记确认消息和事件传送。 如果客户机在接收到确认消息之前就开始接收事件,客户机将静默关闭登记连接。
随着群集中事件的生成,CRNP 服务器将这些事件传送到请求这些类型的事件的所有客户机。 发送过程包括向客户机的回叫地址发送一条 SC_EVENT 消息。 每个事件都是通过一个新的 TCP 连接传送的。
客户机登记事件类型后,服务器立即通过一条包含 ADD_CLIENT 或 ADD_EVENT 消息的 SC_CALLBACK_REG 消息向客户机发送该类型的最新事件。 这样客户机就可以了解发送后续事件的系统的当前状态。
当服务器启动客户机 TCP 连接时,服务器将通过该连接发送一条 SC_EVENT 消息, 然后执行全双工关闭。
等候服务器启动 TCP 连接
接受来自服务器的传入连接
等候 SC_EVENT 消息
读取 SC_EVENT 消息
接收服务器已关闭连接(从套接字读取 0 字节)的指示
关闭连接
所有客户机都登记完成后,必须始终在各自的回叫地址(IP 地址和端口号)上侦听传入的事件传送连接。
如果服务器未能联系客户机以传送事件,将按照您指定的次数和时间间隔重新尝试传送事件。 如果所有尝试均未成功,将从该服务器的客户机列表中删除此客户机。 要接收更多事件,客户机还需要通过发送另一条包含 ADD_CLIENT 消息的 SC_CALLBACK_REG 消息重新登记。
群集中生成的事件具有一个全序,按照传送到每个客户机的顺序保存。 换句话说,如果群集中先生成事件 A,然后生成事件 B,那么客户机 X 将先接收事件 A, 然后接收事件 B。但是不会保存传送到所有客户机的事件的全序。 也就是说,客户机 Y 可以在客户机 X 接收到事件 A 之前接收事件 A 和事件 B。这样,速度慢的客户机将无法容纳传送到所有客户机的事件。
服务器传送的所有事件(子类的第一个事件和出现服务器错误后的事件除外)都是作为响应群集实际生成的事件而发生的,除非服务器遇到错误,导致丢失群集生成的事件。 这种情况下,服务器将为表示系统当前状态的每个事件类型生成一个事件。 每个事件都被发送到登记为对该事件类型感兴趣的客户机。
事件传送遵循“至少一次”的规则。 也就是说,允许服务器将同一个事件多次发送到一台客户机上。 这在服务器发生临时故障,恢复正常后无法判断客户机是否已接收到最新信息时特别有用。
SC_EVENT 消息包含群集中实际生成的消息,该消息已经过转换,符合 SC_EVENT XML 消息的格式要求。 下表描述了 CRNP 传送的事件类型,包括名称和值对、发行商和供应商。
类和子类 |
发行商和供应商 |
名称和值对 |
说明 |
---|---|---|---|
EC_Cluster ESC_cluster_membership |
发行商: rgm 供应商: SUNW |
名称: node_list 值类型: 字符串数组 名称: state_list 值类型: 字符串数组 |
state_list 中数组元素的位置与 node_list 中的位置同步。 即,node_list 数组中第一个列出的节点在 state_list 数组中也是第一个列出。 state_list 仅包含以 ASCII 表示的数字。 每个数字都表示该节点在群集中的当前象征数字。 如果该数字与上一个消息中接收到的数字相同,则说明该节点与群集之间的关系(脱离、连接或重新连接)尚未改变。 如果象征数字是 –1,则说明该节点不是该群集的成员。 如果象征数字非负,则说明该节点是该群集的成员。 可能会有以 ev_ 开头的其它名称及其关联的值,但它们不供客户机使用。 |
EC_Cluster ESC_cluster_rg_state |
发行商: rgm 供应商: SUNW |
名称: rg_name 值类型: 字符串 名称: node_list 值类型: 字符串数组 名称: state_list 值类型: 字符串数组 |
state_list 中数组元素的位置与 node_list 中的位置同步。 即,node_list 数组中第一个列出的节点在 state_list 数组中也是第一个列出。 state_list 包含资源组状态的字符串表示。 有效值是可以使用 scha_cmds(1HA) 命令检索的值。 可能会有以 ev_ 开头的其它名称及其关联的值,但它们不供客户机使用。 |
EC_Cluster ESC_cluster_r_state |
发行商: rgm 供应商: SUNW |
有三个是必需的,如下所示: 名称: r_name 值类型: 字符串 名称: node_list 值类型: 字符串数组 名称: state_list 值类型: 字符串数组 |
state_list 中数组元素的位置与 node_list 中的位置同步。 即,node_list 数组中第一个列出的节点在 state_list 数组中也是第一个列出。 state_list 包含资源状态的字符串表示。 有效值是可以使用 scha_cmds(1HA) 命令检索的值。 可能会有以 ev_ 开头的其它名称及其关联的值,但它们不供客户机使用。 |
服务器使用 TCP 包装的形式鉴别客户机。 服务器上允许的客户机列表中必须包含登记消息的源 IP 地址(也作为发送事件的回叫地址)。 拒绝的客户机列表中不能包含源 IP 地址和登记消息。 如果源 IP 地址和登记不在列表中,服务器将拒绝请求,并向客户机发送一个出错应答。
当服务器接收到 SC_CALLBACK_REG ADD_CLIENT 消息时,该客户机的后续 SC_CALLBACK_REG 消息必须包含一个与第一条消息中的源 IP 地址相同的源 IP 地址。 如果 CRNP 服务器接收到的 SC_CALLBACK_REG 不满足此要求,服务器将:
忽略请求并向客户机发送一个出错应答,或者
假设该请求来自一台新客户机(取决于 SC_CALLBACK_REG 消息的内容)
客户机需要以同样的方式鉴别服务器。 客户机只需接收其源 IP 地址和端口号与该客户机使用的登记 IP 地址和端口号相同的服务器上的事件传送。
由于 CRNP 服务的客户机需要位于保护该群集的防火墙内,因此 CRNP 不包含其它安全机制。
以下实例说明了如何开发名为 CrnpClient 的、使用 CRNP 的简单 Java 应用程序。 应用程序通过群集上的 CRNP 服务器登记事件回叫,侦听事件回叫,并通过打印事件的内容对事件进行处理。 应用程序终止前将撤销登记事件回叫请求。
样例应用程序使用 JAXP(Java API for XML Processing)生成和分析 XML。 此实例并未介绍如何使用 JAXP。 有关 JAXP 的详细信息,请访问 http://java.sun.com/xml/jaxp/index.html。
此实例提供了应用程序的几个片断,完整的应用程序可以从附录 G,CrnpClient.java 应用程序 中找到。 为了更有效地说明某些概念,本章中的实例与附录 G,CrnpClient.java 应用程序 中的完整应用程序略有不同。
为了简洁起见,本章实例中的样例代码不包含注释。 附录 G,CrnpClient.java 应用程序 中的完整应用程序包含注释。
此实例中的应用程序只是通过退出应用程序来处理大多数错误状态。 实际的应用程序需要更有效地处理错误。
首先需要设置环境。
下载并安装 JAXP 以及 Java 编译器和虚拟机的正确版本。
相关指令可以从 http://java.sun.com/xml/jaxp/index.html 上找到。
此实例要求使用 Java 1.3.1 或更高版本。
确保在编译命令行中指定 classpath,以使编译器能够找到 JAXP 类。 在源文件所在的目录下键入以下内容:
% 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 类文件(请注意,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 |
现在已完成环境配置,可以开发应用程序了。
在实例的这一部分,您可以使用分析命令行参数并构造 CrnpClient 对象的主要方法创建一个名为 CrnpClient 的基类。 此对象将命令行参数传递给类,等候用户终止应用程序, 对 CrnpClient 调用 shutdown 命令,然后退出。
设置 XML 处理对象。
创建一个侦听事件回叫的线程。
联系 CRNP 服务器并登记事件回叫。
创建实现上述逻辑的 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; } |
后文详细介绍了成员变量。
要了解如何分析命令行参数,请参考附录 G,CrnpClient.java 应用程序 中的代码。
在本代码中,必须确保在单独的线程中执行事件接收,这样应用程序才能在事件线程中断以及等候事件回叫时继续执行其它工作。
后文介绍了如何设置 XML。
在代码中定义一个名为 EventReceptionThread 的 Thread 子类,用于创建 ServerSocket 并等候事件到达套接字。
在实例代码的这一部分,既没有读取事件也没有处理事件。 后文介绍了如何读取和处理事件。 EventReceptionThread 在通配符网络互联协议地址上创建一个 ServerSocket。 EventReceptionThread 还保留 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; } |
了解 EventReceptionThread 类的工作原理后,就可以构造 createEvtRecepThr 对象了:
private void createEvtRecepThr() throws Exception { evtThr = new EventReceptionThread(this); evtThr.start(); } |
向登记网络互联协议和端口打开一个基本 TCP 套接字
构造 XML 登记消息
通过套接字发送 XML 登记消息
脱离套接字并读取 XML 应答消息
关闭套接字
创建实现上述逻辑的 Java 代码。
以下实例显示了 CrnpClient 类的 registerCallbacks 方法的实现,该方法由 CrnpClient 构造函数调用。 后文详细介绍了 createRegistrationString() 和 readRegistrationReply() 的调用。
regIp 和 regPort 是由构造函数设置的对象成员。
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(); } |
实现 unregister 方法。 此方法是由 CrnpClient 的 shutdown 方法调用的。 后文详细介绍了 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(); } |
设置应用程序结构并编写所有联网代码后, 就可以编写生成和分析 XML 的代码了。 第一步是编写生成 SC_CALLBACK_REG XML 登记消息的代码。
SC_CALLBACK_REG 消息包括登记类型(ADD_CLIENT、REMOVE_CLIENT、ADD_EVENTS 或 REMOVE_EVENTS)、回叫端口和感兴趣的事件列表。 每个事件都包括一个类和一个子类,后跟一个名称和值对列表。
在实例的这一部分,需要编写一个存储登记类型、回叫端口和登记事件列表的 CallbackReg 类。 此类还可以将自身串行化为一个 SC_CALLBACK_REG XML 消息。
此类中有一个有趣的方法,即 convertToXml 方法,它可以从类成员中创建一个 SC_CALLBACK_REG XML 消息字符串。 位于 http://java.sun.com/xml/jaxp/index.html 上的 JAXP 文档详细介绍了此方法的代码。
Event 类的实现如下所示。 请注意,CallbackReg 类使用一个可以存储一个事件并可以将其转换成 XML Element 的 Event 类。
创建实现上述逻辑的 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; } |
实现 Event 和 NVPair 类。
请注意,CallbackReg 类使用一个 Event 类,该 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; } |
创建生成 XML 消息的帮助程序类后,就可以编写 createRegistrationString 方法的实现了。 此方法是由 registerCallbacks 方法调用的,登记和撤销登记回叫中对 registerCallbacks 方法进行了介绍。
createRegistrationString 构造一个 CallbackReg 对象并设置其登记类型和端口。 然后 createRegistrationString 使用 createAllEvent、createMembershipEvent、createRgEvent 和 createREvent 帮助程序方法构造各种事件。 创建 CallbackReg 对象后,每个事件都被添加到该对象中。 最后,createRegistrationString 对 CallbackReg 对象调用 convertToXml 方法,以便在 String 表单中检索 XML 消息。
请注意,regs 成员变量可以存储用户向应用程序提供的命令行参数。 第五个参数及后续参数用于指定应用程序应该登记的事件。 第四个参数用于指定登记的类型,但本实例中忽略了该参数。 附录 G,CrnpClient.java 应用程序 中的完整代码显示了如何使用这个参数。
创建实现上述逻辑的 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); } |
创建撤销登记字符串。
创建撤销登记字符串比创建登记字符串要简单,因为不必考虑事件:
private String createUnregistrationString() throws Exception { CallbackReg cbReg = new CallbackReg(); cbReg.setPort("" + localPort); cbReg.setRegType(CallbackReg.REMOVE_CLIENT); String xmlStr = cbReg.convertToXml(); return (xmlStr); } |
现在已经为应用程序创建了联网和 XML 生成代码。 最后一步是分析及处理登记应答和事件回叫。 CrnpClient 构造函数将调用 setupXmlProcessing 方法。 此方法将创建 DocumentBuilderFactory 对象并设置该对象的各种分析特性。 位于 http://java.sun.com/xml/jaxp/index.html 上的 JAXP 文档详细介绍了此方法。
创建实现上述逻辑的 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); } |
要分析 CRNP 服务器为响应登记消息和撤销登记消息而发送的 SC_REPLY XML 消息,需要使用 RegReply 帮助程序类。 可以从 XML 文档构造此类。 此类提供了状态代码和状态消息的存取程序。 要分析来自服务器的 XML 数据流,需要创建一个新的 XML 文档并使用该文档的分析方法(位于 http://java.sun.com/xml/jaxp/index.html 上的 JAXP 文档详细介绍了此方法)。
创建实现上述逻辑的 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); } |
实现 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; } |
最后一步是分析和处理实际的回叫事件。 要协助执行此任务,需要修改您在生成 XML中创建的 Event 类,使其能够从 XML 文档中构造一个 Event,并创建一个 XML Element。 此更改需要一个附加构造函数(调用 XML 文档)、一个 retrieveValues 方法、两个附加成员变量(vendor 和 publisher)、所有字段的存取程序方法,以及一个打印方法。
创建实现上述逻辑的 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; |
实现支持 XML 分析的 NVPair 类的其它构造函数和方法。
步骤 1 中对 Event 类的更改要求对 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); } } |
在 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(); } |
运行应用程序。
# java CrnpClient crnpHost crnpPort localPort ... |
附录 G,CrnpClient.java 应用程序 中列出了CrnpClient 应用程序的完整代码。