少女祈祷中...

RMI机制

简介

RMI(Remote Method Invocation)为远程方法调用,是允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。 这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中,它的底层是由socketjava序列化和反序列化支撑起来的。
它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。RMI全部的宗旨就是尽可能简化远程接口对象的使用。

我们知道远程过程调用(Remote Procedure Call, RPC)可以用于一个进程调用另一个进程(很可能在另一个远程主机上)中的过程,从而提供了过程的分布能力。Java 的 RMI 则在 RPC 的基础上向前又迈进了一步,即提供分布式对象间的通讯。

RMI依赖的通信协议为JRMP(Java Remote Message Protocol ,Java 远程消息交换协议),该协议为Java定制,要求服务端与客户端都为Java编写。这个协议就像HTTP协议一样,规定了客户端和服务端通信要满足的规范。在RMI中对象是通过序列化方式进行编码传输的。

  • Client-客户端:客户端调用服务端的方法
  • Server-服务端:远程调用方法对象的提供者,也是代码真正执行的地方,执行结束会返回给客户端一个方法执行的结果
  • Registry-注册中心:其实本质就是一个map,相当于是字典一样,用于客户端查询要调用的方法的引用(在低版本的JDK中,Server与Registry是可以不在一台服务器上的,而在高版本的JDK中,Server与Registry只能在一台服务器上,否则无法注册成功)

RMI

远程对象调用过程

在JVM之间通信时,RMI对远程对象和非远程对象的处理方式是不一样的,它并没有直接把远程对象复制一份传递给客户端,而是传递了一个远程对象的Stub(存根),Stub相当于远程对象的引用或者代理。Stub对开发者是透明的,客户端可以像调用本地方法一样直接通过它来调用远程方法。Stub中包含了远程对象的定位信息,如Socket端口、服务端主机地址等等,并实现了远程调用过程中具体的底层网络通信细节。而位于服务器端的Skeleton(骨架),能够读取客户端传递的方法参数,调用服务器方的实际对象方法, 并接收方法执行后的返回值。所以RMI远程调用逻辑大致是这样的

从逻辑上来看,数据是在Client和Server之间横向流动的,但是实际上是从Client到Stub,然后从Skeleton到Server这样纵向流动的。

具体的通信流程如下

  • Server监听一个端口,这个端口是JVM随机选择的
  • Client并不知道Server远程对象的通信地址和端口,但是位于Client的Stub中包含了这些信息,并封装了底层网络操作。Client可以调用Stub上的方法,并且也可以向Stub发送方法参数。
  • Stub连接到Server监听的通信端口并提交参数
  • Server执行具体的方法,并将结果返回给Stub
  • Stub返回执行结果给Client。因此在Clinet看来,就好像是Stub在本地执行了这个方法。

位于Client上的Stub获取到远程Server的通信信息的需要使用RMI Registry了

RMI Registry

RMI Registry的注册

JDK提供了一个RMI注册表(RMI Registry)来解决这个问题。RMI Registry也是一个远程对象,默认监听在1099端口上,可以使用代码启动RMI Registry,也可以使用rmiregistry命令。
要注册远程对象,需要RMI URL和一个远程对象的引用,如:

private void register() throws Exception{
RMIHello rmiHello=new RMIHello();
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://127.0.0.1:1099/hello",rmiHello);
}

上面代码使用 LocateRegistry.createRegistry(port)在本地的某个端口上创建了一个Registry。最后使用Naming.bind()将实例化对象和地址上的hello绑定在一起,作为远程对象的名字。注意这里使用的是rmi://协议。

RMI Registry的使用

注册完RMI Registry以后,我们将要调用的远程对象已经和服务器端的某个地址绑定在了一起。

package learn.rmi;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIClient {
public static void main(String[] args) throws Exception{
Registry registry= LocateRegistry.getRegistry("127.0.0.1",1099);
IHello iHello=(IHello) registry.lookup("hello");
System.out.println(iHello.sayHello("Feng"));
}
}

LocateRegistry.getRegistry()会使用给定的主机和端口等信息在本地创建一个Stub对象作为Registry远程对象的代理,从而启动整个远程调用逻辑。服务端应用程序可以向RMI注册表中注册远程对象,然后客户端向RMI注册表查询某个远程对象名称,来获取该远程对象的Stub。这里我们使用了registry.lookup()来查询获取注册表中的远程对象。还有另一种写法
使用了RMI Registry后,RMI的调用关系如下

RMI调用流程
从客户端角度看,服务端应用是有两个端口的,一个是RMI Registry端口(默认为1099),另一个是远程对象的通信端口(随机分配的)。更详细的通信过程如下

使用

使用远程方法调用,必然会涉及参数的传递和执行结果的返回。参数或者返回值可以是基本数据类型,当然也有可能是对象的引用。所以这些需要被传输的对象必须可以被序列化,这要求相应的类必须实现java.io.Serializable接口,并且客户端的serialVersionUID字段要与服务器端保持一致。

任何可以被远程调用方法的对象必须继承java.rmi.Remote接口,远程对象的实现类必须继承UnicastRemoteObject类。如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法,如下:

  1. 定义远程服务对象接口并继承Remote接口,需要将远程调用的方法定义在接口里面,并必须抛出RemoteException(否则会报错)

    package com.yyjccc.jndi.RMI;

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

    public interface RMIService extends Remote {

    public String say() throws RemoteException;
    }

  2. 定义上面接口的实现类并继承UnicastRemoteObject类

    package com.yyjccc.jndi.RMI;

    import java.rmi.RemoteException;
    import java.rmi.server.UnicastRemoteObject;

    public class MyService extends UnicastRemoteObject implements RMIService{
    protected MyService() throws RemoteException {
    super();
    }
    @Override
    public String say() {
    System.out.println("call say method");
    return "hello";
    }

    // 在没有继承UnicastRemoteObject的时候构造函数也可以写成如下形式
    // protected RMIHello() throws RemoteException{
    // UnicastRemoteObject.exportObject(this,0);
    // }
    }

  3. 定义RMI服务端并将对应的远程对象绑定到注册中心

    package com.yyjccc.jndi.RMI;

    import java.net.MalformedURLException;
    import java.rmi.Naming;
    import java.rmi.RemoteException;
    import java.rmi.registry.LocateRegistry;

    public class RMIServer {
    public static void main(String[] args) {
    try {
    LocateRegistry.createRegistry(1099);
    Naming.rebind("rmi://127.0.0.1:1099/myObj",new MyService());
    } catch (RemoteException | MalformedURLException e) {
    throw new RuntimeException(e);
    }
    }
    }

  4. 定义RMI客户端,lookup获取远程对象并调用方法

    package com.yyjccc.jndi.RMI;

    import java.net.MalformedURLException;
    import java.rmi.Naming;
    import java.rmi.NotBoundException;
    import java.rmi.RemoteException;

    public class RMIClient {
    public static void main(String[] args) {
    try {
    RMIService myService = (RMIService) Naming.lookup("rmi://127.0.0.1:1099/myObj");
    myService.say();
    System.out.println();
    } catch (NotBoundException | MalformedURLException | RemoteException e) {
    throw new RuntimeException(e);
    }
    }
    }

客户端本地必须有远程对象的接口,不然无法指定要调用的方法,而且其全限定名必须与服务器上的对象完全相同

JRMP协议

Java远程方法协议(Java Remote Method Protocol,JRMP)是特定于Java技术的、用于查找和引用远程对象的协议。这是运行在Java远程方法调用(RMI)之下、TCP/IP之上的线路层协议。

开启服务端,在开启WireShark抓包,再启动客户端调用,一次完整的RMI通信数据包如下

可以看到JRMP通信协议双方在完成确认后,随后Clinet向Registry发送”Call”信息,Registry回复”ReturnData”

我们看一下Registry的回复内容

这里传输的是服务器的java序列化数据。注意以上的部分。ac ed是Java序列化的魔术头,该数据流往后的部分就是序列化的内容了。

06 3d转换成十进制为3075,这便是Server在本地开放的随机端口。

RMI流程分析

Registry端

可以通过createRegistry()方法来创建一个Registry

Registry registry = LocateRegistry.createRegistry(1099);

可以看到,在创建Registry是返回的是RegistryImpl对象


跟进setup

继续跟进UnicastServerRef.exportObject():

  1. 创建stub


加上_stub并进行实例化.RegistryImpl_Stub

  1. 创建Skeleton,这就是Server端处理 RMI Client 通信请求的具体操作类


因此最终createRegistry()的结果就是返回了一个RegistryImpl对象,并且赋值this.skel=RegistryImpl_Skel。
完整过程如下:

对远程对象的操作有以下5种

  • bind
  • list
  • lookup
  • rebind
  • unbind

对于Registry端,操作远程对象其实就是操作HashTable,我们来看RegistryImpl中的bind操作

binddings属性其实就是一个HashTable
Registry使用的这张Hash表就类似于一张”路由表”,将name和绑定其上的远程对象联系了起来。

rebind和bind的区别在于后者不会覆盖以前已经绑定的对象

Client端

获取Registry


和使用如下代码效果一样

Registry registry = LocateRegistry.getRegistry(rmi,port);
registry.lookup(name);

上面都是先获取Registry,与Registry端不同的是这里还会做一层封装

得到的是RegistryImpl_Stub对象

操作远程对象

在Client端,对远程对象的操作同样有以下5种

这里我们以RegistryImpl_Stub.bind操作为例进行分析

首先生成了一个RemoteCall通信对象来建立连接,注意参数里的opnum(操作数),上述5个操作中每一个操作都有一个opnum,这里bind操作的opnum为0。参数中的hash其实就是serializeID。
跟进newCall,在此时Client已经通过newConnection()和Server端建立了连接

然后Client通过StreamRemoteCall()提前将ObjID、opnum和serializeID发送给Server端

回到bind中,可以看到使用了writeObject()将我们要发送的name以及Remote远程对象序列化发送了过去

再往下,bind中的invoke和done便是接收处理服务端返回的信息

完整流程

### Server端 我们知道,在RMI过程中,Server端往往是开启两个端口的,一个1099端口用于Registry,另一个是随机端口用于与Client通信。而远程方法最终是在Server端执行的,Server会把执行的结果返回给Client端。我们上面在创建Registry的时候已经生成了一个RegistryImpl_Skel对象,正是这个对象与Client端的RegistryImpl_Stub通信 #### 开启通信端口 在创建我们的远程对象的时候,跟进构造方法

到这里逐层跟进(有点套娃)


最后到TCPTransport.exportObject(),开始监听端口,跟进listen()

这里listen()启动了一个新的线程,跟进看看这个线程做了什么

直接在TCPTransport$AcceptLoop#run处打下断点
此时已经从主线程到了RMI TCP Accept-0线程,调用了executeAcceptLoop(),跟进

在执行完this.serverSocket.accept()之后server才开始真正等待Client的连接

与Client通信

紧接上文,我们在executeAcceptLoop()方法中的TCPTransport.connectionThreadPool.execute()方法处下个断点,然后Client向Server发送连接请求,如下

这里创建了一个ConnectionHandler句柄,我们跟进ConnectionHandler句柄的run()方法,在TCPTransport.run()中

跟进run0(),最终到了该方法中的handleMessages(),这是用来传递消息的句柄

跟进,创建了StreamRemoteCall对象,跟进下面的serviceCall()方法

可以看到serviceCall()方法的参数是一个RemoteCall对象。这里就是Client传来的RemoteCall对象,然后对该对象进行各种操作,读取其中的信息。

最终会调用dispatch()方法

然后调用oldDispatch(),最后跟到this.skel.dispatch()方法中。最终在RegistryImpl_Skel.dispatch()中根据Client发来的信息进行各种操作。


最终在RegistryImpl_Skel.dispatch()进行了各种数据的反序列化

完整流程

参考: