移动测试开发 JDK RMI 探索与使用

opentest-oper@360.cn · 2020年03月26日 · 913 次阅读

探索 JDK RMI

RMI(Remote Method Invoke)是 JDK 自带的一个远程调用框架,可以基于 RMI 构建自己的分布式应用。现在很多分布式服务框架都会选择支持 RMI 协议,比如 dubbo,spring rmi,Hessian 等 RPC 框架。为了对 RMI 有更多的了解,下面我们一起来读代码。

一、代码示例

为了更直观的了解 RMI 的使用,先来看一段代码。
新建三个 maven 工程,分别是 rmiclient,rmidependency,rmiserver,其中 rmiclient 是客服端,rmiserver 是服务端,rmidependency 是一个只包含接口的服务工程。rmiclient 和 rmiserver 引入 rmidependency 依赖。rmidependency 中 HelloWorldService 接口如下:

public interface HelloWorldService extends Remote {
    public String sayHello(String from, String to) throws RemoteException;
}

rmiserver 中 HelloWordServiceImpl 实现了 HelloWorldService 接口的类:

public class HelloWorldServiceImpl extends UnicastRemoteObject implements HelloWorldService {
    public HelloWorldServiceImpl() throws RemoteException {
        super();
    }
    @Override
    public String sayHello(String from, String to) throws RemoteException {
        return from + " say hello to " + to;
    }
}

在 rmiServer 中发布服务

public class RMIServer
{
    public static void main( String[] args )
    {
        try {
    //1.服务创建及发布:HelloWorldService 需要继承自 UnicastRemoteObject,当初始化时会自动将 HelloWorldServiceImpl任务一个服务发布。
            HelloWorldService helloWorldService = new HelloWorldServiceImpl();
    // 2. 创建注册中心:创建本机 1099 端口上的 RMI 注册表
            Registry registry = LocateRegistry.createRegistry(1099);
    // 3. 服务注册:将服务绑定到注册表中
            registry.bind("rmi://localhost:1099/helloWorldService", helloWorldService);
        } catch (RemoteException e) {
            e.printStackTrace();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (AlreadyBoundException e) {
            e.printStackTrace();
        }
    }

在 rmiclent 中调用远程服务

public class RMIClient
{
    public static void main( String[] args )
    {
        try {
            HelloWorldService helloWorldService = (HelloWorldService)   Naming.lookup("rmi://localhost:1099/helloWorldService");
            if(helloWorldService != null) {
                String result = helloWorldService.sayHello("java", "菜鸟");
                System.out.println(result);
            } else  {
                System.out.println("rmi 查找服务错误");
            }
            //Naming.list("rmi://localhost:1099");
        } catch (NotBoundException e) {
            e.printStackTrace();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
}

以上,可以看出 RMI 开发中有三个重要角色,分别是客户端,服务端,注册中心,其中
(1)注册中心:负责服务注册
(2)服务端:是提供服务的地方,是真正实现约定接口的地方
(3)客户端:是服务的调用方,是需要使用服务的一方。

通过上面的代码发现,无论是 HelloWorldServiceImpl 还是 Registry 都是 RemoteObjet 的子类。具体类图如下

RemoteObject 最重要的属性是 RemoteRef ref, RemoteRef 的实现类 UnicastRef,UnicastRef 包含属性 LiveRef ref。LiveRef 类中的 Endpoint、Channel 封装了与网络通信相关的方法。

二、启动服务端

服务端通过 LocateRegistry.createRegistry(1099) 来创建注册中心,如下,在创建注册中心的时候,会初始化一个 HashTable 的 bindings 属性,在使用 bind() 方法注册服务的时候,会保存到 bindings 结构中。之后如果注册中心的端口是 1099 并且系统开启了安全管理器,那么可以在限定的权限集内(listen 和 accept)绕过系统的安全校验,反之需要进行安全校验。之后通过 setup() 中的 exportObject 方法来暴露服务。Ps:继承 UnicastRemoteObject 的类,最终也会调用调用 exportObject 方法。

public RegistryImpl(final int var1) throws RemoteException {
    this.bindings = new Hashtable(101);
    if (var1 == 1099 && System.getSecurityManager() != null) {
        try {
            AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
                public Void run() throws RemoteException {
                    LiveRef var1x = new LiveRef(RegistryImpl.id, var1);
                    RegistryImpl.this.setup(new UnicastServerRef(var1x, (var0) -> {
                        return RegistryImpl.registryFilter(var0);
                    }));
                    return null;
                }
            }, (AccessControlContext)null, new SocketPermission("localhost:" + var1, "listen,accept"));
        } catch (PrivilegedActionException var3) {
            throw (RemoteException)var3.getException();
        }
    } else {
        LiveRef var2 = new LiveRef(id, var1);
        this.setup(new UnicastServerRef(var2, RegistryImpl::registryFilter));
    }

}
private void setup(UnicastServerRef var1) throws RemoteException {
    this.ref = var1;
    var1.exportObject(this, (Object)null, true);
}

setup() 里面调用的是 UnicastServerRef 的 exportObject() 方法,进入该方法,这里面为 RestryImpl 创建了一个代理,代理的作用是创建了服务于客户端的 RegistryImpl 的 Stub 对象,最后用 skeleton、stub、UnicastServerRef 对象、id 和一个 boolean 值构造了一个 Target 对象,也就是这个 Target 对象基本上包含了全部的信息。

public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException {
    Class var4 = var1.getClass();
    Remote var5;
    try {
        var5 = Util.createProxy(var4, this.getClientRef(), this.forceStubUse);
    } catch (IllegalArgumentException var7) {
        throw new ExportException("remote object implements illegal remote interface", var7);
    }
    if (var5 instanceof RemoteStub) {
        this.setSkeleton(var1);
    }
    Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3);
    this.ref.exportObject(var6);
    this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4);
    return var5;
}

上面提到了 Stub(存根)和 Skeleton(骨架)对象,分别是客户端的代理和服务端的代理,Stub 和 Skeleton 通过 socket 进行通信。上面的代码封装了 Target 方法之后,调用了 LiveRef 的 exportObject() 方法,追到了 TCPTransport 的 exportObject() 方法,如下,这个方法里面做了两件事。一是 listen() 方法创建一个 ServerSocket。并启动一条线程等待客户端的请求,接着调用父类 Transport 的 exportObject() 将 Target 对象存放进 ObjectTable 中。

public void exportObject(Target var1) throws RemoteException {
    synchronized(this) {
        this.listen();
        ++this.exportCount;
    }
    boolean var2 = false;
    boolean var12 = false;
    try {
        var12 = true;
        super.exportObject(var1);
        var2 = true;
        var12 = false;
    } finally {
        if (var12) {
            if (!var2) {
                synchronized(this) {
                    this.decrementExportCount();
                }
            }
        }
    }
    if (!var2) {
        synchronized(this) {
            this.decrementExportCount();
        }
    }
}

三、客户端获取获取服务

通过客户端调用远程服务分为两个步骤:一是获取注册中心 registry;二是根据注册中心获取服务的代理类 service。通过示例代码,可以追溯到 LocateRegistry 的 getRegistry() 方法。

public void exportObject(Target var1) throws RemoteException {
    synchronized(this) {
        this.listen();
        ++this.exportCount;
    }
    boolean var2 = false;
    boolean var12 = false;
    try {
        var12 = true;
        super.exportObject(var1);
        var2 = true;
        var12 = false;
    } finally {
        if (var12) {
            if (!var2) {
                synchronized(this) {
                    this.decrementExportCount();
                }
            }
        }
    }
    if (!var2) {
        synchronized(this) {
            this.decrementExportCount();
        }
    }
}

这个方法做的是通过传入的 host 和 port 构造 RemoteRef 对象,并创建了一个本地代理。代理对象其实是 RegistryImpl_Stub 对象。但注意此时这个代理其实还没有和服务端的 RegistryImpl 对象关联,毕竟是两个 JVM 上面的对象,这里我们也可以猜测,代理和远程的 Registry 对象之间是通过 socket 消息来完成的。

public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
    try {
        RemoteCall var2 = this.ref.newCall(this, operations, 2, 4905912898345647071L);

        try {
            ObjectOutput var3 = var2.getOutputStream();
            var3.writeObject(var1);
        } catch (IOException var17) {
            throw new MarshalException("error marshalling arguments", var17);
        }

        this.ref.invoke(var2);

        Remote var22;
        try {
            ObjectInput var4 = var2.getInputStream();
            var22 = (Remote)var4.readObject();
        } catch (IOException var14) {
            throw new UnmarshalException("error unmarshalling return", var14);
        } catch (ClassNotFoundException var15) {
            throw new UnmarshalException("error unmarshalling return", var15);
        } finally {
            this.ref.done(var2);
        }

        return var22;
    } catch (RuntimeException var18) {
        throw var18;
    } catch (RemoteException var19) {
        throw var19;
    } catch (NotBoundException var20) {
        throw var20;
    } catch (Exception var21) {
        throw new UnexpectedException("undeclared checked exception", var21);
    }
}

获取注册中心后,在客户端直接生成代理对象 RegistryImpl_Stub,RegistryImpl_Stub 通过 newCall() 方法和服务端建立连接,并向服务端传输远程接口名称,使用的注册中心方法集合以及集合的索引,来调用远程方法。实际调用 RemoteRef 的 invoke 方法进行网络通信,来获得远程对象实例。

四、总结

通过上面的分析,RMI 的调用流程可以简化成下面的结构。

但是里面还有很多细节值得仔细研究,例如 rmi 在调用的方法中存在引用型数据,是如何进行序列化和反序列化?bindings 数据结构在整个调用过程中到底处于什么样的角色?骨干和存根是如何封装通信?后续笔者也会持续分享。
另外 RMI 优点是避免重复造轮子,降低开发工作量,优化部署结构等,缺点是调用过程是两个 jvm 通信,增加了调用时间,而且如果网络不稳定会影响方法调用,降低了可用性,所以技术选型的时候需要按照业务需要平衡一下,或者可以选择 dubbo 这种支持 rmi 协议的 RPC 框架来进行管理。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册