一個簡單的 CORBA/java 示例

6 月份,我們談過您爲什麼要使用 CORBA 和 Java 技術。本月,我要通過一個可用的簡單示例,讓您開始探索 CORBA 技術的許多領域。不過,別忘了我們的目標是,創建這樣一種分佈式應用程序:使駐留在一臺計算機上的客戶機能向運行於另一臺計算機上的服務發出請求。我們不想爲諸如硬件或操作系統軟件等細節問題操心,而只是想讓這種服務能響應客戶機的請求。

IDL 接口
全部 CORBA 結構是從一個接口開始的,理解接口的最佳方法就是想像我的汽車,對,我的汽車。雖然您不熟悉它,但如果我對您說:“開上我的車,帶些三明治回來當午餐”,恐怕您就不會懷疑自己能不能駕駛我的汽車。您可能想知道它停在哪裏,以及開它是否安全,但是您會確信開我的車與開您的車差別不大。這是因爲,在各種汽車當中,人與汽車之間的接口已高度標準化了。我的轎車和您的跑車之間可能會有一些差異,但汽車的油門踏板、剎車和方向盤的安裝都是標準的,您一定能輕鬆快速上路。

因爲 CORBA 與語言無關,所以它依靠一種接口定義語言 (IDL),來表達客戶機如何向實現接口的服務發出請求。我們的接口就是一個方法:add()。這個方法將取兩個數(兩個 IDL 的 long 型數)並返回這兩個數之和。下面是我們的接口計算程序:

清單 1. calcsimpl.idl


module corbasem {
	module gen {
		module calcsimpl {
			interface calculator {
				long add(in long x, in long y);
			};
		};
	};
};

這個接口中的 IDL 關鍵字有:module、interface、long 和 in。IDL 使用關鍵字 module 來創建名稱空間,並且此關鍵字準確地映射爲 Java 關鍵字 package。運行 IDL-to-Java 編譯器時,生成的 Java 文件將會存到名爲 calcsimpl 的子目錄中。IDL 關鍵字 interface 完美地映射爲 Java 接口,並代表一種抽象類型,因爲兩者都只定義您與對象通訊的方式,而不涉及對象的實現。IDL 關鍵字 long 是一種基本的整數類型,它至少映射爲一個 4 字節的類型,這種類型在 Java 代碼中就是 int。

想一想執行遠程方法調用的機制,您就會發現定義參數傳遞的方向(客戶機到服務器、服務器到客戶機或者雙向傳遞)是多麼的有意義。在 IDL 操作中,這些方向用 inoutinout 關鍵字來聲明,每個參數都必須聲明方向,以便使對象請求代理程序 (ORB) 知道該參數的去向。這會影響到爲發送而進行的參數打包、參數解包以及內存管理。ORB 對參數瞭解得越多,它的效率就越高。關鍵字 in 表明 long xlong y 是從客戶機傳遞到服務器。

圖 1. 參與 CORBA 請求的各個部分
圖 1. 參與 CORBA 請求的各個部分

IDL 編譯器


接口定義以後,必須在 ORB 供應商提供的 IDL-to-Java 編譯器上運行。IDL 編譯器是一種精巧的實用程序,它生成 IDL 的 stub 和 skeleton 以及其它支持文件。生成的這些源文件,大部分將增強 CORBA 標準中定義的特定 IDL 類型的打包功能。編譯器將生成大部分網絡探測 (plumbing),這在分佈式系統中非常重要。在最基本的級別中,IDL-to-Java 編譯器只是一個按 CORBA 2.3 規範的定義來實現從 IDL 到 Java 語言映射的程序。手動生成這些代碼既枯燥又費時,還容易出錯;IDL-to-Java 編譯器會處理這一切,所以您就不用操心啦;同時,它會用一定的規則約束您,並強制您執行封裝。IDL-to-Java 編譯器將把 CORBA-land 規則強加給您的系統。

輸入下面的命令,從 Orbacus 執行 IDL-to-Java 編譯器,把所有生成的文件都放在 CLASSPATH 的輸出目錄下。

清單 2. 調用 IDL-to-Java 編譯器

jidl --output-dir c:/_work/corbasem calculator.idl

生成了什麼呢?這個命令生成了構建實現所需要的全部 Java 源文件。IDL-to-Java 編譯器可確保所定義的接口遵守 CORBA 規範的規則。

圖 2. IDL-to-Java 編譯器文件生成
圖 2. IDL-to-Java 編譯器文件生成

下面是這些文件:

  • calculator.java - 這個文件叫標記接口文件。CORBA 規範指出這個文件必須擴展 IDLEntity,並且與 IDL 接口同名。這個文件提供類型標記,從而使這個接口能用於其它接口的方法聲明。
  • calculatorOperations.java - 這個文件內含 Java 公共接口 -- calculatorOperations。規範指出,這個文件應該與具有 Operations 後綴的 IDL 接口同名,並且這個文件內含此接口映射的操作標記。上面定義的標記接口 (calculator.java) 可擴展這個接口。
  • calculatorHelper.java - 設計 helper 類的目的是,讓所需要的許多內務處理功能脫離我們的接口,但又隨時可用到實現過程中。幫助程序文件含有重要的靜態 narrow 方法,這種方法使 org.omg.CORBA.Object 收縮爲一種更具體的類型的對象引用;在這種情況下,將是一個計算程序類型。
  • calculatorHolder.java - holder 類是一個專門化類,是爲了需要通過引用來傳遞參數的任意數據類型而生成的。這個示例中將不使用 holder 類,但我們將會在以後的欄目中經常見到它。
  • calculatorPOA.java - skeleton 類爲 CORBA 功能提供了請求-響應探測的一大部分。生成 calculatorPOA.java,是因爲缺省方式下的實現是基於繼承的,如果我們選擇基於委託的實現方式,輸出就會不一樣。這些主題將在以後的欄目中詳細介紹。
  • _calculatorStub.java - 顧名思義,這是一個 stub 類。您的客戶機將需要這個類來進行工作。

服務器
現在生成的文件必須在服務器上開始工作,用這個服務器實現我們的接口。所幸的是,大部分探測是適合我們的要求的,但別高興得太早 -- 還有許多工作要做;就是說,所有這些文件都必須用在正確的地方。

讓我們從 add() 方法的實現開始。(您可以下載完整的 文件。)

清單 3. SimpleCalcSvr.java -- add() 方法

SimpleCalcServant extends calculatorPOA {
  public int add(int xint y) {
    return x + y;
  }
}

請注意,我們的實現類擴展了已生成的類 calculatorPOA。從客戶機發來一個請求時,該請求通過 ORB 進入 skeleton,skeleton 最終將調用 SimpleCalcServant,來完成請求並啓動響應。我們的接口很簡單,所以我們的實現也很簡單。

服務器其餘部分的實現,涉及如何圍繞這個接口實現來設置 CORBA 體系結構,由於可移植性和靈活性方面的原因,許多這些調用要按 CORBA 規範執行。

我們需要完成的第一項任務是,詳細說明要使用哪一個 ORB,然後予以初始化。下面的代碼(文件 SimpleCalcSvr.java 的第 18 行到第 29 行)處理此任務:

清單 4. SimpleCalcSvr.java -- 初始化 ORB

  java.util.Properties props = System.getProperties();
  props.put("org.omg.CORBA.ORBClass",
            "com.ooc.CORBA.ORG");
  props.put("org.omg.CORBA.ORBSingletonClass",
            "com.ooc.CORBA.ORBSingleton");
  
  org.omg.CORBA.ORB orbnull;
  // 初始化 ORB
  orb = org.omg.CORBA.ORB.init(args, props); 

初始化 ORB 時,需要準確地告訴它哪一個類將用作 ORBClass,哪一個類將用作 ORBSingleton 類。我們的實現將不考慮這些,但所有相關的探測則都將考慮這些。正如我前面所說的,這種情況下,我使用的是 Object Oriented Concepts, Inc. 的 Orbacus ORB,而 OOC 類在那兩個 props.put() 調用中已給出。一旦填入了屬性,props 就只作爲一個參數傳遞給 ORB.init() 方法。實際情況可能不是這樣;如果我們要把這個服務器移到另一個 ORB,不希望爲服務器重新編碼。所以,在理想情況下,我們寧願改變一個配置文件,使之指向另一個 ORB 類,然後直接重新啓動。

現在,ORB 已經到位並已初始化,並且實現也已經到位,只是尚未創建,此時,需要爲實現創建一個完善的生存地點,而這可不像聽起來那麼容易,在一個分佈式環境中,各個實現要求的環境可能略有不同。可以賦予實現許多特徵。實現既可以是單線程的,也可以是多線程的;既可以是具有高度可伸縮性的對象池,也可以是單元素。這許多不同的服務器特徵已產生了可移植對象適配器 (POA)。POA 使我們可以創建完善的環境,供我們的實現在其中駐留。所有符合 2.3 規範的 ORB 都會有一個根 POA,所有其它 POA 都是從根 POA 創建的。在這個簡單示例中,我已將實現專用的代碼分解爲它自己的方法 runcalc()

爲實現創建一個環境將是我們的第一項任務,所以必須設置一個 POA。本來,CORBA 服務器使用基本對象適配器 (BOA),但是每個供應商的 BOA 都不一樣,在最新版本的 CORBA 規範中,POA 已完全取代了 BOA。

清單 5. SimpleCalcSvr.java -- 設置 POA

  // 從始終存在的 rootPOA
  // 設置可移植對象適配器
  org.omg.PortableServer.POA rootPOA = 
     org.omg.PortableServer.POAHelper.narrow(
        orb.resolve_initial_references("RootPOA"));
  
  org.omg.PortableServer.POAManager manager =
     rootPOA.the_POAManager();

從標題和定義可以看出,這是一個簡單的示例。使用根 POA 而不創建新的 POA,將使事情變得簡單。POA 管理器是一種封裝了 POA 處理狀態的對象,所以,我們使用 POA 管理器,將發給 servant 的請求排隊。

還需要實例化實現:

清單 6. SimpleCalcSvr.java -- 實例化實現

  // 創建計算程序接口的 servant
  SimpleCalcServant calcSvtnew SimpleCalcServant();
  calculator calc = calcSvt._this(orb);

按照 CORBA 2.3 規範,所有 skeleton 均提供一個 _this() 方法,該方法使 servant 能得到目標 CORBA 對象的對象引用,servant 正是用目標 CORBA 對象來與這些請求相關聯的。

完成實現的實例化以後,就必須把機制放到適當的位置,以便客戶機能夠找到它們。有許多不同的方法和服務可用來找到滿足接口請求的對象。CORBA Service 定義 Naming Service 和 Trader Services,來專門幫助客戶機查找對象,以處理請求。也可以通過方法調用來傳遞對象。

在這個示例中,我們將使用所有方法中最直截了當的一種 — 將對象引用寫入一個文件,該文件將由客戶機選取。對於所有的 ORB 來說,創建一個對象引用的字符串表示,或者反過來,創建由字符串到對象的引用,都是必備的功能。

清單 7. SimpleCalcSvr.java -- 編寫對象引用

  // 將對象引用寫入一個文件
  PrintWriter refstrnew PrintWriter(
                       new FileWriter("calcref.ior"));
  refstr.println(orb.object_to_string(calc));
  refstr.close();

最後要做的一件事,就是激活 POA,使客戶機請求開始排隊,並強制服務器輸入其事件循環,以接收這些傳入的請求。

清單 8. SimpleCalcSvr.java -- 激活 POA

  // 使實現成爲可用
  manager.activate();
  System.out.println("SimpleCalcSvr is running!");
  orb.run();

客戶機
如果您考慮一下正在發生的事件的機制,就會明白客戶機和服務器實際上正是互爲映像的。客戶機將所有的參數打包以創建一個請求,然後以它自己的方式來發送這個請求。服務器只是將請求中的參數解包,執行運算,將返回值和輸出參數打包,然後向客戶機發迴響應。客戶機則將返回值和輸出參數解包,然後繼續處理。這樣,客戶機打包什麼,服務器就解包什麼,反之亦然。

這意味着您將會看到客戶機和服務器具有相似的結構。客戶機還必須創建並初始化一個 ORB。它可以是我們正在使用的 ORB,也可以是另一個供應商提供的 ORB;但是,不能是任意的 ORB,而應該是支持 IIOP 的 ORB,IIOP 是由對象管理集團 (OMG) 定義的、基於 TCP/IP 的互操作性協議。如果您的 ORB 比較舊,那麼請小心,它可能無法與其它 ORB 通話。

首先,我們以相同的方式創建 ORB,就像創建服務器一樣。(您可以下載完整的 文件。)

清單 9. SimpleCalcClient.java -- 初始化 ORB

  java.util.Properties props = System.getProperties();
  props.put("org.omg.CORBA.ORBClass",
            "com.ooc.CORBA.ORG");
  props.put("org.omg.CORBA.ORBSingletonClass",
            "com.ooc.CORBA.ORBSingleton");
  
  org.omg.CORBA.ORB orbnull;
  // 初始化 ORB
  orb = ORB.init(args, props); 

看起來眼熟?應該是這樣,它看起來與服務器完全一樣。現在,客戶機已經連接到了一個 ORB 之上,但我們的目標是調用一個服務,而這個服務是在系統中別的地方提供的,需要找到能響應請求的對象。在這個示例中,這意味着要從創建於服務器上的文件中獲取一個對象引用。爲了找到計算程序服務器,需要取得存儲在這個文件中的對象引用的字符串版本,然後把它轉換成對象引用,通過這個對象引用就可以進行調用了。

清單 10. SimpleCalcClient.java -- 獲取對象引用

  System.out.println("Getting reference from string...");
  BufferedReader in = new BufferedReader(
          new FileReader("calcref.ior") );
  String ior = in.readLine();
  in.close();
  
  calculator calc = calculatorHelper.narrow(
          orb.string_to_object(ior));

請注意,這裏使用了由 IDL-to-Java 編譯器生成的 calculatorHelper 類。calcref.ior 文件含有一個對象引用,而不是含有計算程序引用。calculatorHelper 類有一個 narrow 方法,可用來將抽象類型集中到特定的計算程序類型。

仔細看一看計算程序 calc,它表示計算機空間中另外某個地方的一個服務器。最後必須做的一件事,就是調用 calc 上的方法 add()

清單 11. SimpleCalcClient.java -- 調用 add()

  System.out.println( calc.add(2,3) );

結論
已經討論了很多內容,不過請想一想,都學到了什麼。我們的客戶機與服務器是完全隔離的,客戶機不知道服務器在什麼樣的硬件上運行,使用的是什麼操作系統,它是用什麼語言編寫的,它是不是多線程的,還有,它位於何處 — 是在隔壁,還是距離半個地球之遙。它只知道一點,即如果它調用 calc 中的 add(),就會得到可以指望的響應。

提供服務的情形全都是這樣,電話或電力公司也是如此。當您拿起電話的時候,您所期望的是聽到拔號音,然後您的呼叫能暢通連接,您並不在乎電話是通過光纜傳輸的還是通過衛星轉發的,同樣的情況在信息產業中也正在成爲現實。多虧有了 OMG 和這個基本結構,我們才得以加進這個既簡單而又非常有說服力的例子。

下個月,我們將稍微深入地發掘一下,看一看發生在表面現象之下的 IIOP 的神奇力量。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章