01

遠端呼叫總覽:三種溝通典範

處理程序如何跨機器溝通,以及中介軟體層的定位。

先讀原文開場,旁邊就是白話

這是一本英文書。左邊放原文、右邊放白話導讀——你既讀得懂,也順手碰了原文。

原文 · 遠端呼叫 185 5 REMOTE INVOCATION 5. 2 Request-reply protocols 5. 3 Remote procedure call 5. 4 Remote method invocation 5.
白話導讀

處理程序如何跨機器溝通,以及中介軟體層的定位。

為什麼需要遠端呼叫

處理程序如何跨機器溝通,以及中介軟體層的定位。

STEP 1

深度探秘

處理程序要跨機器說話

問題的起點

在一台電腦裡,你的程式呼叫一個函式(function)再自然不過:寫下 f(x) 就好。但在分散式系統裡,程式碼被拆到不同電腦、不同處理程序(process)上跑,它們之間沒有共用的記憶體,只能靠網路互傳訊息

問題是:直接操作網路訊息(打包、送出、等回覆、解包)非常瑣碎又容易出錯。於是工程師發明了一層中介軟體(middleware),蓋在底層的網路溝通(UDP、TCP、sockets)之上,提供更友善的「遠端呼叫」抽象。

本章關注的就是這一層:如何讓「呼叫遠方機器上的程式」變得像呼叫本機函式一樣簡單。

中介軟體的層次大致如下:

flowchart TD
  A[應用程式與服務] --> B[中介軟體 遠端呼叫與間接通訊]
  B --> C[底層通訊 sockets 訊息傳遞 多播]
  C --> D[UDP 與 TCP]
💡
關鍵

遠端呼叫是蓋在網路通訊之上的一層抽象,目的是讓跨機器溝通變簡單。

STEP 2

生活妙喻

請祕書幫你打電話訂貨

一通電話的抽象

想像你想跟外地一家工廠訂貨。

  • 沒有中介軟體:你得自己查電話、撥號、報出每個欄位、等對方覆述、記下回覆——每個細節都要你親手處理(這就像直接寫 socket 程式)。
  • 有中介軟體:你只要對祕書說「幫我訂 100 個 A 零件」,剩下撥號、講話、記回覆全交給祕書。對你而言,就像在公司內部交辦一樣輕鬆。

遠端呼叫的中介軟體,就是那位幫你處理所有網路細節的祕書。你只管「呼叫」,它幫你把呼叫變成訊息送到遠方、再把結果帶回來。

💡
關鍵

中介軟體像幫你打電話的祕書,把繁瑣的網路細節藏起來。

STEP 3

實用超能力

看懂系統分層

在真實系統裡定位它

當你之後看到任何分散式技術(gRPC、Java RMI、CORBA、REST),都可以問自己三個問題:

  1. 它跑在哪一層?幾乎都在中介軟體層
  2. 它底下用什麼?通常是 TCP 或 UDP
  3. 它幫我藏掉了什麼?多半是打包參數、傳訊息、配對回覆、處理逾時

例如你用 gRPC 呼叫遠端服務,寫起來像呼叫本機方法,但底層其實是:打包成訊息 → 走 HTTP/2(TCP)→ 對方解包執行 → 把結果送回。理解這個分層,遇到效能或失敗問題時就知道該往哪一層找原因。

💡
關鍵

認得分層後,任何遠端呼叫技術都能快速定位它在做什麼、藏了什麼。

🔆
生活妙喻:中介軟體層 ≈ 幫你打電話的祕書

你交辦需求,祕書處理撥號、溝通、記錄回覆等所有細節。

🔆
生活妙喻:底層 IPC(UDP/TCP) ≈ 電話線路本身

祕書再厲害,最終也得靠實體電話線把聲音傳過去。

本節字彙

處理程序 (process)
作業系統中正在執行的一個程式實例,彼此記憶體獨立。
🧠 process=各過各的,記憶體不共用。
中介軟體 (middleware)
夾在應用程式與底層網路之間、提供遠端溝通抽象的軟體層。
🧠 middle=中間,幫上下兩層牽線。
IPC (interprocess communication)
處理程序之間互相溝通的機制,遠端呼叫建立在它之上。
🧠 inter-process=程序之間的對話。
為什麼分散式系統裡的處理程序不能像本機函式那樣直接呼叫彼此?
中介軟體層在系統分層中扮演什麼角色?
用「祕書幫你打電話訂貨」比喻遠端呼叫,祕書對應的是哪一層?

三種典範鳥瞰

請求-回覆、RPC、RMI 的關係與層次。

STEP 1

深度探秘

由低到高的三層抽象

三種遠端溝通典範

本章聚焦三種由低到高、愈來愈友善的遠端溝通方式:

典範 抽象層次 一句話說明
請求-回覆協定 (request-reply) 最低 在訊息傳遞上加一層配對,支援 client 送請求、server 回覆
遠端程序呼叫 (RPC) 讓你像呼叫本機程序/函式一樣呼叫遠端程序
遠端方法呼叫 (RMI) 把 RPC 帶進物件世界,可呼叫遠端物件的方法、傳物件參考

請求-回覆是地基,RPC 與 RMI 都建在它之上。RPC 是 1980 年代的重大突破(Birrell 與 Nelson, 1984);RMI 則是 1990 年代物件導向興起後的延伸。

注意:這裡的「RMI」是泛指『遠端方法呼叫』這個概念,別跟特定產品「Java RMI」搞混。

💡
關鍵

請求-回覆是地基,RPC 延伸程序呼叫,RMI 再延伸到物件方法呼叫。

STEP 2

生活妙喻

從便利貼到語音助理

溝通方式的進化

把三種典範想成「交辦事情」的進化:

  • 請求-回覆:像在櫃台遞一張便利貼寫需求,對方做完回你一張紙條。最原始,但你得自己定規矩(怎麼配對哪張回哪張)。
  • RPC:像打內線電話說「請幫我跑一下報表程式」。你不必管對方在哪台機器,講出來就像在自己辦公室喊一聲。
  • RMI:像有個語音助理,你不只能下指令,還能把「另一個物件」當參數交給它——例如「把這份檔案交給會計『那個物件』處理」,甚至對方回你一個可以繼續使喚的『物件參考』。
💡
關鍵

三種典範是溝通能力的進化:便利貼、內線電話、能傳遞物件的語音助理。

STEP 3

實用超能力

選對抽象層次

何時用哪一種

  • 只是要最輕量、最可控的訊息往返(例如自訂的小型協定、NFS 這種固定區塊傳輸)→ 用底層請求-回覆就夠。
  • 想讓跨語言的服務像呼叫本機函式一樣被使用,且資料是程序式 → RPC(如 Sun RPC、gRPC)。
  • 系統是物件導向的,需要傳遞物件參考、用繼承與多型 → RMI(如 Java RMI、CORBA)。

關鍵差異在參數傳遞:RPC 只能傳值(輸入/輸出參數),RMI 額外能傳『物件參考』——當資料很大或很複雜時,傳一個參考讓對方需要時再遠端呼叫,往往比整包複製過去更划算。

💡
關鍵

依需求選層次;RMI 比 RPC 多了傳遞物件參考的能力。

🔆
生活妙喻:請求-回覆協定 ≈ 櫃台遞便利貼

最原始的往返,規則要自己定,但最輕量可控。

🔆
生活妙喻:RPC ≈ 打內線電話請對方跑程式

像在本機喊一聲,不必管對方在哪台機器。

🔆
生活妙喻:RMI 傳物件參考 ≈ 把一個能繼續使喚的『物件』交給對方

不必整包複製,對方拿到參考後可再遠端呼叫。

本節字彙

RPC (remote procedure call)
讓你像呼叫本機程序一樣呼叫遠端程序的機制。
🧠 Procedure=程序/函式,把函式呼叫搬到遠端。
RMI (remote method invocation)
把 RPC 延伸到物件世界,呼叫遠端物件的方法。
🧠 Method=方法,物件版的 RPC。
物件參考 (object reference)
指向某個物件的識別子;RMI 中可當參數在系統間傳遞。
🧠 reference=指路牌,不是物件本體。
三種典範由低到高的抽象層次,正確順序是?
RMI 相較於 RPC,最關鍵的額外能力是什麼?
某團隊要做一個物件導向、需要用繼承與多型、且常把複雜物件交給遠端處理的系統。最適合哪種典範?
02

請求-回覆協定:最基礎的對話

doOperation/getRequest/sendReply 與 requestId 的角色。

三個基本動作與訊息結構

doOperation/getRequest/sendReply 與 requestId 的角色。

STEP 1

深度探秘

doOperation、getRequest、sendReply

請求-回覆的三件法寶

請求-回覆協定為 client-server 的對話量身打造。在正常情況下它是同步的:client 送出請求後會阻塞(block),一直等到 server 的回覆回來才繼續。它也可以是可靠的,因為 server 的回覆本身就等於是對 client 的『確認』。

整個協定靠三個基本動作:

  • doOperation:client 用它來發起遠端操作。傳入『要找哪台 server、要呼叫哪個操作、帶什麼參數』,回傳一個裝著回覆的位元組陣列。呼叫者會被擋住,直到回覆到達。
  • getRequest:server 用它從自己的連接埠取得進來的請求。
  • sendReply:server 執行完操作後,用它把回覆送回給 client;client 的 doOperation 因此解除阻塞、繼續往下跑。
sequenceDiagram
  participant C as Client
  participant S as Server
  C->>S: Request 由 doOperation 送出
  Note over S: getRequest 取得請求 並執行操作
  S-->>C: Reply 由 sendReply 送回
  Note over C: doOperation 解除阻塞 取得結果
💡
關鍵

doOperation 發請求並等待,getRequest 收請求,sendReply 回結果——三者構成一次往返。

STEP 2

生活妙喻

點餐與取餐號碼牌

速食店點餐

把一次請求-回覆想成在速食店點餐:

  • 你走到櫃台點餐(doOperation 送出請求),然後站在旁邊(阻塞)。
  • 店員收到你的單(getRequest),去做餐。
  • 餐做好了叫號交給你(sendReply),你拿到餐就離開(解除阻塞)。

但店裡同時有很多人點餐,怎麼確保你拿到的是『你的餐』而不是別人的?靠號碼牌

這張號碼牌就是 requestId:client 每送一個請求就產生一個獨一無二的編號,server 回覆時會把同一個編號抄回去。這樣 doOperation 就能確認『這份回覆是我這次請求的,而不是上一筆延遲的舊回覆』。

💡
關鍵

requestId 就像點餐號碼牌,確保回覆能正確配對到當初的請求。

STEP 3

實用超能力

看懂訊息欄位

一封請求/回覆訊息長什麼樣

請求與回覆訊息通常包含這些欄位:

欄位 意義
messageType 0 代表 Request,1 代表 Reply
requestId 訊息識別碼,用來配對請求與回覆
remoteReference 要呼叫的遠端物件/服務參考
operationId 要執行哪個操作(可能是編號或操作本身)
arguments 參數,已被打包成位元組陣列

而完整的訊息識別碼其實由兩部分組成:

  1. requestId:由送出端取自一個遞增整數序列。
  2. 送出端的識別:例如它的連接埠與網際網路位址。

第一部分讓編號在『同一個送出端內』唯一,第二部分讓它在『整個分散式系統內』唯一。當 requestId 遞增到無號整數最大值就歸零重來——只要一個編號的『存活期』遠短於整個序列用完的時間,就不會撞號。

💡
關鍵

訊息識別碼=requestId(送出端內唯一)+送出端位址(系統內唯一)。

🔆
生活妙喻:doOperation 阻塞等待 ≈ 點完餐站在旁邊等叫號

送出請求後你不能走,得等餐(回覆)做好。

🔆
生活妙喻:requestId ≈ 點餐號碼牌

確保你拿到的是自己的餐,而不是別人的或上一筆舊餐。

🔆
生活妙喻:訊息識別碼兩部分 ≈ 店名加上當天流水號

流水號在店內唯一,加上店名就在全城唯一。

本節字彙

doOperation
client 用來發起遠端操作並等待回覆的方法。
🧠 do=去做這個遠端操作。
阻塞 (block)
呼叫端暫停執行、等待某事件(如回覆到達)才繼續。
🧠 block=被擋住,動彈不得。
requestId
每個請求的唯一編號,server 會抄進回覆以利配對。
🧠 像號碼牌,配對請求與回覆。
在正常的請求-回覆通訊中,client 呼叫 doOperation 之後會發生什麼?
requestId 最主要解決什麼問題?
完整的訊息識別碼為何要由『requestId+送出端位址』兩部分組成?

面對失敗:逾時、重送與冪等

逾時重送、過濾重複、history 與冪等操作。

STEP 1

深度探秘

訊息會掉,程序會當

不可靠世界的應對

若請求-回覆建在 UDP 之上,它就繼承了 UDP 的毛病:

  • 遺漏失敗(omission failures):請求或回覆可能直接掉包。
  • 不保證順序:訊息不一定照送出順序抵達。
  • 再加上程序當機(crash failure):我們假設程序當掉就停住,不會做出亂七八糟(Byzantine)的行為。

為了應付『server 掛了』或『訊息掉了』,doOperation 在等回覆時會設逾時(timeout)。逾時後最常見的作法不是直接放棄,而是重送請求,直到收到回覆、或合理確認真的是 server 沒回應為止;真的不行時,才用例外通知 client『沒拿到結果』。

但重送帶來新問題:server 可能收到重複的請求,導致同一操作被執行多次

💡
關鍵

UDP 上的請求-回覆會掉包、會亂序;用逾時加重送補救,但重送會引發重複執行。

STEP 2

生活妙喻

催繳信與『重按一次』

寄信沒回音怎麼辦

你寄信請廠商出貨,過了約定時間(逾時)還沒回音。你不知道是:

  • 你的信掉了(對方根本沒收到),還是
  • 對方早出貨了,只是回函在路上掉了。

保險作法是再寄一次(重送)。但若對方其實已出貨,再寄一次可能害你收到兩批貨——這就是重複執行的風險。

解法有兩層:

  1. 過濾重複:對方記得你的訂單編號(requestId),看到重複就不再重做,直接補寄收據。
  2. 冪等操作(idempotent):有些操作天生『做幾次都一樣』。例如『把開關設成 ON』,重複設一百次結果都是 ON;但『餘額加 10 元』就不是——做兩次就多加了。
💡
關鍵

重送像重寄催繳信;用『記住訂單編號去重』與『讓操作冪等』來避免重複造成的傷害。

STEP 3

實用超能力

去重、history 與冪等設計

server 端的三道防線

當重送無可避免時,server 可以這樣自保:

1. 過濾重複請求:認得來自同一 client、帶相同 requestId 的後續訊息並濾掉。若回覆還沒送出,不必特別處理,等做完再送即可。

2. 用 history 保存回覆:history 是一份『已送出回覆』的紀錄,每筆含 requestId、訊息內容、收件 client。當 client 沒收到回覆再次請求時,server 可直接重送舊回覆而不重新執行操作。代價是記憶體成本——history 會變大,所以通常只保留『給每個 client 的最後一筆回覆』,且過一段時間就丟棄。

3. 設計成冪等:若服務的每個操作都是冪等的,server 根本不必特別防範重複執行。例如 NFS 傳固定大小的檔案區塊、操作刻意設計成冪等,就不需要維護 history。

flowchart TD
  A[收到請求] --> B{requestId 重複?}
  B -- 否 --> C[執行操作 並回覆 存入 history]
  B -- 是 --> D{回覆已存在 history?}
  D -- 是 --> E[直接重送舊回覆 不重做]
  D -- 否 --> F[還在處理 做完再回]
💡
關鍵

去重、用 history 重送舊回覆、把操作設計成冪等,是面對重送的三大武器。

🔆
生活妙喻:逾時後重送 ≈ 沒回音就再寄一次催繳信

你分不清是請求掉了還是回覆掉了,只好再試一次。

🔆
生活妙喻:冪等操作 ≈ 把電燈開關設成 ON

設一次或設一百次,結果都是 ON,重複無害。

🔆
生活妙喻:非冪等操作 ≈ 餘額加 10 元

做兩次就多加了 10 元,重複會出錯。

🔆
生活妙喻:history ≈ 保留每位客人的最後一張收據

對方再問就重寄收據,不必重新出貨。

本節字彙

逾時 (timeout)
等回覆超過設定時間後採取行動(通常是重送)的機制。
🧠 time-out=時間到,該採取行動了。
冪等操作 (idempotent operation)
重複執行多次的效果,與恰好執行一次相同的操作。
🧠 做幾次都一樣,像按已亮的開關。
history
server 保存已送出回覆的紀錄,供重送而不重新執行。
🧠 歷史紀錄,存著上次回了什麼。
遺漏失敗 (omission failure)
訊息該到卻沒到(掉包)的失敗類型。
🧠 omission=省略掉了,訊息不見了。
doOperation 逾時後最常見的作法是什麼?
下列哪一個操作是冪等的?
server 維護 history 的主要目的是什麼?

交換協定樣式與 HTTP

R/RR/RRA 三種協定樣式,以及 HTTP 作為請求-回覆協定的實例。

STEP 1

深度探秘

R、RR、RRA 三種樣式

三種交換樣式

面對通訊失敗,可依需求挑選不同的訊息交換樣式(Spector, 1982):

樣式 訊息 適用情境
R(request) 只有 Request 操作沒有回傳值、client 也不需要確認
RR(request-reply) Request + Reply 大多數 client-server 互動;回覆兼當確認
RRA(request-reply-acknowledge) Request + Reply + Acknowledge 需要讓 server 安全清掉 history
  • R 協定:client 送一個請求就走人,不等回覆。
  • RR 協定:最常用。不需要特別的確認訊息,因為 server 的回覆就等於確認了 client 的請求;client 之後的下一個呼叫又等於確認了上一個回覆。
  • RRA 協定:client 再多送一個 Acknowledge,內含被確認回覆的 requestId。server 收到後就能安心從 history 刪除對應紀錄。Acknowledge 的遺失無害(後面的 ack 會涵蓋前面的),且它不必阻塞 client。
💡
關鍵

R 只送請求、RR 一來一回(回覆兼確認)、RRA 多一個確認讓 server 清 history。

STEP 2

生活妙喻

三種寄信習慣

你寄信會選哪種?

  • R:投一張明信片就走,不在乎對方有沒有回(『我搬家了,地址改成這個』,不需要回音)。
  • RR:寄信並等對方回信。對方的回信本身就證明他收到了你的信,不必再另寄一張『我收到你的信囉』。
  • RRA:除了一來一回,你還補一張回執告訴對方『你的回覆我收到了,可以把這筆紀錄歸檔了』。對方因此能清掉抽屜裡為你保留的副本(history)。

為什麼 RR 不必額外確認?因為一來一回本身就互相當確認——回覆確認了請求,下一次請求又確認了上一次回覆,省下大量無謂訊息。

💡
關鍵

R 像明信片、RR 像有來有往的信件、RRA 多一張回執讓對方歸檔。

STEP 3

實用超能力

HTTP 就是請求-回覆的實例

你天天在用的請求-回覆

HTTP 正是請求-回覆協定的經典實例,建在 TCP 之上。它和前面協定最大的不同是:所有資源共用一組固定的方法,而不是每個服務各自定義操作:

  • GET:取得資源(冪等、安全)。
  • HEAD:同 GET 但只回標頭、不回資料。
  • POST:把資料交給某資源處理,可能改變伺服器狀態(非冪等)。
  • PUT:把資料以指定 URL 存起來(冪等)。
  • DELETE:刪除資源(冪等)。

HTTP 還支援內容協商(client 表明能接受哪種語言/媒體型別,server 挑最合適的)與密碼式驗證(首次存取受保護資源時 server 回一個 challenge,client 帶上憑證再試)。

GET /index.html HTTP/1.1
Host: www.example.com
Accept: text/html

早期 HTTP 每次請求都開新連線、回完就關,成本很高。HTTP/1.1 改用持久連線(persistent connection):一條連線可重複用於多次請求-回覆,省下反覆建連線的開銷。連線中斷時,瀏覽器若遇到冪等操作(如 GET)可自動重送;非冪等操作(如 POST)則需詢問使用者。

💡
關鍵

HTTP 是請求-回覆的實例:固定方法集合、建在 TCP 上、用持久連線省成本,並區分冪等與非冪等方法。

🔆
生活妙喻:R 協定 ≈ 投一張明信片就走

通知一聲即可,不需要對方回音。

🔆
生活妙喻:RR 協定 ≈ 有來有往的書信

回信本身就證明收到了請求,不必額外寄確認。

🔆
生活妙喻:RRA 協定 ≈ 多寄一張回執讓對方歸檔

確認回覆已收到,對方就能清掉保留的副本。

🔆
生活妙喻:持久連線 ≈ 一通電話講完多件事

不必每件事都重撥一次號,省下建連線成本。

本節字彙

RR 協定
請求-回覆樣式,回覆兼作對請求的確認,最常用。
🧠 Request-Reply,一來一回。
RRA 協定
請求-回覆-確認,多一個 ack 讓 server 清除 history。
🧠 多一個 A=Acknowledge。
HTTP
建在 TCP 上的請求-回覆協定,用固定方法集合操作所有資源。
🧠 瀏覽器與網站對話的語言。
持久連線 (persistent connection)
一條連線重複用於多次請求-回覆,避免反覆建連線。
🧠 persistent=不關掉,繼續用。
為什麼 RR 協定不需要額外的確認訊息?
RRA 協定中多出來的 Acknowledge 訊息,主要讓 server 能做什麼?
某遠端操作沒有回傳值,client 也不需要任何確認。最適合用哪種樣式?
03

遠端程序呼叫 RPC

服務介面、輸入輸出參數、為何不支援 call by reference、IDL 的用途。

用介面寫程式與 IDL

服務介面、輸入輸出參數、為何不支援 call by reference、IDL 的用途。

STEP 1

深度探秘

介面:規格與實作的分界線

用介面寫程式

RPC 推動一種寫程式的風格:用介面(interface)寫程式。一個模組的介面,明定『有哪些程序可被別人呼叫、各自的參數型別是什麼』,但不揭露實作細節。只要介面不變,內部實作怎麼改都不影響使用者。

在分散式系統裡,server 提供給 client 的那組程序規格稱為服務介面(service interface)。好處包括:

  • 使用者只需面對介面提供的抽象,不必懂實作。
  • 甚至不必知道對方用什麼語言或平台寫的——這是駕馭**異質性(heterogeneity)**的關鍵一步。
  • 支援軟體演進:只要介面相容,實作可自由更新。

但分散式的本質也限制了介面的設計,這正是下一步要談的重點。

💡
關鍵

服務介面把『能呼叫什麼』與『怎麼實作』分開,讓使用者不必懂內部、甚至不必懂對方用什麼語言。

STEP 2

生活妙喻

餐廳菜單與廚房

菜單就是介面

把服務介面想成餐廳的菜單

  • 菜單列出『有哪些菜、需要什麼(辣度、份量)』,這就是介面
  • 廚房怎麼煮、用瓦斯還是電磁爐、廚師是台灣人還是法國人——你都不必知道,這是實作
  • 只要菜單不變,廚房換廚師、換設備,你照點不誤(軟體演進)。

但跨機器點餐有些事辦不到:

  • 不能直接伸手進廚房動別人的鍋子(不能直接存取另一個處理程序的變數)。
  • 你也不能把『你家冰箱的位置』寫在點餐單上要廚房去拿(位址在別的程序裡無效,不能傳位址)。
  • 因此參數只能講清楚『送進去的(in)』與『要帶回來的(out)』——也就是輸入/輸出參數,而不支援 call by reference
💡
關鍵

菜單像介面;跨機器時不能伸手動別人鍋子、不能傳位址,所以只能用輸入/輸出參數。

STEP 3

實用超能力

IDL 讓不同語言互通

介面定義語言 IDL

若 client 與 server 都用同一語言(例如全用 Java),介面可直接用該語言定義。但真實世界常有用 C++、Python 等不同語言寫的服務需要互相呼叫。這時就需要介面定義語言(IDL, Interface Definition Language):一套語言中立的記號,用來描述介面,並標明每個參數是 in(輸入)還是 out(輸出)。

以 CORBA IDL 為例:

struct Person {
  string name;
  string place;
  long year;
};

interface PersonList {
  readonly attribute string listname;
  void addPerson(in Person p);
  void getPerson(in string name, out Person p);
  long number();
};

這裡 addPerson 的參數是 in(送進去),getPerson 的第二個參數是 out(帶回來)。IDL 概念最早為 RPC 而生,但同樣適用於 RMI 與 web services——例如 Sun XDR(RPC)、CORBA IDL(RMI)、WSDL(web services)、Google 的 protocol buffers。

💡
關鍵

IDL 是語言中立的介面記號,標明參數 in/out,讓不同語言寫的程式能互相遠端呼叫。

🔆
生活妙喻:服務介面 ≈ 餐廳菜單

列出能點什麼、要附帶什麼,但不揭露廚房怎麼煮。

🔆
生活妙喻:不支援 call by reference ≈ 不能把自家冰箱位置寫在點餐單上

位址在別的程序裡無效,只能把值送進去、把值帶回來。

🔆
生活妙喻:IDL ≈ 多國語言對照菜單

讓說不同語言(程式語言)的人都能照同一份規格點餐。

本節字彙

服務介面 (service interface)
server 提供給 client 的程序規格,含參數型別。
🧠 service=服務的『菜單』。
輸入/輸出參數 (in/out)
in 是送往 server 的值,out 是回傳給呼叫端的值。
🧠 in 進去、out 出來。
IDL (interface definition language)
語言中立的介面定義記號,讓不同語言互相呼叫。
🧠 Interface Definition Language=介面的共通語。
異質性 (heterogeneity)
系統由不同語言、平台、硬體組成的特性。
🧠 hetero=不一樣的,混搭。
用介面寫程式的核心好處是什麼?
為什麼分散式的服務介面不支援 call by reference?
在 IDL 中把參數標為 out 代表什麼?

呼叫語意:maybe/at-least-once/at-most-once

三種容錯措施組合出的三種呼叫語意。

STEP 1

深度探秘

三種容錯措施組出三種語意

呼叫語意是什麼

本機程序呼叫的語意是 exactly once(恰好一次):每個程序剛好執行一次(除非程序當機)。但遠端呼叫會掉包、會當機,無法輕易保證恰好一次。於是要用三種容錯措施去『組合』出不同的可靠度保證:

  1. 重送請求訊息:要不要重送,直到收到回覆或判定 server 失敗。
  2. 過濾重複:是否在 server 端濾掉重複請求。
  3. 重送結果(history):是否保留結果以便重送,避免重新執行。

不同組合得到不同的呼叫語意

重送請求 過濾重複 重新執行/重送回覆 語意
不適用 不適用 Maybe
重新執行程序 At-least-once
重送回覆 At-most-once
💡
關鍵

呼叫語意由『重送、去重、是否保存結果』三種措施組合而成,對應 maybe、at-least-once、at-most-once。

STEP 2

生活妙喻

三種寄包裹的保障等級

寄包裹的保障

把遠端呼叫想成寄包裹請對方做一件事:

  • Maybe(也許):寄了就不管。包裹可能掉、對方可能當機,你完全不確定事情有沒有被做。最省事,但只適合『偶爾失敗也沒差』的場合。
  • At-least-once(至少一次):沒回音就一直重寄。只要你收到結果,就知道事情至少被做了一次——但可能不只一次!若操作不是冪等(例如『餘額加 10 元』),重複執行會出錯。
  • At-most-once(至多一次):用上全部措施(重送+去重+保存結果)。你收到結果時,就知道恰好被做了一次;若沒收到結果,則是『做了一次或完全沒做』,但絕不會超過一次
💡
關鍵

Maybe 不保證、at-least-once 可能重複執行、at-most-once 用完整措施確保不超過一次。

STEP 3

實用超能力

怎麼選語意

實務上怎麼權衡

  • Maybe:只在『偶爾失敗可接受』時用,例如定期回報的感測器讀數,掉一筆無妨。
  • At-least-once:搭配冪等操作就很好用——既然重做無害,就不必付出 at-most-once 的額外開銷。Sun RPC 就提供 at-least-once 語意。
  • At-most-once:當操作非冪等(轉帳、扣庫存)時幾乎必選,代價是要維護去重與 history 的開銷。
flowchart TD
  A[操作是冪等嗎] -- 是 --> B[at-least-once 即可 省開銷]
  A -- 否 --> C[需要 at-most-once 避免重複執行]
  D[偶爾失敗可接受嗎] -- 是 --> E[maybe 最省]

一句話心法:先問操作是否冪等。冪等 → at-least-once 夠用;非冪等 → 上 at-most-once。

💡
關鍵

選語意先看操作是否冪等:冪等用 at-least-once 省成本,非冪等用 at-most-once 防重複。

🔆
生活妙喻:Maybe 語意 ≈ 寄出就不管的明信片

可能到、可能掉,你無從確認事情有沒有被做。

🔆
生活妙喻:At-least-once ≈ 沒回音就一直重寄

至少做了一次,但可能重複,非冪等操作會出錯。

🔆
生活妙喻:At-most-once ≈ 掛號加去重的寄件

用上全部措施,保證絕不超過一次。

本節字彙

exactly once
本機呼叫的語意:每個程序恰好執行一次。
🧠 exactly=剛剛好一次。
maybe 語意
無任何容錯措施,操作可能執行一次或完全沒執行。
🧠 maybe=也許有做、也許沒做。
at-least-once
靠重送遮蔽掉包,至少執行一次,但可能重複。
🧠 至少一次,可能更多。
at-most-once
用完整容錯措施,保證至多執行一次。
🧠 至多一次,絕不超過。
本機程序呼叫(無當機時)的語意是什麼?
at-least-once 語意對哪一類操作可能造成錯誤?
要達到 at-most-once 語意,需要哪些容錯措施?

RPC 的實作與 stub

client stub、server stub、dispatcher 如何把呼叫變成訊息。

STEP 1

深度探秘

stub 與 dispatcher 的分工

一次 RPC 內部發生了什麼

RPC 想讓你『像呼叫本機程序一樣』呼叫遠端程序,魔法藏在幾個自動產生的元件裡:

  • client stub(客戶端存根):client 端為服務介面的每個程序各配一個 stub。它表面上像本機程序,但其實不執行運算,而是把『程序識別碼+參數』**打包(marshal)成請求訊息,透過通訊模組送到 server;收到回覆後再解包(unmarshal)**結果回傳。
  • dispatcher(分派器):server 端依請求中的程序識別碼,挑出對應的 server stub。
  • server stub(伺服端存根):解開請求中的參數,呼叫真正的service procedure(服務程序),再把回傳值打包成回覆訊息。
  • service procedure:真正實作介面中那些程序的程式碼。

這些 stub 與 dispatcher 都可由介面編譯器從介面定義自動產生,程式設計師不必手寫。

flowchart LR
  A[client 程式] --> B[client stub 打包]
  B --> C[通訊模組]
  C --> D[通訊模組]
  D --> E[dispatcher 選 stub]
  E --> F[server stub 解包]
  F --> G[service procedure 真正執行]
💡
關鍵

client stub 打包並送出、dispatcher 依識別碼選 stub、server stub 解包並呼叫真正的服務程序。

STEP 2

生活妙喻

跨國代購的層層轉手

代購流程

想像你在台灣請代購買日本商品:

  • =client 程式:只說「我要買這個」。
  • client stub=台灣這邊的代購:把你的需求填成標準表單、裝箱(marshal),交給物流。
  • 通訊模組=國際物流:負責把包裹送到日本、把回程包裹帶回。
  • dispatcher=日本倉庫的分揀員:看標籤把包裹分到正確的窗口
  • server stub=日本窗口人員:拆箱、看清楚需求,去找真正的店家。
  • service procedure=日本店家:真正把商品準備好。

你全程只跟『台灣代購』講話,感覺就像在巷口買東西——這正是 RPC 想營造的透明感

💡
關鍵

stub 像兩端的代購與窗口,dispatcher 像分揀員,讓你感覺像在本地買東西。

STEP 3

實用超能力

看懂 Sun RPC 與 stub 自動產生

自動產生省下苦工

RPC 一般建在請求-回覆協定之上,請求/回覆訊息內容就如前面所述。實作時可選 at-least-once 或 at-most-once 語意,由通訊模組去落實重送、去重與重送結果。

Sun RPC(又稱 ONC RPC,為 NFS 而設計)為例:

  • 它用 XDR 作為 IDL,搭配介面編譯器 rpcgen,主要給 C 語言用。
  • rpcgen 能從介面定義自動產生:client stub、server 主程式與 dispatcher、server stub,以及打包/解包用的程序。
  • Sun RPC 不用介面名稱,而是用程式編號+版本編號識別服務;版本編號在簽章改變時更新,讓 client/server 能確認彼此版本一致。
  • 它用 at-least-once 語意,並透過本機的 port mapper(連接埠對應服務) 做繫結:server 啟動時向 port mapper 登記自己的程式編號、版本與連接埠;client 則向目標主機的 port mapper 查詢以找到正確連接埠。
program FILEREADWRITE {
  version VERSION {
    void WRITE(writeargs) = 1;
    Data READ(readargs) = 2;
  } = 2;
} = 9999;

重點:你只要寫好介面定義,繁瑣的打包、分派、通訊程式都能自動生成

💡
關鍵

RPC 建在請求-回覆上;像 Sun RPC 用 rpcgen 從介面定義自動產生 stub 與 dispatcher,並用 port mapper 做繫結。

🔆
生活妙喻:client stub ≈ 台灣這端的代購

把你的需求裝箱(打包)交給物流,你只跟他講話。

🔆
生活妙喻:dispatcher ≈ 倉庫分揀員

依標籤把包裹分到正確的處理窗口。

🔆
生活妙喻:server stub ≈ 對方窗口人員

拆箱解讀需求,再去找真正的店家(服務程序)。

🔆
生活妙喻:port mapper ≈ 公司總機分機表

你報出部門(程式編號),總機告訴你該打哪個分機(連接埠)。

本節字彙

stub(存根)
代理用的程序:client stub 打包送出,server stub 解包並呼叫服務程序。
🧠 stub=替身,幫真正的程序跑腿。
marshalling(打包)
把參數/結果轉成可在網路上傳輸的位元組序列。
🧠 marshal=整隊裝箱,準備上路。
dispatcher(分派器)
server 端依程序識別碼選出對應 server stub 的元件。
🧠 dispatch=分派到正確窗口。
port mapper
Sun RPC 的本機繫結服務,記錄各服務的程式編號、版本與連接埠。
🧠 像總機的分機對照表。
client stub 在一次 RPC 中扮演什麼角色?
dispatcher 的職責是什麼?
為什麼 RPC 的 stub、dispatcher 通常由介面編譯器自動產生?
04

遠端方法呼叫 RMI 與分散式物件

遠端物件、遠端物件參考、遠端介面三大概念。

分散式物件模型

遠端物件、遠端物件參考、遠端介面三大概念。

STEP 1

深度探秘

從物件到分散式物件

把物件搬到不同程序

物件導向程式由一群互相呼叫方法的物件組成,每個物件有自己的資料與方法,並透過物件參考(object reference)互相存取。把這些物件實體分散到不同程序或電腦,就成了分散式物件

當方法呼叫跨越程序或電腦的邊界時,就是遠端方法呼叫(RMI);同一程序內的呼叫則是本機方法呼叫。

分散式物件模型有兩個核心概念:

  • 遠端物件(remote object):能接收遠端呼叫的物件(一般物件只能接收本機呼叫)。
  • 遠端物件參考(remote object reference):一個全分散式系統通用、唯一的識別子,用來指向某個特定遠端物件。
  • 遠端介面(remote interface):每個遠端物件都有,明定它哪些方法可被遠端呼叫
flowchart LR
  A[物件 A 在程序一] -- 遠端呼叫 --> B[遠端物件 B 在程序二]
  B -- 本機呼叫 --> E[物件 E]
💡
關鍵

分散式物件模型的三大概念:遠端物件、全系統唯一的遠端物件參考、明定可遠端呼叫方法的遠端介面。

STEP 2

生活妙喻

公司分機與對外服務窗口

內線與對外窗口

把一群分散式物件想成跨城市的多家分公司:

  • 本機方法呼叫=同一辦公室內走過去找同事(同程序內)。
  • 遠端方法呼叫=打去外地分公司找人(跨程序/電腦)。
  • 遠端物件參考=對方那個人的全公司通用工號——不管他在哪個分公司,這個工號都能唯一指到他。
  • 遠端介面=該員工的對外服務窗口清單:外人只能請他做『窗口上列出的事』,至於他內部還會做哪些雜事,外人碰不到。

注意:一般本機物件(沒有遠端介面的)就像不對外的內部員工,只有同辦公室的人能直接找他。

💡
關鍵

遠端物件參考像全公司通用工號,遠端介面像員工的對外服務窗口清單。

STEP 3

實用超能力

遠端介面的限制與好處

設計遠端物件時要記得

幾個關鍵實務點:

  • 只有遠端介面裡的方法能被遠端呼叫;本機物件則可呼叫遠端物件的『所有』方法(含非遠端介面的)。
  • 遠端介面沒有建構子(constructor)——所以不能用遠端呼叫去 new 一個遠端物件;要建立遠端物件,得透過工廠方法(factory method):在遠端介面裡提供專門用來建立物件的一般方法。
  • 遠端物件參考可當參數與結果傳遞:物件 A 可以從物件 B 的回傳值取得對物件 F 的遠端參考,再去呼叫 F。
  • 例外(exception)更重要:遠端呼叫除了方法本身可能丟的例外,還可能因『程序當機、太忙、訊息遺失』而失敗,所以必須能丟出如逾時這類因分散式而生的例外,呼叫端要準備好處理。

好處:跨程序自然強化了封裝——物件狀態只能透過自己的方法存取,避免未授權的存取與某些並行衝突。

💡
關鍵

遠端介面無建構子需靠工廠方法建物件;遠端參考可當參數傳遞;遠端呼叫必須能處理因分散式而生的例外。

🔆
生活妙喻:遠端物件參考 ≈ 全公司通用工號

不管員工在哪個分公司,工號都能唯一指到他。

🔆
生活妙喻:遠端介面 ≈ 員工的對外服務窗口清單

外人只能請他做清單上的事,內部雜事碰不到。

🔆
生活妙喻:工廠方法 ≈ 請現有窗口幫你『開一個新窗口』

因為不能直接 new 遠端物件,只好請既有遠端物件代為建立。

本節字彙

遠端物件 (remote object)
能接收遠端方法呼叫的物件。
🧠 remote=可被遠方呼叫。
遠端物件參考 (remote object reference)
全分散式系統通用且唯一、指向某遠端物件的識別子。
🧠 全系統唯一的工號。
遠端介面 (remote interface)
明定遠端物件哪些方法可被遠端呼叫的介面。
🧠 對外服務窗口清單。
工廠方法 (factory method)
因遠端介面無建構子,用來建立遠端物件(servant)的一般方法。
🧠 factory=代工建立新物件。
什麼時候一次方法呼叫會被視為『遠端方法呼叫』?
遠端物件參考最重要的性質是什麼?
為什麼要用工廠方法來建立遠端物件,而不是直接遠端呼叫建構子?

RMI 的內部構造:proxy 與 skeleton

proxy、dispatcher、skeleton、servant 與兩個底層模組的分工。

STEP 1

深度探秘

RMI 軟體的五個角色

一次 RMI 內部的五個角色

當物件 A 呼叫遠端物件 B 的方法時,幾個元件協力完成:

  • proxy(代理):在 client 端,為每個持有遠端參考的遠端物件各設一個。它假裝成本機物件:實作了 B 遠端介面的所有方法,但每個方法其實是把『目標參考、operationId、參數』打包成請求訊息送出,再等回覆、解包、回傳。
  • dispatcher(分派器):在 server 端,每個遠端物件類別一個。它用 operationId 選出 skeleton 裡正確的方法。
  • skeleton(骨架):在 server 端,解開請求中的參數,呼叫真正的 servant 的對應方法,等執行完再把結果(含例外)打包成回覆送回給 proxy。
  • servant(伺服體):真正提供遠端物件本體的實例,住在 server 程序裡,實際處理請求。

底下還有兩個共用模組:通訊模組(執行請求-回覆協定、負責提供如 at-most-once 的呼叫語意)與遠端參考模組(在本機參考與遠端參考之間轉換)。

💡
關鍵

proxy 在 client 假裝本機物件並打包送出;dispatcher 選方法、skeleton 解包並呼叫真正的 servant。

STEP 2

生活妙喻

影分身與傳真機

proxy 是遠端物件的『影分身』

想像 B 是住在遠方的專家,你(A)手邊有一個 B 的影分身(proxy)

  • 影分身長得跟 B 一模一樣(實作同樣的遠端介面),你對它喊『幫我算這個』。
  • 但影分身不會真的算,它把你的要求寫成傳真(打包成請求訊息)發到遠方。
  • 遠方的收發室(dispatcher)看傳真上的編號,把它交給對的助理(skeleton)
  • 助理拆開傳真,請真正的**專家 B(servant)**動手,算完把答案傳真回來。
  • 影分身收到回傳,假裝是自己算出來的,把結果交給你。

對你而言,全程就像身邊真的有個 B——這就是 proxy 帶來的透明性

💡
關鍵

proxy 像遠端物件的影分身,把你的呼叫轉成傳真送到遠方,再把答案假裝成自己算的交回。

STEP 3

實用超能力

靜態 vs 動態、繫結與啟動

進階機制

proxy/skeleton 怎麼來:由介面編譯器自動產生。Java RMI 的 RMI 編譯器會從遠端物件的類別產生 proxy、dispatcher、skeleton。

動態呼叫(dynamic invocation):若 client 在編譯期拿不到某遠端介面(例如共享白板要顯示『編譯時還不存在的新圖形』),就無法用靜態 proxy。此時可用通用的 doOperation 形式,client 直接提供遠端參考、方法名稱與參數來呼叫。沒 proxy 方便,但能應付『設計時無法預期的介面』。

binder(繫結器):client 要先拿到至少一個遠端物件的參考。binder 是一個獨立服務,維護『文字名稱 → 遠端物件參考』的對應表;server 用它註冊、client 用它查詢。Java 的 binder 是 RMIregistry。

server 執行緒:為避免一個遠端呼叫卡住另一個,server 通常為每個遠端呼叫配一條執行緒,因此遠端物件的實作者必須考慮並行存取下的狀態安全。

啟動(activation):長壽的物件不必一直佔著執行中的程序。被動(passive)物件=方法實作+封裝過的狀態;需要時由 activator 把它啟動(activation)成可被呼叫的主動(active)物件。Java RMI 支援 activatable 物件。

💡
關鍵

proxy/skeleton 由編譯器產生;動態呼叫應付未知介面;binder 把名稱對應到參考;activator 按需把被動物件啟動成主動物件。

🔆
生活妙喻:proxy ≈ 遠端物件的影分身

長得一樣但不真算,把呼叫轉成傳真送往遠方。

🔆
生活妙喻:dispatcher 與 skeleton ≈ 收發室與助理

收發室依編號分派,助理拆信並請真正的專家動手。

🔆
生活妙喻:servant ≈ 真正的專家本人

實際執行方法、提供遠端物件本體。

🔆
生活妙喻:activation ≈ 需要時才把休眠的店面開張

平時休眠省資源,有客人上門才喚醒成可服務狀態。

本節字彙

proxy(代理)
client 端假裝成遠端物件的本機替身,負責打包送出與解包回傳。
🧠 proxy=影分身。
skeleton(骨架)
server 端解包請求並呼叫真正 servant 的元件。
🧠 拆信交給專家的助理。
servant(伺服體)
提供遠端物件本體、實際執行方法的實例。
🧠 真正動手的專家本人。
activation(啟動)
由 activator 把被動物件喚醒成可被呼叫的主動物件。
🧠 把休眠的店面開張迎客。
proxy 在 RMI 中的核心職責是什麼?
skeleton 與 servant 的關係是什麼?
共享白板要能顯示『編譯時還不存在的新圖形』,最適合用哪種機制?

分散式垃圾回收與租約

以參考計數為基礎的分散式 GC,以及 lease 如何容忍當機。

STEP 1

深度探秘

跨機器的參考計數

沒人用了才回收

**分散式垃圾回收(distributed garbage collection)**的目標:只要還有任何本機或遠端參考指向某物件,它就繼續存在;一旦沒有任何物件持有對它的參考,就回收它與其佔用的記憶體。

Java 的作法以**參考計數(reference counting)**為基礎,與本機垃圾回收器協同運作:

  • 每個 server 為自己的每個遠端物件維護一個 holders 集合,記錄『哪些 client 程序持有指向它的遠端參考』。例如 B.holders 就是持有 B 之 proxy 的 client 集合。
  • 當 client C 第一次取得遠端物件 B 的參考時,先對 B 的 server 發出 addRef(B),server 把 C 加入 B.holders,C 才建立 proxy。
  • 當 C 的本機垃圾回收器發現 B 的 proxy 不再可達時,發出 removeRef(B),server 把 C 從 B.holders 移除,C 再刪掉 proxy。
  • B.holders 變空(且沒有本機持有者)時,server 的本機垃圾回收器就回收 B。

這套機制用程序間成對的請求-回覆搭配 at-most-once 語意完成,不需要全域同步,而且 addRef/removeRef 只在 proxy 建立與刪除時才發生,不影響每次正常的 RMI。

💡
關鍵

分散式 GC 以參考計數為基礎:addRef/removeRef 維護 holders 集合,集合空了才回收物件,且不需全域同步。

STEP 2

生活妙喻

圖書館的借閱名冊

借閱名冊與到期歸還

把遠端物件想成圖書館裡一本珍貴的書:

  • 每本書有一張借閱名冊(holders),記錄誰正借著它。
  • 有人來借(addRef)就登記上名冊;還書(removeRef)就劃掉。
  • 只要名冊上還有人,書就不能銷毀;名冊清空了,館員才能把書下架回收。

但問題來了:萬一某位讀者搬家失聯(client 程序當機),永遠不還書怎麼辦?名冊上的名字會永遠掛著,書永遠下不了架。

解法是租約(lease):借書不是『無限期』,而是『借你到某月某日』。到期不續借,館員就視同歸還,把名字劃掉。讀者若還想繼續借,必須在到期前主動續約

💡
關鍵

holders 像借閱名冊,addRef/removeRef 像借還;lease 用到期機制處理失聯讀者,避免書永遠下不了架。

STEP 3

實用超能力

容忍失敗:冪等與租約

讓 GC 在不可靠網路下也安全

容忍通訊失敗:addRef 與 removeRef 都設計成冪等。若 addRef(B) 回傳例外(代表它『執行了一次或完全沒執行』),client 不建立 proxy,並補發一個 removeRef(B)——無論 addRef 是否真的成功,removeRef 的效果都正確。

避免競態:若某 client 的 removeRef 與另一 client 的 addRef 幾乎同時到達,可能在 addRef 前就把 holders 清空而誤刪 B。解法:當遠端參考被傳出、而當下 holders 為空時,先加一個暫時項目撐到 addRef 到達。

容忍 client 當機:靠租約(lease)。server 把物件『租』給 client 一段時間,租期從 addRef 開始,到時間到或 client 發出 removeRef 為止;client 必須在到期前主動續租。失聯的 client 不續租,租約自然過期,資源得以釋放。

Jini 的租約:Jini 把租約一般化成一種通用機制——任何物件把資源提供給別人時都可附帶租約,避免使用者失去興趣或程式結束後資源仍被永久佔用。Jini 的租期可協商,這點與 Java RMI 不同。

flowchart TD
  A[client 取得 B 的遠端參考] --> B[addRef B 加入 holders 並開始租約]
  B --> C{client 仍需要 B}
  C -- 是 --> D[到期前續租]
  C -- 否或當機 --> E[removeRef 或租約過期]
  E --> F[holders 變空 回收 B]
💡
關鍵

addRef/removeRef 冪等以容忍通訊失敗;租約用到期與續租機制容忍 client 當機,避免資源永遠被佔。

🔆
生活妙喻:holders 集合 ≈ 圖書的借閱名冊

名冊上有人書就不能銷毀,清空才能下架回收。

🔆
生活妙喻:addRef / removeRef ≈ 借書登記與還書劃掉

借出加名、歸還除名,維護誰還在用。

🔆
生活妙喻:租約 lease ≈ 限期借閱與到期續借

失聯讀者不續借,到期視同歸還,避免書永遠下不了架。

本節字彙

分散式垃圾回收 (distributed GC)
跨機器判斷物件是否仍被參考,無人參考即回收。
🧠 沒人用了就清掉。
參考計數 (reference counting)
靠記錄誰持有參考來判斷物件能否回收的方法。
🧠 數一數還有幾個人指著它。
holders
持有某遠端物件參考的 client 程序集合。
🧠 持有者名冊。
租約 (lease)
在限定時間內授予資源使用權,到期需續租,否則收回。
🧠 lease=限期租借,到期要續。
Java 分散式垃圾回收以什麼為基礎?
server 何時可以回收遠端物件 B?
為什麼把 addRef 與 removeRef 設計成冪等很重要?
05

案例研究:Java RMI

extends Remote、RemoteException,以及遠端物件傳參考、一般物件傳值。

Java 遠端介面與參數傳遞

extends Remote、RemoteException,以及遠端物件傳參考、一般物件傳值。

STEP 1

深度探秘

extends Remote 與 RemoteException

Java RMI 怎麼宣告遠端

Java RMI 把 Java 物件模型延伸到分散式世界,讓你用和本機呼叫一樣的語法呼叫遠端物件的方法,連型別檢查也一樣適用。

不過它刻意在介面上暴露分散式的本質

  • 遠端介面要繼承 Remote 介面(在 java.rmi 套件)。
  • 介面裡的方法必須宣告 throws RemoteException——這逼呼叫者正視『遠端呼叫可能因網路而失敗』。
import java.rmi.*;
import java.util.Vector;

public interface Shape extends Remote {
    int getVersion() throws RemoteException;
    GraphicalObject getAllState() throws RemoteException;
}

public interface ShapeList extends Remote {
    Shape newShape(GraphicalObject g) throws RemoteException;
    Vector allShapes() throws RemoteException;
    int getVersion() throws RemoteException;
}

注意:遠端介面裡,一般物件和遠端物件都可以當參數或結果。遠端物件總是用『它的遠端介面名稱』來表示——例如 newShape 回傳的是 Shape(一個遠端介面),代表回傳的是一個遠端物件。

💡
關鍵

Java RMI 用和本機一樣的語法,但遠端介面要 extends Remote、方法要 throws RemoteException,刻意凸顯分散式本質。

STEP 2

生活妙喻

影本 vs 鑰匙

傳一份影本,還是傳一把鑰匙?

Java RMI 傳參數時,分兩種截然不同的方式:

  • 傳一般(非遠端)物件 → 傳值(複製):就像把一份文件影印一份寄過去。對方拿到的是全新的副本,他改他的、你改你的,互不影響。前提是該物件必須可序列化(implements Serializable)
  • 傳遠端物件 → 傳遠端物件參考:就像把一把遠端保險箱的鑰匙交給對方。對方拿到的不是箱子本體,而是能遠端打開那個箱子的鑰匙;他透過這把鑰匙去操作的,仍是『原本那個』遠端物件。

所以判斷依據很簡單:參數型別是遠端介面 → 傳參考(鑰匙);是一般可序列化物件 → 傳值(影本)

💡
關鍵

一般可序列化物件以值複製傳遞(像影本),遠端物件以遠端參考傳遞(像遠端保險箱的鑰匙)。

STEP 3

實用超能力

用白板範例看清差異

共享白板的參數傳遞

以共享白板為例(GraphicalObject 是可序列化、存放圖形狀態的一般物件):

// client 把一個 GraphicalObject 傳給 server
Shape s = shapeList.newShape(myGraphicalObject);

發生了什麼:

  1. myGraphicalObject一般可序列化物件,所以以值複製送到 server——server 端得到一份新的副本。
  2. server 用這份狀態建立一個型別為 Shape遠端物件,並回傳它的遠端物件參考給 client。
  3. client 拿到的 s 是個遠端參考,之後對 s 呼叫方法(如 s.getVersion())都是遠端呼叫到 server 上那個物件。

要點整理:

情境 傳遞方式 結果
參數 GraphicalObject g 傳值(複製) server 得到獨立副本
回傳值 Shape(遠端介面) 傳遠端參考 client 拿到能遠端操作的鑰匙

所有基本型別與遠端物件都是可序列化的;參數與結果的類別在需要時會被 RMI 系統下載到接收端(下一節詳談)。

💡
關鍵

白板範例:GraphicalObject 以值複製送出,server 回傳的 Shape 是遠端參考——『型別是不是遠端介面』決定傳值或傳參考。

🔆
生活妙喻:傳一般物件(傳值) ≈ 把文件影印一份寄過去

對方拿到獨立副本,雙方各改各的互不影響。

🔆
生活妙喻:傳遠端物件(傳參考) ≈ 交出遠端保險箱的鑰匙

對方拿到的是能遠端操作原物件的鑰匙,而非物件本體。

🔆
生活妙喻:throws RemoteException ≈ 合約上白紙黑字寫明『可能因故無法送達』

逼呼叫者事先正視並處理遠端失敗。

本節字彙

Remote 介面
Java RMI 中遠端介面必須繼承的標記介面。
🧠 extends Remote=我可以被遠端呼叫。
RemoteException
遠端方法必須宣告會丟出的例外,代表可能的遠端失敗。
🧠 遠端就可能出包,先寫進簽章。
Serializable
可被序列化(轉成位元組)的物件才能以值傳遞。
🧠 能裝箱寄出的物件。
傳值 vs 傳參考
一般可序列化物件傳值複製;遠端物件傳遠端參考。
🧠 影本給副本,鑰匙給遠端本體。
在 Java RMI 中,一個遠端介面必須怎麼宣告才算是遠端的?
一個可序列化的『一般物件』作為參數傳給遠端方法時,會如何傳遞?
在白板範例 `Shape s = shapeList.newShape(g);` 中,回傳的 s 是什麼?

類別下載與 RMIregistry

動態下載類別的好處,以及 RMIregistry 作為 binder 的角色。

STEP 1

深度探秘

缺類別?自動下載

Java 的招牌:動態下載類別

Java 設計上允許類別(class)從一個虛擬機下載到另一個虛擬機,這對遠端呼叫特別有用。

回憶上一節:非遠端物件以值傳遞、遠端物件以參考傳遞。但接收端若沒有那個物件的類別怎麼辦?

  • 若接收端沒有某個『以值傳遞之物件』的類別 → 其程式碼會被自動下載
  • 若接收端拿到一個遠端參考卻沒有對應 proxy 的類別 → proxy 程式碼也會被自動下載

這帶來兩大好處:

  1. 不必要求每位使用者的環境都預先放齊所有類別。
  2. client 與 server 都能透明地使用新增類別的實例

例如白板一開始的 GraphicalObject 不支援文字;某 client 自己寫了一個處理文字的子類別,把實例當參數丟給 server。這個新類別的程式碼會自動從該 client 下載到 server,再下載到其他需要的 client——大家不必事先約好都裝這個類別。

💡
關鍵

Java RMI 能在缺少類別時自動下載類別程式碼,讓 client/server 透明地使用新類別,不必事先全部裝齊。

STEP 2

生活妙喻

附上組裝說明書的家具

連說明書一起寄

想像你網購一款全新型號的家具寄給朋友,但朋友從沒見過這款,不知道怎麼組。

  • 沒有類別下載:朋友收到零件卻沒有說明書,組不起來(接收端不認得這個類別)。
  • 有類別下載:包裹裡附上組裝說明書(類別程式碼)。朋友照著說明書就能把家具組好、正常使用——即使他以前從沒見過這個型號。

白板的『文字圖形』新子類別就是這樣:你把『新型家具』(物件)寄出時,Java RMI 順便附上說明書(類別)。對方(server 或其他 client)即使沒見過,也能下載說明書後正常處理。

這也是為什麼序列化時,物件的類別資訊會被標註它的位置(URL),好讓接收端知道去哪裡下載。

💡
關鍵

類別下載像『寄家具時附上組裝說明書』,讓從沒見過該類別的接收端也能下載後正常使用。

STEP 3

實用超能力

RMIregistry:找到第一個遠端物件

RMIregistry 是 Java 的 binder

client 要呼叫遠端物件,得先拿到至少一個遠端物件參考。Java RMI 用 RMIregistry 當 binder:每台主機遠端物件的伺服器上通常各跑一個,維護『URL 風格名稱 → 遠端物件參考』的對應表。

透過 Naming 類別操作,名稱格式為 //computerName:port/objectName

方法 用途
rebind(name, obj) server 用名稱註冊遠端物件(已存在就覆蓋)
bind(name, obj) 註冊,但名稱已被綁定就丟例外
unbind(name, obj) 移除繫結
lookup(name) client 用名稱查出遠端物件參考
list() 列出 registry 中已綁定的名稱

典型 server 啟動流程:

public static void main(String[] args) {
    try {
        ShapeList aShapeList = new ShapeListServant();
        ShapeList stub = (ShapeList)
            UnicastRemoteObject.exportObject(aShapeList, 0);
        Naming.rebind("//bruno/ShapeList", stub);
        System.out.println("ShapeList server ready");
    } catch (Exception e) {
        System.out.println("server main " + e.getMessage());
    }
}

步驟拆解:建立 servant → 用 exportObject 讓它能接收遠端呼叫(第二參數 0 表示用匿名連接埠)→ 用 rebind 把『遠端物件參考』綁到 registry 的一個名稱上。client 之後用 lookup("//bruno/ShapeList") 就能拿到參考開始遠端呼叫。

💡
關鍵

RMIregistry 是 Java 的 binder,用 rebind 註冊、lookup 查詢,把 URL 風格名稱對應到遠端物件參考。

🔆
生活妙喻:動態下載類別 ≈ 寄家具時附上組裝說明書

讓沒見過該型號的接收端下載說明書後也能正常使用。

🔆
生活妙喻:RMIregistry ≈ 公司的總機名錄

用名字查到對方的聯絡方式(遠端物件參考)。

🔆
生活妙喻:rebind 與 lookup ≈ 登記到名錄 vs 翻名錄找人

server 登記名字,client 照名字查出參考。

本節字彙

類別下載 (class downloading)
接收端缺少某類別時,自動從來源下載其程式碼。
🧠 缺說明書就自動補寄。
RMIregistry
Java RMI 的 binder,維護名稱到遠端物件參考的對應表。
🧠 RMI 的電話名錄。
rebind
server 用名稱註冊遠端物件(覆蓋既有繫結)。
🧠 把名字(重新)綁到物件上。
lookup
client 用名稱查出遠端物件參考。
🧠 照名字查號。
當接收端拿到一個以值傳遞的物件,卻沒有它的類別時,Java RMI 會怎麼做?
白板中某 client 自訂了支援文字的 GraphicalObject 子類別並傳給 server。類別下載帶來的好處是什麼?
RMIregistry 在 Java RMI 中扮演什麼角色?