什么是RMI
RMI (Remote Method Invocation) 模型是一种分布式对象应用,使用 RMI 技术可以使一个 JVM 中的对象,调用另一个 JVM 中的对象方法并获取调用结果。这里的另一个 JVM 可以在同一台计算机也可以是远程计算机。因此,RMI 意味着需要一个 Server 端和一个 Client 端。
Server 端通常会创建一个对象,并使之可以被远程访问。
这个对象被称为远程对象。Server 端需要注册这个对象可以被 Client 远程访问。
Client 端调用可以被远程访问的对象上的方法,Client 端就可以和 Server 端进行通信并相互传递信息。
RMI 在构建一个分布式应用时十分方便,它和 RPC 一样可以实现分布式应用之间的互相通信,甚至和现在的微服务思想都十分类似。
RMI (Remote Method Invocation) 分布式对象应用的设计可以方便项目的解耦,和微服务,RPC相关思想如出一辙。
RMI和RPC的不同点呢?
RPC是一种思想,一种类似协议的约定,只要是通过网络从而调有远端的某种服务,那么这就是RPC
RMI可以说是Java对RPC的具体实现,或说RMI是RPC的java版的细化要求
Java中所有东西皆是对象,所以没有函数,只有方法(绑定了对象)所以RPC的解释是面向过程的,RMI的解释是面向对象的
不管是何种实现,本质都是为了解决一个问题:我想要调用远程的一个函数或者方法或者其他什么东西的时候,我只需要像调用本地的一样,即可。
知识补充
补充:RPC?
RPC是远程过程调用(Remote Procedure Call) RPC 的主要功能目标是让构建分布式计算(应用)更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。为实现该目标,RPC 框架需提供一种透明调用机制,让使用者不必显式的区分本地调用和远程调用。
RPC的出现可以使得程序直接像调用本地函数一样调用远程的方法,如果调用远程方法时设计到的传入参数是一个实例化对象,那么就会涉及到序列化与反序列化。
比如需要将Apple a 中的 name 成员变量提取处理后返回处理后的Apple a1
一次完整的RPC逻辑
到此,完整的rpc逻辑应该就是这样的:
- client进行connect连接 (创建连接,如果需要可靠第一,就选择TCP连接)
- client序列化req(否则无法传输)
- client根据约定的协议编码,向server发送编码后的数据(编码是为了防止传输出错)
- server接收到数据,解码得到方法名和序列化过后的req二进制流
- server根据方法名找到req的类型,反序列化得到req对象
- server调用本地方法得到res
- server序列化res
- server根据约定的协议编码并发送数据
- client接收到数据并解码
- client反序列化得到res
RPC的目标就是将这些步骤都封装起来,让用户对这些细节透明,用户只知道调用的输入,并最后处理输出即可
RMI是怎么设计的
RMI 中是通过在客户端的Stub对象作为远程接口进行远程方法的调用。 每个远程方法都具有方法签名。 如果一个方法在服务器上执行,但是没有相匹配的签名被添加到这个远程接口(stub)上,那么这个新方法就不能被RMI客户方所调用。
基本流程图:
从图中可以看到,Client 端有一个被称 Stub 的东西,有时也会被成为存根,它是 RMI Client 的代理对象,Stub 的主要功能是请求远程方法时构造一个信息块,RMI 协议会把这个信息块发送给 Server 端。
这里可以了解到的东西
- Client被Stub(存根)代理,所以 Client 眼中只有 Stub。即相对于Client,具体实现细节被Stub隐藏。
- Stub 会请求远程方法,并在这时发送一个信息块
- 信息块是Stub构造的
这个信息块由几个部分组成:
- 远程对象标识符。
- 调用的方法描述。
- 编组后的参数值(RMI协议中使用的是对象序列化)
尝试理解这个信息块的话,信息块内容的作用分别为
- 尝试指定一个唯一的对象
- 指定唯一方法
- 传递参数(参数如果是对象的话 – 其实JAVA里万物皆对象 – 需要序列化对象)
如果在本地调用的话容易实现,远程调用就需要指定唯一性
|
|
既然 Client 端有一个 Stub 可以构造信息块发送给 Server 端,那么 Server 端必定会有一个接收这个信息快的对象,称为 Skeleton
它主要的工作是:
- 解析信息快中的调用对象标识符和方法描述,在 Server 端调用具体的对象方法。
- 取得调用的返回值或者异常值。
- 把返回值进行编组,返回给客户端 Stub.
这里可以了解到的东西
- Server端有一个接受信息块的东西称之为 Skeleton
- Skeleton 也可能是作为Server端的代理,对Server隐藏实现细节
- Skeleton 获得信息块后,取出值交给Server端,并从Server端获得结果(可能是返回值,异常值)
- Skeleton 对相关结果进行 编码 发送回Client端
所以,RMI的整个工作流程即为
RMI工作流程
- 首先,服务器创建一个远程对象并将其注册到注册表中(注册表中写入Stub)
- 客户端可以获取注册表中存储的对象的引用(Stub)
- 当客户端调用远程对象的方法时,实际上会在与客户端 JVM 在同一 JVM 中的存根对象(Stub)上调用该方法。
- 存根对象(Stub)会创建一条消息,其中包含方法的名称以及其参数(称为封装),并将此消息发送到位于服务器 JVM 中的相关骨架对象(Skeleton)。
- 骨架对象(Skeleton)会从消息中提取方法名和参数(称为解封装),并调用与其关联的远程对象上的适当方法。
- 远程对象执行该方法并将返回值传回骨架对象(Skeleton)。
- 骨架对象(Skeleton)再将返回值封装在消息中并将此消息发送到存根对象(Stub)。
- 存根对象(Stub)从消息中解封装返回值,并将该值返回给客户端程序
看到这里有读者或许会有疑惑:Stub不是在Client端的吗,为什么和服务器沾边了?
Stub是Client使用,但是是由Server端创建的,Client 在第一次尝试远程调用前需要通过网络在注册表中取出Stub,之后的再次使用就可以不再重复此过程,直接使用本地已经获取的Stub即可
注意:解答中的Stub可复用的前提是Client端只需要远程调用一个对象,如果需要调用多个不同类对象的话,每个类对象在第一次调用前需要获得Stub
相关术语总结
- RMIRegistry RMI注册表,RMIServer负责将stubs(存根)注册到RMIRegistry,RMIClient从RMIRegistry获取stubs(存根)。
- RMIServer RMIServer负责创建Remote Object(远程对象),并将之导出到 JAVA RMI runtime,远程对象必须被导出到JAVA RMI runtime,这样该远程对象才能接受远程调用。
- RMIClient RMIClient负责发起远程方法调用。
- Remote Object RMIServer负责创建Remote Object(远程对象),并将之导出到 JAVA RMI runtime。
- stubs(存根) JAVA RMI使用一种特殊的类(这些类被称作stubs)来发起对远程对象的方法调用。远程对象导出的结果形成了这种特殊的类(stubs)。stubs与Remote Object实现的接口是一样的,并且包含hostname和port,hostname和port与Remote Object能够形成对应关系。stubs实例实际上就是Remote Object(远程对象)
RMI代码实现
假设需求:Client 端需要查询用户信息,而用户信息存在于 Server 端,所以在 Server 端开放了 RMI 协议接口供客户端调用查询,返回是一个对象
所以Client调用前需要:
- Server开放RMI协议接口
RMI Server
Server 端主要是构建一个可以被传输的类 User,一个可以被远程访问的类 UserService,同时这个对象要注册到 RMI 开放给客户端使用。
- 首先User类需要被传输,所以需要继承 Serializable,使得对象可以被序列化,序列化后的对象才能传输。
|
|
User类只是作为可以传输数据的存在,需要一个类来进行远程通信,通信传输的是User类的实例化对象
所以设计一个UserService类
但这种类不是随意实现的,这种类的具体实现官方给了相关规范:
需要先设置一个接口,再有一个类实现这个接口
所以需要先设计接口
所以有
- UserService 接口
官方对此类接口的规定:需要继承 Remote 类,方法需要抛出 RemoteException
|
|
其次需要设计一个实现了接口的类
- UserServiceImpl 接口实现类
官方对实现类的规定:需要继承 UnicastRemoteObject 类,且需要实现刚才定义的接口
|
|
- 最后是进行通信的主程序的设计
指定对象作为远程访问对象(也叫注册),设置相关参数(比如设置访问端口)
服务端绑定 UserService 对象作为远程访问的对象,启动时端口设置为 1900
解答:代码中设置了具体对象url和对应绑定端口的,所以指定URL路径和端口,可以理解为唯一对象
|
|
新疑问:
-
这里为什么没有绑定 UserServiceImpl 对象而是 UserService
-
LocateRegistry.createRegistry(1900);
这里的1900需要和Naming.rebind("rmi://localhost:1900/user", userService);
中的1900对应吗(是的,详见这里,所以第一步是启动RMI Registry,第二步绑定的时候需要寻找到RMI Registry)
整个Server端的设计是:
- 主程序使用设计好的通信类
- 通信类使用设计好的可序列化对象进行传输
之后是Client端
RMI Client
Client 端简单的多。直接引入可远程访问和需要传输的类,通过端口和 Server 端绑定的地址,就可以发起一次调用。
|
|
疑问:
- Client此处的User类和UserService接口如何确保和远端的User类相同?(可选方案:提前商议,或者默认信任)
运行测试
运行RmiServer.java和RmiClient.java
Server
Client
Client 查询不存在(修改userID变量)
补充
- RmiRegistry 是需要启动的,如果没有注册表,那么服务端无法存入Stub
RmiRegistry 可以单独使用命令启动,也可以使用代码启动
|
|
|
|
|
|
- 获取 RmiRegistry 中的实例对象(Stub)
|
|
但是上个例子中使用的是Naming.lookup
,Naming.lookup
只是封装了相关查询,所以底层还是LocateRegistry.getRegistry
RMI 的另一个例子
RMI Server
|
|
RMI Client
|
|
这个例子的每行代码都很清晰的话,说明已经理解RMI的代码实现了
RMI通信流量
此图为大佬的RMI通信纯净的流量截图,其中 135.1 为Client 135.142 为Server
可以看到整个通信是有两次TCP连接的:
- 第一次是Client连接 Server 的1099端口
- 第二次是Client 连接 Server 的 33769 端口
第二次的端口号存在于第一次的ReturnData中。
最后远程的方法执行是在RMI Server中执行,第二个例子中的System.out.println("call from");
即可证明
解决疑问
1.Client访问静态类方法?