# 前言

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

image-20230407001809673

# 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);

这里首先跟进代码看到

image-20230407015432123

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

image-20230407015543039

这段代码是 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) 方法导出远程对象,并返回导出的远程对象的存根。

image-20230407010207778

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

image-20230407013315581

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

image-20230407014515460

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

image-20230407014619996

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

image-20230407014842429

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

image-20230407015000249

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

# 服务注册

实际上就是绑定过程

image-20230407020030495

首先调用了 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 牛的图

image-20230328002451679

这个是整个流程

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

image-20230407103458372

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

image-20230407105655174

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

image-20230407105719590

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

image-20230407105815867

接着发送 call 请求

image-20230407110109999

服务端返回 Returndata 包

image-20230407110212035

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

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

image-20230407110506927

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

image-20230407110619069

接着就断开 tcp 连接

image-20230407111353918