RMI = RPC + Serializable
RMI(Remote Method Invocation)为远程方法调用,是允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。 这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。RMI使用Java远程消息交换协议JRMP(Java Remote Messaging Protocol)进行通信。JRMP是专为Java的远程对象制定的协议。
当客户代码调用一个远程对象上的远程方法是,实际上是调用一个Java编程语言的普通方法,这个方法是封装在stub(代码存根)的代用对象。调用程序将调用本地stub的方法,而本地stub将负责执行对远程对象的方法调用。Stub担当远程对象的客户本地代表或代理人角色,它将RMI调用传递给服务器端的骨架(Skeleton),后者负责将该调用传递给实际的远程对象实现。
stub和skeleton由rmic编译器生成。stub在服务器端创建,必须驻留于客户端。
调用 stub的方法时将执行下列操作:
(1) 初始化与包含远程对象的远程虚拟机的连接;
(2) 对远程虚拟机的参数进行编组(写入并传输);
(3) 等待方法调用结果;
(4) 解编(读取)返回值或返回的异常;
(5) 将值返回给调用程序。
为了向调用程序展示比较简单的调用机制,stub将参数的序列化和网络级通信等细节隐藏了起来。在远程虚拟机中,每个远程对象都可以 有相应的skeleton(在JDK1.2环境中无需使用skeleton)。Skeleton负责将调用分配给实际的远程对象实现。它在接收方法调用时 执行下列操作:
(1) 解编(读取)远程方法的参数;
(2) 调用实际远程对象实现上的方法;
(3) 将结果(返回值或异常)编组(写入并传输)给调用程序。
rmi的核心问题是如何将一个可用的stub传输到客户端,大致有两种方法:(摘自关于rmi的研究)
1. 最直观的方法,服务器和客户端直接建立连接,用任何可用的协议传输stub,缺点显而
易见,客户端必须明确的知道服务器的地址及相关信息
2. 使用注册中心,如:rmiregistry和JNDI。
有一点需要注意,这里传输的只是stub的实例,而不是stub类的定义。所有stub类文件必须
在客户端和服务器端的classpath中(或者通过系统属性 java.rmi.server.codebase 设置),
否则会抛出ClassNotFoundException。
RMI的好处在于将底层的TCP/IP通信封装起来了,其中内部使用的仍然是通过socket来传输序列化过的对象,然后在远程反序列化,执行。
写一个远程方法要有以下三个程序:
1. 一个Java远程接口:
1)远程接口必须为public属性(不能有“包访问”;也就是说,他不能是“友好的”)。否则,一旦客户试图装载一个实现了远程接口的远程对象,就会得到一个错误。
2)远程接口必须扩展接口java.rmi.Remote。
3)接口中的每一个方法都要抛出异常java.rmi.RemoteException。
4)当把远程对象当作一个参数传递或作为一个值返回时,必须要说明它是一个远程接口,而不是实现该接口的类。(因为远程对象在服务器上)
package rmi; public interface HelloWorld extends java.rmi.Remote { String sayHelloWorld() throws java.rmi.RemoteException; }
2. 一个Java远程对象。远程对象运行在服务器中,它实现了(1)中的远程接口。
1)指定要实现的远程接口。
2)定义这个远程对象的构造函数。
3)实现远程方法。
4)创建并安装一个安全管理员。
5)创建一个或多个实例。
6)用RMI远程对象注册来登记该远程对象。
package rmi; import java.rmi.RMISecurityManager; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.UnicastRemoteObject; public class HelloWorldImpl extends java.rmi.server.UnicastRemoteObject implements HelloWorld { private String name; public HelloWorldImpl(String s) throws RemoteException { super(); name = s; } public String sayHelloWorld() throws RemoteException { return "Hello World!"; } public static void main(String[] args) { //System.setSecurityManager(new RMISecurityManager()); try { HelloWorldImpl obj = new HelloWorldImpl("HelloWorldServer"); //如果远程对象没有extends UnicastRemoteObject,需要调用UnicastRemoteObject.exportObject() //obj = UnicastRemoteObject.exportObject(obj, 0); //可以使用Registry或Naming来bind远程对象 //使用createRegistry()时,不需要运行rmiregistry, 也不需要加载RMISecurityManager Registry registry = LocateRegistry.createRegistry(2099); registry.rebind("HelloWorldServer", obj); //使用getRegistry()时,需要先运行rmiregistry, 需要加载RMISecurityManager //Registry registry = LocateRegistry.getRegistry(2099); //registry.rebind("HelloWorldServer", obj); //java.rmi.Naming.rebind("//scl58192:2099/HelloWorldServer", obj); System.out.println("HelloWorldServer bound in registry"); } catch (Exception e) { e.printStackTrace(); } } }
3. 一个client对象。由它远程调用服务器的远程方法。远程方法在(2)的远程对象中。
package rmi; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class RmiClient { public static void main(String[] args) { String host = "scl58192"; int port = 2099; try { //可以使用Registry或Naming的lookup() Registry registry = LocateRegistry.getRegistry(host, port); HelloWorld obj = (HelloWorld)registry.lookup("HelloWorldServer"); //HelloWorld obj = (HelloWorld)java.rmi.Naming.lookup("//"+host+":"+port+"/HelloWorldServer"); System.out.println("HelloWorldServer found...n"); System.out.println("get message: "+obj.sayHelloWorld()); } catch(Exception e) { e.printStackTrace(); } } }
运行RMI程序:
1. 编译HelloWord.java, HelloWorldImpl.java, RmiClient.java;
2. rmic (JDK1.5以后如果远程对象继承UnicastRemoteObject,不需要生成stub,否则如果使用UnicastRemoteObject.exportObject()还需要生成stub) 我们可以在rmic后面跟-keep参数,保留生成的stub源码,
rmic -d bin -classpath . [-keep] rmi.HelloWorldImpl
3. 启动RMI bootstrap name server。如果服务器程序中使用了LocateRegistry.createRegistry(),就跳过这步。
setenv CLASSPATH .:bin
rmiregistry [port]
4. 运行服务器:
java -classpath .:bin [-Djava.security.policy=Server.policy] rmi.HelloWorldImpl
5. 运行Client程序:
java rmi.RmiClient
异常及解决办法:
1. 执行java HelloWorldImpl出现异常 java.security.AccessControlException: access denied (java.net.SocketPermission scl58192:2099 connect,resolve)
原因是在HelloWorldImpl中设置了安全管理器<System.setSecurityManager(new RMISecurityManager());>,可是又没有设置访问的策略
解决办法:在~/jdk6/jre/lib/security/java.policy增加permission, 或去掉RMISecurityManager
grant { permission java.net.SocketPermission "scl58192:*","accept,connect,resolve"; }
2. 执行java HelloWorldImpl出现异常 java.rmi.ServerException: RemoteException occurred in server thread; nested exception is:
java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is:
java.lang.ClassNotFoundException: HelloWorldImpl_Stub
因为明明看到 HelloWorldImpl_Stub 和 HelloWorldImpl 这两个类是在同一个目录中,并且classpath 也有设置当前目录,按理既然能加载 HelloWorldImpl 类执行,就能加载到 HelloWorldImpl_Stub吧,为什么还提示ClassNotFound呢?其实类 HelloWorldImpl_Stub并非由HelloWorldImpl执行行直接加载,而是HelloWorldImpl在向RMI注册时,要求 rmiregistry去加载 HelloWorldImpl_Stub类的,理解了这一层次上的意义就会知道其实 HelloWorldImpl_Stub是为 rmiregistry所用的。
解决办法是:
1) 在执行 rmiregistry [port]之前,设置CLASSPATH让能查找到HelloWorldImpl_Stub类;
或者
2) 执行java HelloWorldImpl时增加-Djava.rmi.server.codebase=http://scl58192:8080/LearnJSP/ 或 -Djava.rmi.server.codebase=UsersDuraiworkspaceRMI2src
或者
3) HelloWorldImpl.java中使用"LocateRegistry.createRegistry(2099);"启动对象注册表
3.执行客户端程序RmiClient出现异常java.rmi.UnmarshalException: Error unmarshaling return header; nested exception is: java.io.EOFException
原因是 客户端有权限访问服务提供端的某个端口,而服务提供端却无权限在某个端口上或给那个客户端提供服务造成的
解决办法: 把客户端和服务器的安全策略文件都改为能访问任何端口就行。参照问题1中增加permission。如果问题还存在,检查permission中的hostname与程序中的hostname是否一致,例如localhost vs scl58192。
4.调用远程主机上的RMI服务时抛出java.rmi.ConnectException: Connection refused to host: 127.0.0.1异常原因及解决方案.
原因:这个问题其实是由rmi服务器端程序造成的。 客户端程序向服务端请求一个对象的时候,返回的stub对象里面包含了服务器的hostname,客户端的后续操作根据这个hostname来连接服务器端。要想知道这个hostname具体是什么值可以在服务器端bash中打入指令:hostname -i, 如果返回的是127.0.0.1,那么你的客户端肯定会抛如标题的异常了。
解决方法有两种:
方法1:/etc/hosts里的127.0.0.1修改为实际的IP地址(这种方法可能会导致有些应用不能用,不推荐)
方法2:先在/etc/hosts里添加一行"10.0.0.1 scl58192",然后修改/etc/sysconfig/network文件里面的HOSTNAME=scl58192
补充:
-Djava.rmi.server.hostname=scl58192 或者在server程序中加入"System.setProperty("java.rmi.server.hostname",server端的ip);"
rmiregistry -J-Djava.rmi.useLocalHostName=true -J-Djava.rmi.server.hostname=10.2.0.12
this becomes a problem if you have DHCP on your primary interface. In this case, you may need to revert to using the -D:java.rmi.server.hostname=xxx.xxx.xxx.xxx type substitution or if you have a secondary interface, changing the hostname resolution in /etc/hosts to the secondary interface.
Pending issue:
我在applet中调用远程对象时出现Connection refused,但使用RmiClient没有问题。看起来好像是Applet SecurityManager的设置问题。但不知道如何解决?
~/jdk6/bin/appletviewer http://scl58192:8080/LearnJSP/HelloWorldRMI.html
java.rmi.ConnectException: Connection refused to host: scl58192; nested exception is: java.net.ConnectException: Connection refused at sun.rmi.transport.tcp.TCPEndpoint.newSocket(TCPEndpoint.java:601) at sun.rmi.transport.tcp.TCPChannel.createConnection(TCPChannel.java:198) at sun.rmi.transport.tcp.TCPChannel.newConnection(TCPChannel.java:184) at sun.rmi.server.UnicastRef.newCall(UnicastRef.java:322) at sun.rmi.registry.RegistryImpl_Stub.lookup(Unknown Source) at java.rmi.Naming.lookup(Naming.java:84) at rmi.HelloWorldApplet.init(HelloWorldApplet.java:12) at sun.applet.AppletPanel.run(AppletPanel.java:424) at java.lang.Thread.run(Thread.java:662)
参考:
Getting Started Using JavaTM RMI
FAQ JavaTM RMI and Object Serialization
Trail: RMI(The Java Tutorials)
java.rmi Properties
RMI及其调试(JDK1.6)
RMI例子可能产生的几种异常及解决方法
Java出现access denied java.net.SocketPermission解决办法
RMI规范--第五章
关于rmi的研究***
RMI的简单例子
RMI技术讲解***
RMI简单例子