Java RMI:Java远程方法调用,即Java RMI(Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。RMI全部的宗旨就是尽可能简化远程接口对象的使用。
我们知道远程过程调用(Remote Procedure Call, RPC)可以用于一个进程调用另一个进程(很可能在另一个远程主机上)中的过程,从而提供了过程的分布能力。Java 的 RMI 则在 RPC 的基础上向前又迈进了一步,即提供分布式对象间的通讯。
RMI(Remote Method Invocation)为远程方法调用,是允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。
RPC与RMI的区别
1:方法调用方式不同:
RMI中是通过在客户端的Stub对象作为远程接口进行远程方法的调用。每个远程方法都具有方法签名。如果一个方法在服务器上执行,但是没有相匹配的签名被添加到这个远程接口(stub)上,那么这个新方法就不能被RMI客户方所调用。
RPC中是通过网络服务协议向远程主机发送请求,请求包含了一个参数集和一个文本值,通常形成“classname.methodname(参数集)”的形式。RPC远程主机就去搜索与之相匹配的类和方法,找到后就执行方法并把结果编码,通过网络协议发回。
2:适用语言范围不同:
RMI只用于Java;
RPC是网络服务协议,与操作系统和语言无关。
3:调用结果的返回形式不同:
Java是面向对象的,所以RMI的调用结果可以是对象类型或者基本数据类型;
RMI的结果统一由外部数据表示 (External Data Representation, XDR) 语言表示,这种语言抽象了字节序类和数据类型结构之间的差异。
这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。
【JavaRMI】
一、工作原理
RMI能让一个Java程序去调用网络中另一台计算机的Java对象的方法,那么调用的效果就像是在本机上调用一样。通俗的讲:A机器上面有一个class,通过远程调用,B机器调用这个class 中的方法。
RMI,远程方法调用(Remote Method Invocation)是Enterprise JavaBeans的支柱,是建立分布式Java应用程序的方便途径。RMI是非常容易使用的,但是它非常的强大。
RMI的基础是接口,RMI构架基于一个重要的原理:定义接口和定义接口的具体实现是分开的。下面我们通过具体的例子,建立一个简单的远程计算服务和使用它的客户程序
RMI基本实现说明
我们希望在两个JVM虚拟机远程方法调用可以以常规方法进行调用,而无需关心数据的发送接收以及解析之类的问题。解决的思路是,在客户端安装一个代理(proxy),代理是位于客户端虚拟机中的一个对象,他对于客户端对象,看起来就像访问的远程对象一样,客户端代理会使用网络协议与服务器进行通信。同样的实现的服务端代码的程序员也不希望需要手动编码对客户端的调用进行复杂的处理,所以在服务器端也会有一个代理对象来进行通信的繁琐工作。
在RMI中,客户端的代理对象被称为存根(Stub),存根位于客户端机器上,它知道如何通过网络与服务器联系。存根会将远程方法所需的参数打包成一组字节。对参数编码的过程被称为参数编组(parameter marshalling),参数编组的目的是将参数转换成适合在虚拟机之间进行传递的形式。在RMI协议中,对象时使用序列化机制进行编码的。
总的来说,客户端的存根方法构造了一个信息块,它由以下几部分组成
-
被使用的远程对象的标识符
-
被调用的方法的描述
-
编组后的参数
然后,存根将此信息发送给服务器。在服务器的一端,接收器对象执行以下动作:
-
定位要调用的远程对象
-
调用所需的方法,并传递客户端提供的参数
-
捕获返回值或调用产生的异常。
-
将返回值编组,打包送回给客户端存根
客户端存根对来自服务器端的返回值或异常进行反编组,其结果就成为了调用存根返回值。
二、RMI包含部分:
- 远程服务的接口定义
- 远程服务接口的具体实现
- 桩(Stub)和框架(Skeleton)文件
- 一个运行远程服务的服务器
- 一个RMI命名服务,它允许客户端去发现这个远程服务
- 类文件的提供者(一个HTTP或者FTP服务器)
- 一个需要这个远程服务的客户端程序
三、RMI的用途?
RMI的用途是为分布式Java应用程序之间的远程通信提供服务,提供分布式服务。
目前主要应用时封装在各个J2EE项目框架中,例如Spring,EJB(Spring和EJB均封装了RMI技术)
在Spring中实现RMI:
①在服务器端定义服务的接口,定义特定的类实现这些接口;
②在服务器端使用org.springframework.remoting.rmi.RmiServiceExporter类来注册服务;
③在客户端使用org.springframework.remoting.rmi.RmiProxyFactoryBean来实现远程服务的代理功能;
④在客户端定义访问与服务器端服务接口相同的类
四、RMI的局限?
RMI目前使用Java远程消息交换协议JRMP(Java Remote Messaging Protocol)进行通信。JRMP是专为Java的远程对象制定的协议,由于JRMP是专为Java对象制定的,因此,RMI对于用非Java语言开发的应用系统的支持不足。不能与用非Java语言书写的对象进行通信(意思是只支持客户端和服务器端都是Java程序的代码的远程调用)。
五、RMI的使用局限?
由于客户机和服务器都是使用Java编写的,二者平台兼容性的要求仅仅是双方都运行在版本兼容的Java虚拟机上。
六、RMI调用远程方法的参数和返回值
当调用远程对象上的方法时,客户机除了可以将原始类型的数据作为参数一外,还可以将对象作为参数来传递,与之相对应的是返回值,可以返回原始类型或对象,这些都是通过Java的对象序列化(serialization)技术来实现的。(换而言之:参数或者返回值如果是对象的话必须实现Serializable接口)
八. 重要的问题
8.1 数据的传递问题
我们都知道在Java程序中引用类型(不包括基本类型)的参数传递是按引用传递的,对于在同一个虚拟机中的传递时是没有问题的,因为的参数的引用对应的是同一个内存空间,但是对于分布式系统中,由于对象不再存在于同一个内存空间,虚拟机A的对象引用对于虚拟机B没有任何意义,那么怎么解决这个问题呢?
第一种:将引用传递更改为值传递,也就是将对象序列化为字节,然后使用该字节的副本在客户端和服务器之间传递,而且一个虚拟机中对该值的修改不会影响到其他主机中的数据;但是对象的序列化也有一个问题,就是对象的嵌套引用就会造成序列化的嵌套,这必然会导致数据量的激增,因此我们需要有选择进行序列化,在Java中一个对象如果能够被序列化,需要满足下面两个条件之一:
是Java的基本类型;
实现java.io.Serializable接口(String类即实现了该接口);
对于容器类,如果其中的对象是可以序列化的,那么该容器也是可以序列化的;
可序列化的子类也是可以序列化的;
第二种:仍然使用引用传递,每当远程主机调用本地主机方法时,该调用还要通过本地主机查询该引用对应的对象,在任何一台机器上的改变都会影响原始主机上的数据,因为这个对象是共享的;
RMI中的参数传递和结果返回可以使用的三种机制(取决于数据类型):
简单类型:按值传递,直接传递数据拷贝;
远程对象引用(实现了Remote接口):以远程对象的引用传递;
远程对象引用(未实现Remote接口):按值传递,通过序列化对象传递副本,本身不允许序列化的对象不允许传递给远程方法;
8.2 远程对象的发现问题
在调用远程对象的方法之前需要一个远程对象的引用,如何获得这个远程对象的引用在RMI中是一个关键的问题,如果将远程对象的发现类比于IP地址的发现可能比较好理解一些。
在我们日常使用网络时,基本上都是通过域名来定位一个网站,但是实际上网络是通过IP地址来定位网站的,因此其中就需要一个映射的过程,域名系统(DNS)就是为了这个目的出现的,在域名系统中通过域名来查找对应的IP地址来访问对应的服务器。那么对应的,IP地址在这里就相当于远程对象的引用,而DNS则相当于一个注册表(Registry)。而域名在RMI中就相当于远程对象的标识符,客户端通过提供远程对象的标识符访问注册表,来得到远程对象的引用。这个标识符是类似URL地址格式的,它要满足的规范如下:
该名称是URL形式的,类似于http的URL,schema是rmi;
格式类似于rmi://host:port/name,host指明注册表运行的注解,port表明接收调用的端口,name是一个标识该对象的简单名称。
主机和端口都是可选的,如果省略主机,则默认运行在本地;如果端口也省略,则默认端口是1099;
九、 RMI类和接口(完成一个简单RMI需要用到的类)。
(一) Remote接口:是一个不定义方法的标记接口
Public interface Remote{}
在RMI中,远程接口声明了可以从远程Java虚拟机中调用的方法集。远程接口满足下列要求:
1、远程接口必须直接或间接扩展Java.rmi.Remote接口,且必须声明为public,除非客户端于远程接口在同一包中
2、在远程接口中的方法在声明时,除了要抛出与应用程序有关的一场之外,还必须包括RemoteException(或它的超类,IOExcepion或Exception)异常
3、在远程方法声明中,作为参数或返回值声明的远程对象必须声明为远程接口,而非该接口的实现类。
(二) RemoteObject抽象类实现了Remote接口和序列化Serializable接口,它和它的子类提供RMI服务器函数。
(三) LocateRegistry final()类用于获得特定主机的引导远程对象注册服务器程序的引用(即创建stub),或者创建能在特定端口接收调用的远程对象注册服务程序。
服务器端:向其他客户机提供远程对象服务
SomeService servcie=……;//远程对象服务
- Registry registry=LocateRegisty.getRegistry();//Registry是个接口,他继承了Remote,此方法返回本地主机在默认注册表端口 1099 上对远程对象
Registry
的引用。 - getRegistry(int port) 返回本地主机在指定 port 上对远程对象 Registry 的引用;
- getRegistry(String host) 返回指定
host
在默认注册表端口 1099 上对远程对象Registry
的引用; - getRegistry(String host, int port) 返回指定的
host
和port
上对远程对象 Registry 的引用 - registry.bind(“I serve”,service);// bind(String name,Remote obj) 绑定对此注册表中指定 name 的远程引用。name : 与该远程引用相关的名称 obj : 对远程对象(通常是一个 stub)的引用
- unbind(String name)移除注册表中指定name的绑定。
- rebind(String name,Remote obj)重新绑定,如果name已存在,但是Remote不一样则替换,如果Remote一样则丢弃现有的绑定
- lookup(String name) 返回注册表中绑定到指定 name 的远程引用,返回Remote
- String[] list() 返回在此注册表中绑定的名称的数组。该数组将包含一个此注册表中调用此方法时绑定的名称快照。
客户机端:向服务器提供相应的服务请求。
Registry registry=LocateRegisty.getRegistry();
SomeService servcie=(SomeService)registry.lookup(“I serve”);
Servcie.requestService();
(四) Naming类和Registry类类似。
客户端:
Naming.lookup(String url)
url 格式如下"rmi://localhost/"+远程对象引用
服务器端:
Registry registry=LocateRegistry.createRegistry(int port);
Naming.rebind(“service”,service);
(五) RMISecurityManager类
在RMI引用程序中,如果没有设置安全管理器,则只能从本地类路径加载stub和类,这可以确保应用程序不受由远程方法调用所下载的代码侵害
在从远程主机下载代码之前必须执行以下代码来安装RMISecurityManager:
System.setSecurityManager(new RMISecurityManager());
十、demo开发
为了编写一个demo,我们分为两部分,一部分是server端的代码,一部分是client端的代码,client端的代码主要是为了使用server端的代码。当然这个代码是非常简单的,只是为了说明问题,现实的使用会使比较复杂的。
(一) 我们的目的
建立一个server端的java project,包含远程端的代码,定义接口,定义接口实现,然后在建立一个client端的java project,通过RMI使用远端服务中的方法。
(二) 我们的代码结构
(三) 远程服务代码
1. 远程服务的接口定义
第一步就是建立和编译服务接口的Java代码。这个接口定义了所有的提供远程服务的功能,下面是源程序:
UserManagerInterface.java
1 package cn.com.tt.rmiserver.stub;
2
3 import java.rmi.Remote;
4 import java.rmi.RemoteException;
5
6 import cn.com.tt.rmiserver.bean.Account;
7
8 public interface UserManagerInterface extends Remote{
9 public String getUserName() throws RemoteException;
10 public Account getAdminAccount() throws RemoteException;
11 }
接口必须继承Remote类,每一个定义地方法都要抛出RemoteException异常对象。
2. 接口的具体实现
第二步就是对于上面的接口进行实现:
UserManagerImp.java
1 package cn.com.tt.rmiserver;
2
3 import java.rmi.RemoteException;
4
5 import cn.com.tt.rmiserver.stub.UserManagerInterface;
6 import cn.com.tt.rmiserver.bean.Account;
7
8 public class UserManagerImp implements UserManagerInterface {
9 public UserManagerImp() throws RemoteException {
10
11 }
12 private static final long serialVersionUID = -3111492742628447261L;
13
14 public String getUserName() throws RemoteException{
15 return "TT";
16 }
17 public Account getAdminAccount() throws RemoteException{
18 Account account=new Account();
19 account.setUsername("TT");
20 account.setPassword("123456");
21 return account;
22 }
23 }
3. 定义一个bean,实现implements Serializable序列化接口。也就是可以在client和server端进行传输的可序列化对象。
Account.java
1 package cn.com.tt.rmiserver.bean;
2
3 import java.io.Serializable;
4
5 public class Account implements Serializable,Cloneable{
6 private static final long serialVersionUID = -1858518369668584532L;
7 private String username;
8 private String password;
9
10 public String getUsername() {
11 return username;
12 }
13 public void setUsername(String username) {
14 this.username = username;
15 }
16 public String getPassword() {
17 return password;
18 }
19 public void setPassword(String password) {
20 this.password = password;
21 }
22 }
4. 定义server端的主程序入口。
Entry.java
1 package cn.com.tt.rmiserver.entry;
2
3 import java.rmi.AlreadyBoundException;
4 import java.rmi.RemoteException;
5 import java.rmi.registry.LocateRegistry;
6 import java.rmi.registry.Registry;
7 import java.rmi.server.UnicastRemoteObject;
8
9 import cn.com.tt.rmiserver.UserManagerImp;
10 import cn.com.tt.rmiserver.stub.UserManagerInterface;
11
12 public class Entry {
13 public static void main(String []args) throws AlreadyBoundException, RemoteException{
14 UserManagerImp userManager=new UserManagerImp();
15 UserManagerInterface userManagerI=(UserManagerInterface)UnicastRemoteObject.exportObject(userManager,0);
16 // Bind the remote object's stub in the registry
17 Registry registry = LocateRegistry.createRegistry(2002);
18
19 registry.rebind("userManager", userManagerI);
20 System.out.println("server is ready");
21 }
22 }
(四) client端代码
- 把Server端的Account类和接口UserManagerInterface 导出Export成jar包,命名为:RmiServerInterface.jar。导入到client中。
- 项目——右键——Export——java——jar file——next——选择Account类和接口UserManagerInterface——命名为:RmiServerInterface.jar如下图:
3. 新建一个java Project,导入jar包,编写客户端代码。
4. 代码
ClientEntry.java
1 package weiblog.rmi;
2
3 import java.rmi.NotBoundException;
4 import java.rmi.RemoteException;
5 import java.rmi.registry.LocateRegistry;
6 import java.rmi.registry.Registry;
7
8 import cn.com.tt.rmiserver.stub.UserManagerInterface;
9
10 public class ClientEntry {
11
12 public static void main(String []args){
13
14 try {
15 Registry registry = LocateRegistry.getRegistry("localhost",2004);
16 UserManagerInterface userManager = (UserManagerInterface)registry.lookup("userManager");
17 System.out.println("用户名是"+userManager.getAdminAccount().getUsername()
18 +"密码"+userManager.getAdminAccount().getPassword());
19 } catch (RemoteException e) {
20 // TODO Auto-generated catch block
21 e.printStackTrace();
22 } catch (NotBoundException e) {
23 // TODO Auto-generated catch block
24 e.printStackTrace();
25 }
26
27 }
28
29 }
5. 先运行服务器端代码, 然后运行客户端代码,就会显示运行结果,客户端可以运行多次,每次都可以取得服务器端的对象。如果要再次运行客户端代码就需要更改端口号,如果不更改就会显示端口号被占用。
十一、
RMI远程调用步骤:
1,客户对象调用客户端辅助对象上的方法
2,客户端辅助对象打包调用信息(变量,方法名),通过网络发送给服务端辅助对象
3,服务端辅助对象将客户端辅助对象发送来的信息解包,找出真正被调用的方法以及该方法所在对象
4,调用真正服务对象上的真正方法,并将结果返回给服务端辅助对象
5,服务端辅助对象将结果打包,发送给客户端辅助对象
6,客户端辅助对象将返回值解包,返回给客户对象
7,客户对象获得返回值
对于客户对象来说,步骤2-6是完全透明的
十二、搭建一个RMI服务的过程分为以下7步;
1,创建远程方法接口,该接口必须继承自Remote接口
2,创建远程方法接口实现类:
3,利用java自带rmic工具生成sutb存根类(jdk1.5.0_15/bin/rmic),jdk1.2以后的RMI可以通过反射API可以直接将请求发送给真实类,所以不需要skeleton类了、
4.启动RMI注册服务(jdk1.5.0_15/bin/rmiregistry)
a.
4,启动RMI注册服务(jdk1.5.0_15/bin/rmiregistry)
方式一:后台启动rmiregistry服务
[name@name jdk]$ jdk1.5.0_15/bin/rmiregistry 12312 &
[1] 22720
[name@name jdk]$ ps -ef|grep rmiregistry
name 22720 13763 0 16:43 pts/3 00:00:00 jdk1.5.0_15/bin/rmiregistry 12312
name 22737 13763 0 16:43 pts/3 00:00:00 grep rmiregistry
如果不带具体端口号,则默认为1099
方式二:人工创建rmiregistry服务,需要在代码中添加:
LocateRegistry.createRegistry(12312);
5,编写服务端代码
6,运行服务端
7,编写客户端代码
5,运行服务端
RMI 注册表(参见 LocateRegistry 类)
-
为了使得客户端能够查找到服务端对外提供的远程对象,RMI需要维护一个RMI注册表,该注册表维护了对于客户端而言的远程对象位置,对外提供了服务,服务端需要将要外提供服务的对象的代理绑定到RMI注册表中。
-
RMI注册表可以跟服务端不在一台主机上。
-
RMI注册表的启动有两种方式:一种是通过命令行 rmiregistry $port 在命令行启动;另外一种是通过 LocateRegistry 类的 createRegistry(int port) 方法启动。