# 前言

RMI (Remote Method Invocation) 远程方法调用,是一种调用远程位置的对象来执行方法的思想,该模型是一种分布式对象应用,使用 RMI 技术可以使一个 JVM 中的对象,调用另一个 JVM 中的对象方法并获取调用结果。这里的另一个 JVM 可以在同一台计算机也可以是远程计算机。因此,RMI 意味着需要一个 Server 端和一个 Client 端。实际上就是在一个 java 虚拟机上调用另一个 java 虚拟机的对象上的方法 RMI传输数据序列化后的数据 。下图是 RMI 的架构,在后面会详细的介绍到

# RMI 流程(代码)

这里是参考 su18 师傅的文章通过分析源码来了解这个 RMI

首先定义一个能够远程调用的接口,这个接口需要继承 Remote 接口,用来远程调用的对象作为这个接口的实例

package Clown_RMI_code;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface IRemoteHelloWorld extends Remote{
    public String hello() throws RemoteException;
}

接着实现这个接口,这里继承这个接口的同时通常需要扩展 java.rmi.server.UnicastRemoteObject 类,扩展此类后,RMI 会自动将这个类 export 给远程想要调用它的 Client 端,同时还提供了一些基础的 toString 方法,在 export 时会随机绑定一个端口,监听客户端的请求,直接请求这个端口也可以通行

package Clown_RMI_code;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld {
    protected RemoteHelloWorld() throws RemoteException {
        super();
    }
    public String hello() throws RemoteException {
        System.out.println("访问成功");
        return "Hello world";
    }
}

上面这个实现接口的类实际上就是能被远程调用的对象,对于调用方法 RMI 设计了一个 Registry 的思想,类似路由表的概念,实现这个主要通过 java.rmi.registry.Registry 和 java.rmi.Naming 来实现

  1. 是 Java 远程方法调用(RMI)中的一个接口,提供了一个远程对象注册表。它用于将远程对象与名称绑定,然后可以使用该名称从远程客户端查找该对象。 Registry 接口定义了绑定(bind)、解绑(unbind)、列表(list)和查找(lookup)远程对象在注册表中的方法。
  2. 是 Java 远程方法调用(RMI)中的一个类,提供了一个命名服务,用于在远程服务器上绑定和查找远程对象。它可以将一个远程对象绑定到一个 URL 地址上,这个 URL 地址可以被客户端用来查找该远程对象。
    这是一个 final 方法,url 的格式 //host:post/name:
    1. host,表示注册表所在的主机
    2. port 表示注册表接受调用的端口号,默认的是 1099
    3. name 表示一个注册 Remote Object 的引用的名称,不能是注册表中的一些关键词

使用 LocateRegistry#createRegistry() 方法来创建注册中心,将其加入到待调用的类中进行绑定

package Clown_RMI_code;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RemoteServer {
    public static void main(String[] args) throws Exception {
        LocateRegistry.createRegistry(1099);
        // 创建注册中心
        IRemoteHelloWorld iRemoteHelloWorld = new RemoteHelloWorld();
        // 创建远程对象
        Naming.bind("rmi://127.0.0.1/Hello",iRemoteHelloWorld);
        // 绑定
    }
}

最后就是在客户端进行调用

package Clown_RMI_code;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;
public class RMIClient {
    public static void main(String[] args) throws Exception, RemoteException, NotBoundException {
        // 获取 RMI 注册表,参数为 RMI 服务器的 IP 和端口号
        Registry registry = LocateRegistry.getRegistry("192.168.32.6",1099);
        // 打印出 RMI 注册表中所有已注册的服务名
//        System.out.println(Arrays.toString(registry.list()));
        // 根据服务名在 RMI 注册表中查找远程对象的 stub
        IRemoteHelloWorld stub = (IRemoteHelloWorld) registry.lookup("Hello");
        // 调用远程对象的方法并打印结果
        System.out.println(stub.hello());
    }
}

在 RemoteServer 类中,创建注册中心后去创建了一个远程对象

# 注册中心的创建

LocateRegistry.createRegistry(1099);

这里首先跟进代码看到

createRegistry 实际上 new 了 RegistryImpl 对象,继续跟进

这段代码是 Java RMI 中 RegistryImpl 类的构造函数实现,用于创建 RMI 注册表。如果传入的 port 参数为默认的注册表端口 Registry.REGISTRY_PORT ,并且存在安全管理器,则授权注册表使用默认端口,并使用 UnicastServerRef 类导出远程对象,从而创建注册表的 UnicastServerRef 对象。否则,直接使用 UnicastServerRef 类导出远程对象,并创建注册表的 UnicastServerRef 对象。在导出远程对象时,使用 LiveRef 类来表示远程对象的引用,同时使用 RegistryImpl::registryFilter 方法来过滤注册表的传输流。

# 远程对象创建

这个对象是继承了 UnicastRemoteObject,前面也有提到这个类会自动 export 远程,并获取 Stub,Stub 是一个代理类

首先这里会调用的静态方法 exportObject

它是 java.rmi.server.RemoteObject 类中的一个静态方法。它用于导出远程对象并返回一个存根,该存根可用于调用远程对象上的方法。

该方法有两个参数:要导出的 Remote 对象和一个 UnicastServerRef 对象。如果要导出的对象继承了 UnicastRemoteObject 类,那么该方法将设置它的 ref 字段。最后,该方法会调用 sref.exportObject(obj, null, false) 方法导出远程对象,并返回导出的远程对象的存根。

对 RemoteHelloWorld 的 exprot 的创建主要是通过 createprot 方法使用 RemoteObjectInvocationHandler 来为我们测试写的 RemoteObject 实现的 RemoteInterface 接口创建动态代理

接下来首先看 RemoteObjectInvocationHandler 这个动态代理

继承 RemoteObject 实现 InvocationHandler,因此这是一个可序列化的、可使用 RMI 远程传输的动态代理类,对于动态代理我们这里重点看一下 invoke 方法

invoke 方法中,首先判断传入的 proxy 参数是否是一个代理类,如果不是,则抛出一个 IllegalArgumentException 异常。然后,判断传入的 proxy 参数是否与当前的 InvocationHandler 对象匹配,如果不匹配,则抛出一个 IllegalArgumentException 异常。最后,判断要调用的方法是不是 Object 类中的方法,如果是,则调用 invokeObjectMethod 方法来处理方法调用,否则调用 invokeRemoteMethod 方法来处理远程方法调用。

在 invokeObjectMethod 中判断要调用的方法是不是 Object 类中的 equalshashCodetoString 方法,如果是,则直接调用 Object 类中的相应方法,否则抛出一个 UnsupportedOperationException 异常。

invokeRemoteMethod 方法中,会将方法调用序列化成字节流,并通过 JRMP 协议发送给远程对象。远程对象收到方法调用后,会将字节流反序列化成方法调用,并执行该方法,最后将方法的返回值序列化成字节流,通过 JRMP 协议发送给客户端。

# 服务注册

实际上就是绑定过程

首先调用了 parseURL 方法来解析 name 参数,然后通过 getRegistry 方法获取了一个 Registry 对象。接下来,如果 obj 参数为 null ,则会抛出 NullPointerException 异常。最后,使用 registry.bind(parsed.name, obj) 方法将 obj 对象绑定到命名服务中。

这样一个流程来 javasec 上面总结的很详细

RMI 底层通讯采用了 Stub (运行在客户端) 和 Skeleton (运行在服务端) 机制,RMI 调用远程方法的大致如下:

  1. RMI 客户端在调用远程方法时会先创建 Stub ( sun.rmi.registry.RegistryImpl_Stub )。
  2. Stub 会将 Remote 对象传递给远程引用层 ( java.rmi.server.RemoteRef ) 并创建 java.rmi.server.RemoteCall (远程调用) 对象。
  3. RemoteCall 序列化 RMI 服务名称、Remote 对象。
  4. RMI 客户端的远程引用层传输 RemoteCall 序列化后的请求信息通过 Socket 连接的方式传输到 RMI 服务端的远程引用层。
  5. RMI 服务端的远程引用层 ( sun.rmi.server.UnicastServerRef ) 收到请求会请求传递给 Skeleton ( sun.rmi.registry.RegistryImpl_Skel#dispatch )。
  6. Skeleton 调用 RemoteCall 反序列化 RMI 客户端传过来的序列化。
  7. Skeleton 处理客户端请求:bind、list、lookup、rebind、unbind,如果是 lookup 则查找 RMI 服务名绑定的接口对象,序列化该对象并通过 RemoteCall 传输到客户端。
  8. RMI 客户端反序列化服务端结果,获取远程对象的引用。
  9. RMI 客户端调用远程方法,RMI 服务端反射调用 RMI 服务实现类的对应方法并序列化执行结果返回给客户端。
  10. RMI 客户端反序列化 RMI 远程方法调用结果。

# RMI 流程(流量)

这里对 需要注意的是RMI被调用的方法执行在客户端 ,P 牛的 JAVA 漫谈中抓包分析,但是我本地的包有较多的混淆流量流,这里直接用 P 牛的图

这个是整个流程

首先这里有一次 TCP 握手,跟踪这个流

接着是一个协议确认,客户端向服务器发送了 StreamProtocol 用于确认服务器是否支持此协议

接着是服务端向客户端发送的一个确认包确认 ip 和端口号

客户端发给服务器一个 ip 地址,这个 ip 是客户端的 ip 地址

接着发送 call 请求

服务端返回 Returndata 包

这段是 java 序列化后的内容,⾸先客户端连接 Registry,并在其中寻找 Name 是 Hello 的对象,这个对应数据流中的 Call 消息;然后 Registry 返回⼀个序列化的数据,这个就是找到的 Name=Hello 的对象,这个对应数据流中的 ReturnData 消息;客户端反序列化该对象,发现该对象是⼀个远程对象,地址在 192.18.0.1:12137 ,于是再与这个地址建⽴ TCP 连接;在这个新的连接中,才执⾏真正远程⽅法调⽤,也就是 hello ()

然后是服务端和客户端的 ping 包

最后一个输出消息包 DgcAck 指示客户端已接收到服务器返回值中的远程对象

接着就断开 tcp 连接