Java RMI

简介

Java RMI 指的是远程方法调用 (Remote Method Invocation)。它是一种机制,能够让在某个 Java 虚拟机上的对象调用另一个 Java 虚拟机中的对象上的方法。

在Java中,只要一个类继承了java.rmi.Remote接口,即可成为存在于服务器端的远程对象,供客户端访问并提供一定的服务。

RMI框架

RMI框架封装了所有底层通信细节,并且解决了编组、分布式垃圾收集、安全检查和并发性等通用问题,开发人员只需专注于开发与特定问题领域相关的各种本地对象和远程对象即可。

Stub和Skeleton

RMI框架采用代理来负责客户与远程对象之间通过Socket进行通信的细节。RMI框架为远程对象分别生成了客户端代理和服务器端代理,位于客户端的代理类称为Stub,位于服务器端的代理类称为Skeleton。stub(存根)和skeleton( 骨架 ) 在RMI中充当代理角色,在现实开发中主要是用来隐藏系统和网络的的差异, 这一部分的功能在RMI开发中对程序员是透明的。Stub为客户端编码远程命令并把他们发送到服务器。而Skeleton则是把远程命令解码,调用服务端的远程对象的方法,把结果在编码发给stub,然后stub再解码返回调用结果给客户端。

24

实现RMI步骤

  • 定义一个远程接口,此接口需要继承Remote
  • 开发远程接口的实现类
  • 创建一个server并把远程对象注册到端口
  • 创建一个client查找远程对象,调用远程方法

RMI实现

首先我们定义一个远程接口Hello.java

1
2
3
4
5
6
7
8
9
10
package rmi.server;
import java.rmi.Remote;
import java.rmi.RemoteException;


//Remote接口是一个标识接口,本身不包含任何方法,该接口用于标识其子类的方法可以
//被非本地的Java虚拟机调用
public interface Hello extends Remote{
String sayHello(String name) throws RemoteException;
}

接着我们定义一个实现类HelloImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package rmi.server;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class HelloImpl extends UnicastRemoteObject implements Hello {
public static final long serialVersionUID = 1L;

public HelloImpl() throws RemoteException {
super();
}

@Override
public String sayHello(String name) throws RemoteException {
return "hello," + name;
}
}

这里需要注意的是,实现类必须要继承UnicastRemoteObject类,客户端访问获得远程对象时,远程对象才会把自身的一个拷贝以Socket的形式传输给客户端,这个拷贝也就是Stub,也可以叫做”存根”,这个”Stub”可以看作是远程对象在本地的一个代理,其中包含了远程对象的具体信息,客户端可以通过这个代理与服务端进行交互。

最后实现该类的远程接口中的sayHello()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package rmi.server;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;


public class HelloServer {
public static void main(String[] args) {
try {
//HelloImpl 对象在实例化时会自动调用其父类 UnicastRemoteObject 的构造方法
// 生成对应的 Stub 和 Skeleton
Hello h = new HelloImpl();
//在本地创建并启动 RMIService , 被创建的 RMIService 服务将会在指定的端口上监听请求
LocateRegistry.createRegistry(1099);
//将远程对象 " h " 绑定到 rmi://localhost:1099/hello 这个 URL 上 . 客户端可以通过这个 URL 直接访问远程对象 .
Naming.rebind("rmi://localhost:1099/hello",h);
System.out.println("HelloServer 启动成功");
}catch (Exception e){
e.printStackTrace();
}
}
}

以上就是server端的配置

接着我们实现client端的配置,客户端只需要一个连接程序,即可实现远程调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package rmi.client;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

import rmi.server.Hello;
public class HelloClient {
public static void main(String[] args) {
try {
//客户端只需要调用 java.rmi.Naming.lookup 函数
//通过公开的路径从 RMIService 上拿到对应接口的实现类
Hello h = (Hello) Naming.lookup("rmi://localhost:1099/hello");
System.out.println(h.sayHello("Elssm"));
}catch (MalformedURLException e){
System.out.println("url格式异常");
}catch (RemoteException e){
System.out.println("创建对象异常");
e.printStackTrace();
} catch (NotBoundException e){
System.out.println("对象未绑定");
}
}
}

RMI执行过程

我们写好的RMI文件目录结构如下

1
2
3
4
5
6
7
rmi
├── client
│   ├── HelloClient.java
└── server
├── Hello.java
├── HelloImpl.java
└── HelloServer.java
编译
1
java rmi/server/*.java
1
java rmi/client/*.java
生成Stub存根

1

然后将服务端生成的Stub存根复制到客户端目录下,最后我们RMI文件目录结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
caoyifan@MacBookPro src % tree rmi                                      
rmi
├── client
│   ├── HelloClient.class
│   ├── HelloClient.java
│   └── HelloImpl_Stub.class
└── server
├── Hello.class
├── Hello.java
├── HelloImpl.class
├── HelloImpl.java
├── HelloImpl_Stub.class
├── HelloServer.class
└── HelloServer.java
启动RMI服务端

2

启动RMI客户端

3

可以看到,我们已经成功调用服务端的sayHello()方法

抓包分析

我们可以通过Wireshark进行进一步的分析,在客户端请求的过程中,有两次完整的数据交互。可以通过tcp.stream eq 会话序号划分

4

5

tcp.stream eq 17

一开始是TCP三次握手

7

接着是RMI代理的确认工作,这里RMI代理返回了客户端的IP地址和端口,用于确认要进行的RMI服务是否是RMI客户端,如果RMI客户端做出响应,则代表RMI客户端需要RMI服务

8

随后是RMI客户端的确认工作,经过RMI客户端和RMI代理的确认之后,初始化工作就完成了

9

初始化工作完成后,RMI客户端开始请求RMI服务端,这一过程通过RMI Call完成

10

这里我们具体看一下请求的数据包,首先在Wireshark中追踪数据流,之后以RAW格式显示数据

11

12

可以明显的看到ac ed 00 05特征码,我们可以利用SerializationDumper工具进行解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
caoyifan@MacBookPro ~ % java -jar SerializationDumper-v1.13.jar 50aced00057722000000000000000000000000000000000000000000000000000344154dc9d4e63bdf74000568656c6c6f73720019726d692e7365727665722e48656c6c6f496d706c5f537475620000000000000002020000707872001a6a6176612e726d692e7365727665722e52656d6f746553747562e9fedcc98be1651a020000707872001c6a6176612e726d692e7365727665722e52656d6f74654f626a656374d361b4910c61331e0300007078707732000a556e696361737452656600093132372e302e302e310000e2ab53b1d296bd7e099cac7c1a220000017d73a2f78e80010078

RMI Call - 0x50
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_BLOCKDATA - 0x77
Length - 34 - 0x22
Contents - 0x000000000000000000000000000000000000000000000000000344154dc9d4e63bdf
TC_STRING - 0x74
newHandle 0x00 7e 00 00
Length - 5 - 0x00 05
Value - hello - 0x68656c6c6f
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 25 - 0x00 19
Value - rmi.server.HelloImpl_Stub - 0x726d692e7365727665722e48656c6c6f496d706c5f53747562
...

之后是ReturnData的数据包

13

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
caoyifan@MacBookPro ~ % java -jar SerializationDumper-v1.13.jar 51aced0005770f01353650820000017d739f868f800873720019726d692e7365727665722e48656c6c6f496d706c5f537475620000000000000002020000707872001a6a6176612e726d692e7365727665722e52656d6f746553747562e9fedcc98be1651a020000707872001c6a6176612e726d692e7365727665722e52656d6f74654f626a656374d361b4910c61331e0300007078707732000a556e696361737452656600093132372e302e302e310000e25c35c1c19f1dabc88b353650820000017d739f868f80010178

RMI ReturnData - 0x51
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_BLOCKDATA - 0x77
Length - 15 - 0x0f
Contents - 0x01353650820000017d739f868f8008
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 25 - 0x00 19
Value - rmi.server.HelloImpl_Stub - 0x726d692e7365727665722e48656c6c6f496d706c5f53747562
...

ReturnData数据包就包含了RMI服务端的IP和端口

14

之后RMI客户端就可以直接去访问RMI服务端上对应类的方法。

tcp.stream eq 18

一上来还是先进行三次握手

15

之后是RMI服务端和RMI客户端的一个验证过程,服务端询问客户端是否是127.0.0.1,如果客户端返回响应,则代表客户端需要RMI服务

16

客户端在做出回应同时,继续请求127.0.0.1,这一步是调用java.rmi包中的一些类

17

之后完成了RMI CallReturnData的过程

18

Raw数据包中可以看出

19

20

21

这一步完成之后,RMI客户端会将参数传输给RMI服务端,服务端会在本地执行后返回结果。

22

服务端会将参数带入sayHello()函数执行,并将结果返回给客户端

23

最后进行TCP四次挥手并结束此次调用流程。