一、简介
1、RMI
远程方法调用(Remote Method Invocation)
,一种用于实现远程过程调用(RPC)(Remote procedure call)的Java API,是允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法,使得某一台计算机上的对象在调用另外一台计算机上的方法时使用的程序语法规则和在本地机上对象间的方法调用的语法规则一样
。 这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。 它的实现依赖于Java虚拟机(JVM),因此它仅支持从一个JVM到另一个JVM的调用。
2、Java RMI
Java 远程方法调用,即 Java RMI(Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作
。RMI全部的宗旨就是尽可能简化远程接口对象的使用。
3、RPC 与 RMI
RPC(远程过程调用)和 RMI(远程方法调用)是两种可以让用户从一台电脑调用不同电脑上面的方法的的机制(也可以称作规范、协议)。两者的主要不同是他们的使用方式或者称作范式,RMI 使用面向对象的范式,也就是用户需要知道他调用的对象和对象中的方法
;RPC 不是面向对象也不能处理对象,而是调用具体的子程序
。
(1)RPC 远程过程调用
RPC(Remote Procedure Call Protocol)远程过程调用协议,
通过网络从远程计算机上请求调用某种服务
。一次 RPC 调用的过程大概有 10 步:
1)执行客户端调用语句,传送参数
2)调用本地系统发送网络消息
3)消息传送到远程主机
4)服务器得到消息并取得参数
5)根据调用请求以及参数执行远程过程(服务)
6)执行过程完毕,将结果返回服务器句柄
7)服务器句柄返回结果,调用远程主机的系统网络服务发送结果
8)消息传回本地主机
9)客户端句柄由本地主机的网络服务接收消息
10)客户端接收到调用语句返回的结果数据
(2)RMI 远程方法调用
RMI:远程方法调用(Remote Method Invocation)。
能够让在客户端 Java 虚拟机上的对象像调用本地对象一样调用服务端 Java 虚拟机中的对象上的方法
。
RMI 远程调用步骤:1)客户调用客户端辅助对象 stub 上的方法;
2)客户端辅助对象 stub 打包调用信息(变量,方法名),通过网络发送给服务端辅助对象 skeleton;
3)服务端辅助对象 skeleton 将客户端辅助对象发送来的信息解包,找出真正被调用的方法以及该方法所在对象;
4)调用真正服务对象上的真正方法,并将结果返回给服务端辅助对象 skeleton;
5)服务端辅助对象将结果打包,发送给客户端辅助对象 stub;
6)客户端辅助对象将返回值解包,返回给调用者;
7)客户获得返回值;
(3)RPC 与 RMI 的区别
1)方法调用方式不同:
RMI 中是通过在客户端的 Stub 对象作为远程接口进行远程方法的调用。每个远程方法都具有方法签名。如果一个方法在服务器上执行,但是没有相匹配的签名被添加到这个远程接口(stub)上,那么这个新方法就不能被 RMI 客户方所调用。
RPC 中是通过网络服务协议向远程主机发送请求,请求包含了一个参数集和一个文本值,通常形成 “classname.methodname(参数集)” 的形式。RPC 远程主机就去搜索与之相匹配的类和方法,找到后就执行方法并把结果编码,通过网络协议发回。2)适用语言范围不同:
RMI 只用于Java;
RPC 是网络服务协议,与操作系统和语言无关
。3)调用结果的返回形式不同:
Java 是面向对象的,所以 RMI 的调用结果可以是对象类型或者基本数据类型;RMI 的结果统一由外部数据表示,这种语言抽象了字节序类和数据类型结构之间的差异。
二、为什么有 RMI?
首先看本地对象方法的调用:
ObjectClass objectA = new ObjectClass();
String str = objectA.Method();
当我们的程序和程序中调用的对象在同一个JVM中时,我们可以直接调用。
但是,如果objectA对象在JVMa上,而 我们的程序在JVM b上,而且想访问JVM a上的objectA对象方法,如何做呢?对于JVM b上的应用程序来说,是不知道JVM a上创建的ObjectClass实例对象名称是什么,那么JVM b上应用程序又如何能够知道呢?
这时候,RMI横空出世,就解决了这个问题。
三、RMI 原理图
1、架构图
2、数据交互图
方法调用从客户对象经占位程序(Stub)、远程引用层(Remote Reference Layer)和传输层(Transport Layer)向下,传递给主机,然后再次经传输层,向上穿过远程调用层和骨干网(Skeleton),到达服务器对象。
● 占位程序扮演着远程服务器对象的代理的角色,使该对象可被客户激活。
● 远程引用层处理语义、管理单一或多重对象的通信,决定调用是应发往一个服务器还是多个。
● 传输层管理实际的连接,并且追踪可以接受方法调用的远程对象。
● 服务器端的骨干网完成对服务器对象实际的方法调用,并获取返回值。
● 返回值向下经远程引用层、服务器端的传输层传递回客户端,再向上经传输层和远程调用层返回。最后,占位程序获得返回值。
要完成以上步骤需要有以下几个步骤:
1、 生成一个远程接口
2、 实现远程对象(服务器端程序)
3、 生成占位程序和骨干网(服务器端程序)
4、 编写服务器程序
5、编写客户程序
6、 注册远程对象
7、 启动远程对象由于有RMI系统的支持,我们写RMI应用程序时只需要继承相关类,实现相关接口就可以了。也就是说,我们只需要
定义接口
、接口实现
、客户端程序
和服务端程序
就可以了。
四、RMI 核心组件
RMI由3个部分构成:
第一个是RMI Registry
(JDK提供的一个可以独立运行的程序,在bin目录下);
第二个是server端程序
,对外提供远程对象;
第三个是client端程序
,想要调用远程对象的方法。
Registry(注册表)是放置所有服务器对象的命名空间。
每次服务端创建一个对象时,它都会使用bind()或rebind()方法注册该对象,这些是使用称为绑定名称的唯一名称注册的。
要调用远程对象,客户端需要获得该对象的引用(如HelloRegistryFacade):即通过服务端绑定的名称(HelloRegistry)从注册表中获取对象(lookup()方法)。
五、Java RMI 示例
本示例是client端调用server端远程对象的加减法方法。
1、远程接口
(1)定义远程服务接口类(Converter.java)
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface IRemoteMath extends Remote {
// 所有方法必须抛出RemoteException
public double add(double a, double b) throws RemoteException;
public double subtract(double a, double b) throws RemoteException;
}
●
必须继承Remote接口
:在Java中,只有一个类extends了java.rmi.Remote接口,才可成 为存在于服务器端的远程对象, 供客户端访问并提供一定的服务。
● 所有参数和返回类型必须序列化(因为要网络传输)。
●任意远程对象都必须实现此接口
。
● 只有在“远程接口” (扩展 java.rmi.Remote 的接口)中指定的这些方法才可被远程调。。
●接口中的所有方法必须抛出RemoteException
(2)实现远程接口
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import remote.IRemoteMath;
//必须继承UnicastRemoteObject,以允许JVM创建远程的存根/代理
public class RemoteMath extends UnicastRemoteObject implements IRemoteMath {
// 这个实现必须有一个显式的构造函数,并且要抛出一个RemoteException异常
protected RemoteMath() throws RemoteException {
}
@Override
public double add(double a, double b) throws RemoteException {
return (a+b);
}
@Override
public double subtract(double a, double b) throws RemoteException {
return (a-b);
}
}
● 远程对象必须实现java.rmi.server.UniCastRemoteObject类,构造函数中将生成stub和skeleton,这样才能保证客户端访问获得远程对象时,该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为“存根”, 而服务器端本身已存在的远程对象则称之为“骨架”。其实此时的
存根是客户端的一个代理
,用于与服务器端的通信,而骨架也可认为是服务器端的一个代理
,用于接收客户端的请求之后调用远程方法来响应客户端的请求。
●必须有一个显式的构造函数,并且要抛出一个RemoteException异常
。
2、服务器端
创建RemoteMath类的实例并在RMI Registry中注册。
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import remote.IRemoteMath;
public class RMIServer {
public static void main(String[] args) {
try {
// 创建一个远程对象
IRemoteMath remoteMath = new RemoteMath();
// 创建并导出指定port请求的本地主机上的 Registry(远程对象注册表)实例,默认端口是1099。
LocateRegistry.createRegistry(1099);
//返回本地主机在默认注册表端口 1099 上对远程对象 Registry 的引用。
Registry registry = LocateRegistry.getRegistry();
// 把远程对象注册到RMI注册服务器上,并命名为HelloRegistry
registry.bind("Compute", remoteMath);
System.out.println("======= 启动RMI服务成功! =======");
// 如果不想再让该对象被继续调用,使用下面一行
// UnicastRemoteObject.unexportObject(remoteMath, false);
} catch (Exception e) {
e.printStackTrace();
}
}
}
注册远程对象,向客户端提供远程对象服务:远程对象是在远程服务上创建的,你无法确切地知道远程服务器上的对象的名称。但是,
将远程对象注册到RMI Service之后,客户端就可以通过RMI Service请求到该远程服务对象的stub了,利用stub代理就可以访问远程服务对象了
。
3、客户端
客户端向服务端请求远程对象服务
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import remote.IRemoteMath;
public class MathClient {
public static void main(String[] args) {
try {
//返回指定 host 在默认注册表端口 1099 上对远程对象 Registry 的引用。
Registry registry = LocateRegistry.getRegistry("localhost");
//返回与指定 name 关联的远程对象的引用(一个 stub)
IRemoteMath remoteMath = (IRemoteMath)registry.lookup("Compute");
// 调用远程对象的方法
double addResult = remoteMath.add(1.5, 3.5);
System.out.println("1.5 + 3.5 = " + addResult);
double subResult = remoteMath.subtract(1.5, 3.5);
System.out.println("1.5 - 3.5 = " + subResult);
}catch(Exception e) {
e.printStackTrace();
}
}
}
4、运行结果
六、相关 API
1、Registry类
●
static void bind(String name, Remote obj)
将指定 name 绑定到远程对象。
●static String[] list(String name)
返回在注册表中绑定的名称所组成的数组。
●static Remote lookup(String name)
返回与指定 name 关联的远程对象的引用(一个 stub)。
●static void rebind(String name, Remote obj)
将指定名称重新绑定到一个新的远程对象。
●static void unbind(String name)
销毁与远程对象关联的指定名称的绑定。
2、LocateRegistry类
●
static Registry createRegistry(int port)
创建并导出接受指定 port 请求的本地主机上的 Registry 实例。
●static Registry createRegistry(int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf)
在本地主机上创建并导出 Registry 实例,该本地主机使用的是与该实例通信的自定义套接字工厂。
●static Registry getRegistry()
返回本地主机在默认注册表端口 1099 上对远程对象 Registry 的引用。
●static Registry getRegistry(int port)
返回本地主机在指定 port 上对远程对象 Registry 的引用。
●static Registry getRegistry(String host)
返回指定 host 在默认注册表端口 1099 上对远程对象 Registry 的引用。
●static Registry getRegistry(String host, int port)
返回指定的 host 和 port 上对远程对象 Registry 的引用。
●static Registry getRegistry(String host, int port, RMIClientSocketFactory csf)
返回本地创建的指定 host 和 port 上对远程对象 Registry 的远程引用。
LocateRegistry类与Registry类
● LocateRegistry 用于获取特定主机(包括本地主机)上的远程对象注册表的引用,或用于创建一个接受对特定端口调用的远程对象注册表。
● 通过LocateRegistry类方法获取到Registry对象引用(一个stub),然后我们可以通过该Registry对象引用对“远程对象注册表”进行操作。
七、将 RMI 的提供者和客户端部署到不同的机器上
思路:将服务端对外提供的接口打包到jar包中,提供给客户端。
工程分为两个,一个是服务端工程,一个是客户端工程,其中服务端工程我写成了两个模块,一个接口模块,一个服务模块,接口模块的会打成jar包提供给客户端,而服务端的服务模块直接引入接口模块使用就好。