RMI原理

RMI全称是Remote Method Invocation,远程方法调用。它使客户机上运行的程序可以调用远程服务器上的对象。

RMI原理

RMI包括三个组件:

  • Registry:提供服务注册与服务获取。
  • Server:远程方法的提供者,并向Registry注册自身提供的服务
  • Client:远程方法的消费者,从Registry获取远程方法的相关信息并调用

通常,registry与Server在同一个机器中

RMI 的工作流程:

RMI底层通讯采用了Stub(运行在客户端)和Skeleton(运行在服务端)机制。
Stub对象是一个本地对象,它实现了远程对象向外暴露的接口,可以理解为Stub对象是远程对象在本地的一个代理,当客户端调用方法的时候,Stub对象会将调用通过网络传递给远程对象。而Server端也有一个类似的对象,即Skeleton,他接收Stub对象发送的信息块,并进行处理。

RMI的使用

实现RMI需要用到:

  • java.rmi:提供客户端需要的类、接口和异常;
  • java.rmi.server:提供服务端需要的类、接口和异常;
  • java.rmi.registry:提供注册表的创建以及查找和命名远程对象的类、接口和异常;

server端

定义远程接口

首先需要定义一个远程接口,其实现java.rmi.Remote接口的类或者继承java.rmi.Remote接口的所有接口都是远程对象,这个远程对象中可能有很多个方法,但是只有在远程接口中声明的方法才能从远程调用。

1
2
3
4
5
6
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RmiHello extends Remote {
String hello(String name) throws RemoteException;
}

接口的每个方法都必须声明抛出java.rmi.RemoteException异常,该异常是使用RMI时可能抛出的大多数异常的父类。

开发远程接口的实现类

接口的实现类应该直接或者间接继承java.rmi.server.UnicastRemoteObject类,该类提供了很多支持RMI的方法,具体来说,这些方法可以通过JRMP协议导出一个远程对象的引用,并通过动态代理构建一个可以和远程对象交互的Stub对象。

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

public class RmiHelloImpl extends UnicastRemoteObject implements RmiHello{
// 该构造期必须存在,因为集继承了UnicastRemoteObject类,其构造器要抛出RemoteException
public RmiHelloImpl()throws RemoteException{
super();
}

@Override
public String hello(String name) throws RemoteException {
System.out.println("hello rmi");
return "hello" + name;
}
}

创建Registry,并将上述的类实例化后进行绑定

1
2
3
4
5
6
7
8
9
10
11
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RmiServer {

public static void main(String[] args) throws Exception {
RmiHelloImpl hello = new RmiHelloImpl();
LocateRegistry.createRegistry(1099); // 创建Registry
Naming.rebind("rmi://127.0.0.1:1099/hello", hello); // 将实例化后的对象进行绑定
}
}

client端

客户端可以通过Naming.lookup()获取该远程对象的引用。
需要注意的是,远程调用的接口在客户端本地必须可用,不然无法指定要调用的方法,而且其全限定名必须与服务器上的对象完全相同。

1
2
3
4
5
6
7
8
import java.rmi.Naming;

public class RmiClient {
public static void main(String[] args) throws Exception {
RmiHello hello = (RmiHello) Naming.lookup("rmi://127.0.0.1:1099/hello");
System.out.println(hello.hello("test123"));
}
}

除此之外,客户端还可以使用Naming.list来获取所有的远程对象

RMI的通信过程

这里借用P神用wireshark抓到的流量:

对于一次远程调用,建立了两次TCP连接,第一次连接是客户端与Registry进行通信,客户端向远端发送了⼀一个”Call”消息,远端回复了一个”ReturnData”消息。之后进行了第二次连接。

对于这整个过程,结合上面的例子:

  1. 首先客户端连接Registry,发送Call消息,向其寻找名字为hello的对象
  2. Registry返回一个ReturnData消息,内容为hello对象的信息
  3. 客户端反序列化该对象信息,发现该对象是⼀个远程对象,得到远程的ip和端口
  4. 客户端与该ip和端口建立连接,在这个连接中,执行真正的远程调用

需要注意的是,远程调用实在server端执行的,client端得到的只是执行后的返回值。

参考

P神的《Java安全漫谈》——RMI篇
https://blog.csdn.net/lmy86263/article/details/72594760
https://javasec.org/javase/RMI/

鸣谢

感谢我的小兔兔@兔子一直陪在我身边,要一直一直爱你!

文章作者: Dar1in9
文章链接: http://dar1in9s.github.io/2022/09/13/Java/RMI/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Dar1in9's Blog