本章提供有關叢集重新配置通知協定 (CRNP) 的資訊。CRNP 可讓容錯移轉應用程式與可延伸式應用程式能夠「支援叢集」。更具體而言,CRNP 將提供一種機制,該機制可使應用程式註冊 Sun Cluster 重新配置事件,並接收 Sun Cluster 重新配置事件隨後的非同步通知。在叢集內執行的資料服務與在叢集外執行的應用程式可以註冊事件的通知。當叢集中的成員資格發生變更時,以及當資源群組或資源的狀態發生變更時,均會產生事件。
SUNW.Event 資源類型實施在 Sun Cluster 上提供了高度可用的 CRNP 服務。SUNW.Event(5) 線上手冊更詳細地說明了此資源類型的實作。
本章涵蓋下列主題:
CRNP 定義標準七層開放系統互聯模式 (OSI) 協定堆疊的應用層、展示層和會議層。傳輸層必須為 TCP,而網路層必須為 IP。CRNP 與資料連接層和實體層無關。在 CRNP 中所交換的所有應用層訊息均以 XML 1.0 為基礎。
CRNP 提供產生叢集重新配置事件、透過叢集路由事件,並將事件傳送至感興趣的用戶端的機制和常駐程式。
cl_apid 常駐程式與用戶端互動。Sun Cluster 資源群組管理員 (RGM) 產生叢集重新配置事件。此常駐程式使用 syseventd 在每個本機節點上傳輸事件。cl_apid 常駐程式透過 TCP/IP 使用可延伸標記語言 (XML) 與感興趣的用戶端進行通訊。
下圖顯示 CRNP 元件之間的事件流程。在此圖表中,一個用戶端在叢集節點 2 上執行,另一個用戶端在叢集之外的電腦上執行。
用戶端透過向伺服器傳送註冊訊息 (SC_CALLBACK_RG) 來初始化通訊。此註冊訊息指定用戶端要接收通知的事件類型,還指定可以將事件發送至的通訊埠。註冊連線的來源 IP 與指定的通訊埠一起形成回呼位址。
每當叢集內產生用戶端感興趣的事件,伺服器便會在其回呼位址 (IP 與通訊埠) 中聯絡用戶端,並將事件 (SC_EVENT) 發送給該用戶端。該伺服器是在叢集自身內執行的具有高度可用性的伺服器。該伺服器將用戶端註冊儲存在儲存體中,該儲存體在重新啟動叢集之後仍會持久性存在。
用戶端透過將註冊訊息 (SC_CALLBACK_RG,其中包含 REMOVE_CLIENT 訊息) 發送給伺服器來取消註冊。在用戶端接收到來自伺服器的 SC_REPLY 訊息之後會關閉連接。
CRNP 使用三種基於 XML 的訊息類型。這些訊息的使用方法在下表中進行說明。在本章後面對這些訊息類型進行了更加詳細的說明。
CRNP 訊息類型 |
描述 |
---|---|
SC_CALLBACK_REG |
此訊息包括四種形式:ADD_CLIENT、REMOVE_CLIENT、ADD_EVENTS 和 REMOVE_EVENTS。其中每一種形式均包含下列資訊:
ADD_CLIENT、ADD_EVENTS 和 REMOVE_EVENTS 也包含無限的事件類型清單,每種事件類型包含以下資訊︰
事件類別與事件子類別一起定義唯一的「事件類型」。產生 SC_CALLBACK_REG 類別的文件類型定義 (DTD) 為 SC_CALLBACK_REG。附錄 FCRNP 的文件類型定義 更詳細地說明了此 DTD。 |
SC_REPLY |
此訊息包含下列資訊︰
產生 SC_REPLY 類別的 DTD 為 SC_REPLY。附錄 FCRNP 的文件類型定義 更詳細地說明了此 DTD。 |
SC_EVENT |
此訊息包含下列資訊︰ SC_EVENT 中的值尚未經過分類。產生 SC_EVENT 類別的 DTD 為 SC_EVENT。附錄 FCRNP 的文件類型定義 更詳細地說明了此 DTD。 |
本小節說明叢集管理員設置伺服器的方式、識別用戶端的方式、透過應用層和會議層傳送資訊的方式,以及錯誤狀況。
叢集管理員必須使用高度可用的 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、REMOVE_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 在接收到事件 B 之前接收到事件 A。然而,傳送至所有用戶端的事件總順序不會被保存。即,在用戶端 X 接收到事件 A 之前用戶端 Y 可以接收事件 A 與事件 B。以此方式,那些速度慢的用戶端不會阻礙向所有用戶端發送事件。
除了當伺服器遇到導致遺失叢集產生的事件之錯誤以外,該伺服器發送的所有事件 (除了子類別的第 一個事件與包含伺服器錯誤的事件) 均是回應叢集產生的實際事件。在此情況下,伺服器為每個事件類型產生一個事件,表示該事件類型的系統之目前狀態。每個事件發送給表明對該事件類型感興趣的已註冊用戶端。
事件發送遵循「至少一次」的語義進行。即,伺服器可以多次將相同事件傳送給用戶端。在伺服器暫時出現故障,且重新置於線上時無法確定用戶端是否接收到最新資訊時,此許可性是必需的。
該 SC_EVENT 訊息包含在叢集中產生,並轉譯以符合 SC_EVENT XML 訊息格式的實際訊息。下表說明 CRNP 發送的事件類型,包括名稱、值對、出版商及供應商。
state_list 的陣列元素位置與 node_list 的那些陣列元素位置是同步的。即,首先列示在 node_list 陣列中的節點的狀態會首先列示在 state_list 陣列中。
以 ev_ 開始的其他名稱及其關聯的值可能出現,但不是供用戶端使用的。
類別與子類別 |
出版商與供應商 |
名稱與值對 |
---|---|---|
EC_Cluster ESC_cluster_membership |
出版商:rgm 供應商:SUNW |
名稱:node_list 值類型:字串陣列 名稱:state_list state_list 僅包含以 ASCII 表示的編號。每個編號表示叢集中該節點的目前典型編號。如果該編號與先前所接收到訊息中的編號相同,則該節點尚未變更其與叢集的關係 (分離、連結或者重新連結)。如果典型編號為 –1,則該節點不是叢集的成員。如果典型編號是非負數的編號,則該節點是叢集的成員。 值類型:字串陣列 |
EC_Cluster ESC_cluster_rg_state |
出版商:rgm 供應商:SUNW |
名稱:rg_name 值類型:字串 名稱:node_list 值類型:字串陣列 名稱:state_list state_list 包含資源群組狀態的字串表示法。有效值就是您可以使用 scha_cmds(1HA) 指令擷取的值。 值類型:字串陣列 |
EC_Cluster ESC_cluster_r_state |
出版商:rgm 供應商:SUNW |
名稱:r_name 值類型:字串 名稱:node_list 值類型:字串陣列 名稱:state_list state_list 包含資源狀態的字串表示法。有效值就是您可以使用 scha_cmds(1HA) 指令擷取的值。 值類型:字串陣列 |
伺服器透過使用 TCP 包裝程式的一種類型認證用戶端。註冊訊息的來源 IP 位址 (還用做在其上傳送事件的回呼 IP 位址) 必須位於伺服器上允許的用戶端清單中。來源 IP 位址與註冊訊息不能位於被拒用戶端的清單中。如果來源 IP 位址與註冊均不在此清單中,則伺服器會拒絕要求,並會向用戶端發出錯誤回覆。
當伺服器接收 SC_CALLBACK_REGADD_CLIENT 訊息時,該用戶端其後的 SC_CALLBACK_REG 訊息必須包含與第一個訊息中的來源 IP 位址相同的來源 IP 位址。如果 CRNP 伺服器接收到不滿足此要求的 SC_CALLBACK_REG 訊息,則伺服器將執行以下動作之一︰
忽略請求並將錯誤回覆傳送給用戶端
依據 SC_CALLBACK_REG 訊息的內容,認為此請求來自新的用戶端
此安全機制可協助阻止某人試圖取消註冊合法用戶端的服務干預被拒。
用戶端也應該以類似方式授權伺服器。用戶端僅需要接受其來源 IP 位址和通訊埠編號與該用戶端使用的註冊 IP 位址和通訊埠編號相同的伺服器之事件發送。
由於 CRNP 服務的用戶端應位於保護叢集的防火牆內部,因此 CRNP 不包含附加安全機制。
以下範例說明如何開發使用 CRNP 的名為 CrnpClient 的簡易 Java 應用程式。該應用程式使用叢集中的 CRNP 伺服器註冊事件回呼,偵聽事件回呼並透過輸出事件內容處理事件。在終止之前,該應用程式會取消註冊其對事件回呼的要求。
檢閱此範例時,請注意以下幾點︰
範例應用程式使用 JAXP (用於 XML 處理的 Java API) 產生並剖析 XML。此範例未展示如何使用 JAXP。http://java.sun.com/xml/jaxp/index.html 更詳細地說明了 JAXP。
此範例僅表示部分應用程式,而該應用程式的全部位於附錄 GCrnpClient.java 應用程式。為了更有效地說明特定概念,本章中的範例與附錄 GCrnpClient.java 應用程式 中表示的完整應用程式稍有不同。
簡言之,本章中的範例程式碼不包含註釋。附錄 GCrnpClient.java 應用程式 中的完整應用程式包含註釋。
本範例中所顯示的應用程式僅需結束應用程式就可處理大多數錯誤狀況。實際的應用程式需要更牢固地處理錯誤。
下載並安裝 JAXP、正確版本的 Java 編譯器、虛擬機器。
您可在 http://java.sun.com/xml/jaxp/index.html 中找到說明。
此範例要求至少是 Java 1.3.1 版本。
從原始碼檔案所在目錄,鍵入以下內容︰
% 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,以便應用程式可以載入正確的 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 類別的建構元需要執行下列作業:
設置 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; }
成員變數將稍後在本章中詳細論述。
若要剖析指令行引數,請參閱附錄 GCrnpClient.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; }
建構 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 物件,並設定其註冊類型與通訊埠。然後,透過使用 createAllEvent、createMembershipEvent、createRgEvent 和 createREvent 輔助程式方法,createRegistrationString 將建構多種事件。在建立 CallbackReg 物件之後,便會將每個事件加入該物件。最後,createRegistrationString 對 CallbackReg 物件呼叫 convertToXml 方法,以擷取 String 形式的 XML 訊息。
請注意,regs 成員變數會儲存使用者為應用程式提供的指令行引數。第五個引數與後續引數指定應用程式應該註冊的事件。第四個引數指定註冊的類型,但在此範例中忽略。附錄 GCrnpClient.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 物件並在該物件上設定多種剖析特性。JAXP 文件將更詳細地說明此方法。請參閱 http://java.sun.com/xml/jaxp/index.html。
建立實施前導邏輯的 Java 程式碼。
private void setupXmlProcessing() throws Exception { dbf = DocumentBuilderFactory.newInstance(); // We don't need to bother validating dbf.setValidating(false); dbf.setExpandEntityReferences(false); // We want to ignore comments and whitespace dbf.setIgnoringComments(true); dbf.setIgnoringElementContentWhitespace(true); // Coalesce CDATA sections into TEXT nodes. dbf.setCoalescing(true); }
若要剖析 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) 之和、所有欄位的存取方法以及輸出方法。
建立實施前導邏輯的 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 ... |
附錄 GCrnpClient.java 應用程式 列出了 CrnpClient 應用程式的完整程式碼。