JavaSec-Remote Method Invocation
学习大纲
1 | 1. 关于RMI |
关于RMI
RMI概述
RMI-Remote Method Invocation顾名思义即为Java
的远程方法调用;是基于注册中心和服务来进行实现;可以用于实现微服务。RMI
用于构建分布式应用程序,RMI
实现了Java
程序之间跨JVM
的远程通信。
它的功能是让Java
的某一台虚拟机调用另外一台虚拟机中对象的方法,是Java
独有的另一种机制。非常的灵活;再一次的为攻击者提供了方便。
在网络传输过程中,RMI的对象是通过序列化的方式进行编码传输的,这也就意味着RMI在接受到序列化编码的对象之后会进行反序列化。
RMI的三层架构
RMI是由三层架构模式来实现的
- Client-客户端:客户端调用服务端的方法
- Server-服务端:远程方法调用的提供者,也是代码执行真正的地方。在执行结束之后会返回一个代码的执行结果给客户端
- Registry-注册中心:用于客户端查询要调用的方法的引用;其本质是map(相当于字典)
Stubs and Skeletons
而为了屏蔽网络通信的复杂性时,RMI引入了两个概念,分别是Stubs(客户端存根)以及Skeletons(服务端骨架),当客户端试图调用一个远端的对象时,实际调用的是客户端本地的一个代理类(Proxy),这个代理类就称为Stub,而在调用远端的目标类之前也会经过一个对应的代理类Skeletons,它从Stub中接收远程方法调用并传递给真实的类。Stubs 以及 Skeletons 的调用对于 RMI
服务的使用者来讲是隐藏的,我们无需主动的去调用相关的方法。但实际的客户端和服务端的网络通信时通过 Stub 和 Skeleton 来实现的。
RMI的运行机制
调用的机制大概如下:
- RMI客户端在调用远程方式是会先创建
Stub(sun.rmi.registryImpl_Stub)
Stub
会将Remote对象传递给远程引用层(java.rmi.server.RemoteRef)
并创建java.rmi,server.RemoteCall(远程调用)
对象- RMI客户端的远程引用层传输
RemoteCall
序列化后的请求信息通过Socket
连接的方法传输到RMI服务端的远程引用层 - RMI客户端的
远程引用层(sum.rmi.server.UnicasServerRef)
收到请求会请求传输给Skeleton(sun.rmi.registery.RegisterImpl_Skel#diapatch)
Skeleton
调用客户端请求:bind
、list
、lookup
、rebind
、unbind
,如果是lookup
则查找RMI服务名
绑定的接口对象,序列化该对象并通过RemoteCall
传输到客户端。- RMI客户端反序列化服务端结果,获取远程对象的引用
- RMI客户端调用远程方法,
RMI服务端
反射调用RMI服务实现类
的对应方法并序列化执行结果返回给客户端。 - RMI客户端反序列化
RMI
远程方法调用结果。
RMI Registry
就像⼀个⽹关,他⾃⼰是不会执⾏远程⽅法的,但RMI Server
可以在上⾯注册⼀个Name
到对象的绑定关系;RMI Client
通过Name
向RMI Registry
查询,得到这个绑定关系,然后再连接RMI Server
;最后,远程⽅法实际上在RMI Server
上调⽤。
Remote Method Invocation
远程对象
使用远程方法调用,必然会涉及参数的传递和执行结果的返回。参数或者返回值可以是基本数据类型,当然也有可能是对象的引用。所以这些需要被传输的对象必须可以被序列化,这要求相应的类必须实现java.io.Serializable
接口,并且客户端的serialVersionUID
字段要与服务器端保持一致。
任何可以被远程调用方法的对象必须继承java.rmi.Remote
接口,远程对象的实现类必须继承UnicastRemoteObject
类(这个类用于实现远程对象。一个类通过继承UnicastRemoteObject
并实现一个或多个远程接口,可以成为一个远程服务对象,能够接收远程方法调用。)。如果不继承UnicastRemoteObject
类,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()
静态方法,如下:
java.rmi.RemoteException:RemoteException
是一个受检查的异常,表示在RMI
调用过程中可能发生的异常情况;当远程方法抛出RemoteException
时它会被传递给客户端,以便客户端可以捕获处理该异常。在RMI
中,远程接口的方法必须声明 可能抛出RemoteException
。
java.rmi.server.UnicastRemoteObject:UnicastRemoteObject
是实现远程对象的基类,它提供了将对象导出为远程对象的功能。当一个类继承自UnicastRemoteObject
并实现了一个远程接口时,该类的实例可以被注册和访问作为远程对象。
继承UnicastRemoteObject的时候构造函数
1 | package learn.rmi; |
tips
此时上述代码定义了一个RMIHello类为远程对象,此时该远程对象继承了UnicastRemoteObject类并实现了IHello类。此时它的构造函数用于初始化对象并提供一个打印功能然后返回字符串。
没有继承UnicastRemoteObject的时候构造函数
1 | package learn.rmi; |
IHello.java
1 | package learn.rmi; |
tips
这段代码定义了一个RMI的远程接口IHello
,其中包含了一个方法sayHello
,客户端可以通过该方法向远程对象发送消息并获取返回接口。
IHello
是客户端和服务端共用的接口(客户端本地必须有远程对象的接口,不然无法指定要调用的方法,而且其全限定名必须与服务器上的对象完全相同),RMIHello
是一个服务端远程对象,提供了一个sayHello
方法供远程调用。
以上就构成了RMI Server
- 一个继承了
java.rmi.Remote
的接口IHello
,内部定义了我们将要远程调用的对象方法sayHello()
- 一个实现了此接口的类
RMIHello
- 一个主类
RMIServer
,用来创建Registry
,并将类RMIHello
实例化后绑定到一个地址
对象调用过程
本地对象的调用
1 | ObjectClass objectA = new ObjectClass(); |
远程对象的调用
但是如果对象在JVM A上,而客户端在JVM B上;那么此时B如何访问A的对象呢?此时就需要利用到RMI机制了
在JVM之间通信时,RMI对远程对象和非远程对象的处理方式是不一样的,它并没有直接把远程对象复制一份传递给客户端,而是传递了一个远程对象的Stub(存根),Stub相当于远程对象的引用或者代理。Stub对开发者是透明的,客户端可以像调用本地方法一样直接通过它来调用远程方法。Stub中包含了远程对象的定位信息,如Socket端口、服务端主机地址等等,并实现了远程调用过程中具体的底层网络通信细节。而位于服务器端的Skeleton(骨架),能够读取客户端传递的方法参数,调用服务器方的实际对象方法, 并接收方法执行后的返回值。所以RMI远程调用逻辑大致是这样的
从逻辑上来看,数据是在Client和Server之间横向流动的,但是实际上是从Client到Stub,然后从Skeleton到Server这样纵向流动的。
具体的通信流程如下
- Server监听一个端口,这个端口是JVM随机选择的
- Client并不知道Server远程对象的通信地址和端口,但是位于Client的Stub中包含了这些信息,并封装了底层网络操作。Client可以调用Stub上的方法,并且也可以向Stub发送方法参数。
- Stub连接到Server监听的通信端口并提交参数
- Server执行具体的方法,并将结果返回给Stub
- Stub返回执行结果给Client。因此在Clinet看来,就好像是Stub在本地执行了这个方法。
那么问题来了,位于Client上的Stub是怎么获取到远程Server的通信信息的呢?这就需要使用RMI Registry了。
RMI Registry
RMI Registry的注册
JDK提供了一个RMI注册表(RMI Registry)来解决这个问题。RMI Registry也是一个远程对象,默认监听在1099端口上,可以使用代码启动RMI Registry,也可以使用rmiregistry命令。
要注册远程对象,需要RMI URL和一个远程对象的引用
1 | private void register() throws Exception{ |
在主类的register()
方法中,我们首先实例化了一个将被远程调用的类RMIHello
,然后使用 LocateRegistry.createRegistry(port)
在本地的某个端口上创建了一个Registry
。最后使用Naming.bind()
将实例化对象和地址上的hello
绑定在一起,作为远程对象的名字。注意这里使用的是rmi://
协议。这样,我们就完成了对RMI Registry的注册。
RMI Registry的使用
注册完RMI Registry以后,我们将要调用的远程对象已经和服务器端的某个地址绑定在了一起。那么Clinet又是怎么从Registry获取服务器远程对象信息的呢?我们创建一个简单的RMI Client,代码如下
1 | package learn.rmi; |
LocateRegistry.getRegistry()
会使用给定的主机和端口等信息在本地创建一个Stub
对象作为Registry远程对象的代理,从而启动整个远程调用逻辑。服务端应用程序可以向RMI注册表中注册远程对象,然后客户端向RMI注册表查询某个远程对象名称,来获取该远程对象的Stub。这里我们使用了registry.lookup()
来查询获取注册表中的远程对象。还有另一种写法:
1 | public static void main(String[] args) throws Exception{ |
使用了RMI Registry后,RMI的调用关系如下:
RMI的简单实现
Server
一个RMIServer
分为三部分
- 一个继承了
java.rmi.Remote
接口,其中定义了我们要远程调用的函数,比如在这个例子中的hello()
- 一个实现接口的类
- 一个主类,用来创建
Registry
,并将上面的类实例化后绑定到一个地址,这也就是我们所谓的Server
接口实现
首先是接口实现并继承Remote;并在里面创建一个hello()
方法
1 | public interface IRemoteHelloWorld extends Remote{ |
创建类用于远程调用
然后创建一个类去实现这个接口用于远程调用;继承UnicastRemoteObject
类后会使用默认socket
进行通讯,并且该类会一直运行在服务器上。如果不继承UnicastRemoteObject
类,则需要手工初始化对象,在远程对象的构造方法的调用UnicasrReomteObject.exportObject()
静态方法
1 | package com.example.RMI; //包声明 |
此时我们将重点放在类的继承和接口的实现
此时RMIServer
类继承了UnicastRemoteObject
使得该类称为远程对象,能够接受远程调用;并且此时实现了IRemoteHelloWorld
接口;这个接口继承java.rmi.remote
,定义了可以远程调用的方法。这意味着RMIServer
必需提供IRemoteHelloWorld
接口的中所声明的方法的实现
接下来把目光放到实现远程方法
在这里RMIServer
实现了IRemoteHelloWorld
接口的hello()
方法;当客户端调用这个方法时会返回hello world
。方法使用@Override
注解标记,这表明该方法覆盖或实现了父类或接口中的方法。
实现调用
现在可以被远程调用的对象创建好了,接下来就是考虑如何调用了。
在Java RMI中设计了一个Registry的思想,很好理解我们可以使用注册表来查找一个远程对象的使用。通俗来说这就是一个RMI电话本,当我们想在某个人那里获取信息时(Remote Method Invocation)
就在电话本Registry
中通过这个人的名称Name
中来找到这个人的电话Reference
,并通过这个电话找到这个人Remote Object
。
这种电话本的思想,由 java.rmi.registry.Registry
和 java.rmi.Naming
来实现。这里分别来说说这两个东西。
java.rmi.Naming
java.rmi.Naming
提供了在远程注册表(Registry)中存储和获取远程对象的方法。这个类提供的每个方法都有一个 URL 格式的参数,格式如下: //host:port/name
:
- host 表示注册表所在的主机
- port 表示注册表接受调用的端口号,默认为 1099
- name 表示一个注册 Remote Object 的引用的名称,不能是注册表中的一些关键字
那么这样就好理解了,我们现在实现了服务端待调用的对象,现在我们要把他装载进“电话本”,也就是注册(registry)
注册
- 利用
LocateRegistry.createRegistry(1099);
创建注册中心 - 实例化远程对象
- 把这个实例化对象绑定name存入“电话本”
1 | package com.example.RMI; |
服务器端完整代码
IHello.java
1 | package learn.rmi; |
RMIServer.java
1 | package learn.rmi; |
Client
- 使用
Naming
在Registry
中寻找到名字是Hello的对象 - 调用远程对象的方法属性
Naming有很多种方法
这里用lookup来测试,他返回:a reference for a remote object
,远程对象的引用
- 此
Naming.lookup()
调用检查在 localhost 中运行的 RMI 注册表中是否存在名为“Hello”的绑定 - 它返回一个必须转换为我期望的任何远程接口的对象
- 然后就可以使用该对象调用接口中定义的远程方法
客户端完整代码
IHello.java
1 | package learn.rmi; |
RMIClient.java
1 | package learn.rmi; |
JRMP协议分析
Java远程方法协议(Java Remote Method Protocol,JRMP)
是特定于Java技术的、用于查找和引用远程对象的协议。这是运行在Java远
程方法调用(RMI)之下、TCP/IP
之上的线路层协议。
第一条TCP链接
首先是TCP三次握手
来建立第一条TCP
链接,客户端连接服务器的1099
端口,这里真正连接到的其实是RMI Registry
,然后二者建立JRMP链接。
随后Clinet
向Registry
发送”Call”
信息,Registry
回复”ReturnData”
。我们看一下Registry的回复内容。
1 | 0000 00 0c 29 b3 84 37 14 18 c3 e1 a9 29 08 00 45 00 ..)..7.....)..E. |
这里传输的是服务器的序列化数据。注意以上加粗倾斜的部分。\xAC\xED是Java序列化
的魔术头,该数据流往后的部分就是序列化的内容了。\xEC\x3C转换成十进制为60476
,这便是Server
在本地开放的随机端口。
我们分析一下第一条TCP链接
干了什么。首先Client
根据传入的rmi地址链接远端服务器1099端口上的RMI Registry,然后Registry
向Client
发送Server上的序列化数据
,包括IP和开放的随机端口等。
第二条TCP链接
再往下是第二个TCP链接
,Client
连接ReturnData中返回的端口
,这条TCP链接用于Client与Server之间的传输数据。实际上是Client的Stub和Server上的Skeleton之间
进行数据传输的。
两条TCP链接分别断开
再往后就是四次挥手,两条TCP链接分别断开
RMI Registry就像一个网关,Server在Registry中注册绑定在name上的远程对象,Client在Registry中根据name查询远程对象绑定信息。然后Client的Stub连接位于Server上的Skeleton,最终远程方法还是在服务器上执行。