以下の例は、CRNP を使用する CrnpClient というシンプルな Java アプリケーションを作成する方法を示しています。このアプリケーションでは、クラスタ上の CRNP サーバーへのイベントコールバックの登録、イベントコールバックの待機、イベントの処理 (内容の出力) を行い、終了前にイベントコールバック要求の登録解除を行います。
このアプリケーション例は、JAXP (XML 処理用の Java API) による XML 生成と解析を行います。この例は JAXP の使用方法を説明したものではありません。JAXP の詳細は、http://java.sun.com/xml/jaxp/index.html で説明しています。
この例は、付録 G 「CrnpClient.java アプリケーション」 に示されている完全なアプリケーションコードを断片的に示したものです。この章の例は個々の概念を効果的に示すことをねらっており、付録 G 「CrnpClient.java アプリケーション」 に示されている完全なアプリケーションコードと多少異なります。
また、簡潔に示すため、この章の例ではコード例からコメントを除いてあります。付録 G 「CrnpClient.java アプリケーション」 に示されている完全なアプリケーションコードにはコメントが含まれています。
この例に示しているアプリケーションは終了するだけでほとんどのエラー状況に対応できるものですが、ユーザーが実際に使用するアプリケーションではエラーを徹底的に処理する必要があります。
まず、環境の設定を行う必要があります。
JAXP と、正しいバージョンの Java コンパイラおよび Virtual Machine をダウンロードし、インストールを行います。
作業手順は、http://java.sun.com/xml/jaxp/index.html に示されています。
この例は、バージョン 1.3.1 以降の Java を必要とします。
コンパイラが JAXP クラスを見つけることができるように、コンパイルのコマンド行に必ず classpath を指定する必要があります。ソースファイルが置かれているディレクトリから、次のように入力します。
% 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 ソースファイルの名前を指定してください。
アプリケーションの実行時に、アプリケーションが適切な JAXP クラスファイルを読み込むことができるように classpath を指定します (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 を呼び出し、その後終了します。
CrnpClient クラスのコンストラクタは、以下の作業を実行する必要があります。
オブジェクトを処理する XML を設定する
イベントコールバックを待機するスレッドを作成する
CRNP サーバーと通信し、イベントコールバックを受け取る登録をする
上記のロジックを実装する Java コードを作成します。
次の例は、CrnpClient クラスのスケルトンコードを示しています。コンストラクタ内で参照される 4 つのヘルパーメソッドと停止メソッドの実装はあとで示します。ここでは、ユーザーが必要とするパッケージをすべてインポートするコードを示しています。
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 の設定については後述します。
コード内で、ServerSocket を作成してソケットにイベントが到着するのを待機する EventReceptionThread という Thread サブクラスを定義します。
サンプルコードのこの部分では、イベントの読み取りもイベントの処理も行われません。イベントの読み取りと処理については後述します。EventReceptionThread は、ワイルドカード IP アドレス上に ServerSocket を作成します。EventReceptionThread は、CrnpClient オブジェクトにイベントを送信して処理できるように、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();
// ソケットストリームからイベントを作成し、処理する。
sock.close();
}
// 到達不能
} catch (Exception e) {
System.out.println(e);
System.exit(1);
}
}
/* プライベートメンバー変数 */
private ServerSocket listeningSock;
private CrnpClient client;
}
|
以上で、EventReceptionThread クラスがどのように動作するか確認ができました。次は、createEvtRecepThr オブジェクトを構築します。
private void createEvtRecepThr() throws Exception
{
evtThr = new EventReceptionThread(this);
evtThr.start();
}
|
登録用の IP アドレスとポートに対して基本的な 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 メッセージにシリアル化することもできます。
このクラスには、クラスメンバーから SC_CALLBACK_REG XML メッセージ文字列を作成する convertToXml という興味深いメソッドがあります。このメソッドを使用したコードの詳細は、http://java.sun.com/xml/jaxp/index.html の JAXP ドキュメントに記載されています。
次に、Event クラスの実装を示します。CallbackReg クラスは、イベントを 1 つ保存してそのイベントを 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) {
// 指定されたオプションを持つパーサーを構築できない。
pce.printStackTrace();
System.exit(1);
}
// root 要素を作成する。
Element root = (Element) document.createElement(
"SC_CALLBACK_REG");
// 属性を追加する。
root.setAttribute("VERSION", "1.0");
root.setAttribute("PORT", port);
root.setAttribute("regType", regType);
// イベントを追加する。
for (int i = 0; i < regEvents.size(); i++) {
Event tempEvent = (Event)
(regEvents.elementAt(i));
root.appendChild(tempEvent.createXmlElement(
document));
}
document.appendChild(root);
// 全体を文字列に変換する。
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 クラスは、NVPair クラスを使用する Event クラスを使用します。
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 メソッド (詳細は コールバックの登録と登録解除) によって呼び出されます。
createRegistrationString は、CallbackReg オブジェクトを構築し、その登録タイプとポートを設定します。続いて、createRegistrationString は、 createAllEvent、createMembershipEvent、 createRgEvent、および createREvent ヘルパーメソッドを使用して各種のイベントを構築します。各イベントは、CallbackReg オブジェクトが作成されたあとでこのオブジェクトに追加されます。最後に、createRegistrationString は CallbackReg オブジェクト上で convertToXml メソッドを呼び出し、String 形式の XML メッセージを取得します。
regs メンバー変数は、ユーザーがアプリケーションに指定するコマンド行引数を格納します。5 つ目以降の引数は、アプリケーションが登録を行うイベントを指定します。4 つ目の引数は登録のタイプを指定しますが、この例では無視されています。付録 G 「CrnpClient.java アプリケーション」 に挙げられている完全なコードでは、この 4 つ目の引数の使用方法も示されています。
上記のロジックを実装する Java コードを作成します。
private String createRegistrationString() throws Exception
{
CallbackReg cbReg = new CallbackReg();
cbReg.setPort("" + localPort);
cbReg.setRegType(CallbackReg.ADD_CLIENT);
// イベントを追加する。
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();
// わざわざ検証を行う必要はない。
dbf.setValidating(false);
dbf.setExpandEntityReferences(false);
// コメントと空白文字は無視したい。
dbf.setIgnoringComments(true);
dbf.setIgnoringElementContentWhitespace(true);
// CDATA セクションを TEXT ノードに結合する。
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
{
// ドキュメントビルダーを作成する。
DocumentBuilder db = dbf.newDocumentBuilder();
db.setErrorHandler(new DefaultHandler());
// 入力ファイルを解析する。
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;
// SC_REPLY 要素を見つける。
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);
// statusCode 属性の値を取得する。
statusCode = ((Element)n).getAttribute("STATUS_CODE");
// SC_STATUS_MSG 要素を検出する。
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;
}
// TEXT セクションを取得する (存在する場合)。
n = nl.item(0).getFirstChild();
if (n == null || n.getNodeType() != Node.TEXT_NODE) {
// 1 つも存在しなくてもエラーではないため、そのまま戻る。
return;
}
// 値を取得する。
statusMsg = n.getNodeValue();
}
private String statusCode;
private String statusMsg;
}
|
最後のステップは、実際のコールバックイベントの解析と処理です。この作業をスムーズに行うため、XML の生成で作成した Event クラスを変更します。このクラスを使用して XML ドキュメントから Event を構築し、XML Element を作成できます。この変更は、XML ドキュメントを受け付ける別のコンストラクタ、retrieveValues メソッド、2 つの新たなメンバー変数 (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;
// SC_EVENT 要素を検出する。
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);
//
// CLASS、SUBCLASS、VENDOR、および PUBLISHER
// 属性の値を取得する。
//
regClass = ((Element)n).getAttribute("CLASS");
regSubclass = ((Element)n).getAttribute("SUBCLASS");
publisher = ((Element)n).getAttribute("PUBLISHER");
vendor = ((Element)n).getAttribute("VENDOR");
// すべての nv ペアを取得する。
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;
// NAME 要素を検出する。
nl = elem.getElementsByTagName("NAME");
if (nl.getLength() != 1) {
System.out.println("Error in parsing: can't find "
+ "NAME node.");
return;
}
// TEXT セクションを取得する。
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;
}
// 値を取得する。
name = n.getNodeValue();
// ここで値要素を取得する。
nl = elem.getElementsByTagName("VALUE");
if (nl.getLength() != 1) {
System.out.println("Error in parsing: can't find "
+ "VALUE node.");
return;
}
// TEXT セクションを取得する。
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;
}
// 値を取得する。
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 ... |
完全な CrnpClient アプリケーションコードは、付録 G 「CrnpClient.java アプリケーション」 に示されています。