Java 中最簡單的分布式調用 RMI

lm_lxt 7年前發布 | 22K 次閱讀 Java RMI 分布式系統 Java開發

JAVA中最簡單的分布式調用 RMI

前言

我們先來看一個例子:

系統中目前存在兩個 JAVA 服務,分別是服務A、服務B。現在服務A 想要調用服務B中的某個服務,我們怎么實現呢?

有人覺得這不很簡單,服務B暴露一個服務接口,服務A通過 RPC 的方式來訪問這個接口,這里的 RPC 可以引用第三方實現,也可以通過簡單的 REST 請求的方式實現。

是的,解決這場景的方法有很多,其實 JAVA 自身也提供了一種更簡單的方式,即通過 RMI 實現跨 JVM 虛擬機的遠程調用。雖然它和現在主流的 RPC 相比,可能顯得比較無力。但是其設計思想,加上它的簡單易用,我們不妨來看一下。

RMI 簡介

RMI(Remote Method Invocation)是一種用于實現遠程過程調用的應用程序編程接口。它使客戶機上運行的程序可以調用遠程服務器上的對象。遠程方法調用特性使Java編程人員能夠在網絡環境中分布操作。

特點

  • 是 JAVA 自帶的功能,無需集成任何的外部擴展;
  • 數據傳輸是面向對象的;
  • 動態下載對象資源;
  • 僅限 JAVA 間通信;

通信協議

服務間的通信通過 TCP 傳輸。協議約定為 rmi://,僅限JAVA之間的遠程通信;

成員

  • RMI Registry:作為存儲遠程服務的代理對象的倉庫
  • Server:服務端,暴露遠程對象,并將其代理對象注冊進 RMI Registry
  • Client:客戶端,查找遠程代理對象,遠程調用服務對象

運行機制

從上圖可以看出,雖然 RMI 目前看上去有點過時了,但其思想和現在的服務注冊與發現還是很相似的。歸納起來,包含以下幾點:

  1. 啟動注冊中心
  2. 服務端:暴露服務
  3. 服務端:服務注冊
  4. 客戶端:獲取服務地址(代理對象)
  5. 客戶端:遠程調用服務

使用方法

  • 啟動 RMI Registry

這里啟動倉庫有兩種方式,一種是在程序中啟動:

import java.rmi.registry.LocateRegistry;
Registry registry = LocateRegistry.createRegistry(REGISTRY_PORT);

另一種通過命令啟動:

/usr/bin/rmiregistry REGISTRY_PORT
  • 獲取 RMI Registry
    • 通過環境變量 java.rmi.server.hostname 來設置倉庫地址
    </li> </ul>
    import java.rmi.registry.LocateRegistry;
    Registry registry = LocateRegistry.getRegistry(REGISTRY_PORT)
    • 定義遠程服務接口

      • 接口繼承 Remote
      • 接口方法必須拋出 RemoteException
      </li> </ul>
      import java.rmi.Remote;
      public interface RemoteService extends Remote {

      //define your function
      Object run() throws RemoteException;
      
      

      }</code></pre>

      • UnicastRemoteObject.exportObject(Remote obj, int port)

        • 創建 Remote 對象的代理類,并實現 Serializable 接口
        • 在 TCP 上暴露遠程服務
        • port 為 0 表示使用匿名隨機端口 ( 使用1~1023的已知端口時,注意權限問題
        </li> </ul>
        import java.rmi.server.UnicastRemoteObject;
        Remote remoteProxy = UnicastRemoteObject.exportObject(your_remote_service, 0);
        • 注冊遠程對象到 RMI Registry( 在 Registry 中的都是對象的遠程代理類,并非真正的對象

        獲取 Registry 的遠程代理類,然后調用它的 rebind 將代理對象注冊進倉庫中 ( Naming.rebind(String name, Remote obj) 本質上也是解析 name 中的倉庫地址,獲取倉庫的代理對象,進而進行遠程注冊

        // 本地創建或遠程獲取 Registry
        Registry registry = ...
        registry.rebind(String name, Remote obj);
        • 查找遠程調用對象
        Registry registry = LocateRegistry.getRegistry(REGISTRY_PORT);
        Remote obj = registry.lookup(REMOTE_NAME);

        示例

        ###準備工作: 定義遠程對象接口

        package com.test.remote;

        import java.rmi.Remote; import java.rmi.RemoteException;

        public interface RemoteService extends Remote {

        Object run() throws RemoteException;
        
        Object run(Object obj) throws RemoteException;
        
        

        }</code></pre>

        ###服務B:注冊遠程服務

        • 實現遠程服務對象
        package com.test.serviceB.publishService;

        import com.test.remote.RemoteService; import java.rmi.RemoteException;

        public class pService1 implements RemoteService {

        public Object run() {
            System.out.println("invoke pService1.");
            return "success";
        }
        
        public Object run(Object obj) throws RemoteException {
            System.out.println("invoke pService1, params is " + obj.toString());
            return "success";
        }
        
        

        }</code></pre>

        • 啟動服務

          • 創建 RMI Registry(也可在通過命令 rmiregistry 在應用外創建)
          • 實例化遠程服務
          • 導出遠程對象,使其能接受遠程調用
          • 將導出的遠程對象綁定到倉庫中
          • 等待服務調用
          </li> </ul>
          public class Boot {

          private static final String REMOTE_P1 = "serviceB:p1";
          private static final int REGISTRY_PORT = 9999;
          
          public static void main(String[] args) throws RemoteException {
          
               // 實例化遠程對象,并創建遠程代理類
              RemoteService p1 = new pService1();
              Remote stub1 = UnicastRemoteObject.exportObject(p1, 0);
          
              // 本地創建 Registry,并注冊遠程代理類
              Registry registry = LocateRegistry.createRegistry(REGISTRY_PORT);
              registry.rebind(REMOTE_P1, stub1);
          
              System.out.println("service b bound");
          
          }
          

          }</code></pre>

          ###服務A:調用遠程服務

          • 啟動服務
            • 連接倉庫
            • 在 Registry 中查找所調用服務的遠程代理類
            • 調用代理類方法
            </li> </ul>
            public class Boot {

            private static final String REMOTE_P1 = "serviceB:p1";
            private static final int REGISTRY_PORT = 9999;
            
            public static void main(String[] args) throws RemoteException {
            
                try {
            
                    Registry registry = LocateRegistry.getRegistry(REGISTRY_PORT);
                    // 從倉庫中獲取遠程代理類
                    RemoteService p1 = (RemoteService) registry.lookup(REMOTE_P1);
                    // 遠程動態代理
                    String res1 = (String)p1.run();
                    System.out.printf("The remote call for %s %s \n", REMOTE_P1, res1);
            
                } catch (NotBoundException e){
                    e.printStackTrace();
                } catch (RemoteException e){
                    e.printStackTrace();
                }
            
            }
            

            }</code></pre>

            演示結果

            • 啟動服務B
            service b bound
            • 啟動服務A
            The remote call for serviceB:p1 success

            Process finished with exit code 0</code></pre>

            • 查看服務B 調用情況
            service b bound
            invoke pService1.

            高級用法

            上面示例沒有涉及到遠程調用的傳參問題。如果需要傳參,且傳參的類型不是基本類型時,遠程服務就需要動態的去下載資源。

            這里通過設置環境變量來實現遠程下載:

            • java.rmi.server.codebase:遠程資源下載路徑(必須是絕對路徑),可以是file://, ftp://, http:// 等形式的;
            • java.rmi.server.useCodebaseOnly:默認為 true, 表示僅依賴當前的 codebase, 如果使用外部的 codebase( 服務B 需要使用 服務A 提供的下載地址時 ),需將此參數設置為false;

            對于跨主機的訪問,RMI 加入了安全管理器(SecurityManager),那么也需要對應的安全策略文件

            • java.security.policy:指定策略文件地址;

            其他設置:

            • java.rmi.server.hostname:設置倉庫的主機地址;
            • sun.rmi.transport.tcp.handshakeTimeout:設置連接倉庫的超時時間;

            核心代碼

            關于源碼的閱讀,網上曾經看到一句話講的很好,“源碼閱讀的什么樣程度算好,閱讀到自己能放過自己了,那就夠了。”

            我一般喜歡帶著問題來閱讀,這里我從幾個問題入手,簡單分享下我的理解。

            • 獲取到的 Registry 對象到底是什么東西?

            從這段代碼來分析:

            Registry registry = LocateRegistry.getRegistry(REGISTRY_PORT);

            • 遠程對象到底是什么,原始對象又在哪里呢?

            從這段代碼來分析:

            Remote obj = UnicastRemoteObject.exportObject(Remote obj, int port);

            • 知道了倉庫中存放、獲取的都是 遠程對象的代理類,那么實際的遠程通信是如何完成的?

            知道 JDK 動態代理的同學,肯定有一個 invoke 方法,是方法調用的關鍵。這里的 invoke() 具體代碼在 UnicastRef 中。

            問題

            從源碼中可以看出,遠程調用每次都會 新建一個 connection,感覺這里會成為一個性能的瓶頸。

            總結

            雖然 RMI 在目前看來有些過時了,但它的思想:遠程倉庫、服務注冊、服務查找、代理調用等,和目前主流的 RPC 是不是很相似呢?一種技術的過時,往往是跟不上業務的快速發展,但它的產生至少是滿足了當時的需求。

            個人覺得,技術的實現會隨著業務的發展不斷的變化,但是核心思想一定是小步的進行,畢竟這些都是不斷積累的經驗總結出來的。希望本篇對大家能有所收獲!

             

            來自:https://github.com/jasonGeng88/blog/blob/master/201704/rmi.md

             

 本文由用戶 lm_lxt 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!