java RMI原理详解

简介: 【本文转载自java RMI原理详解】 定义 RMI(Remote Method Invocation)为远程方法调用,是允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。 这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。 Java RMI:Java远程方法调用,即Java RMI(Java Remote M

【本文转载自java RMI原理详解


定义

RMI(Remote Method Invocation)为远程方法调用,是允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。 这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。

Java RMI:Java远程方法调用,即Java RMI(Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。RMI全部的宗旨就是尽可能简化远程接口对象的使用。

我们知道远程过程调用(Remote Procedure Call, RPC)可以用于一个进程调用另一个进程(很可能在另一个远程主机上)中的过程,从而提供了过程的分布能力。Java 的 RMI 则在 RPC 的基础上向前又迈进了一步,即提供分布式对象间的通讯。

本地对象调用

我们先看看本地对象方法的调用:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. ObjectClass objectA = new ObjectClass();    
  2. String retn = objectA.Method();    

但是想想,如果objectA对象在JVM a上;而我们的程序在JVM b上,而且想访问JVM a上的objectA对象方法,如何做呢?对于JVM b上的应用程序来说,是不知道JVM a上创建的ObjectClass实例对象名称是什么,因为这次我创建的实例对象可能是objectA,下次我程序一改,我创建的实例对象又叫objectB了,另外,我创没创建ObjectClass实例对象,JVM b上应用程序又怎么知道呢?

RMI就解决了这个问题。

工作原理


方法调用从客户对象经占位程序(Stub)、远程引用层(Remote Reference Layer)和传输层(Transport Layer)向下,传递给主机,然后再次经传 输层,向上穿过远程调用层和骨干网(Skeleton),到达服务器对象。 占位程序扮演着远程服务器对象的代理的角色,使该对象可被客户激活。 远程引用层处理语义、管理单一或多重对象的通信,决定调用是应发往一个服务器还是多个。传输层管理实际的连接,并且追踪可以接受方法调用的远程对象。服务器端的骨干网完成对服务器对象实际的方法调用,并获取返回值。返回值向下经远程引用层、服务器端的传输层传递回客户端,再向上经传输层和远程调用层返回。最后,占位程序获得返回值。 
要完成以上步骤需要有以下几个步骤: 
1、 生成一个远程接口 
2、 实现远程对象(服务器端程序)
3、 生成占位程序和骨干网(服务器端程序)
4、 编写服务器程序 
5、 编写客户程序 
6、 注册远程对象 
7、 启动远程对象  

由于有RMI系统的支持,我们写RMI应用程序时只需要继承相关类,实现相关接口就可以了。也就是说,我们只需要定义接口、接口实现、客户端程序和服务端程序就可以了。


上图中的stub和skeleton代理都是在服务端程序中由RMI系统动态生成,服务端程序只需要继承java.rmi.server.UnicastRemoteObject类。

那么上图中的RMI Service(RMI registry)是怎么回事呢?

先卖个关子:

可以说,RMI由3个部分构成,第一个是RMIService即JDK提供的一个可以独立运行的程序(bin目录下的rmiregistry),第二个是RMIServer即我们自己编写的一个java项目,这个项目对外提供服务。第三个是RMIClient即我们自己编写的另外一个java项目,这个项目远程使用RMIServer提供的服务。
首先,RMIService必须先启动并开始监听对应的端口。
其次,RMIServer将自己提供的服务的实现类注册到RMIService上,并指定一个访问的路径(或者说名称)供RMIClient使用。
最后,RMIClient使用事先知道(或和RMIServer约定好)的路径(或名称)到RMIService上去寻找这个服务,并使用这个服务在本地的接口调用服务的具体方法。
通俗的讲完了再稍微技术的讲下:
首先,在一个JVM中启动rmiregistry服务,启动时可以指定服务监听的端口,也可以使用默认的端口。
其次,RMIServer在本地先实例化一个提供服务的实现类,然后通过RMI提供的Naming,Context,Registry等类的bind或rebind方法将刚才实例化好的实现类注册到RMIService上并对外暴露一个名称。
最后,RMIClient通过本地的接口和一个已知的名称(即RMIServer暴露出的名称)再使用RMI提供的Naming,Context,Registry等类的lookup方法从RMIService那拿到实现类。这样虽然本地没有这个类的实现类,但所有的方法都在接口里了,想怎么调就怎么调吧。
值得注意的是理论上讲RMIService,RMIServer,RMIClient可以部署到3个不同的JVM中,这个时候的执行的顺序是RMIService---RMIServer—RMIClient。另外也可以由RMIServer来启动RMIService这时候执行的顺序是RMIServer—RMIService—RMIClient。

实际应用中很少有单独提供一个RMIService服务器,开发的时候可以使用Registry类在RMIServer中启动RMIService。

Java RMI 简单示例

1. 定义一个远程接口

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. /* IHello.java */  
  2. package mytest;  
  3. /*  
  4.  * 在Java中,只要一个类extends了java.rmi.Remote接口,即可成为存在于服务器端的远程对象,  
  5.  * 供客户端访问并提供一定的服务。JavaDoc描述:Remote 接口用于标识其方法可以从非本地虚拟机上  
  6.  * 调用的接口。任何远程对象都必须直接或间接实现此接口。只有在“远程接口”  
  7.  * (扩展 java.rmi.Remote 的接口)中指定的这些方法才可被远程调用。  
  8.  */  
  9. import java.rmi.Remote;  
  10.   
  11. public interface IHello extends Remote {  
  12.     /* extends了Remote接口的类或者其他接口中的方法若是声明抛出了RemoteException异常, 
  13.      * 则表明该方法可被客户端远程访问调用。 
  14.      */  
  15.     public String sayHello(String name) throws java.rmi.RemoteException;  
  16. }  

 

2. 远程接口实现类

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. /* HelloImpl.java */  
  2. package mytest;  
  3. import java.rmi.RemoteException;  
  4. import java.rmi.server.UnicastRemoteObject;  
  5.   
  6. /* 
  7.  * 远程对象必须实现java.rmi.server.UniCastRemoteObject类,这样才能保证客户端访问获得远程对象时, 
  8.  * 该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为“存根”, 
  9.  * 而服务器端本身已存在的远程对象则称之为“骨架”。其实此时的存根是客户端的一个代理,用于与服务器端的通信, 
  10.  * 而骨架也可认为是服务器端的一个代理,用于接收客户端的请求之后调用远程方法来响应客户端的请求。 
  11.  */   
  12.   
  13. /* java.rmi.server.UnicastRemoteObject构造函数中将生成stub和skeleton */  
  14. public class HelloImpl extends UnicastRemoteObject implements IHello {  
  15.     // 这个实现必须有一个显式的构造函数,并且要抛出一个RemoteException异常    
  16.     protected HelloImpl() throws RemoteException {  
  17.         super();  
  18.     }  
  19.       
  20.     private static final long serialVersionUID = 4077329331699640331L;  
  21.     public String sayHello(String name) throws RemoteException {  
  22.         return "Hello " + name + " ^_^ ";  
  23.     }  
  24. }  

 

3. 服务端

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. /* HelloServer.java */  
  2. package mytest;  
  3. import java.rmi.registry.LocateRegistry;  
  4. import java.rmi.registry.Registry;  
  5.   
  6. /* 注册远程对象,向客户端提供远程对象服务  
  7.  * 远程对象是在远程服务上创建的,你无法确切地知道远程服务器上的对象的名称 
  8.  * 但是,将远程对象注册到RMI Service之后,客户端就可以通过RMI Service请求 
  9.  * 到该远程服务对象的stub了,利用stub代理就可以访问远程服务对象了 
  10.  */  
  11.    
  12. public class HelloServer {  
  13.     public static void main(String[] args) {  
  14.         try {  
  15.             IHello hello = new HelloImpl(); /* 生成stub和skeleton,并返回stub代理引用 */  
  16.             /* 本地创建并启动RMI Service,被创建的Registry服务将在指定的端口上侦听到来的请求  
  17.              * 实际上,RMI Service本身也是一个RMI应用,我们也可以从远端获取Registry: 
  18.              *     public interface Registry extends Remote; 
  19.              *     public static Registry getRegistry(String host, int port) throws RemoteException; 
  20.              */  
  21.             LocateRegistry.createRegistry(1099);  
  22.             /* 将stub代理绑定到Registry服务的URL上 */  
  23.             java.rmi.Naming.rebind("rmi://localhost:1099/hello", hello);  
  24.             System.out.print("Ready");  
  25.         } catch (Exception e) {  
  26.             e.printStackTrace();  
  27.         }  
  28.     }  
  29. }  

4. 客户端

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. /* Hello_RMI_Client.java */  
  2. package mytest;  
  3. import java.rmi.Naming;  
  4.   
  5. /* 客户端向服务端请求远程对象服务 */  
  6. public class Hello_RMI_Client {  
  7.     public static void main(String[] args) {  
  8.         try {  
  9.             /* 从RMI Registry中请求stub 
  10.              * 如果RMI Service就在本地机器上,URL就是:rmi://localhost:1099/hello 
  11.              * 否则,URL就是:rmi://RMIService_IP:1099/hello 
  12.              */  
  13.             IHello hello = (IHello) Naming.lookup("rmi://localhost:1099/hello");  
  14.             /* 通过stub调用远程接口实现 */  
  15.             System.out.println(hello.sayHello("zhangxianxin"));  
  16.         } catch (Exception e) {  
  17.             e.printStackTrace();  
  18.         }  
  19.     }  
  20. }  

RMI 应用各个类的交互时序图


RMI应用需要用到的类






1. RemoteObject抽象类实现了Remote接口和序列化Serializable接口,它和它的子类提供RMI服务器函数。
为什么远端调用方法抛出的RemoteException异常的父类竟然是IOException呢?
这是因为,远端调用方法其实际上是通过网络IO进行的。

2. 当客户端通过RMI注册表找到一个远程接口的时候,所得到的其实是远程接口的一个动态代理对象。当客户端调用其中的方法的时候,方法的参数对象会在序列化之后,传输到服务器端。服务器端接收到之后,进行反序列化得到参数对象。并使用这些参数对象,在服务器端调用实际的方法。调用的返回值Java对象经过序列化之后,再发送回客户端。客户端再经过反序列化之后得到Java对象,返回给调用者。这中间的序列化过程对于使用者来说是透明的,由动态代理对象自动完成。除了序列化之外,RMI还使用了动态类加载技术。当需要进行反序列化的时候,如果该对象的类定义在当前JVM中没有找到,RMI会尝试从远端下载所需的类文件定义。可以在RMI程序启动的时候,通过JVM参数java.rmi.server.codebase来指定动态下载Java类文件的URL。

实现一个stub和skeleton程序

明白了RMI应用的原理后,可以自行实现一个stub和skeleton程序,进一步探索RMI的代理访问原理。
RMI的本质就是实现在不同JVM之间的调用,它的实现方法就是在两个JVM中各开一个Stub和Skeleton,二者通过socket通信来实现参数和返回值的传递。

1. 定义一个Person的接口,其中有两个business method, getAge() 和getName()
Person代码:
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. public interface Person {        
  2.     public int getAge() throws Throwable;        
  3.     public String getName() throws Throwable;        
  4. }  

2. Stub的实现 
Person_Stub代码:   
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. import java.io.ObjectOutputStream;        
  2. import java.io.ObjectInputStream;        
  3. import java.net.Socket;        
  4. public class Person_Stub implements Person {        
  5.     private Socket socket;        
  6.     public Person_Stub() throws Throwable {        
  7.         // connect to skeleton        
  8.         socket = new Socket("computer_name"9000);        
  9.     }        
  10.     public int getAge() throws Throwable {        
  11.         // pass method name to skeleton        
  12.         ObjectOutputStream outStream =        
  13.             new ObjectOutputStream(socket.getOutputStream());        
  14.         outStream.writeObject("age");        
  15.         outStream.flush();        
  16.         ObjectInputStream inStream =        
  17.             new ObjectInputStream(socket.getInputStream());        
  18.         return inStream.readInt();        
  19.     }        
  20.     public String getName() throws Throwable {        
  21.         // pass method name to skeleton        
  22.         ObjectOutputStream outStream =        
  23.             new ObjectOutputStream(socket.getOutputStream());        
  24.         outStream.writeObject("name");        
  25.         outStream.flush();        
  26.         ObjectInputStream inStream =        
  27.             new ObjectInputStream(socket.getInputStream());        
  28.         return (String)inStream.readObject();        
  29.     }  
  30. }   
Person_Stub是建立socket连接,并向Skeleton发请求,然后通过Skeleton调用PersonServer的方法,最后接收返回的结果。    

3. 骨架(Skeleton)的实现
Person_Skeleton代码:
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. import java.io.ObjectOutputStream;        
  2. import java.io.ObjectInputStream;        
  3. import java.net.Socket;        
  4. import java.net.ServerSocket;        
  5. public class Person_Skeleton extends Thread {        
  6.     private PersonServer myServer;        
  7.     public Person_Skeleton(PersonServer server) {        
  8.         // get reference of object server        
  9.         this.myServer = server;        
  10.     }        
  11.     public void run() {        
  12.         try {        
  13.             // new socket at port 9000        
  14.             ServerSocket serverSocket = new ServerSocket(9000);        
  15.             // accept stub's request        
  16.             Socket socket = serverSocket.accept();        
  17.             while (socket != null) {        
  18.                 // get stub's request        
  19.                 ObjectInputStream inStream =        
  20.                     new ObjectInputStream(socket.getInputStream());        
  21.                 String method = (String)inStream.readObject();        
  22.                 // check method name        
  23.                 if (method.equals("age")) {        
  24.                     // execute object server's business method        
  25.                     int age = myServer.getAge();        
  26.                     ObjectOutputStream outStream =        
  27.                         new ObjectOutputStream(socket.getOutputStream());        
  28.                     // return result to stub        
  29.                     outStream.writeInt(age);        
  30.                     outStream.flush();        
  31.                 }        
  32.                 if(method.equals("name")) {        
  33.                     // execute object server's business method        
  34.                     String name = myServer.getName();        
  35.                     ObjectOutputStream outStream =        
  36.                         new ObjectOutputStream(socket.getOutputStream());        
  37.                     // return result to stub        
  38.                     outStream.writeObject(name);        
  39.                     outStream.flush();        
  40.                 }        
  41.             }        
  42.         } catch(Throwable t) {        
  43.             t.printStackTrace();        
  44.             System.exit(0);        
  45.         }        
  46.     }             
  47. }  

Skeleton类 extends from Thread,它长驻在后台运行,随时接收client发过来的request。并根据发送过来的key去调用相应的business method。

4. Person的实现PersonServer类
PersonServer代码:
 
 
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. public class PersonServer implements Person {        
  2.     private int age;        
  3.     private String name;        
  4.     public PersonServer(String name, int age) {        
  5.         this.age = age;        
  6.         this.name = name;        
  7.     }        
  8.     public int getAge() {        
  9.         return age;        
  10.     }        
  11.     public String getName() {        
  12.         return name;        
  13.     }        
  14.       
  15.     public static void main(String args []) {        
  16.         // new object server        
  17.         PersonServer person = new PersonServer("Richard"34);        
  18.         Person_Skeleton skel = new Person_Skeleton(person);        
  19.         skel.start();        
  20.     }   
  21. }  
PersonServer中对Person的接口进行了真正的实现,创建PersonServer实例对象,并启动Person_Skeleton服务。

5. Client的实现
PersonClient 代码:
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. public class PersonClient {        
  2.     public static void main(String [] args) {        
  3.         try {        
  4.             Person person = new Person_Stub();        
  5.             int age = person.getAge();        
  6.             String name = person.getName();        
  7.             System.out.println(name + " is " + age + " years old");        
  8.         } catch(Throwable t) {        
  9.             t.printStackTrace();        
  10.         }        
  11.     }        
  12. }        

Client(PersonClient)的本质是,它要知道Person接口的定义,并实例一个Person_Stub,通过Stub来调用business method,至于Stub怎么去和Server沟通,Client就不用管了。
注意它的写法:
Person person = new Person_Stub();而不是Person_Stub person = new Person_Stub();为什么?因为要面向接口编程嘛,呵呵。
实际上,对于PersonClient来说,它只关注Person接口,而且它的本质就是远程调用Person接口,而Person_Stub只是负责代理PersonClient去与Server交互。

参考

1. http://www.blogjava.NET/zhenyu33154/articles/320245.html

2. http://guojuanjun.blog.51cto.com/277646/1423392/

3. http://blog.sina.com.cn/s/blog_4918a7d90100oftg.html

4. http://haolloyin.blog.51cto.com/1177454/332426/

5. http://www.it165.Net/pro/html/201404/11411.html

6. http://spidermanzy.iteye.com/blog/1741045

7. http://www.infoq.com/cn/articles/cf-java-object-serialization-rmi/

8. http://www.cnblogs.com/yin-jingyu/archive/2012/06/14/2549361.html

目录
相关文章
|
21天前
|
存储 Java 关系型数据库
高效连接之道:Java连接池原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。频繁创建和关闭连接会消耗大量资源,导致性能瓶颈。为此,Java连接池技术通过复用连接,实现高效、稳定的数据库连接管理。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接池的基本操作、配置和使用方法,以及在电商应用中的具体应用示例。
40 5
|
1月前
|
存储 算法 Java
Java HashSet:底层工作原理与实现机制
本文介绍了Java中HashSet的工作原理,包括其基于HashMap实现的底层机制。通过示例代码展示了HashSet如何添加元素,并解析了add方法的具体过程,包括计算hash值、处理碰撞及扩容机制。
|
10天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
11天前
|
Java
Java之CountDownLatch原理浅析
本文介绍了Java并发工具类`CountDownLatch`的使用方法、原理及其与`Thread.join()`的区别。`CountDownLatch`通过构造函数接收一个整数参数作为计数器,调用`countDown`方法减少计数,`await`方法会阻塞当前线程,直到计数为零。文章还详细解析了其内部机制,包括初始化、`countDown`和`await`方法的工作原理,并给出了一个游戏加载场景的示例代码。
Java之CountDownLatch原理浅析
|
13天前
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
Java ArrayList扩容的原理
|
2月前
|
算法 Java
JAVA并发编程系列(8)CountDownLatch核心原理
面试中的编程题目“模拟拼团”,我们通过使用CountDownLatch来实现多线程条件下的拼团逻辑。此外,深入解析了CountDownLatch的核心原理及其内部实现机制,特别是`await()`方法的具体工作流程。通过详细分析源码与内部结构,帮助读者更好地理解并发编程的关键概念。
|
19天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
36 2
|
22天前
|
Java 数据格式 索引
使用 Java 字节码工具检查类文件完整性的原理是什么
Java字节码工具通过解析和分析类文件的字节码,检查其结构和内容是否符合Java虚拟机规范,确保类文件的完整性和合法性,防止恶意代码或损坏的类文件影响程序运行。
|
19天前
|
算法 Java 数据库连接
Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性
本文详细介绍了Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性。连接池通过复用数据库连接,显著提升了应用的性能和稳定性。文章还展示了使用HikariCP连接池的示例代码,帮助读者更好地理解和应用这一技术。
31 1
|
25天前
|
存储 安全 Java
深入理解Java中的FutureTask:用法和原理
【10月更文挑战第28天】`FutureTask` 是 Java 中 `java.util.concurrent` 包下的一个类,实现了 `RunnableFuture` 接口,支持异步计算和结果获取。它可以作为 `Runnable` 被线程执行,同时通过 `Future` 接口获取计算结果。`FutureTask` 可以基于 `Callable` 或 `Runnable` 创建,常用于多线程环境中执行耗时任务,避免阻塞主线程。任务结果可通过 `get` 方法获取,支持阻塞和非阻塞方式。内部使用 AQS 实现同步机制,确保线程安全。