RMI原理及实现
简介
RMI是远程方法调用的简称,象其名称暗示的那样,它能够帮助我们查找并执行远程对象的方法。通俗地说,远程调用就象将一个class放在A机器上,然后在B机器中调用这
个class的方法。
我个人认为,尽管RMI不是唯一的企业级远程对象访问方案,但它却是最容易实现的。与能够使不同编程语言开发的CORBA不同的是,RMI是一种纯Java解决方案。在RMI
中,程序的所有部分都由Java编写。
在看本篇文章时,我假定读者都已经具备了较扎实的Java基础知识,在这方面有欠缺的读者请自行阅读有关资料。
概念
我在前面已经提到,RMI是一种远程方法调用机制,其过程对于最终用户是透明的:在进行现场演示时,如果我不说它使用了RNI,其他人不可能知道调用的方法存储在其他
机器上。当然了,二台机器上必须都安装有Java虚拟机(JVM)。
其他机器需要调用的对象必须被导出到远程注册服务器,这样才能被其他机器调用。因此,如果机器A要调用机器B上的方法,则机器B必须将该对象导出到其远程注册服务器
。注册服务器是服务器上运行的一种服务,它帮助客户端远程地查找和访问服务器上的对象。一个对象只有导出来后,然后才能实现RMI包中的远程接口。例如,如果想使机器A
中的Xyz对象能够被远程调用,它就必须实现远程接口。
RMI需要使用占位程序和框架,占位程序在客户端,框架在服务器端。在调用远程方法时,我们无需直接面对存储有该方法的机器。
在进行数据通讯前,还必须做一些准备工作。占位程序就象客户端机器上的一个本机对象,它就象服务器上的对象的代理,向客户端提供能够被服务器调用的方法。然
后,Stub就会向服务器端的Skeleton发送方法调用,Skeleton就会在服务器端执行接收到的方法。
Stub和Skeleton之间通过远程调用层进行相互通讯,远程调用层遵循TCP/IP协议收发数据。下面我们来大致了解一种称为为“绑定”的技术。
客户端无论何时要调用服务器端的对象,你可曾想过他是如何告诉服务器他想创建什么样的对象吗?这正是“绑定”的的用武之地。在服务器端,我们将一个字符串变量与一个
对象联系在一起(可以通过方法来实现),客户端通过将那个字符串传递给服务器来告诉服务器它要创建的对象,这样服务器就可以准确地知道客户端需要使用哪一个对象了。所
有这些字符串和对象都存储在的远程注册服务器中。
在编程中需要解决的问题
在研究代码之前,我们来看看必须编写哪些代码:
远程对象:这个接口只定义了一个方法。我们应当明白的是,这个接口并非总是不包括方法的代码而只包括方法的定义。远程对象包含要导出的每个方法的定义,它还实
现Java.rmi中的远程接口。
远程对象实现:这是一个实现远程对象的类。如果实现了远程对象,就能够覆盖该对象中的所有方法,因此,远程对象的实现类将真正包含我们希望导出的方法的代码。
远程服务器:这是一个作为服务器使用的类,它是相对于要访问远程方法的客户端而言的。它存储着绑定的字符串和对象。
远程客户端:这是一个帮助我们访问远程方法提供帮助的类,它也是最终用户。我们将使用查找和调用远程方法的方法在该类中调用远程方法。
编程
我们将首先编写远程对象,并将代码保存为名字为AddServer.Java的文件:
import Java.rmi.*;
public interface AddServer extends Remote {
public int AddNumbers(int firstnumber,int secondnumber) throws RemoteException;
}
我们来看看上面的代码。首先,为了使用其内容,我们导入rmi包。然后,我们创建一个扩展了Java.rmi中远程接口的接口。所有的远程对象必须扩展该远程接口,我们将该
远程接口称为AddServer。在该远程对象中,有一个名字为AddNumbers的方法,客户端可以调用这一方法。我们必须记住的是,所有的远程方法都需要启
动RemoteException方法,有错误发生时就会调用该方法。
下面我们开始编写远程对象的实现。这是一个实现远程对象并包含有所有方法代码的类,将下面的代码保存为名字为AddServerImpl.Java的文件:
import Java.rmi.*;
public class AddServerImpl extends UnicastRemoteObject implements AddServer {
public AddServerImpl() {
super();
}
public int AddNumbers(int firstnumber,int secondnumber) throws RemoteException {
return firstnumber + secondnumber;
}
}
首先,我们导入rmi包,然后创建一个扩展UnicastRemoteObject和实现创建的远程对象的类;其次,我们可以为类创建一个缺省的构建器。我们还了解了AddNumbers方
法的代码,它启动RemoteException。这样我们就覆盖了创建的远程对象中的方法。AddNumbers方法的代码非常好理解,它接受2个整型参数,然后相加并返回它们的和。
至此,我们已经有了二个Java文件:远程对象和远程对象的实现。下面我们将使用Javac命令编译这二个文件:
编译远程对象:
C:\jdk\bin\Javac workingdir\AddServer.Java
编译远程对象实现:
C:\jdk\bin\Javac workingdir\AddServerImpl.Java 这样,就会达到二个Java文件和二个类文件,下面我们将创建stub和skeleton。为了创建stub和skeleton文件,
我们必须使用rmic编译器编译远程对象实现文件。
用Rmic编译远程对象实现文件:
C:\jdk\bin\rmic workingdir\AddServerImpl.Java 然后,我们就会发现多了2个新建的类文件,它们分别是AddServerImpl_Stub.class
和AddServerImpl_Skel.class 。
The Coding (Contd.)
我们已经编译了所有的源代码,下面我们来创建客户端和服务器端,将下面的代码保存为名字为RmiServer.Java的文件:
import Java.rmi.*;
import Java.net.*;
public class RmiServer {
public static void main (String args[]) throws RemoteException, MalformedURLException {
AddServerImpl add = new AddServerImpl();
Naming.rebind("addnumbers",add);
}
}
首先,我们导入Java.rmi包和Java.net包。另外,我们还使用throws从句捕获任何异常。我们从对象中得出远程对象实现,使用rebind方法将字符串addnumbers与该对
象绑定。下面的例子显示了绑定的含义:
从现在开始,无论何时客户端要调用远程对象,使用字符串addnumbers就可以实现。rebind方法有二个参数:第一个参数是字符串变量,第二个参数是远程对象实现类的
对象。
下面我们来创建客户端,将下面的代码保存为名字为RmiClient.Java的文件:
import Java.rmi.*;
import Java.net.*;
public class RmiClient {
public static void main(String args[]) throws RemoteException, MalformedURLException {
String url="rmi://127.0.0.1/addnumbers";
AddServer add;
add = (AddServer)Naming.lookup(url);
int result = add.AddNumbers(10,5);
System.out.println(result);
}
}
首先,我们导入Java.rmi包和Java.net包,并使用throws从句捕获所有必要的异常。然后通过利用Naming类中的静态lookup方法从远程对象中得到一个对象。(这也是我
们无需从Naming类中得到一个对象并调用它。而只使用类名字的原因。)
lookup方法接受远程对象的完整的URL名字,该URL由完整的机器IP地址以及与对象绑定的字符串(也誻对象的绑定名)组成。在调用远程对象时,我们使用了RMI协
议。lookup方法向我们返回一个对象,在能够使用它前,我们必须将它的数据类型转换为与远程对象的数据类型一致。
Since we have both our server and client source ready, let's compile them both:
至此,我们已经有了服务器端和客户端的源代码,下面我们来编译这二个源文件:
编译远程服务器:
C:\jdk\bin\Javac workingdir\RmiServer.Java
编译远程客户端:
C:\jdk\bin\Javac workingdir\RmiClient.Java
在对我们的代码进行测试前,还必须首先启动RMI Registry。RMI Registry存储有所有绑定的数据,没有它,RMI就不能正常地运行!
启动Rmi Registry服务器:
C:\jdk\bin\start rmiregistry
我们会注意到,这时会出现一个空白的DOS提示符窗口,这表明Rmi Registry服务器在运行,注意不要关闭该窗口。然后,我们首先在一个DOS提示符窗口中运行Rmi服务
器,然后在另一个DOS提示符窗口中运行Rmi客户端。
启动RMI服务器:
C:\jdk\bin\Java workingdir\RmiServer
启动RMI客户端:
C:\jdk\bin\Java workingdir\RmiClient
如果一切正常,我们应该能够得到15这个输出。我们向AddNumbers方法输入10和5二个数字,该方法将这二者加起来,并将其和15返回给我们。如果得到了15这个输出,
说明我们已经成功地执行了一个远程方法。当然,在这里,我们并没有执行真正意义上的远程方法,因为我们的计算机既是服务器,又是客户机。如果有计算机网络,我们就可以
方便地进行执行远程方法的试验了。
一、RMI的工作原理
RMI能让一个Java程序去调用网络中另一台计算机的Java对象的方法,那么调用的效果就像是在本机上调用一样。
二、RMI远程地调用的步骤
- 调用者用通常方式调用对应java的一个远程对象的方法
- 客户stub的功能是把相关的参数组装成一个消息包或一组消息包。运行此方法的那个“站点”的地址和对象及方法的“标识符”、调用方法的参数都应包含在这条消息中。
- 将这条消息发送给对应的RMI运行时系统,该程序在转至指定的站点。
- 在接受此消息时,远程RMI运行时系统引用与被调用者对应的服务stub,并让它来处理这条消息。
- 服务器stub来反序列化有关参数,并用本地方法调用方式调用所需方法,然后把调用结果序列化,向调用者返回结果。
- 调用方RMI运行时系统棘手返回结果。调用者stub反序列化参数,然后,返回结果到调用方法。
三、RMI应用的基本开发步骤
案例:开发一个远程调用方法 rmb2Dollar(double rmb),它把客户(调用者)提供的人民币(rmb)数值传送个服务器,服务器按照一定的美元汇率将其返回。
1、定义远程服务接口类(Converter.java)
远程服务接口类必须继承java.rmi.Remote接口,并且在业务方法中抛出RemoteException异常
package cn.wzh;
import java.rmi.*;
public interface Converter extends Remote
{
public double rmb2Dollar(double rmb) throws RemoteException;
}
2、定义远程接口的实现类(ConverterImpl.java)
远程接口必须实现服务接口并且继承java.rmi.server.UnicastRemoteObject对象,构造方法必须抛出RemoteException
package cn.wzh;
import java.rmi.*;
import java.rmi.server.*;public class ConverterImpl extends UnicastRemoteObject implements Converter
{
public ConverterImpl() throws RemoteException
{
super();
}
public double rmb2Dollar(double rmb)
{
return rmb * 0.125;
}
}
3、编写RMI服务器端代码(RMIServer.java)
出于对RMI调用的安全考虑,需要对RMI应用设置安全管理器。使用命名绑定关键字到对象。
package cn.wzh;
import java.rmi.*;
public class RMIServer
{
// 由于这里测试我们是最终用户,所以直接将异常抛给虚拟机
public static void main(String[] args) throws Exception
{
System.setSecurityManager(new RMISecurityManager());
Converter c = new ConverterImpl();
Naming.bind("convert", c);
System.out.println("rmi server start ...");
}
}
4、编写RMI客户端调用类(RMIClient.java)
package cn.wzh;
import java.rmi.*;
import java.net.MalformedURLException;public class RMIClient
{
// 同样为了方便,直接异常抛出
public static void main(String[] args) throws Exception
{
System.setSecurityManager(new RMISecurityManager());
try
{
// 这里因为是在本地所以省略了地址跟协议,若在网络中的远程方法调用,需要这样写
// Converter c = (Converter)Naming.lookup("rmi://192.168.0.13/convert");
Converter c = (Converter)Naming.lookup("convert");
double rmb = c.rmb2Dollar(1000);
System.out.println("converter result : " + rmb);
}
catch (MalformedURLException e)
{
e.printStackTrace();
}
catch (RemoteException e)
{
e.printStackTrace();
}
catch (NotBoundException e)
{
e.printStackTrace();
}
}
}
5、整体编译代码
javac –d . *.java
6、编译生成stub
stub是什么,stub是java为rmi提供的封装你远程调用底层的一些细节处理(如socket和序列化操作),使你只需要去处理业务的实现而不必关注与底层网络的处理(有兴趣可以反编译去看看它是如何实现的,其实也很简单)。
rmic cn.wzh.ConverterImpl
其中rmic是RMI编译器,ConverterImpl为远程接口实现类名,它是编译过的class文件。
远程方法实现接口类(ConverterImpl)生成的stub名字规定为 `ConverterImpl` + `_stub`(ConverterImpl_stub.class)。
7、启动RMI服务器(RMIServer)
java cn.wzh.RMIServer
若出现这样的异常
需要在编写权限文件(Converter.policy),这里不对权限文件做太多讲解,我将会另外用一边文章来讲解权限文件。
内容为:
grant {
permission java.security.AllPermission;
};
再次编译运行,注意-D传入权限文件位置
java -Djava.security.policy=cn/wzh/converter.policy cn.wzh.RMIServer
不好意思,又一次运行出错
这是因为你本地java RMI服务器没有启动,现在就启动吧
start rmiregistry
再次运行 java -Djava.security.policy=cn/wzh/converter.policy cn.wzh.RMIServer
终于是启动成功了,那么开始运行RMI客户端。
8、运行RMI客户端
值得注意的是RMI客户端也需要加入权限文件的位置,不然也会抛出权限异常。
java -Djava.security.policy=cn/wzh/converter.policy cn.wzh.RMIClient
Java RMI之HelloWorld篇Java RMI 指的是远程方法调用 (Remote Method Invocation)。它是一种机制,能够让在某个 Java 虚拟机上的对象调用另一个 Java 虚拟机中的对象上的方法。可以用此方法调用的任何对象必须实现该远程接口。Java RMI不是什么新技术(在Java1.1的时代都有了),但却是是非常重要的底层技术。大名鼎鼎的EJB都是建立在rmi基础之上的,现在还有一些开源的远程调用组件,其底层技术也是rmi。在大力鼓吹Web Service、SOA的时代,是不是每个应用都应该选用笨拙的Web Service组件来实现,通过对比测试后,RMI是最简单的,在一些小的应用中是最合适的。下面通过一个简单的例子来说明RMI的原理和应用,下面这个例子是一个简单HelloWorld,但已涵盖RMI的核心应用与开发模式。/**
* Created by IntelliJ IDEA.
* User: leizhimin
* Date: 2008-8-7 21:50:02
* 定义一个远程接口,必须继承Remote接口,其中需要远程调用的方法必须抛出RemoteException异常
*/
public interface IHello extends Remote {
/**
* 简单的返回“Hello World!"字样
* @return 返回“Hello World!"字样
* @throws java.rmi.RemoteException
*/
public String helloWorld() throws RemoteException;
/**
* 一个简单的业务方法,根据传入的人名返回相应的问候语
* @param someBodyName 人名
* @return 返回相应的问候语
* @throws java.rmi.RemoteException
*/
public String sayHelloToSomeBody(String someBodyName) throws RemoteException;
}/**
* Created by IntelliJ IDEA.
* User: leizhimin
* Date: 2008-8-7 21:56:47
* 远程的接口的实现
*/
public class HelloImpl extends UnicastRemoteObject implements IHello {
/**
* 因为UnicastRemoteObject的构造方法抛出了RemoteException异常,因此这里默认的构造方法必须写,必须声明抛出RemoteException异常
*
* @throws RemoteException
*/
public HelloImpl() throws RemoteException {
}
/**
* 简单的返回“Hello World!"字样
*
* @return 返回“Hello World!"字样
* @throws java.rmi.RemoteException
*/
public String helloWorld() throws RemoteException {
return "Hello World!";
}
/**
* 一个简单的业务方法,根据传入的人名返回相应的问候语
*
* @param someBodyName 人名
* @return 返回相应的问候语
* @throws java.rmi.RemoteException
*/
public String sayHelloToSomeBody(String someBodyName) throws RemoteException {
return "你好," + someBodyName + "!";
}
}/**
* Created by IntelliJ IDEA.
* User: leizhimin
* Date: 2008-8-7 22:03:35
* 创建RMI注册表,启动RMI服务,并将远程对象注册到RMI注册表中。
*/
public class HelloServer {
public static void main(String args[]) {
try {
//创建一个远程对象
IHello rhello = new HelloImpl();
//本地主机上的远程对象注册表Registry的实例,并指定端口为8888,这一步必不可少(Java默认端口是1099),必不可缺的一步,缺少注册表创建,则无法绑定对象到远程注册表上
LocateRegistry.createRegistry(8888);
//把远程对象注册到RMI注册服务器上,并命名为RHello
//绑定的URL标准格式为:rmi://host:port/name(其中协议名可以省略,下面两种写法都是正确的)
Naming.bind( "rmi://localhost:8888/RHello",rhello);
// Naming.bind("//localhost:8888/RHello",rhello);
System.out.println(">>>>>INFO:远程IHello对象绑定成功!");
} catch (RemoteException e) {
System.out.println("创建远程对象发生异常!");
e.printStackTrace();
} catch (AlreadyBoundException e) {
System.out.println("发生重复绑定对象异常!");
e.printStackTrace();
} catch (MalformedURLException e) {
System.out.println("发生URL畸形异常!");
e.printStackTrace();
}
}
}/**
* Created by IntelliJ IDEA.
* User: leizhimin
* Date: 2008-8-7 22:21:07
* 客户端测试,在客户端调用远程对象上的远程方法,并返回结果。
*/
public class HelloClient {
public static void main(String args[]){
try {
//在RMI服务注册表中查找名称为RHello的对象,并调用其上的方法
IHello rhello =(IHello) Naming.lookup( "rmi://localhost:8888/RHello");
System.out.println(rhello.helloWorld());
System.out.println(rhello.sayHelloToSomeBody("熔岩"));
} catch (NotBoundException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
}
}
}运行RMI服务端程序:运行RMI客户端程序:总结:从上面的过程来看,RMI对服务器的IP地址和端口依赖很紧密,但是在开发的时候不知道将来的服务器IP和端口如何,但是客户端程序依赖这个IP和端口。这也是RMI的局限性之一。这个问题有两种解决途径:一是通过DNS来解决,二是通过封装将IP暴露到程序代码之外。RMI的局限性之二是RMI是Java语言的远程调用,两端的程序语言必须是Java实现,对于不同语言间的通讯可以考虑用Web Service或者公用对象请求代理体系(CORBA)来实现。本文出自 “熔 岩” 博客,转载请与作者联系!