RMI 介绍

RMI全称是 Remote Method Invocation,远程方法调用。从这个名字就可以看出,他的目标和RPC其实 是类似的,是让某个Java虚拟机上的对象调⽤另⼀个Java虚拟机中对象上的方法,只不过RMI是Java独 有的⼀种机制。

RMI 服务演示

示意图

the RMI system, using an existing web server, communicates from serve to client and from client to server

一个简单的RMI服务可以由一下内容组成。

  1. 定义继承 java.rmi.Remote 的接口
  2. 定义实现上述接口的类
  3. 创建服务端将 远程对象的类 注册到 registry 中并绑定到一个地址
  4. 创建客户端连接远程 RMI 服务,到的对应实现接口的类对象

通过 RMI 远程执行的方法还是执行在 RMI 服务器上的,客户端接收方法执行的返回结果。

本地搭建演示

image-20221012184645850

定义了三个包 clientregistryserver

registry 包

这个包里定义了一个接口和实现这个接口的类,这个接口要求 client 和 server都要导入, client 通过 rmi 服务得到的对象返回的就是实现了改接口的类对象。

RemoteMethod.java

package registry;

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

//定义接口,该接口必须继承 java.rmi.Remote 接口
public interface RemoteMethod extends Remote {
    //自定义要远程调用的方法,这些方法必须有抛出 RemoteException 异常
    public String sayHello() throws RemoteException;
    public int calcAdd(int num1, int num2) throws RemoteException;
}

RemoteObj.java

package registry;

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

//要远程实现的类,该类要实现对应的接口,并且要继承 UnicastRemoteObj 类
public class RemoteObj extends UnicastRemoteObject implements RemoteMethod {
    public RemoteObj() throws RemoteException {
        super();
    }

    @Override
    public String sayHello() throws RemoteException {
        return "hello RMI";
    }

    @Override
    public int calcAdd(int num1, int num2) throws RemoteException {
        return num1 + num2;
    }
}

server 包

该包用于创建 RMI 服务器,并将 registry 包中的类对象注册到 registry 仓库中,使得客户端能够调用该对象实现的接口中对应的方法。

CreateRmiServer.java

package server;

import registry.RemoteObj;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;

public class CreateRmiServer {
    RemoteObj obj;
    int port;
    public CreateRmiServer(int port) throws RemoteException {
        //实例化远程对象
        this.obj = new RemoteObj();
        this.port = port;
    }
    public void start() throws Exception {
        //创建 registry 并绑定端口
        LocateRegistry.createRegistry(this.port);
        //将实例化的远程对象绑定到一个地址
        Naming.bind("rmi://127.0.0.1:" + this.port + "/execMethod", this.obj);
        System.out.println("Run in port " + this.port);
    }
}

Server.java

RMI 服务器主方法,用于启动

package server;

public class Server {
    public static void main(String[] args) throws Exception {
        new CreateRmiServer(1099).start();
    }
}

client 包

创建客户端用于访问 RMI 服务器

CreateClient.java

客户端要导入 要调用的方法的接口,通过 RMI 服务器远程得到的对象是实现了这个接口的对象,这时接口的重要性就体现出来了,告诉客户端你可以调用的方法有哪些,从而正常调用方法。

package client;

import registry.RemoteMethod;
import registry.RemoteObj;
import java.rmi.Naming;

public class CreateClient {
    int port;
    public CreateClient(int port) {
        this.port = port;
    }
    public RemoteMethod getObj() throws Exception {
        //返回一个 实现了对应接口的对象 (接口中对应着一些方法)
        return (RemoteMethod) Naming.lookup("rmi://127.0.0.1:" + this.port + "/execMethod");
    }
}

Client.java

package client;

import registry.RemoteMethod;

public class Client {
    public static void main(String[] args) throws Exception {
        //创建客户端对象
        CreateClient client = new CreateClient(1099);
        //获得远程对象
        RemoteMethod obj = client.getObj();
        //调用远程方法执行
        String hello = obj.sayHello();
        int addRes = obj.calcAdd(1, 1);
        System.out.println("this is client");
        System.out.println(hello);
        System.out.println(addRes);
    }
}

编译运行 Server.javaClient.java

服务端

image-20221012190835663

客户端

image-20221012190910555

成功调用 sayHellocalcAdd 方法

RMI通信过程分析

使用 wireshark 抓下包

image-20221012195712118

这是完整的通信过程,我们可以发现,整个过程进行了两次TCP握手,也就是我们实际建立了两次 TCP连接。

第⼀次建立TCP连接是连接远端 127.0.0.11099 端口 ,就是我们在代码里看到的端口,⼆ 者进行沟通后,客户端向远端发送了⼀个 “Call” 消息,远端回复了⼀个 “ReturnData” 消息,然后客户端新建了⼀ 个TCP连接,连到远端的 2802 端口。

为什么客户端会连接33769端口呢?

细细阅读数据包我们会发现,在”ReturnData”这个包中,返回了目标的IP地址 172.28.128.1 ,其 后跟的⼀个字节 \x00\x00\x0a\xf2 ,刚好就是整数 2802 的网络序列:

0000   02 00 00 00 45 00 01 65 b9 a3 40 00 80 06 00 00   ....E..e..@.....
0010   7f 00 00 01 7f 00 00 01 04 4b 18 d1 76 8f 8c d5   .........K..v...
0020   6b cd 58 cb 50 18 20 0f d3 98 00 00 51 ac ed 00   k.X.P. .....Q...
0030   05 77 0f 01 94 17 4f 3e 00 00 01 83 cb e7 b7 f5   .w....O>........
0040   80 11 73 7d 00 00 00 02 00 0f 6a 61 76 61 2e 72   ..s}......java.r
0050   6d 69 2e 52 65 6d 6f 74 65 00 15 72 65 67 69 73   mi.Remote..regis
0060   74 72 79 2e 52 65 6d 6f 74 65 4d 65 74 68 6f 64   try.RemoteMethod
0070   70 78 72 00 17 6a 61 76 61 2e 6c 61 6e 67 2e 72   pxr..java.lang.r
0080   65 66 6c 65 63 74 2e 50 72 6f 78 79 e1 27 da 20   eflect.Proxy.'. 
0090   cc 10 43 cb 02 00 01 4c 00 01 68 74 00 25 4c 6a   ..C....L..ht.%Lj
00a0   61 76 61 2f 6c 61 6e 67 2f 72 65 66 6c 65 63 74   ava/lang/reflect
00b0   2f 49 6e 76 6f 63 61 74 69 6f 6e 48 61 6e 64 6c   /InvocationHandl
00c0   65 72 3b 70 78 70 73 72 00 2d 6a 61 76 61 2e 72   er;pxpsr.-java.r
00d0   6d 69 2e 73 65 72 76 65 72 2e 52 65 6d 6f 74 65   mi.server.Remote
00e0   4f 62 6a 65 63 74 49 6e 76 6f 63 61 74 69 6f 6e   ObjectInvocation
00f0   48 61 6e 64 6c 65 72 00 00 00 00 00 00 00 02 02   Handler.........
0100   00 00 70 78 72 00 1c 6a 61 76 61 2e 72 6d 69 2e   ..pxr..java.rmi.
0110   73 65 72 76 65 72 2e 52 65 6d 6f 74 65 4f 62 6a   server.RemoteObj
0120   65 63 74 d3 61 b4 91 0c 61 33 1e 03 00 00 70 78   ect.a...a3....px
0130   70 77 35 00 0a 55 6e 69 63 61 73 74 52 65 66 00   pw5..UnicastRef.
0140   0c 31 37 32 2e 32 38 2e 31 32 38 2e 31 00 00 0a   .172.28.128.1...
0150   f2 be 5d ce 36 ed b5 55 22 94 17 4f 3e 00 00 01   ..].6..U"..O>...
0160   83 cb e7 b7 f5 80 01 01 78                        ........x

image-20221012200130413

同时分析这个数据包不难发现这是 java 反序列化的数据,从 \xAC\xED 开始往后就是 java 反序列化的数据了,IP和端口是这个对象的⼀部分。

所以整个过程,就是客户端先连接Registry,并在其中寻找Name是execMethod的对象,这个对应数据流中的Call消息

image-20221012202651543

然后Registry返回⼀个序列化的数据,这个就是找到的 RemoteMethod 的对象,这个对应 数据流中的ReturnData消息;客户端反序列化该对象,发现该对象是⼀个远程对象,地址在 172.28.128.1:2802 ,于是再与这个地址建立TCP连接;在这个新的连接中,才执行真正远程方法调用,也就是执行 sayHello()

关系图如下:

image-20221012203014485

RMI Registry就像⼀个网关,他自己是不会执行远程方法的,但RMI Server可以在上⾯注册⼀个Name
到对象的绑定关系;RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接RMI
Server;最后,远程方法实际上在RMI Server上调用。