Java RMI指的是远程方法调用(Remote Method Invocation)。它是一种机制,能够让在某个机器上的Java程序调用另一个机器上的Java程序的方法,用此方法调用的任何对象必须实现该远程接口。EJB就是建立在RMI基础之上的。
本节将讲解RMI的基本概念和系统原理,通过一些入门实例讲解RMI程序的开发步骤和编译运行方法。
RMI是RPC(远程过程调用)的Java版本的实现,远程过程调用是一种计算机分布式通信协议,包括Client/Server模型和分布式对象模型。
RPC(Remote Procedure Call)是计算机分布式通信协议。该协议允许运行与一台计算机的程序调用另一台计算机的子程序,而程序员无须为这个交互进行编程。
RPC总是由客户端对服务端发出一个执行若干过程的请求,并且利用客户端提供的参数,执行结果返回客户端。
为了允许不同的客户端均能访问服务端,许多RPC采用了接口描述语言IDL,方便跨平台的远程过程调用。
客户机/服务器模型Client/Server是分布式通信的一种形式。在这种新式中,客户机和服务器通信一遍交换信息。客户机/服务器的典型做法是使用底层套接字。Socket和NIO Socket编程就是套接字编程的两种Java实现
分布式对象模型
基于分布式对象的系统是一组对象的组合,这些对象以一种明确定义封装的接口把服务的请求着和服务的提供者分隔开。Java远程方法调用RMI和共用对象请求代理体系(CORBA)就是这种模型的例子。RMI仅适用于Java语言,CORBA支持多语言比较复杂。
Java远程方法调用RMI是Java编程语言里一种用于实现远程过程调用的应用程序编程接口,它使客户机上运行的程序可以调用远程服务器上的对象。RMI极大地依赖于接口。
RMI系统原理与开发步骤
RMI通信机制:RMI通常包括两个独立的程序:服务器和客户机程序
在与远程对象的通信过程中,RMI使用标准机制:Stub(存根)和Skeleton(框架)
在RMI分布式应用系统中,服务器和客户机之间的传递的Java对象必须是可序列化的对象,不可序列化的对象不能在对象流中进行传递。
RMI程序的实现步骤
将远程类的功能定义为Java接口。在Java中远程对象是实现远程接口的类的实例。
- 远程接口必须声明为public。
- 远程对象扩展java.rmi.Remote接口
- 每个方法必须抛出java.rim.RemoteException
- 任何作为参数或者返回值传送的远程对象的数据类型必须声明为远程接口类型。
RMI程序开发详解
- 创建一个远程接口类
- 创建一个接口实现类
- 开发服务器端程序,启动远程接口服务
- 开发客户端,访问远程接口服务
编译和运行的步骤如下:
- 编译生成Stub和Skeleton
- 启动RMI注册表
- 启动服务器
- 运行客户端
我们将通过一个开发一个HelloWorld入门程序来实践这些步骤。
实现类包括:
远程借口类ITestRemote
实现类TestRemote
服务器RMIServer
客户端RMIClient
调用关系
创建远程接口类ITestRemote.java
package org.test.rmi; import java.rmi.Remote;import java.rmi.RemoteException; /** * @author kucs * 定义远程接口,提供了dealData的方法 * 扩展了Remote * 远程接口类必须声明为public * 必须继承自java.rmi.Remote * 必须抛出一个java.rmi.RemoteExcetion例外 */public interface ITestRemote extends Remote { public String dealData(String world) throws RemoteException; }
远程借口实现类TestRemote.java
package org.test.rmi; import java.rmi.RemoteException;import java.rmi.server.UnicastRemoteObject; /** * @author kucs * * 远程接口ITestRemote的实现类 * 扩展了UnicastRemoteObject, * 表示TestRemote类将用于创建一个单独的、不可复制的远程对象 * 它使用RMI默认的TCP的传送通道进行通信 */public class TestRemote extends UnicastRemoteObject implements ITestRemote { /** * */ private static final long serialVersionUID = 1L; protected TestRemote() throws RemoteException { super(); } @Override public String dealData(String world) throws RemoteException { // TODO Auto-generated method stub return "你好,"+world; } }
服务器RMIServer.java
package org.test.rmi; import java.net.MalformedURLException;import java.rmi.Naming;import java.rmi.RMISecurityManager;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry; /** * @author kucs * 服务器需要做的3件事 * 1、创建RMI安全管理器RMISecurityManager的一个实例并安装它 * 2、创建远程对象的一个实例,在本实例中是TestRemote * 3、在RMI注册表中登记这个创建的对象 */public class RMIServer { public static void main(String[] args) { // TODO 创建并安装安全管理器 if(System.getSecurityManager() == null){ System.setSecurityManager(new RMISecurityManager()); } try { // TODO 创建远程对象 ITestRemote test = new TestRemote(); // TODO 启动注册表 LocateRegistry.createRegistry(1099); //将名称绑定到对象 Naming.rebind("//localhost:1099/TestRemoteService", test); System.out.println("RMI服务器正在运行...."); } catch (RemoteException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (MalformedURLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
客户端RMIClient.java
package org.test.rmi; import java.net.MalformedURLException;import java.rmi.Naming;import java.rmi.NotBoundException;import java.rmi.RMISecurityManager;import java.rmi.RemoteException; /** * @author kucs * 客户端可以远程调用远程接口中指定的任何方法。 * 客户端必须从RMI注册表中获得指向该远程对象的引用 * 客户端需要做3件事 * 1、创建RMI安全管理器RMISecurityManager的一个实例并安装它 * 2、根据IP、端口号、服务名查找远程主机的服务并取得该服务的实例 * 调用该实例的本机接口方法 * */public class RMIClient { public static void main(String[] args) { // TODO 创建并安装安全管理器 if(System.getSecurityManager() == null){ System.setSecurityManager(new RMISecurityManager());; } try { ITestRemote test = (ITestRemote) Naming.lookup("rmi://localhost:1099/TestRemoteService"); System.out.println(test.dealData("我是客户端")); System.out.println(test.dealData("中国")); } catch (MalformedURLException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (RemoteException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (NotBoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
编译并运行应用程序
为了运行应用程序,我们需要生成Stub和Skeleton,编译服务器和客户端程序,启动RMI注册表,最后是启动服务器和运行客户端程序
(1)编译生成Stub和Skeleton
为了生成Stub和Skeleton,我们使用rmi编译器编译实现类TestRemote.class。在项目功能目录的bin目录下,启动cmd。执行如下命令及结果如图示。这将生成TestRemote_Stub.class。TestRemote_Stub.class是一个客户机代理,TestRemote_Skel.class是一个服务器基干。 (2)启动RMI注册表,因为D我们在代码中添加了LocateRegitry.createRegistry(1099)语句,所以在启动服务器时自动自动RMI注册表
(3)编写安全许可文件
因为在服务器应用程序中正在使用RMI安全管理员,所以需要一个安全许可文件来与之匹配。安全许可文件policy.txt,我们将它保存在无包的目录下。
grant{ permission java.security.AllPermission "*:1000-9999","accept,connect,listen,resolve";};
(4)启动服务器命令java -Djava.security.policy=policy.txt org.test.rmi.RMIServer
(5)运行客户机命令java -Djava.security.policy=policy.txt org.test.rmi.RMIClient
运行结果
安全管理器
当没有写策略文件覆盖C:\Program Files\Java\jre1.6.0_05\lib\security里的java.policy时,调用
if(System.getSecurityManager()==null)
System.setSecurityManager(new RMISecurityManager());
系统会抛出异常 java.security.AccessControlException。
原因:每个Java应用都可以有自己的安全管理器,它是防范恶意攻击的主要安全卫士。安全管理器通过执行运行阶段检查和访问授权,以实施应用所需的安全策略,从而保护资源免受恶意操作的攻击。实际上,安全管理器根据Java安全策略文件决定将哪组权限授予类。然而,当不可信的类和第三方应用使用JVM 时,Java安全管理器将使用与JVM相关的安全策略来识别恶意操作。在很多情况下,威胁模型不包含运行于JVM中的恶意代码,此时Java安全管理器便不是必需的。当安全管理器检测到违反安全策略的操作时,JVM将引发 AccessControlException或SecurityException。
在Java应用中,安全管理器是由System类中的方法setSecurityManager设置的。要获得当前的安全管理器,可以使用方法 getSecurityManager。 java.lang.SecurityManager类包含了很多checkXXXX方法,如用于判断对文件访问权限的checkRead(String file)方法。这些检查方法调用SecurityManager.checkPermission方法,后者根据安全策略文件判断调用应用是否有执行所请求的操作权限。如果没有,将引发SecurityException。 如果想让应用使用安全管理器和安全策略,可在启动JVM时设定-Djava.security.manager选项,还可以同时指定安全策略文件。如果在应用中启用了Java安全管理器,却没有指定安全策略文件,那么Java安全管理器将使用默认的安全策略,它们是由位于目录$JAVA_HOME/jre /lib/security中的java.policy定义的。 类装载器用Policy对象帮助它们决定,把一段代码导入虚拟机时应该给它们什么样的权限. 任何时候,每一个应用程序都只有一个Policy对象.
2.有关rmic的疑惑rmic是为客户端和服务器端生成相关的存根和骨架(实际上就是一种代理类),但是我们直接在javac java的过程就可以直接调用远程方法,好像没有rmic的调用。
原因:在jdk5.0以前的版本中,需要用rmic命令来为远程对象生成静态的代理类(包括存根和骨架类),而在jdk5.0中rmi框架会在运行的过程中自动为远程 对象生成动态代理类(包括存根和骨架类),从而更彻底的封装了rmi框架的实现细节。简化了rmi框架的使用方式。
3.registry本地注册表
实际上LocateRegistry.createRegistry(rmiPort)就是创建对远程或者本地注册表的本地引用,创建并导出Registry实例。
RMI实现了客户端与远程主机的调用,但是客户端和服务器必须都是基于Java语言的。如果要实现非Java语言之间的调用,就必须使用CORBA了。