JAVA RMI

GTL-JU Lv3

一、什么是 RMI

RMI是远程方法调用,RMI技术可以使一个java虚拟机中的对象去调用另一个java虚拟中的对象方法并获取调用结果。也就是说RMI实现了客户端调用服务端的对象方法像调用本地的对象方法。

二、RMI原理分析

既然是解决远程调用的问题,那么肯定要有client(客户端)和服务端(server),也就是方法的调用者和被调用者,从客户端-服务器模型来看,客户端程序之间调用服务端,两者之间是通过JRMP协议实现的。

这里简单了解一下JRMP协议,类似于HTTP协议,规定了客户端和服务端要满足的规范:

1
JRMP(Java远程方法协议)可以定义为特定于Java的,基于流的协议,该协议查找并引用远程对象。它要求客户端和服务器都使用Java对象。它是线级协议,在RMI下和TCP / IP上运行。

下面通过流程图去进行RMI原理的分析:

image-20230331150932629

RMI 客户端在调用远程方法时会先创建一个stub(sun.rmi.registry.RegistryImpl_Stub)也称为存根,Stub是RMI client的代理对象,Stub的主要功能是请求远程方法时构造一个信息块,然后通过RMI机制发送给客户端。

stub构造的信息块由几个部分组成:

1
2
3
1.远程对象标识符
2.调用的方法描述
3.编组后的参数值

Stub会Remote对象传递给客户端的远程引用层(java.rmi.server.RemoteRef)并创建远程调用对象(java.rmi.server.RemoteCall)

Remotecall会对RMI的服务名称和Remote进行序列化,然后通过Socket连接的方式传输到服务端的远程应用层

在上面我们看到client有一个stub构造信息块发送到服务端,那么在Skeleton就是在服务端接收这个信息的对象。

Skeleton在接收到client传递来的信息块后调用Remotecall反序列化RMI客户端传过来的序列化

然后Skeleton会处理客户端请求,调用相应服务端的对象进行调用,并将方法的返回值打包成响应消息并发送回客户端

1
2
Skeleton 接收到客户端请求后,会调用远程对象方法并返回方法的执行结果。客户端不会直接访问远程对象,而是通过 Skeleton 间接访问远程对象。Skeleton 的作用是隐藏远程对象的实现细节,使客户端可以像调用本地对象一样调用远程对象。
需要注意的是,当远程对象方法抛出异常时,Skeleton 会将异常打包成响应消息并发送回客户端。客户端需要处理这些异常,并根据需要采取相应的措施。

三、RMI代码实现

1、RMI服务端注册服务代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.anbai.sec.rmi;

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

public class RMIServerTest {

// RMI服务器IP地址
public static final String RMI_HOST = "127.0.0.1";

// RMI服务端口
public static final int RMI_PORT = 9527;

// RMI服务名称
public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/test";

public static void main(String[] args) {
try {
// 注册RMI端口
LocateRegistry.createRegistry(RMI_PORT);

// 绑定Remote对象
Naming.bind(RMI_NAME, new RMITestImpl());

System.out.println("RMI服务启动成功,服务地址:" + RMI_NAME);
} catch (Exception e) {
e.printStackTrace();//createRegistry() 或 bind() 方法抛出异常,则会在控制台上输出异常信息。
}
}

}

前几行代码定义了RMI服务的ip,端口以及名字

1
LocateRegistry.createRegistry(RMI_PORT);

LocateRegistry.createRegistry(RMI_PORT) 是在 Java RMI 中创建 RMI 注册表的方法。它将在指定的 RMI 端口上启动 RMI 注册表,并返回一个对该注册表的远程引用。

在JAVA RMI中RMI注册表是一种服务,在 RMI 中,客户端必须知道远程对象的位置(主机名和端口号),才能与之通信。RMI 注册表提供了一种机制,使客户端可以通过名称查找远程对象,而不必知道其位置。

当你在 RMI 中启动一个远程对象时,你需要将其注册到 RMI 注册表中,以便客户端可以查找和访问它。这个注册代表着将远程对象绑定到一个名称上,这个名称可以被客户端用来查找远程对象。在 Java RMI 中,这个名称通常是一个字符串,被称为绑定名称 (binding name)。

当客户端需要访问远程对象时,它可以使用 RMI 注册表来查找该对象。客户端使用绑定名称向 RMI 注册表发出请求,RMI 注册表会返回绑定名称所对应的远程对象的引用。然后客户端可以使用该引用来调用远程对象的方法。

如果 RMI 注册表已经在指定的端口上运行,那么 createRegistry() 方法将不会创建新的注册表,而是返回对现有注册表的引用。如果你希望在另一个虚拟机上创建 RMI 注册表,可以使用 LocateRegistry.getRegistry(host, port) 方法来获取对远程 RMI 注册表的引用。

1
Naming.bind(RMI_NAME, new RMITestImpl());

Naming.bind() 是 Java RMI 中用于将远程对象绑定到指定名称的方法。具体来说,它会将指定的远程对象绑定到一个指定的名称上,并将这个名称注册到 RMI 注册表中。这个名称可以用来在客户端中查找远程对象。

使用 Naming.bind() 方法绑定远程对象时,需要指定一个 URL,该 URL 包含了 RMI 注册表的主机名、端口号和绑定名称。

代码运行:

image-20230406172910934

2、RMITestImpl()类的实现

在javaRMI中如果想将一个对象作为远程对象暴露给客户端使用,这个对象必须要满足以下要求:

1
2
3
1、实现一个远程接口(即扩展java.rmi,Remote接口)
2、必须是可序列化(即实现java.serializable接口)
3、必须扩展 UnicastRemoteObject 类或 Activatable 类之一。

UnicastRemoteObject 是一个抽象类,它实现了 Remote 接口,并提供了一些默认的远程方法实现。当一个类继承了 UnicastRemoteObject 类后,它就可以直接暴露为远程对象,客户端可以通过 RMI 协议访问这个对象。

RMITestImpl()类代码:

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 com.anbai.sec.rmi;

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

public class RMITestImpl extends UnicastRemoteObject implements RMITestInterface {

private static final long serialVersionUID = 1L;

protected RMITestImpl() throws RemoteException {
super();
}

/**
* RMI测试方法
*
* @return 返回测试字符串
*/
@Override
public String test() throws RemoteException {
return "Hello RMI~";
}

}

远程接口RMITestInterface代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.anbai.sec.rmi;

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

/**
* RMI测试接口
*/
public interface RMITestInterface extends Remote {

/**
* RMI测试方法
*
* @return 返回测试字符串
*/
String test() throws RemoteException;

}

在RMITestImpl 这段代码中,定义了一个RMITestImpl类,并实现了一个RMITestInterface接口,这个类的作用是将test()方法暴露为远程方法,以便客户端可以通过RMI协议调用它。在这个类中我们重写了RMITestInterface 接口中的 test() 方法,该方法返回了一个字符串hello Rmi 。由于这个类继承了UnicastRemoteObject 因此它可以直接暴露为远程对象,客户端可以通过 RMI 协议访问它。

那么为什么要继承UnicastRemoteObject 类呢?

1
2
3
4
	1、这是因为 RMI 通过序列化和反序列化对象来进行远程通信。当客户端调用远程对象的方法时,它实际上是在向远程对象发送序列化后的方法调用请求。而远程对象接收到请求后,需要将序列化后的数据反序列化成方法调用,并执行这个方法。如果远程对象没有实现 UnicastRemoteObject 类,那么 RMI 将无法序列化和传输这个对象,也就无法将它暴露为远程对象。
2、因此,为了让一个对象可以作为远程对象暴露给客户端使用,必须将它的类继承 UnicastRemoteObject 类,并实现一个远程接口。这样,RMI 就可以将这个对象序列化并传输到客户端,客户端就可以通过 RMI 协议访问这个对象了。

3、UnicastRemoteObject 是一个抽象类,它实现了 Remote 接口,并提供了一些默认的远程方法实现。当一个类继承了 UnicastRemoteObject 类后,它就可以直接暴露为远程对象,客户端可以通过 RMI 协议访问这个对象。

由上面我们可以知道

在 Java RMI 中,如果要将一个对象暴露为远程对象,这个对象必须实现一个远程接口。这个远程接口必须继承 Remote 接口,并且其中的所有方法都必须声明抛出 RemoteException 异常。这个远程接口定义了客户端可以通过 RMI 协议调用的方法。

在这个示例代码中,RMITestImpl 类实现了一个名为 RMITestInterface 的远程接口。这个接口中只有一个方法 test(),它声明了抛出 RemoteException 异常。由于 RMITestImpl 类实现了 RMITestInterface 接口,因此它必须实现 test() 方法,并且在方法声明中也必须声明抛出 RemoteException 异常。

3、客户端代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.anbai.sec.rmi;

import java.rmi.Naming;

import static com.anbai.sec.rmi.RMIServerTest.RMI_NAME;

public class RMIClientTest {

public static void main(String[] args) {
try {
// 查找远程RMI服务
RMITestInterface rt = (RMITestInterface) Naming.lookup(RMI_NAME);

// 调用远程接口RMITestInterface类的test方法
String result = rt.test();

// 输出RMI方法调用结果
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
  1. RMIClientTest 类中定义了一个 main 方法,用于启动客户端程序。
  2. main 方法中,通过调用 Naming.lookup 方法查找指定名称的远程对象,该名称在常量 RMI_NAME 中定义。
  3. 通过将 Naming.lookup 方法的返回结果转换为 RMITestInterface 类型,获取了远程接口对象的引用 rt
  4. 通过调用 rt 对象的 test 方法,执行了远程接口的方法调用。
  5. 将远程方法调用的返回值打印到控制台。
  6. 在代码中使用了 try-catch 语句来捕获可能发生的异常,比如在远程调用时可能发生的 RemoteException 异常等。

代码运行:

image-20230406172954315

四、关于RMI实现代码和RMI机制的对照。

通过上面的学习我们可以知道Skeleton是RMI 服务器端用于接收远程方法地调用请求并将其转发到相应远程对象地中间件,skeleton通过解组远程方法调用请求,调用相应地远程对象方法,然后将结果打包发送回客户端。

RMI 注册表是用于维护对象引用的中心存储库。客户端通过查找注册表来获取对远程对象的引用。在 RMI 服务器端,Skeleton 和注册表通常是一起使用的,以便为客户端提供完整的远程方法调用服务。

具体来说,当 RMI 服务器端接收到客户端的远程方法调用请求时,Skeleton 会解组该请求并确定调用哪个远程对象的方法。然后,Skeleton 会调用相应的远程对象方法,并将结果打包成一个响应并发送回客户端。在这个过程中,Skeleton 可能需要查找注册表来获取对远程对象的引用。

因此,可以说 Skeleton 和 RMI 注册表是 RMI 服务器端的两个核心组件,它们共同协作以提供完整的远程方法调用服务。

客户端通过 Stub 对象来调用远程对象的方法。在 Java RMI 中,Stub 是客户端用于调用远程方法的代理对象,它封装了与远程对象的通信细节,使得客户端可以像调用本地对象一样调用远程对象的方法。

当客户端需要调用远程对象的方法时,它会首先从 RMI 注册表中查找远程对象的引用。然后,客户端使用引用来获取远程对象的 Stub 对象。客户端使用 Stub 对象来调用远程对象的方法,就好像调用本地对象的方法一样。当客户端调用远程方法时,Stub 对象会将方法调用打包成一个请求并发送到 RMI 服务器端。RMI 服务器端接收到请求后,会使用 Skeleton 对象来解组请求并调用相应的远程对象方法,然后将结果打包成一个响应并发送回客户端。客户端接收到响应后,Stub 对象会将响应解包并返回给客户端调用方。

  • 标题: JAVA RMI
  • 作者: GTL-JU
  • 创建于: 2023-04-08 01:28:24
  • 更新于: 2023-04-08 14:29:40
  • 链接: https://gtl-ju.github.io/2023/04/08/RMI/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。