RMI(Remote Method Invocation)远程方法调用是一种计算机之间利用远程对象互相调用实现双方通讯的一种通讯机制。使用这种机制,某一台计算机上的对象可以调用另外一台计算机上的对象来获取远程数据。RMI是Enterprise JavaBeans的支柱,是建立分布式Java应用程序的方便途径。在过去,TCP/IP套接字通讯是远程通讯的主要手段,但此开发方式没有使用面向对象的方式实现开发,在开发一个如此的通讯机制时往往令程序员感觉到乏味,对此RPC(Remote Procedure Call)应运而生,它使程序员更容易地调用远程程序,但在面对复杂的信息传讯时,RPC依然未能很好的支持,而且RPC未能做到面向对象调用的开发模式。针对RPC服务遗留的问题,RMI出现在世人面前,它被设计成一种面向对象的通讯方式,允许程序员使用远程对象来实现通信,并且支持多线程的服务,这是一次远程通讯的革命,为远程通信开辟新的里程碑。
1.服务端
Calculator c=new CalculatorImpl();//Calculator接口继承Remote,且每个方法抛出异常throws RemoteException Registry reg=LocateRegistry.createRegistry(12345);//指定的端口创建并启动了RMI服务器(也可同时指定主机) reg.rebind("CalculatorServer", c);//以指定的名字发布RMI对象到注册表
2.客户端
Registry reg=LocateRegistry.getRegistry(12345);//得到RMI服务器注册表的引用 Calculator c=(Calculator) reg.lookup("CalculatorServer");//查找并得到RMI对象的实例
RMI的开发步骤
1.先创建远程接口及声明远程方法,注意这是实现双方通讯的接口,需要继承Remote 2.开发一个类来实现远程接口及远程方法,值得注意的是实现类需要继承UnicastRemoteObject 3.通过javac命令编译文件,通过java -server 命令注册服务,启动远程对象 4.最后客户端查找远程对象,并调用远程方法
首先为服务建立一个Model层,注意因为此对象需要现实进行远程传输,所以必须继承Serializable
package rmi.model;
import java.io.Serializable;
//注意对象必须继承Serializable publicclass PersonEntity implements Serializable { privateint id; private String name; privateint age;
publicvoid setId(int id) { this.id = id; }
publicint getId() { return id; }
publicvoid setName(String name) { this.name = name; }
public String getName() { return name; }
publicvoid setAge(int age) { this.age = age; }
publicint getAge() { return age; } }
创建远程接口PersonService,注意远程接口需要继承Remote
package rmi.service;
import java.rmi.Remote; import java.rmi.RemoteException; import java.util.List; import rmi.model.*;
//此为远程对象调用的接口,必须继承Remote类 publicinterface PersonService extends Remote { public List<PersonEntity> GetList() throws RemoteException; }
建立PersonServiceImpl实现远程接口,注意此为远程对象实现类,需要继承UnicastRemoteObject
package rmi.serviceImpl;
import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; import java.util.LinkedList; import java.util.List;
import rmi.model.PersonEntity; import rmi.service.*;
//此为远程对象的实现类,须继承UnicastRemoteObject publicclass PersonServiceImpl extends UnicastRemoteObject implements PersonService {
public PersonServiceImpl() throws RemoteException { super(); // TODO Auto-generated constructor stub }
@Override public List<PersonEntity> GetList() throws RemoteException { // TODO Auto-generated method stub System.out.println("Get Person Start!"); List<PersonEntity> personList=new LinkedList<PersonEntity>();
PersonEntity person1=new PersonEntity(); person1.setAge(25); person1.setId(0); person1.setName("Leslie"); personList.add(person1);
PersonEntity person2=new PersonEntity(); person2.setAge(25); person2.setId(1); person2.setName("Rose"); personList.add(person2);
return personList; } }
建立服务器端,在服务器端注册RMI通讯端口与通讯路径,然后通讯javac命令编译文件,通过java -server 命令注册服务。以下面代码为例,如果阁下将项目建立于D:\\RMI\RemotingService文件夹上时,则先输入D:\\RMI\RemotingService\src>javac rmi/remotingservice/Program.java获取Program.class(如何阁下使用的MyEclipse等开发工具,可跳过此步,直接在*/bin文件夹中直接调用已经生成的Program.class),然后输入D:\\RMI\RemotingService\src>java rmi/remotingservice/Program启动服务。
package rmi.remotingservice;
import java.rmi.Naming; import java.rmi.registry.LocateRegistry;
import rmi.service.*; import rmi.serviceImpl.*;
publicclass Program{
publicstaticvoid main(String[] args) { try { PersonService personService=new PersonServiceImpl(); //注册通讯端口 LocateRegistry.createRegistry(6600); //注册通讯路径 Naming.rebind("rmi://127.0.0.1:6600/PersonService", personService); System.out.println("Service Start!"); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
最后建立客户端进行测试,注意客户调用的RMI路径必须服务器配置一致
package rmi.remotingclient;
import java.rmi.Naming; import java.util.List;
import rmi.model.PersonEntity; import rmi.service.*;
publicclass Program { publicstaticvoid main(String[] args){ try{ //调用远程对象,注意RMI路径与接口必须与服务器配置一致 PersonService personService=(PersonService)Naming.lookup("rmi://127.0.0.1:6600/PersonService"); List<PersonEntity> personList=personService.GetList(); for(PersonEntity person:personList){ System.out.println("ID:"+person.getId()+" Age:"+person.getAge()+" Name:"+person.getName()); } }catch(Exception ex){ ex.printStackTrace(); } } }
常见错误
1.在命令提示符调用java命令时,显示并无此命令。这是因为未在“环境变量”中绑定JAVA的JDK命令造成的,你首先单击“计算机右键”->“属性”->“高级”->“环境变量”。在系统变量Path设置中加载为JDK的路径 .;D:\Program Files\Genuitec\Common\binary\com.sun.java.jdk.win32.x86_1.6.0.013\bin。然后在ClassPath加载服务器端的Program.class地址 .;D:\\RMI\RemotingService\bin 2.在调用javac命令时出现“javac 找不到文件 ..... ”此错误,可能是因为阁下输入的文件路径出现错误造成,注意不要把D:\\RMI\RemotingService\src>javac rmi/remotingservice/Program.java写错为D:\\RMI\RemotingService\src>javac rmi.remotingservice.Program.java 3.在调用D:\\RMI\RemotingService\bin>java rmi/remotingservice/Program命令时出现“Exception in thread 'main' java.lang.NoClassEdfoundError”错误,第一这可能是阁下把Program错写为Program.class,注意java命令不需要加后缀名。第二可能是阁下把“java rmi/remotingservice/Program”错写为“java rmi\remotingservice\Program"。
4.如果服务端与客户端不在同一机器上开发,则服务接口与相关JAVA模型类在服务端/客户端类路径要保持一致,即服务端/客户端有相同的类.
1.在客户端程序前打开安全管理器: try { System.setSecurityManager(new java.rmi.RMISecurityManager()); mds = (IMongoDBScan) Naming.lookup(urlStr); } catch (MalformedURLException e) { //.......... }
2.修改/usr/java/jdk1.6.0_21/jre/lib/security/java.policy后重启 grant { permission java.security.AllPermission; }
1. 执行java PerfectTime出现异常 java.security.AccessControlException: access denied (java.net.SocketPermission 127.0.0.1:2005 connect,resolve)
无法解析和连接到127.0.0.1的2005端口上,原因是在PerfectTime中设置了安全管理器<System.setSecurityManager(new RMISecurityManager());>,可是又没有设置访问的策略,解决办法有四(解决这种异常的办法同样适用于DisplayPerfectTime):
(1) 可以把代码System.setSecurityManager(new RMISecurityManager());去掉,不设置安全管理器 (2) 修改JRE的安全策略文件,这就要求你能确定执行时是用的哪个JRE,比如在Eclipse中用JDK是c:\Java\jdk1.5.0_06,相应的安全策略文件就是c:\java\jdk1.5.0_06\jre\lib\security\java.policy,如果是Applet中的java程序就应该是在 jre 目录中,如文件C:\Java\jre1.5.0_06\lib\security\java.policy。修改安全策略文件,在grant {},大括号中加上permission java.net.SocketPermission “localhost:2005″,”connect,resolve”; (3) 建立自己的策略文件,如c:\MyPolicy.policy ,内容为:
grant { permission java.net.SocketPermission “localhost:2005″,”connect,resolve”; }
执行PerfectTime时用命令 java -Djava.security.policy=c:\MyPolicy.policy PerfectTime 指定了安全策略文件
(4) 把 System.setSecurityManager (new RMISecurityManager()) 改为匿名类实现,覆盖两个方法
System.setSecurityManager (new RMISecurityManager() { public void checkConnect (String host, int port) {} public void checkConnect (String host, int port, Object context) {} });
当然最简单的解决方法莫过于第一种。
2. 同样是执行 PerfectTime 出现的异常 java.rmi.ServerException: RemoteException occurred in server thread; nested exception is: java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is: java.lang.ClassNotFoundException: PerfectTime_Stub
很多人对这个问题有些莫名其妙,因为明明看到 PerfectTime_Stub 和 PerfectTime 这两个类是在同一个目录中,并且classpath 也有设置当前目录,按理既然能加载 PerfectTime 类执行,就能加载到 PerfectTime_Stub吧,为什么还提示ClassNotFound呢?其实类 PerfectTime_Stub并非由PerfectTime执行行直接加载,而是PerfectTime在向RMI注册时,要求rmiregistry去加载 PerfectTime_Stub类的,理解了这一层次上的意义就会知道其实 PerfectTime_Stub是为 rmiregistry所用的。所以解决办法是:
(1) 在执行 rmiregistry 之前,设置classpath让能查找到PerfectTime_Stub类,如在同一Dos窗口中,假设 PerfectTime_Stub类是在E:\workspace\TestRMI\bin目录中,执行过程那就是
C:\Documents and Settings\unmi>set classpath=%classpath%;E:\workspace\TestRMI\bin
C:\Documents and Settings\unmi>rmiregistry 2005
(2) 或者在命令行中先进入到 PerfectTime_Stub类所在的目录,然后再执行 rmiregistry (这种方法实质是与上面一样的,只是恰当的应用的classpath中的当前目录 “.” ),执行过程如下
C:\Documents and Settings\unmi>e:
E:\>cd E:\workspace\TestRMI\bin
E:\workspace\TestRMI\bin>rmiregistry 2005
参看:rmiregistry was finding the stubs in its CLASSPATH
3. 执行客户端程序 DisplayPerfectTime 出现异常 java.security.AccessControlException: access denied (java.net.SocketPermission 127.0.0.1:1276 connect,resolve),同时在服务器端也产生异常 Exception in thread “RMI TCP Connection(6)-127.0.0.1″ java.security.AccessControlException: access denied (java.net.SocketPermission 127.0.0.1:1296 accept,resolve)
直接能想到的解决办法是把127.0.0.1:1276,127.0.0.1:1276的解析连接权限也加上,方法可取第 1 种异常所列的方法,但这个端口是随机的。在此解析一下这些端口的用途,2005是直接指定的供客户端查找注册的服务对象引用的端口,这是固定的,而上面产生的在客户端和服务器上的1276和1296的端口,是随机的,是在方法调用时真正的客户端与提供服务的服务器(而非注册服务器)之间的数据通信的端口。
为了满足上面的端口应用,可以在安全策略文件中只加上 permission java.net.SocketPermission “localhost:*”,”accept,connect,resolve”; 允许在所有端口上的接受,连接,解析。再如果要访问的IP很多,又要写成 permission java.net.SocketPermission “*:*”,”accept,connect,resolve”; 方便。
4. 执行客户端程序 DisplayPerfectTime出现异常 java.rmi.UnmarshalException: Error unmarshaling return header; nested exception is: java.io.EOFException,这种异常应该比较少见,出现情况是 客户端有权限访问服务提供端的某个端口,而服务提供端却无权限在某个端口上或给那个客户端提供服务造成的,解决办法把客户端和服务器的安全策略文件都改为能访问任何端口就行。
总结:上面1、3、4三种情况都是因为权限不足所造成的,如果安全控制的粒度不要求太细的化,在服务器端和客户端可以不用设置定全管理器,或者策略文件中设置为能接受、连接、解析任何IP及端口:permission java.net.SocketPermission “*:*”,”accept,connect,resolve”; 或者用1(4)的方法忽略所有IP及端口的检测。
在项目中使用rmi,发布到封闭环境所用到的端口需要配置防火墙端口(即开放指定端口)。 刚开始配置了一个固定端口,死都连接不通,后来同netstat -antup |grep pid 查看进程监听的端口, 发现原来rmi有2个端口一个是固定的。一个是随机的。 RMI之所以使用的范围受限制主要有两方面原因, 其一:必须要是java,平台的异构性受到限制; 其二:穿越防火墙不方便。 这里主要谈谈RMI如何通过固定分配端口来穿越防火墙。 RMI穿越防火墙不方便主要是因为除了RMI服务注册的端口(默认1099)外, 与RMI的通讯还需要另外的端口来传送数据. 而另外的端口是随机分配的,所以要想RMI的客户能通过防火墙来与RMI服务通讯,则需要能让随机分配的端口固定下来. 具体做法如下: 1、增加一个文件RMISocketFactoryEx.java
import java.rmi.server.*; import java.io.*; import java.net.*; public class RMISocketFactoryEx extends RMISocketFactory{ // 提供一个固定的数据端口 public static final int DATA_PORT = 10999; @Override public Socket createSocket(String host, int port)throws IOException{ return new Socket(host,port); } @Override public ServerSocket createServerSocket(int port) throws IOException{ if(port == 0){ port = DATA_PORT; //不指定就随机分配了 } return new ServerSocket(port); } } 2、在注册服务前设置SocketFactory工厂代码: // 启动 rmi 服务,端口为8081 LocateRegistry.createRegistry(8081); // 设置SocketFactory工厂 RMISocketFactory.setSocketFactory(new RMISocketFactoryEx()); // 在 rmi 中注册一个对象 IHelloWordService helloWordService = new HelloWordServiceImpl(); Naming.rebind("helloWordService", helloWordService); 这样的话RMI分配的端口就被固定了,防火墙只需要打开8081和10999端口即可。