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

创建使用 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 应用程序的完整代码。