Sun Cluster データサービス開発ガイド (Solaris OS 版)

第 12 章 CRNP

この章では、Cluster Reconfiguration Notification Protocol (CRNP) について説明します。CRNP を使用することで、フェイルオーバー用のアプリケーションや拡張性のあるアプリケーションを「クラスタ対応」として設定できます。具体的には、Sun Cluster 再構成イベントにアプリケーションを登録し、それらのイベントの後続の非同期通知を受け取ることができます。イベント通知の受信登録が可能なのは、クラスタの内部で動作するデータサービスと、クラスタの外部で動作するアプリケーションです。イベントは、クラスタ内のメンバーシップに変化があった場合と、リソースグループまたはリソースの状態に変化があった場合に生成されます。


注 –

SUNW.Event のリソース型実装は、 高可用性を備えた Sun Cluster の CRNP サービスを提供します。SUNW.Event のリソース型実装については、SUNW.Event(5) のマニュアルページを参照してください。


CRNP の概要

CRNP は、クラスタ再構成イベントの生成、クラスタへの配信、それらのイベントを要求しているクライアントへの送信を行うメカニズムとデーモンを提供します。

クライアントとの通信を行うのは、cl_apid デーモンです。クラスタ再構成イベントの生成は、Sun Cluster Resource Group Manager (RGM) によって行われます。これらのデーモンは、syseventd(1M) を使用して各ローカルノードにイベントを転送します。cl_apid デーモンは、TCP/IP 上で XML (Extensible Markup Language) を使用して要求クライアントとの通信を行います。

次の図は、CRNP コンポーネント間のイベントの流れを簡単に示したものです。この図では、一方のクライアントはクラスタノード 2 で動作し、他方のクライアントはクラスタに属していないコンピュータ上で動作しています。

図 12–1 CRNP の動作

CRNP の動作の流れを示す図

CRNP プロトコルの概要

CRNP は、標準の 7 層 OSI (Open System Interconnect) プロトコルスタックにおけるアプリケーション層、プレゼンテーション層、およびセッション層を定義します。トランスポート層は TCP でなければならず、ネットワーク層は IP でなければなりません。CRNP は、データリンク層および物理層とは無関係です。CRNP 内で交換されるアプリケーション層メッセージはすべて、XML 1.0 をベースとしたものです。

CRNP プロトコルのセマンティクス

クライアントは、サーバーへ登録メッセージ (SC_CALLBACK_RG) を送信することによって通信を開始します。この登録メッセージは、通知を受信したいイベントタイプと、イベントの配信先として使用できるポートを指定するものです。登録用接続のソース IP と指定ポートから、コールバックアドレスが構成されます。

クライアントが配信を希望しているイベントがクラスタ内で生成されるたびに、サーバーはこのコールバックアドレス (IP とポート) を持つクライアントと通信を行い、イベント (SC_EVENT) をクライアントに配信します。サーバーには、そのクラスタ内で稼動している高可用マシンが使用されます。サーバーは、クラスタの再起動後も維持されるストレージにクライアントの登録情報を格納します。

登録解除を行う場合、クライアントはサーバーに登録メッセージ (REMOVE_CLIENT メッセージが入った SC_CALLBACK_RG) を送信します。サーバーから SC_REPLY メッセージを受け取ったあとで、クライアントは接続を閉じることができます。

次の図は、クライアントとサーバー間の通信の流れを示します。

図 12–2 クライアントとサーバー間の通信の流れ

クライアントとサーバー間の通信の流れを示す図

CRNP が使用するメッセージのタイプ

CRNP は、3 種類のメッセージを使用します。これらはすべて、次の表に示すように XML ベースのメッセージです。これらのメッセージタイプの内容と使用法の詳細は、この章で後述します。

メッセージのタイプ 

説明 

SC_CALLBACK_REG

このメッセージには、4 つのフォーム、ADD_CLIENT REMOVE_CLIENTADD_EVENTS、および REMOVE_EVENTS を指定できます。これらの各フォームには、次の情報が含まれます。

  • プロトコルバージョン

  • ASCII 形式で示されたコールバックポート (バイナリ形式ではない)

ADD_CLIENTADD_EVENTS REMOVE_EVENTS の各フォームには制約のないイベントタイプリストも含まれ、それぞれに次の情報が含まれます。

  • イベントクラス

  • イベントサブクラス (省略可能)

  • 名前と値がペアになったリスト (省略可能)

イベントクラスとイベントサブクラスにより一意の「イベントタイプ」 が定義されます。SC_CALLBACK_REG のクラスを生成する DTD (ドキュメントタイプ定義) は、SC_CALLBACK_REG です。この DTD の詳細は、付録 F 「CRNP のドキュメントタイプ定義」を参照してください。

SC_EVENT

このメッセージには次の情報が含まれます。

  • プロトコルバージョン

  • イベントクラス

  • イベントサブクラス

  • ベンダー

  • パブリッシャー

  • 名前と値のペアリスト (名前と値をペアにした 0 個以上のデータ構造)

    • 名前 (文字列)

    • 値 (文字列または文字列配列)

SC_EVENT 内の値はタイプとしては分類されていません。SC_EVENT のクラスを生成する DTD (ドキュメントタイプ定義) は SC_EVENT です。この DTD の詳細は、付録 F 「CRNP のドキュメントタイプ定義」を参照してください。

SC_REPLY

このメッセージには次の情報が含まれます。

  • プロトコルバージョン

  • エラーコード

  • エラーメッセージ:

SC_REPLY のクラスを生成する DTD (ドキュメントタイプ定義) は SC_REPLY です。この DTD の詳細は、付録 F 「CRNP のドキュメントタイプ定義」を参照してください。

クライアントをサーバーに登録する方法

この節では、サーバーの設定、クライアントの識別、アプリケーション層とセッション層での情報送信、エラー状況などについて説明します。

管理者によるサーバー設定の前提

システム管理者は、汎用 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_EVENTS REMOVE_CLIENT メッセージを送信できません。また、ADD_CLIENT メッセージを送信しないかぎり REMOVE_CLIENT メッセージを送信できません。

クライアントが ADD_CLIENT メッセージを送信したが、そのクライアントがすでに登録されていたという場合は、サーバーがこのメッセージを黙認することがあります。このような場合、サーバーは報告なしに古いクライアント登録を削除し、2 つめの ADD_CLIENT メッセージに指定された新しいクライアント登録に置き換えます。

通常、クライアントはその起動時に ADD_CLIENT メッセージを送信することによって、サーバーに一度だけ登録を行います。また、登録の解除もサーバーに REMOVE_CLIENT メッセージを送信して一度だけ行います。しかし、CRNP はクライアントが必要に応じてイベントタイプリストを動的に変更できるだけの柔軟性を備えています。

SC_CALLBACK_REG メッセージの概念

ADD_CLIENTADD_EVENTS、および REMOVE_EVENTS メッセージには、それぞれイベントリストが含まれます。次の表は、CRNP が受け付けるイベントタイプを、必要となる名前と値のペアと共に示して説明しています。

クライアントが以下の作業のどちらか一方を行うと、

サーバーはクライアントに通知することなくこれらのメッセージを無視します。

クラスとサブクラス 

名前と値のペア 

説明 

EC_Cluster

ESC_cluster_membership

必須: なし 

(省略可能) なし 

クラスタメンバーシップの変更 (ノードの停止または結合) に関連するあらゆるイベントに登録する 

EC_Cluster

ESC_cluster_rg_state

次の条件で 1 つ必要: 

rg_name

値のタイプ: 文字列型 

(省略可能) なし 

rg_name (リソースグループ名) のあらゆる状態変更イベントに登録する

EC_Cluster

ESC_cluster_r_state

次の条件で 1 つ必要: 

r_name

値のタイプ: 文字列型 

(省略可能) なし 

r_name (リソース名) のあらゆる状態変更イベントに登録する

EC_Cluster

なし 

必須: なし 

(省略可能) なし 

あらゆる Sun Cluster イベントに登録する 

クライアントに対するサーバーの応答方法

登録を処理したあと、サーバーは SC_REPLY メッセージを送信します。この送信は、登録要求を行なったクライアントがオープン状態に維持している TCP 接続に対して行われます。このあとサーバーはこの接続を閉じます。クライアントは、サーバーから SC_REPLY メッセージを受信するまで TCP 接続をオープン状態に保つ必要があります。

クライアントは次のような作業を行います。

  1. サーバーに対して TCP 接続を開きます。

  2. 接続が「writeable (書き込み可能)」になるまで待機します。

  3. SC_CALLBACK_REG メッセージ (このメッセージには ADD_CLIENT メッセージが入っている) を送信します。

  4. SC_REPLY メッセージの到着を待機します。

  5. SC_REPLY メッセージを受け取ります。

  6. サーバーが接続を閉じたことを示すインジケータを受信します (ソケットから 0 バイトを読み取る)。

  7. 接続を閉じます。

その後クライアントは以下の作業を行います。
  1. サーバーに対して TCP 接続を開きます。

  2. 接続が「writeable (書き込み可能)」になるまで待機します。

  3. SC_CALLBACK_REG メッセージ (このメッセージには REMOVE_CLIENT メッセージが入っている) を送信します。

  4. SC_REPLY メッセージの到着を待機します。

  5. SC_REPLY メッセージを受け取ります。

  6. サーバーが接続を閉じたことを示すインジケータを受信します (ソケットから 0 バイトを読み取る)。

  7. 接続を閉じます。

クライアントから SC_CALLBACK_REG メッセージを受け取るたびに、サーバーは同じ接続に SC_REPLY メッセージを送信します。このメッセージは、処理が正常に完了したか失敗したかを示すものです。SC_REPLY メッセージの XML ドキュメントタイプ定義とこのメッセージ内で示されるエラーメッセージについては、SC_REPLY XML DTD」を参照してください。

SC_REPLY メッセージの内容

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 メッセージの送信を妨げるエラーが発生することがあります。この場合、エラーが発生する前に登録が正常に完了することも、登録が失敗することも、あるいは登録処理が行われないまま終了することもあります。

サーバーはクラスタ上でフェイルオーバー (高可用) サーバーとして機能するため、このエラーがサービスの終了を意味するわけではありません。実際、サーバーは新しく登録されたクライアントに対してすぐにイベント送信を開始できます。

これらの状況を修復するには、クライアントは次の 2 つの作業を行う必要があります。

サーバーがクライアントにイベントを配信する方法

クラスタ内でイベントが生成されると、CRNP サーバーはそのタイプのイベントを要求したすべてのクライアントにイベントの配信を行います。この配信では、クライアントのコールバックアドレスに SC_EVENT メッセージが送信されます。各イベントの配信は、新たな TCP 接続で行われます。

クライアントが ADD_CLIENT メッセージまたは ADD_EVENT メッセージが入った SC_CALLBACK_REG メッセージを通してイベントタイプの配信登録を行うと、サーバーはただちにクライアントに対してそのタイプの最新イベントを送信します。続いてクライアントは、後続のイベントを送信するシステムの現在の状態を検出できます。

クライアントに対して TCP 接続を開始する際に、サーバーはその接続に SC_EVENT メッセージを 1 つだけ送信します。続いてサーバーは全二重通信を閉じます。

クライアントは次のような作業を行います。

  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 の両方を受け取る可能性があります。この方法では、低速のクライアントのために全クライアントへの配信が停滞するということがありません。

サーバーが配信するイベントはすべて (サブクラス用の最初のイベントとサーバーエラーのあとに発生するイベントを除く)、クラスタが生成する実際のイベントに応答して発生します。ただし、クラスタで生成されるイベントを見逃すようなエラーが発生する場合は、サーバーは各イベントタイプの現在のシステム状態を示すイベントをそれらのイベントタイプごとに生成します。各イベントは、そのイベントタイプの配信登録を行なったクライアントに送信されます。

イベント配信は、「1 回以上」というセマンティクスに従って行われます。つまり、サーバーは 1 台のクライアントに対して同じイベントを複数回送信できます。この許可は、サーバーが一時的に停止して復帰した際に、クライアントが最新の情報を受け取ったかどうかをサーバーが判断できないという場合に不可欠なものです。

SC_EVENT メッセージの内容

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 

次の条件で 3 つ必要:  

名前: r_name

値のタイプ: 文字列型 

名前: node_list

値のタイプ: 文字配列 

名前: state_list

値のタイプ: 文字配列 

state_list の配列要素は、node_list の配列要素と同期をとるように配置されます。つまり、node_list 配列内で最初に出現しているノードの状態は、state_list 配列の先頭に示されます。

state_list には、リソースの状態を示す文字列が入っています。有効な値は、scha_cmds(1HA) コマンドで取得できる値です。

ev_ で始まるほかの名前や、それらの名前に関連した値が存在する場合がありますが、クライアントによる使用を意図したものではありません。

CRNP によるクライアントとサーバーの認証

サーバーは、TCP ラッパーを使用してクライアントの認証を行います。この場合、登録メッセージのソース IP アドレス (これはイベントの配信先であるコールバック IP アドレスとしても使用される) がサーバー側の「許可されたユーザー」リストに含まれていなければなりません。ソース IP アドレスと登録メッセージが「拒否されたクライアント」リストに存在してはなりません。ソース IP アドレスと登録メッセージがリスト中に存在しない場合、サーバーは要求を拒否し、クライアントに対してエラー応答を返します。

サーバーが SC_CALLBACK_REG ADD_CLIENT メッセージを受けとる場合、そのクライアントの後続の SC_CALLBACK_REG メッセージには最初のメッセージ内のものと同じソース IP アドレスが含まれていなければなりません。この条件を満たさない SC_CALLBACK_REG を受信した場合、CRNP サーバーは次のどちらかを選択します。

このセキュリティメカニズムは、正規クライアントの登録の解除を試みるサービス拒否攻撃の防止に役立ちます。

クライアントも、同様のサーバー認証を行う必要があります。クライアントは、それ自体が使用した登録 IP アドレスおよびポート番号と同じソース IP アドレスおよびポート番号を持つサーバーからのイベント配信を受け入れるだけです。

CRNP サービスのクライアントはクラスタを保護するファイアウォール内に配置されるのが一般的なため、CRNP にセキュリティメカニズムは提供されていません。

CRNP を使用する Java アプリケーションの作成

以下の例は、CRNP を使用する CrnpClient というシンプルな Java アプリケーションを作成する方法を示しています。このアプリケーションでは、クラスタ上の CRNP サーバーへのイベントコールバックの登録、イベントコールバックの待機、イベントの処理 (内容の出力) を行い、終了前にイベントコールバック要求の登録解除を行います。

この例を参照する場合は、以下の点に注意してください。

環境の設定

まず、環境の設定を行う必要があります。

  1. JAXP と、正しいバージョンの Java コンパイラおよび Virtual Machine をダウンロードし、インストールを行います。

    作業手順は、http://java.sun.com/xml/jaxp/index.html に示されています。


    注 –

    この例は、バージョン 1.3.1 以降の Java を必要とします。


  2. コンパイラが 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 ソースファイルの名前を指定してください。

  3. アプリケーションの実行時に、アプリケーションが適切な 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 という基本的なクラスを作成します。このオブジェクトは、コマンド行引数をこのクラスに渡し、ユーザーがアプリケーションを終了するのを待って CrnpClientshutdown を呼び出し、その後終了します。

CrnpClient クラスのコンストラクタは、以下の作業を実行する必要があります。

    上記のロジックを実装する 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 の設定については後述します。


  1. コード内で、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;
    }

  2. 以上で、EventReceptionThread クラスがどのように動作するか確認ができました。次は、createEvtRecepThr オブジェクトを構築します。


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

コールバックの登録と登録解除

登録は以下の作業によって行います。

  1. 上記のロジックを実装する Java コードを作成します。

    以下の例は、CrnpClient クラスの registerCallbacks メソッド (CrnpClient コンストラクタによって呼び出される) の実装を示しています。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();
    }

XML の生成

以上で、アプリケーション構造の設定と、通信用のコードの作成が終了しました。次は、XML の生成と解析を行うコードを作成します。初めに、SC_CALLBACK_REG XML 登録メッセージを生成するコードを作成します。

SC_CALLBACK_REG メッセージは、登録のタイプ (ADD_CLIENTREMOVE_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 クラスを使用します。

  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) {
                            // 指定されたオプションを持つパーサーを構築できない。
                            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;
    }

  2. 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 は、 createAllEventcreateMembershipEvent createRgEvent、および createREvent ヘルパーメソッドを使用して各種のイベントを構築します。各イベントは、CallbackReg オブジェクトが作成されたあとでこのオブジェクトに追加されます。最後に、createRegistrationStringCallbackReg オブジェクト上で convertToXml メソッドを呼び出し、String 形式の XML メッセージを取得します。

regs メンバー変数は、ユーザーがアプリケーションに指定するコマンド行引数を格納します。5 つ目以降の引数は、アプリケーションが登録を行うイベントを指定します。4 つ目の引数は登録のタイプを指定しますが、この例では無視されています。付録 G 「CrnpClient.java アプリケーション」 に挙げられている完全なコードでは、この 4 つ目の引数の使用方法も示されています。

  1. 上記のロジックを実装する 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);
    }

  2. 登録解除文字列を作成します。

    イベントを指定する必要がない分、登録解除文字列の作成は登録文字列の作成よりも簡単です。


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

XML パーサーの設定

以上で、アプリケーションの通信用コードと 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 ドキュメントを参照)。

  1. 上記のロジックを実装する 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);
    }

  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;
    
                    // 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 つの新たなメンバー変数 (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;
    
                    // 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;

  2. XML 解析をサポートする、NVPair クラスのコンストラクタとメソッドを別途実装します。

    手順 1Event クラスに変更を加えたため、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);
            }
    }

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

アプリケーションの実行

    アプリケーションを実行します。


    # java CrnpClient crnpHost crnpPort localPort ...
    

    完全な CrnpClient アプリケーションコードは、付録 G 「CrnpClient.java アプリケーション」 に示されています。