Sun Cluster 資料服務開發者指南 (適用於 Solaris 作業系統)

第 12章 CRNP

本章提供有關叢集重新配置通知協定 (CRNP) 的資訊。 CRNP 使故障轉移應用程式與可延伸應用程式能夠「支援叢集」。 更具體地來說,CRNP 提供了一種機制,可讓應用程式註冊 Sun Cluster 重新配置事件,並接收 Sun Cluster 重新配置事件之後續非同步的通知 。 在叢集內執行的資料服務與在叢集外執行的應用程式可以註冊事件的通知。 當叢集中的成員資格發生變更時,以及當資源群組或資源的狀態發生變更時,均會產生事件。

CRNP 概觀

CRNP 提供一些機制與常駐程式,這些機制與常駐程式產生叢集重新配置事件、透過叢集路由這些事件,並將其發送給感興趣的用戶端。

cl_apid 常駐程式與用戶端互動。 Sun Cluster 資源群組管理員 (RGM) 產生叢集重新配置事件。 這些常駐程式使用 syseventd(1M) 在每個本機節點上傳送事件。 cl_apid 常駐程式在 TCP/IP 中使用可延伸標記語言 (XML) 與感興趣的用戶端通訊。

以下圖表展示了 CRNP 元件之間事件流程的概觀。 在此圖表中,一個用戶端在叢集節點 2 上執行,另一個用戶端在叢集之外的電腦上執行。

圖 12–1 CRNP 的工作方式

顯示 CRNP 工作方式的流程圖

CRNP 協定概觀

CRNP 定義了標準七層開放式系統互連 (OSI) 協定堆疊的應用層、表示層和工作時段層。 傳輸層必須是 TCP,網路層必須是 IP。 CRNP 與資料連接層和實體層無關。 在 CRNP 中所交換的所有應用層訊息均以 XML 1.0 為基礎。

CRNP 協定的語義

用戶端透過將註冊訊息 (SC_CALLBACK_RG) 發送給伺服器來開始通訊。 此註冊訊息指定用戶端要接收通知的事件類型,以及可以將事件投遞至的通訊埠。 註冊連線的來源 IP 與指定的通訊埠一起形成回呼位址。

每當叢集內產生用戶端感興趣的事件,伺服器便會在其回呼位址 (IP 與通訊埠) 中聯絡用戶端,並將事件 (SC_EVENT) 發送給該用戶端。 該伺服器是在叢集自身內執行的具有高度可用性的伺服器。 該伺服器將用戶端註冊儲存在儲存體中,該儲存體在重新啟動叢集之後仍會持久性存在。

用戶端透過將註冊訊息 (SC_CALLBACK_RG,其中包含 REMOVE_CLIENT 訊息) 發送給伺服器來取消註冊。 在用戶端接收到來自伺服器的 SC_REPLY 訊息之後會關閉連接。

以下圖表顯示用戶端與伺服器之間的通訊流程。

圖 12–2 用戶端與伺服器之間的通訊流程

顯示用戶端與伺服器之間通訊流程的流程圖

CRNP 使用的訊息類型

CRNP 使用三種訊息類型,所有這些類型均基於 XML,如下表中所述。 在本章後面對這些訊息類型進行了更加詳細的說明。 在本章後面還對其用法進行了更加詳細的說明。

訊息類型  

說明 

SC_CALLBACK_REG

此訊息包括四種形式: ADD_CLIENTREMOVE_CLIENTADD_EVENTSREMOVE_EVENTS。 其中每一種形式均包含下列資訊:

  • 協定版本

  • ASCII 格式的回呼通訊埠 (非二進制格式)

ADD_CLIENTADD_EVENTSREMOVE_EVENTS 形式還包含不受限制的事件類型清單,其中每種形式均包括下列資訊:

  • 事件類別

  • 事件子類別 (可選用的)

  • 名稱與值對的清單 (可選用的)

事件類別與事件子類別一起定義唯一的「事件類型」。 產生類別 SC_CALLBACK_REG 所依照的 DTD (文件類型定義) 為 SC_CALLBACK_REG。 在附錄 F, CRNP 的文件類型定義 中對此 DTD 進行了更詳細的說明。

SC_EVENT

此訊息包含下列資訊︰

  • 協定版本

  • 事件類別

  • 事件子類別

  • 供應商

  • 出版商

  • 名稱與值對清單 (0 個或更多個名稱與值對的資料結構)

    • 名稱 (字串)

    • 值 (字串或字串陣列)

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 訊息均來自同一用戶端,即使發送這些訊息的來源通訊埠不同亦如此。

在用戶端與伺服器之間傳送 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 訊息,並且該用戶端已經註冊,則伺服器可以容許此訊息。 在此情形下,伺服器會以靜音方式將舊的用戶端註冊取代為在第二條 ADD_CLIENT 訊息中所指定的新的用戶端註冊。

在大多數情形下,用戶端透過在啟動時發送 ADD_CLIENT 訊息,在伺服器中註冊一次。 並且用戶端會透過將 REMOVE_CLIENT 訊息發送給伺服器,僅取消註冊一次。 然而,CRNP 會為需要動態修改其事件類型清單的那些用戶端提供更多靈活性。

SC_CALLBACK_REG 訊息的內容

每個 ADD_CLIENTADD_EVENTSREMOVE_EVENTS 訊息均包含事件的清單。 下表說明 CRNP 接受的事件類型,其中包括必需的名稱與值對。

如果用戶端:

則該伺服器會以靜音方式忽略這些訊息。

類別與子類別  

名稱與值對 

說明  

EC_Cluster

ESC_cluster_membership

必需的: 無 

可選用的: 無  

註冊所有叢集成員資格變更事件 (節點失效或節點連結) 

EC_Cluster

ESC_cluster_rg_state

有一項是必需的,如下所示: 

rg_name

值類型: 字串 

可選用的: 無  

註冊資源群組 name 的所有狀態變更事件

EC_Cluster

ESC_cluster_r_state

有一項是必需的,如下所示: 

r_name

值類型: 字串 

可選用的: 無  

註冊資源 name 的所有狀態變更事件

EC_Cluster

無 

必需的: 無  

可選用的: 無 

註冊所有 Sun Cluster 事件 

伺服器回覆用戶端的方式

處理註冊之後,伺服器會發送 SC_REPLY 訊息。 該伺服器會透過從用戶端 (已接收到其註冊要求) 開啟的 TCP 連接發送此訊息。 然後,便關閉該連接。 在該用戶端接收到來自伺服器的 SC_REPLY 訊息之前,必須保持 TCP 連接的開啟狀態。

例如,該用戶端執行下列動作:

  1. 開啟至伺服器的 TCP 連接

  2. 等待連接變得「可寫入」

  3. 發送 SC_CALLBACK_REG 訊息 (其中包含 ADD_CLIENT 訊息)

  4. 等待 SC_REPLY 訊息

  5. 接收 SC_REPLY 訊息

  6. 接收表明該伺服器已關閉連接 (從套接字讀取 0 位元組) 的指示器

  7. 關閉連接

稍後,用戶端便執行下列動作:
  1. 開啟至伺服器的 TCP 連接

  2. 等待連接變得「可寫入」

  3. 發送 SC_CALLBACK_REG 訊息 (其中包含 REMOVE_CLIENT 訊息)

  4. 等待 SC_REPLY 訊息

  5. 接收 SC_REPLY 訊息

  6. 接收表明該伺服器已關閉連接 (從套接字讀取 0 位元組) 的指示器

  7. 關閉連接

每當伺服器接收到來自用戶端的 SC_CALLBACK_REG 訊息時,該伺服器均會透過開啟的同一連接發送 SC_REPLY 訊息。 此訊息指定該作業是成功還是失敗。 SC_REPLY XML DTD 包含 SC_REPLY 訊息的 XML 文件類型定義,以及此訊息可以包含的可能錯誤訊息。

SC_REPLY 訊息的內容

SC_REPLY 訊息指定作業是成功還是失敗。 此訊息包含 CRNP 協定訊息的版本、狀況碼、詳細說明狀況碼的狀況訊息。 下表說明狀況碼的可能值。

狀況碼  

說明 

OK

已成功處理訊息。 

RETRY

伺服器由於暫態錯誤而拒絕了用戶端的註冊 (該用戶端應嘗試使用其他參數再次註冊)。 

LOW_RESOURCE

叢集資源不足,用戶端只能稍後再嘗試 (叢集的系統管理員還可以新增叢集上的資源)。 

SYSTEM_ERROR

發生了嚴重的問題。 請聯絡叢集的系統管理員。 

FAIL

授權失敗,或者其他問題導致了註冊失敗。 

MALFORMED

XML 要求的形式異常,因此無法進行剖析。 

INVALID

XML 要求無效 (不符合 XML 規格)。 

VERSION_TOO_HIGH

訊息的版本太高,以致無法成功處理該訊息。 

VERSION_TOO_LOW

訊息的版本太低,以致無法成功處理該訊息。 

用戶端處理錯誤狀況的方式

在正常狀況下,發送 SC_CALLBACK_REG 訊息的用戶端會接收到回覆,指示註冊成功或失敗。

然而,伺服器可能在用戶端註冊時,遇到禁止該伺服器將 SC_REPLY 訊息發送給用戶端的錯誤狀況。 在此情況下,註冊在錯誤狀況發生之前可能已成功,或者已失敗,還可能尚未經過處理。

由於該伺服器在叢集上必須作為故障轉移伺服器或者高度可用的伺服器來執行功能,此錯誤狀況並不意味著服務的結束。 事實上,該伺服器不久就可以開始將事件發送給新註冊的用戶端。

若要修正這些狀況,您的用戶端應該進行以下兩項作業:

伺服器如何將事件發送給用戶端

由於事件在叢集內產生,CRNP 伺服器會將其發送給要求這些類型事件的所有用戶端。 發送過程包括將 SC_EVENT 訊息發送給用戶端的回呼位址。 每個事件的發送均發生在新的 TCP 連接中。

用戶端註冊事件類型之後,伺服器會立即透過包含 ADD_CLIENT 訊息或 ADD_EVENT 訊息的 SC_CALLBACK_REG 訊息將該類型的最新事件發送給該用戶端。 然後,該用戶端就可以知曉發送後續事件的系統之目前狀態。

當伺服器開始與用戶端的 TCP 連接後,會透過此連接準確發送一條 SC_EVENT 訊息。 然後,該伺服器發怖全雙工關閉。

例如,該用戶端執行下列動作:

  1. 等待伺服器開始 TCP 連接

  2. 接受從伺服器進來的連接

  3. 等待 SC_EVENT 訊息

  4. 讀取 SC_EVENT 訊息

  5. 接收表明該伺服器已關閉連接 (從套接字讀取 0 位元組) 的指示器

  6. 關閉連接

所有用戶端均已註冊時,必須一直為進來的事件發送連接偵聽其回呼位址 (IP 位址與通訊埠編號)。

如果伺服器無法聯絡到用戶端,以致不能發送事件,則該伺服器會以您所定義的時間間隔與次數再次嘗試發送該事件。 如果所有嘗試均失敗,則會從伺服器的用戶端清單中移除該用戶端。 該用戶端還需要發送包含 ADD_CLIENT 訊息的另一個 SC_CALLBACK_REG 訊息來重新註冊,然後才可以接收更多事件。

如何保證事件的發送

叢集內事件的產生有一個總的排序,向每個用戶端發送事件的順序也會依照此順序。 換句話說,如果在叢集內先產生事件 A 而後產生事件 B,則用戶端 X 會先接收到事件 A ,然後接收到事件 B。然而,將事件發送給所有用戶端時不會保持總的排序。 即,在用戶端 X 接收到事件 A 之前用戶端 Y 可以接收事件 A 與事件 B。以此方式,那些速度慢的用戶端不會阻礙向所有用戶端發送事件。

除了當伺服器遇到導致遺失叢集產生的事件之錯誤以外,該伺服器發送的所有事件 (除了子類別的第 一個事件與包含伺服器錯誤的事件) 均是回應叢集產生的實際事件。 在此情況下,伺服器為每個事件類型產生一個事件,表示該事件類型的系統之目前狀態。 每個事件發送給表明對該事件類型感興趣的已註冊用戶端。

事件發送遵循「至少一次」的語義進行。 即,允許伺服器將同一事件多次發送給某一用戶端。 萬一伺服器暫時關閉,當它重新啟動後無法判斷該用戶端是否已接收到最新資訊,此時該允許是必要的。

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  

有三項是必需的,如下所示:  

名稱: 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 位址必須與第一條訊息中的來源 IP 位址相同。 如果 CRNP 伺服器接收到不符合此需求的 SC_CALLBACK_REG,則該伺服器會:

此安全機制可協助阻止某人試圖取消註冊合法用戶端的服務干預被拒。

用戶端也應該以類似方式授權伺服器。 用戶端僅需要接受其來源 IP 位址和通訊埠編號與該用戶端使用的註冊 IP 位址和通訊埠編號相同的伺服器之事件發送。

因為預期 CRNP 服務的用戶端位於保護叢集的防火牆內,CRNP 不包括其他安全機制。

建立使用 CRNP 的 Java 應用程式

以下範例說明如何開發使用 CRNP 的名為 CrnpClient 的簡單 Java 應用程式。 該應用程式在叢集上的 CRNP 伺服器中註冊事件回呼、偵聽事件回呼、透過列印事件內容來處理這些事件。 在終止之前,該應用程式會取消註冊其對事件回呼的要求。

檢查此範例時,請注意以下幾點。

設定環境

首先,您需要設定環境。

  1. 下載並安裝 JAXP、正確版本的 Java 編譯器、虛擬機器。

    您可以在 http://java.sun.com/xml/jaxp/index.html 上找到說明。


    註解 –

    此範例需要 Java 1.3.1 或更高版本的 Java。


  2. 確定您在編譯指令行中指定 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 來源檔的名稱。

  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
    

    既然已配置了環境,您可以開發應用程式。

開始

在範例的此部分,您將使用剖析指令行引數與建構 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;
    }

    將在以後對成員變數進行更詳細的論述。

剖析指令行引數

    若要查看剖析指令行引數的方式,請參閱附錄 G, CrnpClient.java 應用程式 中的程式碼。

定義事件接收執行緒

在程式碼中,您需要確定事件接收是在獨立的執行緒中執行的,以便應用程式可以在事件執行緒封鎖並等待事件回呼時繼續進行其他工作。


註解 –

將在以後對設定 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. 既然您看到了 EventReceptionThread 類別的工作方式,因此請建構 createEvtRecepThr 物件:


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

註冊與取消註冊回呼

註冊作業包括:

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

產生 XML

既然您已設定應用程式的結構,並已寫入所有網路程式碼,因此您可以寫入產生與剖析 XML 的程式碼。 透過寫入產生 SC_CALLBACK_REG XML 註冊訊息的程式碼來啟動。

SC_CALLBACK_REG 訊息由註冊類型 (ADD_CLIENTREMOVE_CLIENTADD_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 ElementEvent 類別。

  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. 實施 Event 類別與 NVPair 類別。

    請注意,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;
    }

建立註冊訊息與取消註冊訊息

既然您已建立了產生 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);
    }

設定 XML 剖析器

您現在已經為應用程式建立了網路程式碼與 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 文件建構此類別。 此類別為狀況碼與狀況訊息提供 accessor。 若要剖析來自伺服器的 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;
    }

剖析回呼事件

最後一個步驟是剖析與處理實際的回呼事件。 若要輔助此作業,您要修改在產生 XML中建立的 Event 類別,以便此類別可以從 XML 文件建構 Event,以及建立 XML Element。 此變更需要其他建構元 (採用 XML 文件)、retrieveValues 方法、加入兩個成員變數 (vendorpublisher)、適用於所有欄位的 accessor 方法,最後還需要列印方法。

  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 類別實施其他建構元與方法。

    變更步驟 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);
            }
    }

  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 ...
    

    附錄 G, CrnpClient.java 應用程式 中列出了 CrnpClient 應用程式的完整程式碼。