远程方法调用 (RMI)





5.00/5 (3投票s)
本文的主要目的是描述在开发使用Java的分布式应用程序过程中使用RMI的优点。RMI是一种经典方法,与其它现代方法(例如.NET Remoting、WCF服务等)相比,用于远程访问对象。
1. 引言
当我们需要远程调用位于其它系统上的对象的方法时,就会使用RMI。RMI非常有限(这是它唯一能做的事情),它为我们提供了平台独立的理解。
当我们开始使用RMI时,流和套接字的编程就消失了。通过远程存储对象,程序员访问它们变得非常透明。
我想从一开始就声明,本文适用于那些(学生及其他)希望理解远程访问对象基本原理的人。这里的示例基于[1]中的原始示例。
为了使用远程对象,我们必须获取该对象的引用。用于该对象的方法的调用方式与本地对象相同。RMI使用字节流来传输数据和方法调用。这个过程由RMI基础设施自动完成。
2. 如何实现?
我们上面所说的只是对实际发生的事情的简化。服务器组件将通过使用命名服务注册所有对象来控制它们。完成此过程后,客户端程序将访问此接口。
该接口由服务器将公开的对象特有的方法签名组成。通过使用相同的命名服务,客户端将获得对该接口的引用,称为存根(stub)。在本地占位符存根(stub)中,存储着远程对象。在远程的服务器系统上,有一个骨架(skeleton)。当客户端调用远程对象的特定方法时,客户端会觉得它直接在对象上被调用。实际上,在存根中调用了一个等效的方法。存根会将调用和所有参数转发给位于远程服务器上的骨架。可以使用的参数是一些实现Serializable接口的原始类型。将这些类型的参数序列化的过程称为编组(marshalling)[3]。接下来,服务器端的骨架会将流转换为带有给定参数的方法调用。参数被反序列化,此过程称为解组(unmarshalling)[3]。最后,骨架将调用(在服务器上实现)的方法。我们上面描述的阶段如图1所示。该图以非常简化的方式展示了网络层面实际发生的情况。传输的两端还涉及两个层,传输层和远程引用层。要获取有关网络层的更多知识,请参见[2]和[3]。从J2SE 1.2开始,骨架被完全移除,服务器组件与引用层通信。基础和主要阶段(原理)保持不变,图1描绘了如何完成这些事情的非常有用的表示。
图1. 调用远程对象的方法[1]
如果方法返回一个值会发生什么?上面描述的过程是相反的,返回的值在服务器上被序列化(由谁?由骨架),并在客户端被反序列化(由谁?由存根)。
3. 如何实现?
为了在编程中使用RMI,我们必须添加以下包:
- java.rmi
- java.rmi.server
- java.rmi.registry
添加完这些包后,我们必须遵循一些基本步骤。这些步骤列表描述如下。
3.1. 创建接口;
3.2. 开发一个实现3.1中接口的类;
3.3. 开发服务器组件(进程);
3.4. 开发客户端组件(进程)。
在开始实现这些步骤之前,我们先描述一个简单的应用程序作为示例,我们将在其上应用上述步骤。
第一个应用程序示例是一个简单的问候消息显示给客户端,该客户端使用已在命名服务中注册的接口来调用服务器上关联的方法实现。在实际应用程序中,我们将有更多方法要调用,这些方法在某个类中实现(这将在下一个示例中展示)。
对于上述示例,我们开始实现具体步骤。
3.1. 创建接口
接口将始终
- 导入包java.rmi;
- 扩展Remote类,它像Serializable接口一样是一个“标记”接口,没有任何方法签名。在我们的例子中,接口的定义必须包含getGreetingMessage()方法的签名。该方法将对客户端可用。
- 此方法将抛出RemoteException。
该接口将如图2和代码清单1所示。为了让一切完美运行,请保持文件如图所示。稍后您会明白原因。
图2. 接口
清单1 - 接口结构
import java.rmi.*; public interface IHello extends Remote { public String getGreetingMessage() throws RemoteException; }
3.2. 开发一个实现3.1中接口(图2)的类
实现该接口的类应该
- 导入包java.rmi和java.rmi.server;
- 扩展RemoteObject类或其特定的子类(在一些实际应用中,您会经常发现UnicastRemoteObject子类的扩展,因为该类通过TCP与流进行点对点通信);
- 实现3.1中的接口IHello。在这里将实现getGreetingMessage()方法的正文。
- 有必要提供一个用于实现对象的构造函数。如果我们没有为该对象提供任何实现,我们将将其留空。构造函数需要抛出RemoteException。为了使一切正常且易于理解,我们将在接口名称旁边添加(附加)Implementation关键字,以形成实现类的名称。
实现IHello的类将如图3和代码清单2所示。关键字@Override表示getGreetingMessage()方法被重写。
图3. 实现IHello接口(参见步骤3.1)
清单2 - IHello接口的实现
import java.rmi.*; import java.rmi.server.*; public class HelloImplementation extends UnicastRemoteObject implements IHello { public HelloImplementation() throws RemoteException { //There is no action need in this moment. } @Override public String getGreetingMessage() throws RemoteException { return ("Hello there, student."); } }
3.3. 开发服务器组件(进程)
服务器组件的任务是创建我们上面实现的类的一个或多个对象。下一步是借助一个名为注册表(registry)的命名服务来注册它们。
注册过程通过Naming类(此类来自java.rmi包)的rebind()方法完成,它有两个参数:
- 一个字符串,用于保存远程对象的名称。该字符串具有URL形式,前面是rmi协议。
- 对远程对象的引用(作为Remote类型的参数)。
接下来,我们将提供服务器组件的代码(图4和代码清单3)。服务器只包含一个方法,即main。为了捕获不同类型的异常,main方法抛出Exception。
图4. 服务器
清单3 - 服务器实现
import java.rmi.*; public class HelloServerComponent { private static final String host = "localhost"; public static void main(String[] args) throws Exception { //** Step 1 //** Declare a reference for the object that will be implemented HelloImplementation temp = new HelloImplementation(); //** Step 2 //** Declare a string variable for holding the URL of the object's name String rmiObjectName = "rmi://" + host + "/Hello"; //Step 3 //Binding the object reference to the object name. Naming.rebind(rmiObjectName, temp); //Step 4 //Tell to the user that the process is completed. System.out.println("Binding complete...\n"); } }
该方法将在对象名称与其引用之间建立连接。客户端将能够使用远程对象的名称,通过注册表检索该对象的特定引用。
URL字符串指示存储在主机上的远程对象的名称。为了保持简单,我们将使用localhost(RMI假定的默认值)。RMI的默认端口是1099,您可以根据需要更改。
3.4. 开发客户端组件(进程)
客户端的目标是通过注册表获取远程对象的引用。为了实现这一点,我们使用Naming类中的lookup方法。lookup方法接收服务器在将对象引用绑定到注册表中的对象名称时生成的URL作为参数。lookup方法返回一个Remote引用。此引用需要强制转换为Hello引用(而不是HelloImplementation引用!)。获取Hello引用后,即可用于调用(call)我们在接口中提供的T方法。
代码如下所示,在图5和代码清单4中。
图5. 客户端
清单4 - 客户端实现
import java.rmi.*; public class HelloClientComponent { private static final String host = "localhost"; public static void main(String[] args) { try { //We obtain a reference to the object from the registry and next, //it will be typecasted into the most appropiate type. IHello greeting_message = (IHello) Naming.lookup("rmi://" + host + "/Hello"); //Next, we will use the above reference to invoke the remote //object method. System.out.println("Message received: " + greeting_message.getGreetingMessage()); } catch (ConnectException conEx) { System.out.println("Unable to connect to server!"); System.exit(1); } catch (Exception ex) { ex.printStackTrace(); System.exit(1); } } }
4. 运行过程如何进行?
为了运行应用程序,我们需要编译所有四个文件。这可以通过命令提示符窗口使用javac命令来完成。首先,我们需要导航到文件所在的路径,在我的情况下是E:\Proiecte\Java\RMI\src(参见图6)。如果您是使用NetBeans创建的文件(我建议初学者这样做),则文件存储在src文件夹中。
图6. 文件的位置
接下来,我们将使用javac命令按以下顺序编译文件(请按照我给出的顺序)
- javac IHello.java
- javac HelloImplementation.java
- javac HelloServerComponent.java
- javac HelloClientComponent.java
当您尝试编译其中一个文件并收到图7所示的消息时,您有两种选择:第一,您可以将所有文件复制到您的Java文件夹(在我的情况下路径是C:\Program Files\Java\jdk1.8.0_40\bin);第二,您可以将其添加为环境变量(请参阅文章的附件以了解如何操作)。
图7. 未找到命令javac
步骤1 - 配置好javac命令到环境变量后,我们开始编译所有文件。如果一切顺利,您的命令提示符应该如图8所示。
图8. 编译文件
步骤2 - 接下来,对HelloImplementation执行rmic命令(参见图9)。命令执行后阅读消息。
图9. 运行rmi命令
步骤3 – 运行rmiregistry命令(参见图10)。您不会收到任何消息,只有窗口下一行的闪烁光标和窗口标题的改变。
图10. 运行rmiregistry命令
步骤4 – 打开一个新的命令提示符,然后运行java HelloServerComponent(参见图11)。如果收到消息Binding complete….,则表示正常。
图11. 运行HelloServerComponent
步骤5 – 打开一个新的命令提示符,然后运行java HelloServerComponent。如果一切顺利,您应该会收到以下消息(参见图12):Message received: Hello there, student.
图12. 运行HelloClientComponent
至此,我们已经涵盖了设置RMI客户端-服务器应用程序所需的所有基本步骤。
5. 附件
5.1. 添加javac作为环境变量
为了将javac添加为环境变量,请遵循以下步骤:
- 进入“开始”菜单,输入environment,等待几秒钟,然后选择编辑系统环境变量(图13)。
图13. 访问环境变量
- 窗口打开(图14),然后单击窗口底部的环境变量…按钮。
图14. 添加环境变量的系统属性
- 您可以通过两种方式(您决定)将javac命令添加为环境变量:
- 作为您的Windows帐户的用户变量,或
- 作为整个计算机的系统变量。
决定好要放置路径的位置后,选择变量Path,然后按编辑(图15)。
图15. 选择所需的变量类型
- 转到整个包含不同路径的字符串的末尾,然后添加您的javac文件路径。在我的例子中是:C:\Program Files\Java\jdk1.8.0_40\bin,然后按确定按钮(图16)。
图16. 设置路径
- 为了验证您的路径是否设置正确,打开一个命令提示符并输入java –version。如果一切正常,您应该会收到图17所示的消息。
图18. 路径检查
6. 参考文献
[1] Jan Graba, An Introduction to Java Networking Programming, 2013, Springer Publishing House, ISBN: 978-1-4471-5253-8。
[2] 互联网协议套件 (TCP), http://en.wikipedia.org/wiki/Internet_protocol_suite
[3] 系统架构, http://www.cs.mun.ca/~michael/java/jdk1.1-beta2-docs/guide/rmi/rmi-arch.doc.html
历史
2015年3月8日:发布第一版文章
2015年3月9日:添加了文件代码清单。附上了项目文件的压缩包。