物件式分散式系統的通訊,核心是讓遠端用戶端能夠呼叫物件,機制大體建立在遠端程序呼叫(Remote Procedure Call, RPC)之上。但在呼叫真正發生之前,還有一連串問題要先解決:繫結、參考的實作、靜態與動態呼叫、參數傳遞等。

把用戶端繫結到物件#

傳統 RPC 系統與分散式物件系統的一個有趣差異:後者通常提供全系統範圍的物件參考(systemwide object reference)

  • 物件參考可以在不同機器的行程之間自由傳遞,例如作為方法呼叫的參數。
  • 把參考的實作隱藏起來(使其不透明、甚至作為參照物件的唯一手段),比傳統 RPC 更能提升分佈透明性。

行程持有物件參考後,必須先**繫結(bind)**到該物件才能呼叫方法。繫結的結果是行程位址空間裡多了一個 proxy,實作了可呼叫的方法介面。繫結有兩種形式:

  • 隱式繫結(implicit binding):用戶端只憑物件參考就能直接呼叫方法,在參考被解析為實際物件的那一刻,系統透明地完成繫結。例如 C++ 可多載一元成員選取運算子,讓物件參考用起來像普通指標。
  • 顯式繫結(explicit binding):用戶端必須先呼叫特殊函式繫結物件,通常會得到一個指向本地 proxy 的指標,之後才能呼叫方法。

圖 10-7:(a) 只使用全域參考的隱式繫結範例;(b) 同時使用全域與本地參考的顯式繫結範例

物件參考的實作#

物件參考必須含有足夠資訊讓用戶端完成繫結。最簡單的設計——「機器網路位址+識別伺服器的端點(end point)+物件指示」——有一連串缺點,逐步改良如下:

  • 問題一:伺服器重啟後端點改變,所有參考立刻失效。
    • 解法(DCE 的做法):每台機器跑一個本地 daemon,監聽一個眾所週知的端點,維護「伺服器 → 端點」對照表。參考裡改放伺服器 ID 作為查表索引;伺服器必須向本地 daemon 註冊。
  • 問題二:把機器網路位址編進參考,伺服器就永遠不能搬到別台機器,否則所有參考失效。
    • 解法:把「本地 daemon +端點表」的概念擴大成定位伺服器(location server),追蹤物件伺服器目前跑在哪台機器上。參考改含定位伺服器的網路位址+全系統的伺服器識別碼。這其實已接近扁平命名空間(flat name space)的實作。
  • 問題三:至今默默假設 client 與 server 使用相同的協定堆疊(相同傳輸協定、相同封送格式、相同連線建立與錯誤/流量控制方式)。
    • 解法:在參考中加入更多資訊——標明繫結該物件所用的協定、以及物件伺服器支援的協定(例如同時支援 TCP 與 UDP)。由用戶端負責取得至少其中一種協定的 proxy 實作。

還可以更進一步:在參考中放實作把手(implementation handle),指向一份完整的 proxy 實作(例如指向壓縮檔的 URL),用戶端繫結時動態下載、解包、安裝、實例化。好處是用戶端不必操心自己有沒有特定協定的實作,物件開發者也能自由設計物件專屬的 proxy。

動態下載 proxy 實作必須搭配特別的安全措施,確保用戶端可以信任下載來的程式碼。

靜態呼叫 vs. 動態呼叫#

繫結完成後,用戶端即可透過 proxy 呼叫物件方法,這稱為遠端方法呼叫(Remote Method Invocation, RMI)。RMI 在封送與參數傳遞上與 RPC 非常相似,本質差異在於:

  • RMI 通常支援全系統物件參考。
  • 不必侷限於通用的 client/server stub,可以輕鬆使用物件專屬的 stub。

兩種呼叫方式:

  • 靜態呼叫(static invocation):用介面定義語言(IDL)指定物件介面(或用 Java 這類物件式語言自動產生 stub)。前提是開發用戶端應用時就已知道物件介面;介面一改,用戶端就得重新編譯。
  • 動態呼叫(dynamic invocation):在執行期組合方法呼叫,應用在執行期才決定要呼叫遠端物件的哪個方法。一般形式如下:
invoke(object, method, input_parameters, output_parameters);

其中 object 識別分散式物件,method 指定要呼叫的方法,後兩者分別是輸入參數的資料結構與存放輸出值的資料結構。

動態呼叫的應用場景
  • 物件瀏覽器(object browser):用來檢視物件集合的工具。它繫結到分散式物件後,把物件介面呈現給使用者,讓使用者挑選方法、填入參數值,再由瀏覽器實際發出呼叫。這種瀏覽器必須支援任何可能的介面,因此要求介面能在執行期被檢視(inspect)、方法呼叫能動態組合。
  • 批次處理服務:接受「呼叫請求+指定執行時間」,內部用一個依執行時間排序的請求佇列實作;主迴圈等到下一個排程時間、取出請求、呼叫 invoke 即可。

參數傳遞#

由於多數 RMI 系統支援全系統物件參考,方法呼叫的參數傳遞比 RPC 少了許多限制,但也有些微妙之處。

  • 若系統中只有分散式物件:一律以物件參考作為參數,參考本身以值傳遞(複製到另一台機器)。收到參考的行程需要時再繫結即可。
  • 但這樣效率可能極差——物件很小(整數、甚至布林值)時,每次非同址的呼叫都會產生跨位址空間、甚至跨機器的請求。
  • 因此遠端物件與本地物件的參考通常區別對待
    • 參數參照遠端物件:複製並傳遞參考本身——物件實質上是傳參考(pass by reference)
    • 參數參照本地物件(與用戶端同位址空間):整個物件被複製、隨呼叫一起傳過去——物件實質上是傳值(pass by value)

例如機器 A 上的用戶端呼叫機器 C 上的伺服器,參數包括本地物件 O1 與位於機器 B 的遠端物件 O2:伺服器收到的是 O1 的完整拷貝,以及 O2 的參考拷貝。

圖 10-8:以參考傳遞與以值傳遞物件時的情形

「以參考當參數可能導致整個物件被複製」這件事不能隱藏,於是我們被迫在語言中明確區分本地物件與分散式物件。這不僅違反分佈透明性,也讓分散式應用更難寫。在 Java 中此區別只表現在資料型別上(本地與遠端物件屬不同型別),其餘處理方式幾乎一致;在 C 這類語言中,本地參考可能只是個指標,根本無法指涉遠端物件。

範例:Java RMI#

Java 把分散式物件整合進語言本身,目標是盡量保留非分散式物件的語意,追求高度分佈透明性——但在透明性效率太差、太難或不可能實現之處,也刻意讓分佈「現形」。

Java 分散式物件模型#

  • Java 只採用遠端物件這一種分散式物件:狀態永遠在單一機器上,介面透過 proxy 開放給遠端行程;proxy 在用戶端位址空間中就像一個本地物件。
  • 本地與遠端物件的差異少而微妙,**複製(cloning)**是一例:
    • 複製本地物件 O 會得到型別與狀態完全相同的新物件。
    • 這個語意很難套用到遠端物件——真要「完全拷貝」就得連同各用戶端的 proxy 一起複製。因此遠端物件的 clone 只能由伺服器執行,只複製伺服器位址空間裡的實際物件,proxy 不會被複製;遠端用戶端要用複製出來的物件,得重新繫結。

Java 的遠端物件呼叫#

  • 任何可**序列化(serializable)**的原始型別或物件型別都能當 RMI 參數。多數物件原則上可序列化,但平台相依的物件(檔案描述子、socket 等)不行。
  • 本地與遠端物件在 RMI 中的唯一區別:本地物件傳值(包括陣列等大型物件),遠端物件傳參考
  • 遠端物件參考的內容:伺服器的網路位址與端點+物件在伺服器位址空間內的本地識別碼(僅伺服器使用),另需編碼通訊用的協定堆疊。

遠端物件由兩個類別構成:

  • 伺服器類別(server class):伺服器端執行部分的實作——物件狀態的描述與操作狀態的方法;skeleton 由介面規格產生。
  • 用戶端類別(client class):proxy 的實作,同樣由介面規格產生。最簡單的 proxy 只是把方法呼叫轉成訊息送給伺服器端、把回覆訊息轉回結果,每次呼叫建立連線、結束時拆除。伺服器的網路位址、端點與物件本地識別碼都存於 proxy 的狀態中。

Java 的 proxy 是可序列化的:可以封送後傳給另一個行程,解封送後直接用來呼叫遠端物件的方法——換言之,proxy 本身可以當作遠端物件的參考。這與 Java「本地物件傳值、遠端物件傳全系統參考」的整合方式一致:proxy 被當成單純的本地物件傳遞,副作用是它同時就是遠端參考。

  • 實際封送 proxy 時並不是把全部程式碼轉成位元組(效率差、參考太大),而是產生一個實作把手,精確指出建構該 proxy 需要哪些類別(有些類別可能得先從遠端站點下載)。因此 Java 的遠端物件參考只有幾百位元組。
  • 這種做法非常靈活,是 Java RMI 的一大特色,允許物件專屬解法。例如某遠端物件狀態很少變動:可在繫結時把整份狀態複製到用戶端,方法都在本地副本上執行,每次呼叫時檢查伺服器狀態是否已變、變了就更新本地副本;修改狀態的方法則轉送伺服器。開發者只需實作必要的用戶端程式碼,讓它在繫結時動態下載。
對照:為什麼 DCE 做不到傳遞 stub

Proxy 能當參數傳遞,前提是每個行程都跑同一個 Java 虛擬機器——即相同的執行環境,封送過的 proxy 在接收端解封送後就能直接執行。反觀 DCE,各行程的語言、作業系統、硬體都可能不同,傳遞 stub 根本不可行;DCE 行程必須(動態)連結一份事先為其執行環境編譯好的本地 stub,再靠「以 stub 的參考作為 RPC 參數」來跨行程指涉物件。

物件式訊息傳遞#

RMI 雖是物件式系統的首選通訊方式,**訊息傳遞(messaging)**也是重要替代方案。這裡以 CORBA 的訊息服務為例,因為它展示了方法呼叫與訊息導向通訊的有趣結合。

CORBA 是知名的分散式系統規格,多年來有多種實作;其規格完整(對許多人來說也意味著非常複雜)。CORBA 訊息服務的特別之處在於堅持一切通訊都以呼叫物件的形式進行,這個設計約束產生了兩種非同步方法呼叫模型。非同步方法呼叫類似非同步 RPC:呼叫端發起呼叫後不等結果就繼續執行。

兩種模型的共同關鍵設計:非同步化完全不影響物件的原始(伺服器端)實作。把同步呼叫轉換成非同步是用戶端的責任,伺服器看到的仍是普通的同步呼叫請求。

回呼模型(callback model)#

用戶端提供一個實作回呼介面的物件,底層通訊系統呼叫其中的回呼方法來傳回非同步呼叫的結果。建構分兩步:

  1. 把物件實作的原始介面換成兩個僅由用戶端軟體實作的新介面:一個包含用戶端可呼叫的方法(皆無回傳值、無輸出參數);另一個是回呼介面,為原介面每個操作提供一個由用戶端 RTS 呼叫、用來傳回結果的方法。原方法的所有輸出參數與回傳值,都變成回呼操作的輸入參數。
  2. 編譯產生的介面。用戶端得到可非同步呼叫的 stub,但必須自行實作回呼介面;回呼方法由用戶端本地 RTS 呼叫,形成對應用程式的 upcall。

例如同步方法 int add(in int i, in int j, out int k)(回傳 i+j 於 k,失敗回傳 -1),轉換後變成一個送出請求的 sendcb_add(i, j) 與一個接收結果的回呼 replycb_add(...)

圖 10-9:CORBA 用於非同步方法呼叫的回呼模型

輪詢模型(polling model)#

  • 用戶端獲得一組操作,用來輪詢本地 RTS 是否有結果進來。
  • 同樣由用戶端負責把同步呼叫轉為非同步,方法規格大多可從原介面自動推導。
  • 與回呼模型最重要的差別:接收結果的方法(如 replypoll_add由用戶端的 RTS 實作,且可從介面規格自動產生——就像 RPC 的 client stub 一樣。
  • 伺服器端物件實作同樣不需修改。

圖 10-10:CORBA 用於非同步方法呼叫的輪詢模型

持久通訊#

上述模型還缺一塊:當 client 或 server 尚未執行時,雙方之間的訊息(呼叫請求或回應)應由底層系統暫存。所幸持久通訊的議題大多不影響前述非同步呼叫模型,需要的只是架設一批訊息伺服器暫存訊息直到可投遞為止。為此 CORBA 規格也定義了 router 的介面(類似訊息路由器,可用 IBM WebSphere 佇列管理器等實作);Java 陣營則有功能相近的 Java Messaging Service(JMS)