遠端呼叫總覽:三種溝通典範
處理程序如何跨機器溝通,以及中介軟體層的定位。
先讀原文開場,旁邊就是白話
這是一本英文書。左邊放原文、右邊放白話導讀——你既讀得懂,也順手碰了原文。
處理程序如何跨機器溝通,以及中介軟體層的定位。
為什麼需要遠端呼叫
處理程序如何跨機器溝通,以及中介軟體層的定位。
深度探秘
處理程序要跨機器說話
問題的起點
在一台電腦裡,你的程式呼叫一個函式(function)再自然不過:寫下 f(x) 就好。但在分散式系統裡,程式碼被拆到不同電腦、不同處理程序(process)上跑,它們之間沒有共用的記憶體,只能靠網路互傳訊息。
問題是:直接操作網路訊息(打包、送出、等回覆、解包)非常瑣碎又容易出錯。於是工程師發明了一層中介軟體(middleware),蓋在底層的網路溝通(UDP、TCP、sockets)之上,提供更友善的「遠端呼叫」抽象。
本章關注的就是這一層:如何讓「呼叫遠方機器上的程式」變得像呼叫本機函式一樣簡單。
中介軟體的層次大致如下:
flowchart TD A[應用程式與服務] --> B[中介軟體 遠端呼叫與間接通訊] B --> C[底層通訊 sockets 訊息傳遞 多播] C --> D[UDP 與 TCP]
遠端呼叫是蓋在網路通訊之上的一層抽象,目的是讓跨機器溝通變簡單。
生活妙喻
請祕書幫你打電話訂貨
一通電話的抽象
想像你想跟外地一家工廠訂貨。
- 沒有中介軟體:你得自己查電話、撥號、報出每個欄位、等對方覆述、記下回覆——每個細節都要你親手處理(這就像直接寫 socket 程式)。
- 有中介軟體:你只要對祕書說「幫我訂 100 個 A 零件」,剩下撥號、講話、記回覆全交給祕書。對你而言,就像在公司內部交辦一樣輕鬆。
遠端呼叫的中介軟體,就是那位幫你處理所有網路細節的祕書。你只管「呼叫」,它幫你把呼叫變成訊息送到遠方、再把結果帶回來。
中介軟體像幫你打電話的祕書,把繁瑣的網路細節藏起來。
實用超能力
看懂系統分層
在真實系統裡定位它
當你之後看到任何分散式技術(gRPC、Java RMI、CORBA、REST),都可以問自己三個問題:
- 它跑在哪一層?幾乎都在中介軟體層。
- 它底下用什麼?通常是 TCP 或 UDP。
- 它幫我藏掉了什麼?多半是打包參數、傳訊息、配對回覆、處理逾時。
例如你用 gRPC 呼叫遠端服務,寫起來像呼叫本機方法,但底層其實是:打包成訊息 → 走 HTTP/2(TCP)→ 對方解包執行 → 把結果送回。理解這個分層,遇到效能或失敗問題時就知道該往哪一層找原因。
認得分層後,任何遠端呼叫技術都能快速定位它在做什麼、藏了什麼。
你交辦需求,祕書處理撥號、溝通、記錄回覆等所有細節。
祕書再厲害,最終也得靠實體電話線把聲音傳過去。
本節字彙
三種典範鳥瞰
請求-回覆、RPC、RMI 的關係與層次。
深度探秘
由低到高的三層抽象
三種遠端溝通典範
本章聚焦三種由低到高、愈來愈友善的遠端溝通方式:
| 典範 | 抽象層次 | 一句話說明 |
|---|---|---|
| 請求-回覆協定 (request-reply) | 最低 | 在訊息傳遞上加一層配對,支援 client 送請求、server 回覆 |
| 遠端程序呼叫 (RPC) | 中 | 讓你像呼叫本機程序/函式一樣呼叫遠端程序 |
| 遠端方法呼叫 (RMI) | 高 | 把 RPC 帶進物件世界,可呼叫遠端物件的方法、傳物件參考 |
請求-回覆是地基,RPC 與 RMI 都建在它之上。RPC 是 1980 年代的重大突破(Birrell 與 Nelson, 1984);RMI 則是 1990 年代物件導向興起後的延伸。
注意:這裡的「RMI」是泛指『遠端方法呼叫』這個概念,別跟特定產品「Java RMI」搞混。
請求-回覆是地基,RPC 延伸程序呼叫,RMI 再延伸到物件方法呼叫。
生活妙喻
從便利貼到語音助理
溝通方式的進化
把三種典範想成「交辦事情」的進化:
- 請求-回覆:像在櫃台遞一張便利貼寫需求,對方做完回你一張紙條。最原始,但你得自己定規矩(怎麼配對哪張回哪張)。
- RPC:像打內線電話說「請幫我跑一下報表程式」。你不必管對方在哪台機器,講出來就像在自己辦公室喊一聲。
- RMI:像有個語音助理,你不只能下指令,還能把「另一個物件」當參數交給它——例如「把這份檔案交給會計『那個物件』處理」,甚至對方回你一個可以繼續使喚的『物件參考』。
三種典範是溝通能力的進化:便利貼、內線電話、能傳遞物件的語音助理。
實用超能力
選對抽象層次
何時用哪一種
- 只是要最輕量、最可控的訊息往返(例如自訂的小型協定、NFS 這種固定區塊傳輸)→ 用底層請求-回覆就夠。
- 想讓跨語言的服務像呼叫本機函式一樣被使用,且資料是程序式 → RPC(如 Sun RPC、gRPC)。
- 系統是物件導向的,需要傳遞物件參考、用繼承與多型 → RMI(如 Java RMI、CORBA)。
關鍵差異在參數傳遞:RPC 只能傳值(輸入/輸出參數),RMI 額外能傳『物件參考』——當資料很大或很複雜時,傳一個參考讓對方需要時再遠端呼叫,往往比整包複製過去更划算。
依需求選層次;RMI 比 RPC 多了傳遞物件參考的能力。
最原始的往返,規則要自己定,但最輕量可控。
像在本機喊一聲,不必管對方在哪台機器。
不必整包複製,對方拿到參考後可再遠端呼叫。
本節字彙
請求-回覆協定:最基礎的對話
doOperation/getRequest/sendReply 與 requestId 的角色。
三個基本動作與訊息結構
doOperation/getRequest/sendReply 與 requestId 的角色。
深度探秘
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 回結果——三者構成一次往返。
生活妙喻
點餐與取餐號碼牌
速食店點餐
把一次請求-回覆想成在速食店點餐:
- 你走到櫃台點餐(
doOperation送出請求),然後站在旁邊等(阻塞)。 - 店員收到你的單(
getRequest),去做餐。 - 餐做好了叫號交給你(
sendReply),你拿到餐就離開(解除阻塞)。
但店裡同時有很多人點餐,怎麼確保你拿到的是『你的餐』而不是別人的?靠號碼牌。
這張號碼牌就是 requestId:client 每送一個請求就產生一個獨一無二的編號,server 回覆時會把同一個編號抄回去。這樣 doOperation 就能確認『這份回覆是我這次請求的,而不是上一筆延遲的舊回覆』。
requestId 就像點餐號碼牌,確保回覆能正確配對到當初的請求。
實用超能力
看懂訊息欄位
一封請求/回覆訊息長什麼樣
請求與回覆訊息通常包含這些欄位:
| 欄位 | 意義 |
|---|---|
| messageType | 0 代表 Request,1 代表 Reply |
| requestId | 訊息識別碼,用來配對請求與回覆 |
| remoteReference | 要呼叫的遠端物件/服務參考 |
| operationId | 要執行哪個操作(可能是編號或操作本身) |
| arguments | 參數,已被打包成位元組陣列 |
而完整的訊息識別碼其實由兩部分組成:
- requestId:由送出端取自一個遞增整數序列。
- 送出端的識別:例如它的連接埠與網際網路位址。
第一部分讓編號在『同一個送出端內』唯一,第二部分讓它在『整個分散式系統內』唯一。當 requestId 遞增到無號整數最大值就歸零重來——只要一個編號的『存活期』遠短於整個序列用完的時間,就不會撞號。
訊息識別碼=requestId(送出端內唯一)+送出端位址(系統內唯一)。
送出請求後你不能走,得等餐(回覆)做好。
確保你拿到的是自己的餐,而不是別人的或上一筆舊餐。
流水號在店內唯一,加上店名就在全城唯一。
本節字彙
面對失敗:逾時、重送與冪等
逾時重送、過濾重複、history 與冪等操作。
深度探秘
訊息會掉,程序會當
不可靠世界的應對
若請求-回覆建在 UDP 之上,它就繼承了 UDP 的毛病:
- 遺漏失敗(omission failures):請求或回覆可能直接掉包。
- 不保證順序:訊息不一定照送出順序抵達。
- 再加上程序當機(crash failure):我們假設程序當掉就停住,不會做出亂七八糟(Byzantine)的行為。
為了應付『server 掛了』或『訊息掉了』,doOperation 在等回覆時會設逾時(timeout)。逾時後最常見的作法不是直接放棄,而是重送請求,直到收到回覆、或合理確認真的是 server 沒回應為止;真的不行時,才用例外通知 client『沒拿到結果』。
但重送帶來新問題:server 可能收到重複的請求,導致同一操作被執行多次。
UDP 上的請求-回覆會掉包、會亂序;用逾時加重送補救,但重送會引發重複執行。
生活妙喻
催繳信與『重按一次』
寄信沒回音怎麼辦
你寄信請廠商出貨,過了約定時間(逾時)還沒回音。你不知道是:
- 你的信掉了(對方根本沒收到),還是
- 對方早出貨了,只是回函在路上掉了。
保險作法是再寄一次(重送)。但若對方其實已出貨,再寄一次可能害你收到兩批貨——這就是重複執行的風險。
解法有兩層:
- 過濾重複:對方記得你的訂單編號(requestId),看到重複就不再重做,直接補寄收據。
- 冪等操作(idempotent):有些操作天生『做幾次都一樣』。例如『把開關設成 ON』,重複設一百次結果都是 ON;但『餘額加 10 元』就不是——做兩次就多加了。
重送像重寄催繳信;用『記住訂單編號去重』與『讓操作冪等』來避免重複造成的傷害。
實用超能力
去重、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,重複無害。
做兩次就多加了 10 元,重複會出錯。
對方再問就重寄收據,不必重新出貨。
本節字彙
交換協定樣式與 HTTP
R/RR/RRA 三種協定樣式,以及 HTTP 作為請求-回覆協定的實例。
深度探秘
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。
生活妙喻
三種寄信習慣
你寄信會選哪種?
- R:投一張明信片就走,不在乎對方有沒有回(『我搬家了,地址改成這個』,不需要回音)。
- RR:寄信並等對方回信。對方的回信本身就證明他收到了你的信,不必再另寄一張『我收到你的信囉』。
- RRA:除了一來一回,你還補一張回執告訴對方『你的回覆我收到了,可以把這筆紀錄歸檔了』。對方因此能清掉抽屜裡為你保留的副本(history)。
為什麼 RR 不必額外確認?因為一來一回本身就互相當確認——回覆確認了請求,下一次請求又確認了上一次回覆,省下大量無謂訊息。
R 像明信片、RR 像有來有往的信件、RRA 多一張回執讓對方歸檔。
實用超能力
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 上、用持久連線省成本,並區分冪等與非冪等方法。
通知一聲即可,不需要對方回音。
回信本身就證明收到了請求,不必額外寄確認。
確認回覆已收到,對方就能清掉保留的副本。
不必每件事都重撥一次號,省下建連線成本。
本節字彙
遠端程序呼叫 RPC
服務介面、輸入輸出參數、為何不支援 call by reference、IDL 的用途。
用介面寫程式與 IDL
服務介面、輸入輸出參數、為何不支援 call by reference、IDL 的用途。
深度探秘
介面:規格與實作的分界線
用介面寫程式
RPC 推動一種寫程式的風格:用介面(interface)寫程式。一個模組的介面,明定『有哪些程序可被別人呼叫、各自的參數型別是什麼』,但不揭露實作細節。只要介面不變,內部實作怎麼改都不影響使用者。
在分散式系統裡,server 提供給 client 的那組程序規格稱為服務介面(service interface)。好處包括:
- 使用者只需面對介面提供的抽象,不必懂實作。
- 甚至不必知道對方用什麼語言或平台寫的——這是駕馭**異質性(heterogeneity)**的關鍵一步。
- 支援軟體演進:只要介面相容,實作可自由更新。
但分散式的本質也限制了介面的設計,這正是下一步要談的重點。
服務介面把『能呼叫什麼』與『怎麼實作』分開,讓使用者不必懂內部、甚至不必懂對方用什麼語言。
生活妙喻
餐廳菜單與廚房
菜單就是介面
把服務介面想成餐廳的菜單:
- 菜單列出『有哪些菜、需要什麼(辣度、份量)』,這就是介面。
- 廚房怎麼煮、用瓦斯還是電磁爐、廚師是台灣人還是法國人——你都不必知道,這是實作。
- 只要菜單不變,廚房換廚師、換設備,你照點不誤(軟體演進)。
但跨機器點餐有些事辦不到:
- 你不能直接伸手進廚房動別人的鍋子(不能直接存取另一個處理程序的變數)。
- 你也不能把『你家冰箱的位置』寫在點餐單上要廚房去拿(位址在別的程序裡無效,不能傳位址)。
- 因此參數只能講清楚『送進去的(in)』與『要帶回來的(out)』——也就是輸入/輸出參數,而不支援 call by reference。
菜單像介面;跨機器時不能伸手動別人鍋子、不能傳位址,所以只能用輸入/輸出參數。
實用超能力
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,讓不同語言寫的程式能互相遠端呼叫。
列出能點什麼、要附帶什麼,但不揭露廚房怎麼煮。
位址在別的程序裡無效,只能把值送進去、把值帶回來。
讓說不同語言(程式語言)的人都能照同一份規格點餐。
本節字彙
呼叫語意:maybe/at-least-once/at-most-once
三種容錯措施組合出的三種呼叫語意。
深度探秘
三種容錯措施組出三種語意
呼叫語意是什麼
本機程序呼叫的語意是 exactly once(恰好一次):每個程序剛好執行一次(除非程序當機)。但遠端呼叫會掉包、會當機,無法輕易保證恰好一次。於是要用三種容錯措施去『組合』出不同的可靠度保證:
- 重送請求訊息:要不要重送,直到收到回覆或判定 server 失敗。
- 過濾重複:是否在 server 端濾掉重複請求。
- 重送結果(history):是否保留結果以便重送,避免重新執行。
不同組合得到不同的呼叫語意:
| 重送請求 | 過濾重複 | 重新執行/重送回覆 | 語意 |
|---|---|---|---|
| 否 | 不適用 | 不適用 | Maybe |
| 是 | 否 | 重新執行程序 | At-least-once |
| 是 | 是 | 重送回覆 | At-most-once |
呼叫語意由『重送、去重、是否保存結果』三種措施組合而成,對應 maybe、at-least-once、at-most-once。
生活妙喻
三種寄包裹的保障等級
寄包裹的保障
把遠端呼叫想成寄包裹請對方做一件事:
- Maybe(也許):寄了就不管。包裹可能掉、對方可能當機,你完全不確定事情有沒有被做。最省事,但只適合『偶爾失敗也沒差』的場合。
- At-least-once(至少一次):沒回音就一直重寄。只要你收到結果,就知道事情至少被做了一次——但可能不只一次!若操作不是冪等(例如『餘額加 10 元』),重複執行會出錯。
- At-most-once(至多一次):用上全部措施(重送+去重+保存結果)。你收到結果時,就知道恰好被做了一次;若沒收到結果,則是『做了一次或完全沒做』,但絕不會超過一次。
Maybe 不保證、at-least-once 可能重複執行、at-most-once 用完整措施確保不超過一次。
實用超能力
怎麼選語意
實務上怎麼權衡
- 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 防重複。
可能到、可能掉,你無從確認事情有沒有被做。
至少做了一次,但可能重複,非冪等操作會出錯。
用上全部措施,保證絕不超過一次。
本節字彙
RPC 的實作與 stub
client stub、server stub、dispatcher 如何把呼叫變成訊息。
深度探秘
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 解包並呼叫真正的服務程序。
生活妙喻
跨國代購的層層轉手
代購流程
想像你在台灣請代購買日本商品:
- 你=client 程式:只說「我要買這個」。
- client stub=台灣這邊的代購:把你的需求填成標準表單、裝箱(marshal),交給物流。
- 通訊模組=國際物流:負責把包裹送到日本、把回程包裹帶回。
- dispatcher=日本倉庫的分揀員:看標籤把包裹分到正確的窗口。
- server stub=日本窗口人員:拆箱、看清楚需求,去找真正的店家。
- service procedure=日本店家:真正把商品準備好。
你全程只跟『台灣代購』講話,感覺就像在巷口買東西——這正是 RPC 想營造的透明感。
stub 像兩端的代購與窗口,dispatcher 像分揀員,讓你感覺像在本地買東西。
實用超能力
看懂 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 做繫結。
把你的需求裝箱(打包)交給物流,你只跟他講話。
依標籤把包裹分到正確的處理窗口。
拆箱解讀需求,再去找真正的店家(服務程序)。
你報出部門(程式編號),總機告訴你該打哪個分機(連接埠)。
本節字彙
遠端方法呼叫 RMI 與分散式物件
遠端物件、遠端物件參考、遠端介面三大概念。
分散式物件模型
遠端物件、遠端物件參考、遠端介面三大概念。
深度探秘
從物件到分散式物件
把物件搬到不同程序
物件導向程式由一群互相呼叫方法的物件組成,每個物件有自己的資料與方法,並透過物件參考(object reference)互相存取。把這些物件實體分散到不同程序或電腦,就成了分散式物件。
當方法呼叫跨越程序或電腦的邊界時,就是遠端方法呼叫(RMI);同一程序內的呼叫則是本機方法呼叫。
分散式物件模型有兩個核心概念:
- 遠端物件(remote object):能接收遠端呼叫的物件(一般物件只能接收本機呼叫)。
- 遠端物件參考(remote object reference):一個全分散式系統通用、唯一的識別子,用來指向某個特定遠端物件。
- 遠端介面(remote interface):每個遠端物件都有,明定它哪些方法可被遠端呼叫。
flowchart LR A[物件 A 在程序一] -- 遠端呼叫 --> B[遠端物件 B 在程序二] B -- 本機呼叫 --> E[物件 E]
分散式物件模型的三大概念:遠端物件、全系統唯一的遠端物件參考、明定可遠端呼叫方法的遠端介面。
生活妙喻
公司分機與對外服務窗口
內線與對外窗口
把一群分散式物件想成跨城市的多家分公司:
- 本機方法呼叫=同一辦公室內走過去找同事(同程序內)。
- 遠端方法呼叫=打去外地分公司找人(跨程序/電腦)。
- 遠端物件參考=對方那個人的全公司通用工號——不管他在哪個分公司,這個工號都能唯一指到他。
- 遠端介面=該員工的對外服務窗口清單:外人只能請他做『窗口上列出的事』,至於他內部還會做哪些雜事,外人碰不到。
注意:一般本機物件(沒有遠端介面的)就像不對外的內部員工,只有同辦公室的人能直接找他。
遠端物件參考像全公司通用工號,遠端介面像員工的對外服務窗口清單。
實用超能力
遠端介面的限制與好處
設計遠端物件時要記得
幾個關鍵實務點:
- 只有遠端介面裡的方法能被遠端呼叫;本機物件則可呼叫遠端物件的『所有』方法(含非遠端介面的)。
- 遠端介面沒有建構子(constructor)——所以不能用遠端呼叫去 new 一個遠端物件;要建立遠端物件,得透過工廠方法(factory method):在遠端介面裡提供專門用來建立物件的一般方法。
- 遠端物件參考可當參數與結果傳遞:物件 A 可以從物件 B 的回傳值取得對物件 F 的遠端參考,再去呼叫 F。
- 例外(exception)更重要:遠端呼叫除了方法本身可能丟的例外,還可能因『程序當機、太忙、訊息遺失』而失敗,所以必須能丟出如逾時這類因分散式而生的例外,呼叫端要準備好處理。
好處:跨程序自然強化了封裝——物件狀態只能透過自己的方法存取,避免未授權的存取與某些並行衝突。
遠端介面無建構子需靠工廠方法建物件;遠端參考可當參數傳遞;遠端呼叫必須能處理因分散式而生的例外。
不管員工在哪個分公司,工號都能唯一指到他。
外人只能請他做清單上的事,內部雜事碰不到。
因為不能直接 new 遠端物件,只好請既有遠端物件代為建立。
本節字彙
RMI 的內部構造:proxy 與 skeleton
proxy、dispatcher、skeleton、servant 與兩個底層模組的分工。
深度探秘
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。
生活妙喻
影分身與傳真機
proxy 是遠端物件的『影分身』
想像 B 是住在遠方的專家,你(A)手邊有一個 B 的影分身(proxy):
- 影分身長得跟 B 一模一樣(實作同樣的遠端介面),你對它喊『幫我算這個』。
- 但影分身不會真的算,它把你的要求寫成傳真(打包成請求訊息)發到遠方。
- 遠方的收發室(dispatcher)看傳真上的編號,把它交給對的助理(skeleton)。
- 助理拆開傳真,請真正的**專家 B(servant)**動手,算完把答案傳真回來。
- 影分身收到回傳,假裝是自己算出來的,把結果交給你。
對你而言,全程就像身邊真的有個 B——這就是 proxy 帶來的透明性。
proxy 像遠端物件的影分身,把你的呼叫轉成傳真送到遠方,再把答案假裝成自己算的交回。
實用超能力
靜態 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 按需把被動物件啟動成主動物件。
長得一樣但不真算,把呼叫轉成傳真送往遠方。
收發室依編號分派,助理拆信並請真正的專家動手。
實際執行方法、提供遠端物件本體。
平時休眠省資源,有客人上門才喚醒成可服務狀態。
本節字彙
分散式垃圾回收與租約
以參考計數為基礎的分散式 GC,以及 lease 如何容忍當機。
深度探秘
跨機器的參考計數
沒人用了才回收
**分散式垃圾回收(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 集合,集合空了才回收物件,且不需全域同步。
生活妙喻
圖書館的借閱名冊
借閱名冊與到期歸還
把遠端物件想成圖書館裡一本珍貴的書:
- 每本書有一張借閱名冊(holders),記錄誰正借著它。
- 有人來借(addRef)就登記上名冊;還書(removeRef)就劃掉。
- 只要名冊上還有人,書就不能銷毀;名冊清空了,館員才能把書下架回收。
但問題來了:萬一某位讀者搬家失聯(client 程序當機),永遠不還書怎麼辦?名冊上的名字會永遠掛著,書永遠下不了架。
解法是租約(lease):借書不是『無限期』,而是『借你到某月某日』。到期不續借,館員就視同歸還,把名字劃掉。讀者若還想繼續借,必須在到期前主動續約。
holders 像借閱名冊,addRef/removeRef 像借還;lease 用到期機制處理失聯讀者,避免書永遠下不了架。
實用超能力
容忍失敗:冪等與租約
讓 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 當機,避免資源永遠被佔。
名冊上有人書就不能銷毀,清空才能下架回收。
借出加名、歸還除名,維護誰還在用。
失聯讀者不續借,到期視同歸還,避免書永遠下不了架。
本節字彙
案例研究:Java RMI
extends Remote、RemoteException,以及遠端物件傳參考、一般物件傳值。
Java 遠端介面與參數傳遞
extends Remote、RemoteException,以及遠端物件傳參考、一般物件傳值。
深度探秘
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,刻意凸顯分散式本質。
生活妙喻
影本 vs 鑰匙
傳一份影本,還是傳一把鑰匙?
Java RMI 傳參數時,分兩種截然不同的方式:
- 傳一般(非遠端)物件 → 傳值(複製):就像把一份文件影印一份寄過去。對方拿到的是全新的副本,他改他的、你改你的,互不影響。前提是該物件必須可序列化(implements Serializable)。
- 傳遠端物件 → 傳遠端物件參考:就像把一把遠端保險箱的鑰匙交給對方。對方拿到的不是箱子本體,而是能遠端打開那個箱子的鑰匙;他透過這把鑰匙去操作的,仍是『原本那個』遠端物件。
所以判斷依據很簡單:參數型別是遠端介面 → 傳參考(鑰匙);是一般可序列化物件 → 傳值(影本)。
一般可序列化物件以值複製傳遞(像影本),遠端物件以遠端參考傳遞(像遠端保險箱的鑰匙)。
實用超能力
用白板範例看清差異
共享白板的參數傳遞
以共享白板為例(GraphicalObject 是可序列化、存放圖形狀態的一般物件):
// client 把一個 GraphicalObject 傳給 server
Shape s = shapeList.newShape(myGraphicalObject);
發生了什麼:
myGraphicalObject是一般可序列化物件,所以以值複製送到 server——server 端得到一份新的副本。- server 用這份狀態建立一個型別為
Shape的遠端物件,並回傳它的遠端物件參考給 client。 - client 拿到的
s是個遠端參考,之後對s呼叫方法(如s.getVersion())都是遠端呼叫到 server 上那個物件。
要點整理:
| 情境 | 傳遞方式 | 結果 |
|---|---|---|
參數 GraphicalObject g |
傳值(複製) | server 得到獨立副本 |
回傳值 Shape(遠端介面) |
傳遠端參考 | client 拿到能遠端操作的鑰匙 |
所有基本型別與遠端物件都是可序列化的;參數與結果的類別在需要時會被 RMI 系統下載到接收端(下一節詳談)。
白板範例:GraphicalObject 以值複製送出,server 回傳的 Shape 是遠端參考——『型別是不是遠端介面』決定傳值或傳參考。
對方拿到獨立副本,雙方各改各的互不影響。
對方拿到的是能遠端操作原物件的鑰匙,而非物件本體。
逼呼叫者事先正視並處理遠端失敗。
本節字彙
類別下載與 RMIregistry
動態下載類別的好處,以及 RMIregistry 作為 binder 的角色。
深度探秘
缺類別?自動下載
Java 的招牌:動態下載類別
Java 設計上允許類別(class)從一個虛擬機下載到另一個虛擬機,這對遠端呼叫特別有用。
回憶上一節:非遠端物件以值傳遞、遠端物件以參考傳遞。但接收端若沒有那個物件的類別怎麼辦?
- 若接收端沒有某個『以值傳遞之物件』的類別 → 其程式碼會被自動下載。
- 若接收端拿到一個遠端參考卻沒有對應 proxy 的類別 → proxy 程式碼也會被自動下載。
這帶來兩大好處:
- 不必要求每位使用者的環境都預先放齊所有類別。
- client 與 server 都能透明地使用新增類別的實例。
例如白板一開始的 GraphicalObject 不支援文字;某 client 自己寫了一個處理文字的子類別,把實例當參數丟給 server。這個新類別的程式碼會自動從該 client 下載到 server,再下載到其他需要的 client——大家不必事先約好都裝這個類別。
Java RMI 能在缺少類別時自動下載類別程式碼,讓 client/server 透明地使用新類別,不必事先全部裝齊。
生活妙喻
附上組裝說明書的家具
連說明書一起寄
想像你網購一款全新型號的家具寄給朋友,但朋友從沒見過這款,不知道怎麼組。
- 沒有類別下載:朋友收到零件卻沒有說明書,組不起來(接收端不認得這個類別)。
- 有類別下載:包裹裡附上組裝說明書(類別程式碼)。朋友照著說明書就能把家具組好、正常使用——即使他以前從沒見過這個型號。
白板的『文字圖形』新子類別就是這樣:你把『新型家具』(物件)寄出時,Java RMI 順便附上說明書(類別)。對方(server 或其他 client)即使沒見過,也能下載說明書後正常處理。
這也是為什麼序列化時,物件的類別資訊會被標註它的位置(URL),好讓接收端知道去哪裡下載。
類別下載像『寄家具時附上組裝說明書』,讓從沒見過該類別的接收端也能下載後正常使用。
實用超能力
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 風格名稱對應到遠端物件參考。
讓沒見過該型號的接收端下載說明書後也能正常使用。
用名字查到對方的聯絡方式(遠端物件參考)。
server 登記名字,client 照名字查出參考。