# 前言
RMI (Remote Method Invocation) 远程方法调用,是一种调用远程位置的对象来执行方法的思想,该模型是一种分布式对象应用,使用 RMI 技术可以使一个 JVM 中的对象,调用另一个 JVM 中的对象方法并获取调用结果。这里的另一个 JVM 可以在同一台计算机也可以是远程计算机。因此,RMI 意味着需要一个 Server 端和一个 Client 端。实际上就是在一个 java 虚拟机上调用另一个 java 虚拟机的对象上的方法 RMI传输数据序列化后的数据
。下图是 RMI 的架构,在后面会详细的介绍到
# RMI 流程(代码)
这里是参考 su18 师傅的文章通过分析源码来了解这个 RMI
首先定义一个能够远程调用的接口,这个接口需要继承 Remote 接口,用来远程调用的对象作为这个接口的实例
package Clown_RMI_code; | |
import java.rmi.Remote; | |
import java.rmi.RemoteException; | |
public interface IRemoteHelloWorld extends Remote{ | |
public String hello() throws RemoteException; | |
} |
接着实现这个接口,这里继承这个接口的同时通常需要扩展 java.rmi.server.UnicastRemoteObject 类,扩展此类后,RMI 会自动将这个类 export 给远程想要调用它的 Client 端,同时还提供了一些基础的 toString 方法,在 export 时会随机绑定一个端口,监听客户端的请求,直接请求这个端口也可以通行
package Clown_RMI_code; | |
import java.rmi.RemoteException; | |
import java.rmi.server.UnicastRemoteObject; | |
public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld { | |
protected RemoteHelloWorld() throws RemoteException { | |
super(); | |
} | |
public String hello() throws RemoteException { | |
System.out.println("访问成功"); | |
return "Hello world"; | |
} | |
} |
上面这个实现接口的类实际上就是能被远程调用的对象,对于调用方法 RMI 设计了一个 Registry 的思想,类似路由表的概念,实现这个主要通过 java.rmi.registry.Registry 和 java.rmi.Naming 来实现
- 是 Java 远程方法调用(RMI)中的一个接口,提供了一个远程对象注册表。它用于将远程对象与名称绑定,然后可以使用该名称从远程客户端查找该对象。
Registry
接口定义了绑定(bind)、解绑(unbind)、列表(list)和查找(lookup)远程对象在注册表中的方法。 - 是 Java 远程方法调用(RMI)中的一个类,提供了一个命名服务,用于在远程服务器上绑定和查找远程对象。它可以将一个远程对象绑定到一个 URL 地址上,这个 URL 地址可以被客户端用来查找该远程对象。
这是一个 final 方法,url 的格式 //host:post/name:- host,表示注册表所在的主机
- port 表示注册表接受调用的端口号,默认的是 1099
- name 表示一个注册 Remote Object 的引用的名称,不能是注册表中的一些关键词
使用 LocateRegistry#createRegistry()
方法来创建注册中心,将其加入到待调用的类中进行绑定
package Clown_RMI_code; | |
import java.rmi.Naming; | |
import java.rmi.registry.LocateRegistry; | |
public class RemoteServer { | |
public static void main(String[] args) throws Exception { | |
LocateRegistry.createRegistry(1099); | |
// 创建注册中心 | |
IRemoteHelloWorld iRemoteHelloWorld = new RemoteHelloWorld(); | |
// 创建远程对象 | |
Naming.bind("rmi://127.0.0.1/Hello",iRemoteHelloWorld); | |
// 绑定 | |
} | |
} |
最后就是在客户端进行调用
package Clown_RMI_code; | |
import java.rmi.NotBoundException; | |
import java.rmi.RemoteException; | |
import java.rmi.registry.LocateRegistry; | |
import java.rmi.registry.Registry; | |
import java.util.Arrays; | |
public class RMIClient { | |
public static void main(String[] args) throws Exception, RemoteException, NotBoundException { | |
// 获取 RMI 注册表,参数为 RMI 服务器的 IP 和端口号 | |
Registry registry = LocateRegistry.getRegistry("192.168.32.6",1099); | |
// 打印出 RMI 注册表中所有已注册的服务名 | |
// System.out.println(Arrays.toString(registry.list())); | |
// 根据服务名在 RMI 注册表中查找远程对象的 stub | |
IRemoteHelloWorld stub = (IRemoteHelloWorld) registry.lookup("Hello"); | |
// 调用远程对象的方法并打印结果 | |
System.out.println(stub.hello()); | |
} | |
} |
在 RemoteServer 类中,创建注册中心后去创建了一个远程对象
# 注册中心的创建
LocateRegistry.createRegistry(1099); |
这里首先跟进代码看到
createRegistry 实际上 new 了 RegistryImpl 对象,继续跟进
这段代码是 Java RMI 中 RegistryImpl
类的构造函数实现,用于创建 RMI 注册表。如果传入的 port
参数为默认的注册表端口 Registry.REGISTRY_PORT
,并且存在安全管理器,则授权注册表使用默认端口,并使用 UnicastServerRef
类导出远程对象,从而创建注册表的 UnicastServerRef
对象。否则,直接使用 UnicastServerRef
类导出远程对象,并创建注册表的 UnicastServerRef
对象。在导出远程对象时,使用 LiveRef
类来表示远程对象的引用,同时使用 RegistryImpl::registryFilter
方法来过滤注册表的传输流。
# 远程对象创建
这个对象是继承了 UnicastRemoteObject,前面也有提到这个类会自动 export 远程,并获取 Stub,Stub 是一个代理类
首先这里会调用的静态方法 exportObject
它是 java.rmi.server.RemoteObject
类中的一个静态方法。它用于导出远程对象并返回一个存根,该存根可用于调用远程对象上的方法。
该方法有两个参数:要导出的 Remote
对象和一个 UnicastServerRef
对象。如果要导出的对象继承了 UnicastRemoteObject
类,那么该方法将设置它的 ref
字段。最后,该方法会调用 sref.exportObject(obj, null, false)
方法导出远程对象,并返回导出的远程对象的存根。
对 RemoteHelloWorld 的 exprot 的创建主要是通过 createprot 方法使用 RemoteObjectInvocationHandler 来为我们测试写的 RemoteObject 实现的 RemoteInterface 接口创建动态代理
接下来首先看 RemoteObjectInvocationHandler 这个动态代理
继承 RemoteObject 实现 InvocationHandler,因此这是一个可序列化的、可使用 RMI 远程传输的动态代理类,对于动态代理我们这里重点看一下 invoke 方法
在 invoke
方法中,首先判断传入的 proxy
参数是否是一个代理类,如果不是,则抛出一个 IllegalArgumentException
异常。然后,判断传入的 proxy
参数是否与当前的 InvocationHandler
对象匹配,如果不匹配,则抛出一个 IllegalArgumentException
异常。最后,判断要调用的方法是不是 Object
类中的方法,如果是,则调用 invokeObjectMethod
方法来处理方法调用,否则调用 invokeRemoteMethod
方法来处理远程方法调用。
在 invokeObjectMethod 中判断要调用的方法是不是 Object
类中的 equals
、 hashCode
或 toString
方法,如果是,则直接调用 Object
类中的相应方法,否则抛出一个 UnsupportedOperationException
异常。
在 invokeRemoteMethod
方法中,会将方法调用序列化成字节流,并通过 JRMP 协议发送给远程对象。远程对象收到方法调用后,会将字节流反序列化成方法调用,并执行该方法,最后将方法的返回值序列化成字节流,通过 JRMP 协议发送给客户端。
# 服务注册
实际上就是绑定过程
首先调用了 parseURL
方法来解析 name
参数,然后通过 getRegistry
方法获取了一个 Registry
对象。接下来,如果 obj
参数为 null
,则会抛出 NullPointerException
异常。最后,使用 registry.bind(parsed.name, obj)
方法将 obj
对象绑定到命名服务中。
这样一个流程来 javasec 上面总结的很详细
RMI 底层通讯采用了 Stub (运行在客户端) 和 Skeleton (运行在服务端) 机制,RMI 调用远程方法的大致如下:
- RMI 客户端在调用远程方法时会先创建 Stub (
sun.rmi.registry.RegistryImpl_Stub
)。 - Stub 会将 Remote 对象传递给远程引用层 (
java.rmi.server.RemoteRef
) 并创建java.rmi.server.RemoteCall
(远程调用) 对象。 - RemoteCall 序列化 RMI 服务名称、Remote 对象。
- RMI 客户端的远程引用层传输 RemoteCall 序列化后的请求信息通过 Socket 连接的方式传输到 RMI 服务端的远程引用层。
- RMI 服务端的远程引用层 (
sun.rmi.server.UnicastServerRef
) 收到请求会请求传递给 Skeleton (sun.rmi.registry.RegistryImpl_Skel#dispatch
)。 - Skeleton 调用 RemoteCall 反序列化 RMI 客户端传过来的序列化。
- Skeleton 处理客户端请求:bind、list、lookup、rebind、unbind,如果是 lookup 则查找 RMI 服务名绑定的接口对象,序列化该对象并通过 RemoteCall 传输到客户端。
- RMI 客户端反序列化服务端结果,获取远程对象的引用。
- RMI 客户端调用远程方法,RMI 服务端反射调用 RMI 服务实现类的对应方法并序列化执行结果返回给客户端。
- RMI 客户端反序列化 RMI 远程方法调用结果。
# RMI 流程(流量)
这里对 需要注意的是RMI被调用的方法执行在客户端
,P 牛的 JAVA 漫谈中抓包分析,但是我本地的包有较多的混淆流量流,这里直接用 P 牛的图
这个是整个流程
首先这里有一次 TCP 握手,跟踪这个流
接着是一个协议确认,客户端向服务器发送了 StreamProtocol 用于确认服务器是否支持此协议
接着是服务端向客户端发送的一个确认包确认 ip 和端口号
客户端发给服务器一个 ip 地址,这个 ip 是客户端的 ip 地址
接着发送 call 请求
服务端返回 Returndata 包
这段是 java 序列化后的内容,⾸先客户端连接 Registry,并在其中寻找 Name 是 Hello 的对象,这个对应数据流中的 Call 消息;然后 Registry 返回⼀个序列化的数据,这个就是找到的 Name=Hello 的对象,这个对应数据流中的 ReturnData 消息;客户端反序列化该对象,发现该对象是⼀个远程对象,地址在 192.18.0.1:12137 ,于是再与这个地址建⽴ TCP 连接;在这个新的连接中,才执⾏真正远程⽅法调⽤,也就是 hello ()
然后是服务端和客户端的 ping 包
最后一个输出消息包 DgcAck 指示客户端已接收到服务器返回值中的远程对象
接着就断开 tcp 连接