Java 中最简单的分布式调用 RMI

lm_lxt 3年前
   <h2>JAVA中最简单的分布式调用 RMI</h2>    <h2>前言</h2>    <p>我们先来看一个例子:</p>    <p>系统中目前存在两个 JAVA 服务,分别是服务A、服务B。现在服务A 想要调用服务B中的某个服务,我们怎么实现呢?</p>    <p>有人觉得这不很简单,服务B暴露一个服务接口,服务A通过 RPC 的方式来访问这个接口,这里的 RPC 可以引用第三方实现,也可以通过简单的 REST 请求的方式实现。</p>    <p>是的,解决这场景的方法有很多,其实 JAVA 自身也提供了一种更简单的方式,即通过 RMI 实现跨 JVM 虚拟机的远程调用。虽然它和现在主流的 RPC 相比,可能显得比较无力。但是其设计思想,加上它的简单易用,我们不妨来看一下。</p>    <h2>RMI 简介</h2>    <p>RMI(Remote Method Invocation)是一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。</p>    <h3>特点</h3>    <ul>     <li>是 JAVA 自带的功能,无需集成任何的外部扩展;</li>     <li>数据传输是面向对象的;</li>     <li>动态下载对象资源;</li>     <li>仅限 JAVA 间通信;</li>    </ul>    <h3>通信协议</h3>    <p>服务间的通信通过 TCP 传输。协议约定为 rmi://,仅限JAVA之间的远程通信;</p>    <h3>成员</h3>    <ul>     <li>RMI Registry:作为存储远程服务的代理对象的仓库</li>     <li>Server:服务端,暴露远程对象,并将其代理对象注册进 RMI Registry</li>     <li>Client:客户端,查找远程代理对象,远程调用服务对象</li>    </ul>    <h3>运行机制</h3>    <p><img src="https://simg.open-open.com/show/73ce42b264c9802da39745f0eff49449.png"></p>    <p>从上图可以看出,虽然 RMI 目前看上去有点过时了,但其思想和现在的服务注册与发现还是很相似的。归纳起来,包含以下几点:</p>    <ol>     <li>启动注册中心</li>     <li>服务端:暴露服务</li>     <li>服务端:服务注册</li>     <li>客户端:获取服务地址(代理对象)</li>     <li>客户端:远程调用服务</li>    </ol>    <h3>使用方法</h3>    <ul>     <li>启动 RMI Registry</li>    </ul>    <p>这里启动仓库有两种方式,一种是在程序中启动:</p>    <pre>  <code class="language-java">import java.rmi.registry.LocateRegistry;  Registry registry = LocateRegistry.createRegistry(REGISTRY_PORT);</code></pre>    <p>另一种通过命令启动:</p>    <pre>  <code class="language-java">/usr/bin/rmiregistry REGISTRY_PORT</code></pre>    <ul>     <li>获取 RMI Registry      <ul>       <li>通过环境变量 java.rmi.server.hostname 来设置仓库地址</li>      </ul> </li>    </ul>    <pre>  <code class="language-java">import java.rmi.registry.LocateRegistry;  Registry registry = LocateRegistry.getRegistry(REGISTRY_PORT)</code></pre>    <ul>     <li> <p>定义远程服务接口</p>      <ul>       <li>接口继承 Remote</li>       <li>接口方法必须抛出 RemoteException</li>      </ul> </li>    </ul>    <pre>  <code class="language-java">import java.rmi.Remote;  public interface RemoteService extends Remote {        //define your function      Object run() throws RemoteException;    }</code></pre>    <ul>     <li> <p>UnicastRemoteObject.exportObject(Remote obj, int port)</p>      <ul>       <li>创建 Remote 对象的代理类,并实现 Serializable 接口</li>       <li>在 TCP 上暴露远程服务</li>       <li>port 为 0 表示使用匿名随机端口 ( <em>使用1~1023的已知端口时,注意权限问题</em> )</li>      </ul> </li>    </ul>    <pre>  <code class="language-java">import java.rmi.server.UnicastRemoteObject;  Remote remoteProxy = UnicastRemoteObject.exportObject(your_remote_service, 0);</code></pre>    <ul>     <li>注册远程对象到 RMI Registry( <em>在 Registry 中的都是对象的远程代理类,并非真正的对象</em> )</li>    </ul>    <p>获取 Registry 的远程代理类,然后调用它的 rebind 将代理对象注册进仓库中 ( <em>Naming.rebind(String name, Remote obj) 本质上也是解析 name 中的仓库地址,获取仓库的代理对象,进而进行远程注册</em> )</p>    <pre>  <code class="language-java">// 本地创建或远程获取 Registry  Registry registry = ...  registry.rebind(String name, Remote obj);</code></pre>    <ul>     <li>查找远程调用对象</li>    </ul>    <pre>  <code class="language-java">Registry registry = LocateRegistry.getRegistry(REGISTRY_PORT);  Remote obj = registry.lookup(REMOTE_NAME);</code></pre>    <h2>示例</h2>    <p>###准备工作: 定义远程对象接口</p>    <pre>  <code class="language-java">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>    <p>###服务B:注册远程服务</p>    <ul>     <li>实现远程服务对象</li>    </ul>    <pre>  <code class="language-java">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>    <ul>     <li> <p>启动服务</p>      <ul>       <li>创建 RMI Registry(也可在通过命令 rmiregistry 在应用外创建)</li>       <li>实例化远程服务</li>       <li>导出远程对象,使其能接受远程调用</li>       <li>将导出的远程对象绑定到仓库中</li>       <li>等待服务调用</li>      </ul> </li>    </ul>    <pre>  <code class="language-java">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>    <p>###服务A:调用远程服务</p>    <ul>     <li>启动服务      <ul>       <li>连接仓库</li>       <li>在 Registry 中查找所调用服务的远程代理类</li>       <li>调用代理类方法</li>      </ul> </li>    </ul>    <pre>  <code class="language-java">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>    <h3>演示结果</h3>    <ul>     <li>启动服务B</li>    </ul>    <pre>  <code class="language-java">service b bound</code></pre>    <ul>     <li>启动服务A</li>    </ul>    <pre>  <code class="language-java">The remote call for serviceB:p1 success     Process finished with exit code 0</code></pre>    <ul>     <li>查看服务B 调用情况</li>    </ul>    <pre>  <code class="language-java">service b bound  invoke pService1.</code></pre>    <h2>高级用法</h2>    <p>上面示例没有涉及到远程调用的传参问题。如果需要传参,且传参的类型不是基本类型时,远程服务就需要动态的去下载资源。</p>    <p>这里通过设置环境变量来实现远程下载:</p>    <ul>     <li>java.rmi.server.codebase:远程资源下载路径(必须是绝对路径),可以是file://, ftp://, http:// 等形式的;</li>     <li>java.rmi.server.useCodebaseOnly:默认为 true, 表示仅依赖当前的 codebase, 如果使用外部的 codebase( <em>服务B 需要使用 服务A 提供的下载地址时</em> ),需将此参数设置为false;</li>    </ul>    <p>对于跨主机的访问,RMI 加入了安全管理器(SecurityManager),那么也需要对应的安全策略文件</p>    <ul>     <li>java.security.policy:指定策略文件地址;</li>    </ul>    <p>其他设置:</p>    <ul>     <li>java.rmi.server.hostname:设置仓库的主机地址;</li>     <li>sun.rmi.transport.tcp.handshakeTimeout:设置连接仓库的超时时间;</li>    </ul>    <h2>核心代码</h2>    <p>关于源码的阅读,网上曾经看到一句话讲的很好,“源码阅读的什么样程度算好,阅读到自己能放过自己了,那就够了。”</p>    <p>我一般喜欢带着问题来阅读,这里我从几个问题入手,简单分享下我的理解。</p>    <ul>     <li>获取到的 Registry 对象到底是什么东西?</li>    </ul>    <p>从这段代码来分析:</p>    <pre>  <code class="language-java">Registry registry = LocateRegistry.getRegistry(REGISTRY_PORT);</code></pre>    <p style="text-align:center"><img src="https://simg.open-open.com/show/707886925ca7d21fbced96d9c286b82a.png"></p>    <ul>     <li>远程对象到底是什么,原始对象又在哪里呢?</li>    </ul>    <p>从这段代码来分析:</p>    <pre>  <code class="language-java">Remote obj = UnicastRemoteObject.exportObject(Remote obj, int port);</code></pre>    <p style="text-align:center"><img src="https://simg.open-open.com/show/e3e48472eb570debd87bd50c360ff77d.png"></p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/7325d5bae871912ed51e87d6c625a09f.png"></p>    <ul>     <li>知道了仓库中存放、获取的都是 远程对象的代理类,那么实际的远程通信是如何完成的?</li>    </ul>    <p>知道 JDK 动态代理的同学,肯定有一个 invoke 方法,是方法调用的关键。这里的 invoke() 具体代码在 UnicastRef 中。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/9afb5421dfec82985643f95013401f12.png"></p>    <h2>问题</h2>    <p>从源码中可以看出,远程调用每次都会 新建一个 connection,感觉这里会成为一个性能的瓶颈。</p>    <h2>总结</h2>    <p>虽然 RMI 在目前看来有些过时了,但它的思想:远程仓库、服务注册、服务查找、代理调用等,和目前主流的 RPC 是不是很相似呢?一种技术的过时,往往是跟不上业务的快速发展,但它的产生至少是满足了当时的需求。</p>    <p>个人觉得,技术的实现会随着业务的发展不断的变化,但是核心思想一定是小步的进行,毕竟这些都是不断积累的经验总结出来的。希望本篇对大家能有所收获!</p>    <p> </p>    <p>来自:https://github.com/jasonGeng88/blog/blob/master/201704/rmi.md</p>    <p> </p>