Stub 與 Skeleton:RMI 通訊的雙邊代理
RMI 採用經典 RPC 機制——Stub(存根)與Skeleton(骨架)實現遠程通訊。
Stub(客戶端代理)職責
- 初始化與遠程 JVM 的連接
- 參數編組(序列化與傳輸)
- 等待調用結果
- 解編返回值或異常
- 將結果返還調用者
Stub 實現了遠程對象的所有遠程接口,對調用者隱藏了序列化與網絡通訊細節,呈現簡單的本地調用體驗。
Skeleton(服務端分發器)職責 (JDK 1.2+ 純環境不再需要)
- 解編遠程方法參數
- 調用實際遠程對象實現
- 編組返回值或異常發回調用者
JDK 1.2 引入新 Stub 協議,通用代碼取代 Skeleton,由 rmic 編譯器生成 Stub/Skeleton。
遠程方法調用中的線程使用
RMI 運行時不保證遠程對象與線程的映射關係。同一遠程對象的多個調用可能並發執行,因此遠程對象實現必須確保線程安全。
遠程對象的分布式垃圾收集(DGC)
RMI 採用引用計數算法(類似 Modula-3 Network Objects)自動回收無客戶端引用的遠程對象,釋放程序員手動追蹤負擔。
核心機制
- 運行時追蹤每個 JVM 內的活動引用計數
- 引用進入 JVM 計數 +1,發送 "referenced" 消息
- 引用失效計數 -1,最後一個引用釋放發送 "unreferenced" 消息
- 協議維護消息順序防止過早回收
弱引用與回收條件
- 無客戶端引用時,RMI 對遠程對象持有弱引用
- 無本地引用時,JVM GC 可回收該對象
- 存在本地引用時不可回收,且可繼續傳遞/返回
- 需通知的遠程對象實現
java.rmi.server.Unreferenced 接口,引用集為空時調用 unreferenced()
網絡分區風險
若客戶端與服務器發生網絡分區,傳輸層可能誤判客戶端失效導致過早回收。此時遠程引用可能指向不存在對象,調用將拋出 RemoteException,應用程序必須處理。
動態類加載:實現代碼移動性
RMI 允許傳遞任意可序列化對象,使用對象序列化傳輸數據,並用 codebase URL 註解調用流,接收端據此加載類定義。
加載流程
- 解編時優先嘗試本地類加載器(當前線程上下文類加載器)按名稱解析
- 失敗則使用動態加載:從發送端指定的網絡位置(codebase)下載類文件
- 包括 Stub 類、參數聲明類型的子類等
關鍵實現類
RMIClassLoader:提供靜態方法支持網絡類加載,供內部編組流調用
ObjectOutputStream 子類覆蓋 annotateClass 寫入 codebase
ObjectInputStream 子類覆蓋 resolveClass 讀取 codebase 並加載
通過代理服務器穿透防火牆的 RMI
默認 RMI 嘗試直接 Socket 連接,但許多防火牆禁止。RMI 提供兩種基於 HTTP 的穿透機制:
HTTP 封裝兩種模式
- 直接轉發:代理可轉發至任意端口 → HTTP POST 直接發送至 RMI 服務器監聽端口,服務器 Socket 識別並解碼 RMI 調用
- CGI 轉發:代理僅允許 80 端口 → 請求轉發至 HTTP 服務器,執行 CGI 腳本再轉發至 RMI 服務器端口
默認 Socket 工廠自動處理
- 客戶端 Socket 自動嘗試 HTTP 連接無法直連的主機
- 服務端 Socket 自動檢測 HTTP POST 請求,提取主體交給傳輸層,輸出格式化為 HTTP 響應
- 無需特殊配置即可啟用;可設置
java.rmi.server.disableHttp=true 禁用
服務器配置要點
- 主機名必須使用全限定域名(非 IP),啟動時指定
-Djava.rmi.server.hostname=FQDN
- 若需支持防火牆外客戶端:HTTP 服務器監聽 80 端口,CGI 腳本路徑
/cgi-bin/java-rmi.cgi,腳本調用本地 Java 解釋器轉發請求
性能與限制
- HTTP 封裝調用比直接 Socket 慢至少一個數量級
- 防火牆外主機無法回調客戶端導出的遠程對象(單向初始化限制)