本章提供有關叢集重新配置通知協定 (CRNP) 的資訊。 CRNP 使故障轉移應用程式與可延伸應用程式能夠「支援叢集」。 更具體地來說,CRNP 提供了一種機制,可讓應用程式註冊 Sun Cluster 重新配置事件,並接收 Sun Cluster 重新配置事件之後續非同步的通知 。 在叢集內執行的資料服務與在叢集外執行的應用程式可以註冊事件的通知。 當叢集中的成員資格發生變更時,以及當資源群組或資源的狀態發生變更時,均會產生事件。
CRNP 提供一些機制與常駐程式,這些機制與常駐程式產生叢集重新配置事件、透過叢集路由這些事件,並將其發送給感興趣的用戶端。
cl_apid 常駐程式與用戶端互動。 Sun Cluster 資源群組管理員 (RGM) 產生叢集重新配置事件。 這些常駐程式使用 syseventd(1M) 在每個本機節點上傳送事件。 cl_apid 常駐程式在 TCP/IP 中使用可延伸標記語言 (XML) 與感興趣的用戶端通訊。
以下圖表展示了 CRNP 元件之間事件流程的概觀。 在此圖表中,一個用戶端在叢集節點 2 上執行,另一個用戶端在叢集之外的電腦上執行。
CRNP 定義了標準七層開放式系統互連 (OSI) 協定堆疊的應用層、表示層和工作時段層。 傳輸層必須是 TCP,網路層必須是 IP。 CRNP 與資料連接層和實體層無關。 在 CRNP 中所交換的所有應用層訊息均以 XML 1.0 為基礎。
用戶端透過將註冊訊息 (SC_CALLBACK_RG) 發送給伺服器來開始通訊。 此註冊訊息指定用戶端要接收通知的事件類型,以及可以將事件投遞至的通訊埠。 註冊連線的來源 IP 與指定的通訊埠一起形成回呼位址。
每當叢集內產生用戶端感興趣的事件,伺服器便會在其回呼位址 (IP 與通訊埠) 中聯絡用戶端,並將事件 (SC_EVENT) 發送給該用戶端。 該伺服器是在叢集自身內執行的具有高度可用性的伺服器。 該伺服器將用戶端註冊儲存在儲存體中,該儲存體在重新啟動叢集之後仍會持久性存在。
用戶端透過將註冊訊息 (SC_CALLBACK_RG,其中包含 REMOVE_CLIENT 訊息) 發送給伺服器來取消註冊。 在用戶端接收到來自伺服器的 SC_REPLY 訊息之後會關閉連接。
CRNP 使用三種訊息類型,所有這些類型均基於 XML,如下表中所述。 在本章後面對這些訊息類型進行了更加詳細的說明。 在本章後面還對其用法進行了更加詳細的說明。
訊息類型 |
說明 |
---|---|
SC_CALLBACK_REG |
此訊息包括四種形式: ADD_CLIENT、REMOVE_CLIENT、ADD_EVENTS 與 REMOVE_EVENTS。 其中每一種形式均包含下列資訊:
ADD_CLIENT、ADD_EVENTS 與 REMOVE_EVENTS 形式還包含不受限制的事件類型清單,其中每種形式均包括下列資訊:
事件類別與事件子類別一起定義唯一的「事件類型」。 產生類別 SC_CALLBACK_REG 所依照的 DTD (文件類型定義) 為 SC_CALLBACK_REG。 在附錄 F, CRNP 的文件類型定義 中對此 DTD 進行了更詳細的說明。 |
SC_EVENT |
SC_EVENT 中的值尚未經過分類。 產生類別 SC_EVENT 所依照的 DTD (文件類型定義) 是 SC_EVENT。 在附錄 F, CRNP 的文件類型定義 中對此 DTD 進行了更詳細的說明。 |
SC_REPLY |
產生類別 SC_REPLY 所依照的 DTD (文件類型定義) 是 SC_REPLY。 在附錄 F, CRNP 的文件類型定義 中對此 DTD 進行了更詳細的說明。 |
本節描述管理員如何設定伺服器、識別用戶端、資訊如何在應用層與工作時段層中發送以及錯誤狀況。
系統管理員必須為伺服器配置高度可用的 IP 位址 (即,IP 位址不固定給叢集中的某台特定機器使用) 及通訊埠編號。 該管理員還必須將此網路位址發佈給預期的用戶端。 CRNP 未定義如何使此伺服器名稱成為用戶端可用的伺服器名稱。 管理員將使用命名服務 (可使用戶端動態地尋找伺服器的網路位址),或者將網路名稱加入配置檔供用戶端讀取。 該伺服器將作為故障轉移資源類型在叢集內執行。
每個用戶端由其回呼位址 (即,其 IP 位址與通訊埠編號) 唯一識別。 通訊埠在 SC_CALLBACK_REG 訊息中指定,IP 位址從 TCP 註冊連線獲得。 CRNP 假定具有同一回呼位址的後續 SC_CALLBACK_REG 訊息均來自同一用戶端,即使發送這些訊息的來源通訊埠不同亦如此。
用戶端透過開啟與伺服器的 IP 位址和埠號碼的 TCP 連線開始註冊。 在建立了 TCP 連接並為寫入做好準備之後,用戶端必須發送其註冊訊息。 該註冊訊息必須是一條已正確格式化的 SC_CALLBACK_REG 訊息,在其前後都不包含額外位元組。
在將所有位元組寫入至串流之後,用戶端必須保持其連接的開啟狀態,才能接收到來自伺服器的回覆。 如果用戶端沒有正確格式化該訊息,則伺服器不會註冊該用戶端,並且會將錯誤的回覆發送給該用戶端。 如果該用戶端在伺服器發送回覆之前已關閉套接字連接,則伺服器會正常註冊該用戶端。
用戶端可以隨時聯絡伺服器。 每當用戶端聯絡伺服器時,該用戶端必須發送 SC_CALLBACK_REG 訊息。 如果該伺服器接收到形式錯誤的、順序紊亂的或者無效的訊息,便會將錯誤的回覆發送給該用戶端。
用戶端無法在發送 ADD_CLIENT 訊息之前發送 ADD_EVENTS、REMOVE_EVENTS 或者 REMOVE_CLIENT 訊息。 用戶端無法在發送 ADD_CLIENT 訊息之前發送 REMOVE_CLIENT 訊息。
如果用戶端發送了 ADD_CLIENT 訊息,並且該用戶端已經註冊,則伺服器可以容許此訊息。 在此情形下,伺服器會以靜音方式將舊的用戶端註冊取代為在第二條 ADD_CLIENT 訊息中所指定的新的用戶端註冊。
在大多數情形下,用戶端透過在啟動時發送 ADD_CLIENT 訊息,在伺服器中註冊一次。 並且用戶端會透過將 REMOVE_CLIENT 訊息發送給伺服器,僅取消註冊一次。 然而,CRNP 會為需要動態修改其事件類型清單的那些用戶端提供更多靈活性。
每個 ADD_CLIENT、ADD_EVENTS 與 REMOVE_EVENTS 訊息均包含事件的清單。 下表說明 CRNP 接受的事件類型,其中包括必需的名稱與值對。
發送了指定一個或多個事件類型 (用戶端先前尚未註冊) 的 REMOVE_EVENTS 訊息,或者
兩次註冊同一事件類型
類別與子類別 |
名稱與值對 |
說明 |
---|---|---|
EC_Cluster ESC_cluster_membership |
必需的: 無 可選用的: 無 |
註冊所有叢集成員資格變更事件 (節點失效或節點連結) |
EC_Cluster ESC_cluster_rg_state |
有一項是必需的,如下所示: rg_name 值類型: 字串 可選用的: 無 |
註冊資源群組 name 的所有狀態變更事件 |
EC_Cluster ESC_cluster_r_state |
有一項是必需的,如下所示: r_name 值類型: 字串 可選用的: 無 |
註冊資源 name 的所有狀態變更事件 |
EC_Cluster 無 |
必需的: 無 可選用的: 無 |
註冊所有 Sun Cluster 事件 |
處理註冊之後,伺服器會發送 SC_REPLY 訊息。 該伺服器會透過從用戶端 (已接收到其註冊要求) 開啟的 TCP 連接發送此訊息。 然後,便關閉該連接。 在該用戶端接收到來自伺服器的 SC_REPLY 訊息之前,必須保持 TCP 連接的開啟狀態。
開啟至伺服器的 TCP 連接
等待連接變得「可寫入」
發送 SC_CALLBACK_REG 訊息 (其中包含 ADD_CLIENT 訊息)
等待 SC_REPLY 訊息
接收 SC_REPLY 訊息
接收表明該伺服器已關閉連接 (從套接字讀取 0 位元組) 的指示器
關閉連接
開啟至伺服器的 TCP 連接
等待連接變得「可寫入」
發送 SC_CALLBACK_REG 訊息 (其中包含 REMOVE_CLIENT 訊息)
等待 SC_REPLY 訊息
接收 SC_REPLY 訊息
接收表明該伺服器已關閉連接 (從套接字讀取 0 位元組) 的指示器
關閉連接
每當伺服器接收到來自用戶端的 SC_CALLBACK_REG 訊息時,該伺服器均會透過開啟的同一連接發送 SC_REPLY 訊息。 此訊息指定該作業是成功還是失敗。 SC_REPLY XML DTD 包含 SC_REPLY 訊息的 XML 文件類型定義,以及此訊息可以包含的可能錯誤訊息。
SC_REPLY 訊息指定作業是成功還是失敗。 此訊息包含 CRNP 協定訊息的版本、狀況碼、詳細說明狀況碼的狀況訊息。 下表說明狀況碼的可能值。
狀況碼 |
說明 |
---|---|
OK |
已成功處理訊息。 |
RETRY |
伺服器由於暫態錯誤而拒絕了用戶端的註冊 (該用戶端應嘗試使用其他參數再次註冊)。 |
LOW_RESOURCE |
叢集資源不足,用戶端只能稍後再嘗試 (叢集的系統管理員還可以新增叢集上的資源)。 |
SYSTEM_ERROR |
發生了嚴重的問題。 請聯絡叢集的系統管理員。 |
FAIL |
授權失敗,或者其他問題導致了註冊失敗。 |
MALFORMED |
XML 要求的形式異常,因此無法進行剖析。 |
INVALID |
XML 要求無效 (不符合 XML 規格)。 |
VERSION_TOO_HIGH |
訊息的版本太高,以致無法成功處理該訊息。 |
VERSION_TOO_LOW |
訊息的版本太低,以致無法成功處理該訊息。 |
在正常狀況下,發送 SC_CALLBACK_REG 訊息的用戶端會接收到回覆,指示註冊成功或失敗。
然而,伺服器可能在用戶端註冊時,遇到禁止該伺服器將 SC_REPLY 訊息發送給用戶端的錯誤狀況。 在此情況下,註冊在錯誤狀況發生之前可能已成功,或者已失敗,還可能尚未經過處理。
由於該伺服器在叢集上必須作為故障轉移伺服器或者高度可用的伺服器來執行功能,此錯誤狀況並不意味著服務的結束。 事實上,該伺服器不久就可以開始將事件發送給新註冊的用戶端。
對等待 SC_REPLY 訊息的註冊連接強行加上應用層逾時,在此逾時後用戶端需要重試註冊。
先開始為事件發送偵聽用戶端的回呼 IP 位址與通訊埠編號,然後再註冊事件回呼。 該用戶端應該同時等待註冊確認訊息及事件發送。 如果用戶端在接收到確認訊息之前就開始接收事件,則用戶端應該以靜音方式關閉註冊連接。
由於事件在叢集內產生,CRNP 伺服器會將其發送給要求這些類型事件的所有用戶端。 發送過程包括將 SC_EVENT 訊息發送給用戶端的回呼位址。 每個事件的發送均發生在新的 TCP 連接中。
用戶端註冊事件類型之後,伺服器會立即透過包含 ADD_CLIENT 訊息或 ADD_EVENT 訊息的 SC_CALLBACK_REG 訊息將該類型的最新事件發送給該用戶端。 然後,該用戶端就可以知曉發送後續事件的系統之目前狀態。
當伺服器開始與用戶端的 TCP 連接後,會透過此連接準確發送一條 SC_EVENT 訊息。 然後,該伺服器發怖全雙工關閉。
等待伺服器開始 TCP 連接
接受從伺服器進來的連接
等待 SC_EVENT 訊息
讀取 SC_EVENT 訊息
接收表明該伺服器已關閉連接 (從套接字讀取 0 位元組) 的指示器
關閉連接
所有用戶端均已註冊時,必須一直為進來的事件發送連接偵聽其回呼位址 (IP 位址與通訊埠編號)。
如果伺服器無法聯絡到用戶端,以致不能發送事件,則該伺服器會以您所定義的時間間隔與次數再次嘗試發送該事件。 如果所有嘗試均失敗,則會從伺服器的用戶端清單中移除該用戶端。 該用戶端還需要發送包含 ADD_CLIENT 訊息的另一個 SC_CALLBACK_REG 訊息來重新註冊,然後才可以接收更多事件。
叢集內事件的產生有一個總的排序,向每個用戶端發送事件的順序也會依照此順序。 換句話說,如果在叢集內先產生事件 A 而後產生事件 B,則用戶端 X 會先接收到事件 A ,然後接收到事件 B。然而,將事件發送給所有用戶端時不會保持總的排序。 即,在用戶端 X 接收到事件 A 之前用戶端 Y 可以接收事件 A 與事件 B。以此方式,那些速度慢的用戶端不會阻礙向所有用戶端發送事件。
除了當伺服器遇到導致遺失叢集產生的事件之錯誤以外,該伺服器發送的所有事件 (除了子類別的第 一個事件與包含伺服器錯誤的事件) 均是回應叢集產生的實際事件。 在此情況下,伺服器為每個事件類型產生一個事件,表示該事件類型的系統之目前狀態。 每個事件發送給表明對該事件類型感興趣的已註冊用戶端。
事件發送遵循「至少一次」的語義進行。 即,允許伺服器將同一事件多次發送給某一用戶端。 萬一伺服器暫時關閉,當它重新啟動後無法判斷該用戶端是否已接收到最新資訊,此時該允許是必要的。
SC_EVENT 訊息包含叢集內所產生的實際訊息,該實際訊息已經過轉換以適應 SC_EVENT XML 訊息格式。 下表說明 CRNP 發送的事件類型,包括名稱、值對、出版商及供應商。
類別與子類別 |
出版商與供應商 |
名稱與值對 |
註解 |
---|---|---|---|
EC_Cluster ESC_cluster_membership |
出版商: rgm 供應商: SUNW |
名稱: node_list 值類型: 字串陣列 名稱: state_list 值類型: 字串陣列 |
state_list 的陣列元素位置與 node_list 的那些陣列元素位置是同步的。 即在 node_list 陣列中首先列出的節點之狀態就是 state_list 陣列中的第一個。 state_list 僅包含以 ASCII 表示的編號。 每個編號表示叢集中該節點的目前典型編號。 如果該編號與先前所接收到訊息中的編號相同,則該節點尚未變更其與叢集的關係 (分離、連結或者重新連結)。 如果典型編號為 –1,則該節點不是叢集的成員。 如果典型編號是非負數的編號,則該節點是叢集的成員。 以 ev_ 開始的其他名稱及其關聯的值可能出現,但不是供用戶端使用的。 |
EC_Cluster ESC_cluster_rg_state |
出版商: rgm 供應商: SUNW |
名稱: rg_name 值類型: 字串 名稱: node_list 值類型: 字串陣列 名稱: state_list 值類型: 字串陣列 |
state_list 的陣列元素位置與 node_list 的那些陣列元素位置是同步的。 即在 node_list 陣列中首先列出的節點之狀態就是 state_list 陣列中的第一個。 state_list 包含資源群組狀態的字串表示法。 有效值為可使用 scha_cmds(1HA) 指令擷取的那些值。 以 ev_ 開始的其他名稱及其關聯的值可能出現,但不是供用戶端使用的。 |
EC_Cluster ESC_cluster_r_state |
出版商: rgm 供應商: SUNW |
有三項是必需的,如下所示: 名稱: r_name 值類型: 字串 名稱: node_list 值類型: 字串陣列 名稱: state_list 值類型: 字串陣列 |
state_list 的陣列元素位置與 node_list 的那些陣列元素位置是同步的。 即在 node_list 陣列中首先列出的節點之狀態就是 state_list 陣列中的第一個。 state_list 包含資源狀態的字串表示法。 有效值為可使用 scha_cmds(1HA) 指令擷取的那些值。 以 ev_ 開始的其他名稱及其關聯的值可能出現,但不是供用戶端使用的。 |
伺服器使用 TCP 包裝函式的形式來授權用戶端。 註冊訊息的來源 IP 位址 (也用來作為發送事件的回呼 IP 位址) 必須位於伺服器上所允許的用戶端之清單中。 來源 IP 位址與註冊訊息不能位於被拒用戶端的清單中。 如果來源 IP 位址與註冊均不在此清單中,則伺服器會拒絕要求,並會向用戶端發出錯誤回覆。
當伺服器接收到 SC_CALLBACK_REG ADD_CLIENT 訊息時,則該用戶端的後續 SC_CALLBACK_REG 訊息包含的來源 IP 位址必須與第一條訊息中的來源 IP 位址相同。 如果 CRNP 伺服器接收到不符合此需求的 SC_CALLBACK_REG,則該伺服器會:
忽略要求,並將錯誤回覆發送給用戶端,或者
假定該要求來自於新的用戶端 (依照 SC_CALLBACK_REG 訊息的內容)
用戶端也應該以類似方式授權伺服器。 用戶端僅需要接受其來源 IP 位址和通訊埠編號與該用戶端使用的註冊 IP 位址和通訊埠編號相同的伺服器之事件發送。
因為預期 CRNP 服務的用戶端位於保護叢集的防火牆內,CRNP 不包括其他安全機制。
以下範例說明如何開發使用 CRNP 的名為 CrnpClient 的簡單 Java 應用程式。 該應用程式在叢集上的 CRNP 伺服器中註冊事件回呼、偵聽事件回呼、透過列印事件內容來處理這些事件。 在終止之前,該應用程式會取消註冊其對事件回呼的要求。
應用程式範例執行 XML 的產生,並使用 JAXP (用於 XML 處理的 Java API ) 進行剖析。 此範例並不教您如何使用 JAXP。 在 http://java.sun.com/xml/jaxp/index.html 上對 JAXP 進行了更詳細的說明。
此範例展示完整應用程式的片段,在附錄 G, CrnpClient.java 應用程式 中可以找到整個應用程式。 為了更有效地說明一些特殊概念,本章中所展示的範例與附錄 G, CrnpClient.java 應用程式 中所展示的完整應用程式稍有不同。
為了簡潔,本章範例中的範例程式碼不包括註釋。 附錄 G, CrnpClient.java 應用程式 中的完整應用程式包括註釋。
本範例中所顯示的應用程式僅需結束應用程式就可處理大多數錯誤狀況。 實際的應用程式需要更牢固地處理錯誤。
首先,您需要設定環境。
下載並安裝 JAXP、正確版本的 Java 編譯器、虛擬機器。
您可以在 http://java.sun.com/xml/jaxp/index.html 上找到說明。
此範例需要 Java 1.3.1 或更高版本的 Java。
確定您在編譯指令行中指定 classpath,以便編譯器可以找到 JAXP 類別。 從來源檔所在的目錄,輸入:
% javac -classpath JAXP_ROOT/dom.jar:JAXP_ROOTjaxp-api. \ jar:JAXP_ROOTsax.jar:JAXP_ROOTxalan.jar:JAXP_ROOT/xercesImpl \ .jar:JAXP_ROOT/xsltc.jar -sourcepath . SOURCE_FILENAME.java |
其中,JAXP_ROOT 是 JAXP 的 jar 檔案所在目錄的絕對路徑或相對路徑,SOURCE_FILENAME 是 Java 來源檔的名稱。
執行應用程式時,請指定 classpath,以便該應用程式可以載入適當的 JAXP 類別檔案 (請注意,classpath 中的第一條路徑為目前目錄):
java -cp .:JAXP_ROOT/dom.jar:JAXP_ROOTjaxp-api. \ jar:JAXP_ROOTsax.jar:JAXP_ROOTxalan.jar:JAXP_ROOT/xercesImpl \ .jar:JAXP_ROOT/xsltc.jar SOURCE_FILENAME ARGUMENTS |
既然已配置了環境,您可以開發應用程式。
在範例的此部分,您將使用剖析指令行引數與建構 CrnpClient 物件的主要方法,來建立名為 CrnpClient 的基本類別。 此物件將指令行引數傳送至類別、等待使用者終止該應用程式、對 CrnpClient 呼叫 shutdown,然後結束。
設定 XML 處理物件。
建立偵聽事件回呼的執行緒。
聯絡 CRNP 伺服器與註冊事件回呼。
建立實施前導邏輯的 Java 程式碼。
以下範例顯示 CrnpClient 類別的架構程式碼。 建構元中所參考的四種輔助說明方法與關閉方法的實作將在以後加以說明。 請注意,匯入您需要的所有套件之程式碼顯示如下。
import javax.xml.parsers.*; import javax.xml.transform.*; import javax.xml.transform.dom.*; import javax.xml.transform.stream.*; import org.xml.sax.*; import org.xml.sax.helpers.*; import org.w3c.dom.*; import java.net.*; import java.io.*; import java.util.*; class CrnpClient { public static void main(String []args) { InetAddress regIp = null; int regPort = 0, localPort = 0; try { regIp = InetAddress.getByName(args[0]); regPort = (new Integer(args[1])).intValue(); localPort = (new Integer(args[2])).intValue(); } catch (UnknownHostException e) { System.out.println(e); System.exit(1); } CrnpClient client = new CrnpClient(regIp, regPort, localPort, args); System.out.println("Hit return to terminate demo..."); try { System.in.read(); } catch (IOException e) { System.out.println(e.toString()); } client.shutdown(); System.exit(0); } public CrnpClient(InetAddress regIpIn, int regPortIn, int localPortIn, String []clArgs) { try { regIp = regIpIn; regPort = regPortIn; localPort = localPortIn; regs = clArgs; setupXmlProcessing(); createEvtRecepThr(); registerCallbacks(); } catch (Exception e) { System.out.println(e.toString()); System.exit(1); } } public void shutdown() { try { unregister(); } catch (Exception e) { System.out.println(e); System.exit(1); } } private InetAddress regIp; private int regPort; private EventReceptionThread evtThr; private String regs[]; public int localPort; public DocumentBuilderFactory dbf; } |
將在以後對成員變數進行更詳細的論述。
若要查看剖析指令行引數的方式,請參閱附錄 G, CrnpClient.java 應用程式 中的程式碼。
在程式碼中,您需要確定事件接收是在獨立的執行緒中執行的,以便應用程式可以在事件執行緒封鎖並等待事件回呼時繼續進行其他工作。
將在以後對設定 XML 加以論述。
在程式碼中,定義稱為 EventReceptionThread 的 Thread 子類別 (用來建立 ServerSocket 並等待事件到達套接字)。
在範例程式碼的此部分,事件既未被讀取,也未經過處理。 將在以後對讀取與處理事件加以論述。 EventReceptionThread 在萬用字元網際網路協定位址建立 ServerSocket。 EventReceptionThread 也保持對 CrnpClient 物件的參考,以便 EventReceptionThread 可以將事件發送給 CrnpClient 物件以進行處理。
class EventReceptionThread extends Thread { public EventReceptionThread(CrnpClient clientIn) throws IOException { client = clientIn; listeningSock = new ServerSocket(client.localPort, 50, InetAddress.getLocalHost()); } public void run() { try { DocumentBuilder db = client.dbf.newDocumentBuilder(); db.setErrorHandler(new DefaultHandler()); while(true) { Socket sock = listeningSock.accept(); // Construct event from the sock stream and process it sock.close(); } // UNREACHABLE } catch (Exception e) { System.out.println(e); System.exit(1); } } /* private member variables */ private ServerSocket listeningSock; private CrnpClient client; } |
既然您看到了 EventReceptionThread 類別的工作方式,因此請建構 createEvtRecepThr 物件:
private void createEvtRecepThr() throws Exception { evtThr = new EventReceptionThread(this); evtThr.start(); } |
開啟註冊網際網路協定與通訊埠的基本 TCP 套接字
建構 XML 註冊訊息
將 XML 註冊訊息發送給套接字
讀取來自套接字的 XML 回覆訊息
關閉套接字
建立實施前導邏輯的 Java 程式碼。
以下範例顯示 CrnpClient 類別 (由 CrnpClient 建構元呼叫) 的 registerCallbacks 方法的實作。 將在以後對 createRegistrationString() 與 readRegistrationReply() 的呼叫加以更詳細的說明。
regIp 與 regPort 均為由建構元設定的物件成員。
private void registerCallbacks() throws Exception { Socket sock = new Socket(regIp, regPort); String xmlStr = createRegistrationString(); PrintStream ps = new PrintStream(sock.getOutputStream()); ps.print(xmlStr); readRegistrationReply(sock.getInputStream(); sock.close(); } |
實施 unregister 方法。 此方法由 CrnpClient 的 shutdown 方法呼叫。 將在以後對 createUnregistrationString 的實作加以更詳細的說明。
private void unregister() throws Exception { Socket sock = new Socket(regIp, regPort); String xmlStr = createUnregistrationString(); PrintStream ps = new PrintStream(sock.getOutputStream()); ps.print(xmlStr); readRegistrationReply(sock.getInputStream()); sock.close(); } |
既然您已設定應用程式的結構,並已寫入所有網路程式碼,因此您可以寫入產生與剖析 XML 的程式碼。 透過寫入產生 SC_CALLBACK_REG XML 註冊訊息的程式碼來啟動。
SC_CALLBACK_REG 訊息由註冊類型 (ADD_CLIENT、REMOVE_CLIENT、ADD_EVENTS 或者 REMOVE_EVENTS)、回呼通訊埠、使人感興趣的事件之清單所組成。 每個事件由類別與子類別組成,其後跟隨名稱與值對的清單。
在範例的此部分,您將寫入儲存註冊類型、回呼通訊埠及註冊事件清單的 CallbackReg 類別。 此類別還可以將自身串列化為 SC_CALLBACK_REG XML 訊息。
此類別使人感興趣的方法為 convertToXml 方法,該方法建立類別成員的 SC_CALLBACK_REG XML 訊息字串。 在 http://java.sun.com/xml/jaxp/index.html 上的 JAXP 說明文件中對此方法中的程式碼進行了更加詳細的說明。
Event 類別的實作顯示如下。 請注意,CallbackReg 類別使用儲存一個事件並可以將該事件轉換為 XML Element 的 Event 類別。
建立實施前導邏輯的 Java 程式碼。
class CallbackReg { public static final int ADD_CLIENT = 0; public static final int ADD_EVENTS = 1; public static final int REMOVE_EVENTS = 2; public static final int REMOVE_CLIENT = 3; public CallbackReg() { port = null; regType = null; regEvents = new Vector(); } public void setPort(String portIn) { port = portIn; } public void setRegType(int regTypeIn) { switch (regTypeIn) { case ADD_CLIENT: regType = "ADD_CLIENT"; break; case ADD_EVENTS: regType = "ADD_EVENTS"; break; case REMOVE_CLIENT: regType = "REMOVE_CLIENT"; break; case REMOVE_EVENTS: regType = "REMOVE_EVENTS"; break; default: System.out.println("Error, invalid regType " + regTypeIn); regType = "ADD_CLIENT"; break; } } public void addRegEvent(Event regEvent) { regEvents.add(regEvent); } public String convertToXml() { Document document = null; DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); try { DocumentBuilder builder = factory.newDocumentBuilder(); document = builder.newDocument(); } catch (ParserConfigurationException pce) { // Parser with specified options can't be built pce.printStackTrace(); System.exit(1); } // Create the root element Element root = (Element) document.createElement( "SC_CALLBACK_REG"); // Add the attributes root.setAttribute("VERSION", "1.0"); root.setAttribute("PORT", port); root.setAttribute("regType", regType); // Add the events for (int i = 0; i < regEvents.size(); i++) { Event tempEvent = (Event) (regEvents.elementAt(i)); root.appendChild(tempEvent.createXmlElement( document)); } document.appendChild(root); // Convert the whole thing to a string DOMSource domSource = new DOMSource(document); StringWriter strWrite = new StringWriter(); StreamResult streamResult = new StreamResult(strWrite); TransformerFactory tf = TransformerFactory.newInstance(); try { Transformer transformer = tf.newTransformer(); transformer.transform(domSource, streamResult); } catch (TransformerException e) { System.out.println(e.toString()); return (""); } return (strWrite.toString()); } private String port; private String regType; private Vector regEvents; } |
實施 Event 類別與 NVPair 類別。
請注意,CallbackReg 類別使用 Event 類別 (其自身使用 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 使用 createAllEvent、createMembershipEvent、createRgEvent 與 createREvent 輔助說明方法建構各種事件。 在建立 CallbackReg 物件之後,便會將每個事件加入該物件。 最後,createRegistrationString 對 CallbackReg 物件呼叫 convertToXml 方法,以擷取 String 形式的 XML 訊息。
請注意,regs 成員變數儲存使用者提供給應用程式的指令行引數。 第五個引數與後續引數指定應用程式應該註冊的事件。 第四個引數指定註冊的類型,但在此範例中忽略。 附錄 G, CrnpClient.java 應用程式 中的完整程式碼顯示如何使用此第四個引數。
建立實施前導邏輯的 Java 程式碼。
private String createRegistrationString() throws Exception { CallbackReg cbReg = new CallbackReg(); cbReg.setPort("" + localPort); cbReg.setRegType(CallbackReg.ADD_CLIENT); // add the events for (int i = 4; i < regs.length; i++) { if (regs[i].equals("M")) { cbReg.addRegEvent( createMembershipEvent()); } else if (regs[i].equals("A")) { cbReg.addRegEvent( createAllEvent()); } else if (regs[i].substring(0,2).equals("RG")) { cbReg.addRegEvent(createRgEvent( regs[i].substring(3))); } else if (regs[i].substring(0,1).equals("R")) { cbReg.addRegEvent(createREvent( regs[i].substring(2))); } } String xmlStr = cbReg.convertToXml(); return (xmlStr); } private Event createAllEvent() { Event allEvent = new Event(); allEvent.setClass("EC_Cluster"); return (allEvent); } private Event createMembershipEvent() { Event membershipEvent = new Event(); membershipEvent.setClass("EC_Cluster"); membershipEvent.setSubclass("ESC_cluster_membership"); return (membershipEvent); } private Event createRgEvent(String rgname) { Event rgStateEvent = new Event(); rgStateEvent.setClass("EC_Cluster"); rgStateEvent.setSubclass("ESC_cluster_rg_state"); NVPair rgNvpair = new NVPair(); rgNvpair.setName("rg_name"); rgNvpair.setValue(rgname); rgStateEvent.addNvpair(rgNvpair); return (rgStateEvent); } private Event createREvent(String rname) { Event rStateEvent = new Event(); rStateEvent.setClass("EC_Cluster"); rStateEvent.setSubclass("ESC_cluster_r_state"); NVPair rNvpair = new NVPair(); rNvpair.setName("r_name"); rNvpair.setValue(rname); rStateEvent.addNvpair(rNvpair); return (rStateEvent); } |
建立取消註冊字串。
建立取消註冊字串要比建立註冊字串容易,因為您無需考慮事件:
private String createUnregistrationString() throws Exception { CallbackReg cbReg = new CallbackReg(); cbReg.setPort("" + localPort); cbReg.setRegType(CallbackReg.REMOVE_CLIENT); String xmlStr = cbReg.convertToXml(); return (xmlStr); } |
您現在已經為應用程式建立了網路程式碼與 XML 產生程式碼。 最後一個步驟是剖析與處理註冊回覆與事件回呼。 CrnpClient 建構元呼叫 setupXmlProcessing 方法。 此方法建立 DocumentBuilderFactory 物件,並設定有關該物件的各種剖析屬性。 在 http://java.sun.com/xml/jaxp/index.html 上的 JAXP 說明文件中對此方法進行了更加詳細的說明。
建立實施前導邏輯的 Java 程式碼。
private void setupXmlProcessing() throws Exception { dbf = DocumentBuilderFactory.newInstance(); // We don't need to bother validating dbf.setValidating(false); dbf.setExpandEntityReferences(false); // We want to ignore comments and whitespace dbf.setIgnoringComments(true); dbf.setIgnoringElementContentWhitespace(true); // Coalesce CDATA sections into TEXT nodes. dbf.setCoalescing(true); } |
若要剖析 CRNP 伺服器為回應註冊訊息與取消註冊訊息所發送的 SC_REPLY XML 訊息,您需要 RegReply 輔助說明類別。 您可以從 XML 文件建構此類別。 此類別為狀況碼與狀況訊息提供 accessor。 若要剖析來自伺服器的 XML 串流,您需要建立新的 XML 文件,並使用該文件的剖析方法 (http://java.sun.com/xml/jaxp/index.html 上的 JAXP 說明文件中對此方法進行了更加詳細的說明)。
建立實施前導邏輯的 Java 程式碼。
請注意,readRegistrationReply 方法使用新的 RegReply 類別。
private void readRegistrationReply(InputStream stream) throws Exception { // Create the document builder DocumentBuilder db = dbf.newDocumentBuilder(); db.setErrorHandler(new DefaultHandler()); //parse the input file Document doc = db.parse(stream); RegReply reply = new RegReply(doc); reply.print(System.out); } |
實施 RegReply 類別。
請注意,retrieveValues 方法在 XML 文件的 DOM 樹狀結構中行走,然後取出狀況碼與狀況訊息。 http://java.sun.com/xml/jaxp/index.html 上的 JAXP 說明文件包含更多詳細資訊。
class RegReply { public RegReply(Document doc) { retrieveValues(doc); } public String getStatusCode() { return (statusCode); } public String getStatusMsg() { return (statusMsg); } public void print(PrintStream out) { out.println(statusCode + ": " + (statusMsg != null ? statusMsg : "")); } private void retrieveValues(Document doc) { Node n; NodeList nl; String nodeName; // Find the SC_REPLY element. nl = doc.getElementsByTagName("SC_REPLY"); if (nl.getLength() != 1) { System.out.println("Error in parsing: can't find " + "SC_REPLY node."); return; } n = nl.item(0); // Retrieve the value of the statusCode attribute statusCode = ((Element)n).getAttribute("STATUS_CODE"); // Find the SC_STATUS_MSG element nl = ((Element)n).getElementsByTagName("SC_STATUS_MSG"); if (nl.getLength() != 1) { System.out.println("Error in parsing: can't find " + "SC_STATUS_MSG node."); return; } // Get the TEXT section, if there is one. n = nl.item(0).getFirstChild(); if (n == null || n.getNodeType() != Node.TEXT_NODE) { // Not an error if there isn't one, so we just silently return. return; } // Retrieve the value statusMsg = n.getNodeValue(); } private String statusCode; private String statusMsg; } |
最後一個步驟是剖析與處理實際的回呼事件。 若要輔助此作業,您要修改在產生 XML中建立的 Event 類別,以便此類別可以從 XML 文件建構 Event,以及建立 XML Element。 此變更需要其他建構元 (採用 XML 文件)、retrieveValues 方法、加入兩個成員變數 (vendor 與 publisher)、適用於所有欄位的 accessor 方法,最後還需要列印方法。
建立實施前導邏輯的 Java 程式碼。
請注意,此程式碼類似於剖析註冊回覆中說明的 RegReply 類別之程式碼。
public Event(Document doc) { nvpairs = new Vector(); retrieveValues(doc); } public void print(PrintStream out) { out.println("\tCLASS=" + regClass); out.println("\tSUBCLASS=" + regSubclass); out.println("\tVENDOR=" + vendor); out.println("\tPUBLISHER=" + publisher); for (int i = 0; i < nvpairs.size(); i++) { NVPair tempNv = (NVPair) (nvpairs.elementAt(i)); out.print("\t\t"); tempNv.print(out); } } private void retrieveValues(Document doc) { Node n; NodeList nl; String nodeName; // Find the SC_EVENT element. nl = doc.getElementsByTagName("SC_EVENT"); if (nl.getLength() != 1) { System.out.println("Error in parsing: can't find " + "SC_EVENT node."); return; } n = nl.item(0); // // Retrieve the values of the CLASS, SUBCLASS, // VENDOR and PUBLISHER attributes. // regClass = ((Element)n).getAttribute("CLASS"); regSubclass = ((Element)n).getAttribute("SUBCLASS"); publisher = ((Element)n).getAttribute("PUBLISHER"); vendor = ((Element)n).getAttribute("VENDOR"); // Retrieve all the nv pairs for (Node child = n.getFirstChild(); child != null; child = child.getNextSibling()) { nvpairs.add(new NVPair((Element)child)); } } public String getRegClass() { return (regClass); } public String getSubclass() { return (regSubclass); } public String getVendor() { return (vendor); } public String getPublisher() { return (publisher); } public Vector getNvpairs() { return (nvpairs); } private String vendor, publisher; |
針對支援 XML 剖析的 NVPair 類別實施其他建構元與方法。
變更步驟 1 中所顯示的 Event 類別需要對 NVPair 類別進行類似變更。
public NVPair(Element elem) { retrieveValues(elem); } public void print(PrintStream out) { out.println("NAME=" + name + " VALUE=" + value); } private void retrieveValues(Element elem) { Node n; NodeList nl; String nodeName; // Find the NAME element nl = elem.getElementsByTagName("NAME"); if (nl.getLength() != 1) { System.out.println("Error in parsing: can't find " + "NAME node."); return; } // Get the TEXT section n = nl.item(0).getFirstChild(); if (n == null || n.getNodeType() != Node.TEXT_NODE) { System.out.println("Error in parsing: can't find " + "TEXT section."); return; } // Retrieve the value name = n.getNodeValue(); // Now get the value element nl = elem.getElementsByTagName("VALUE"); if (nl.getLength() != 1) { System.out.println("Error in parsing: can't find " + "VALUE node."); return; } // Get the TEXT section n = nl.item(0).getFirstChild(); if (n == null || n.getNodeType() != Node.TEXT_NODE) { System.out.println("Error in parsing: can't find " + "TEXT section."); return; } // Retrieve the value value = n.getNodeValue(); } public String getName() { return (name); } public String getValue() { return (value); } } |
在等待事件回呼的 EventReceptionThread 中實施 while 迴圈 (在定義事件接收執行緒中有對 EventReceptionThread 的說明)。
while(true) { Socket sock = listeningSock.accept(); Document doc = db.parse(sock.getInputStream()); Event event = new Event(doc); client.processEvent(event); sock.close(); } |
執行應用程式。
# java CrnpClient crnpHost crnpPort localPort ... |
在附錄 G, CrnpClient.java 應用程式 中列出了 CrnpClient 應用程式的完整程式碼。