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 | import java.rmi.Remote; |
接口的每个方法都必须声明抛出
java.rmi.RemoteException
异常,该异常是使用RMI时可能抛出的大多数异常的父类。
开发远程接口的实现类
接口的实现类应该直接或者间接继承java.rmi.server.UnicastRemoteObject
类,该类提供了很多支持RMI的方法,具体来说,这些方法可以通过JRMP协议导出一个远程对象的引用,并通过动态代理构建一个可以和远程对象交互的Stub对象。
1 | import java.rmi.RemoteException; |
创建Registry,并将上述的类实例化后进行绑定
1 | import java.rmi.Naming; |
client端
客户端可以通过Naming.lookup()
获取该远程对象的引用。
需要注意的是,远程调用的接口在客户端本地必须可用,不然无法指定要调用的方法,而且其全限定名必须与服务器上的对象完全相同。
1 | import java.rmi.Naming; |
除此之外,客户端还可以使用Naming.list
来获取所有的远程对象
RMI的通信过程
这里借用P神用wireshark抓到的流量:
对于一次远程调用,建立了两次TCP连接,第一次连接是客户端与Registry进行通信,客户端向远端发送了⼀一个”Call”消息,远端回复了一个”ReturnData”消息。之后进行了第二次连接。
对于这整个过程,结合上面的例子:
- 首先客户端连接Registry,发送Call消息,向其寻找名字为hello的对象
- Registry返回一个ReturnData消息,内容为hello对象的信息
- 客户端反序列化该对象信息,发现该对象是⼀个远程对象,得到远程的ip和端口
- 客户端与该ip和端口建立连接,在这个连接中,执行真正的远程调用
需要注意的是,远程调用实在server端执行的,client端得到的只是执行后的返回值。
参考
P神的《Java安全漫谈》——RMI篇
https://blog.csdn.net/lmy86263/article/details/72594760
https://javasec.org/javase/RMI/
鸣谢
感谢我的小兔兔@兔子一直陪在我身边,要一直一直爱你!