博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java RMI详解
阅读量:6360 次
发布时间:2019-06-23

本文共 15503 字,大约阅读时间需要 51 分钟。

hot3.png

RMI:远程方法调用(Remote Method Invocation)。能够让在某个java虚拟机上的对象像调用本地对象一样调用另一个java 虚拟机中的对象上的方法。

08122529_4DLZ.jpg

RMI远程调用步骤:

1,客户对象调用客户端辅助对象上的方法

2,客户端辅助对象打包调用信息(变量,方法名),通过网络发送给服务端辅助对象

3,服务端辅助对象将客户端辅助对象发送来的信息解包,找出真正被调用的方法以及该方法所在对象

4,调用真正服务对象上的真正方法,并将结果返回给服务端辅助对象

5,服务端辅助对象将结果打包,发送给客户端辅助对象

6,客户端辅助对象将返回值解包,返回给客户对象

7,客户对象获得返回值

对于客户对象来说,步骤2-6是完全透明的

搭建一个RMI服务的过程分为以下7步;

1,创建远程方法接口,该接口必须继承自Remote接口

Remote 接口是一个标识接口,用于标识所包含的方法可以从非本地虚拟机上调用的接口,Remote接口本身不包含任何方法

package com.timer.rmi;import java.rmi.Remote;import java.rmi.RemoteException;/** * Created by zhubo on 2018/1/7. */public interface Hello extends Remote {    public String sayHello(String name) throws RemoteException;}

由于远程方法调用的本质依然是网络通信,只不过隐藏了底层实现,网络通信是经常会出现异常的,所以接口的所有方法都必须抛出RemoteException以说明该方法是有风险的

2,创建远程方法接口实现类:

UnicastRemoteObject类的构造函数抛出了RemoteException,故其继承类不能使用默认构造函数,继承类的构造函数必须也抛出RemoteException

由于方法参数与返回值最终都将在网络上传输,故必须是可序列化的

import java.rmi.RemoteException;import java.rmi.server.UnicastRemoteObject;/** * Created by zhubo on 2018/1/7. */public class HelloImpl extends UnicastRemoteObject implements Hello {    public HelloImpl() throws RemoteException{        super();    }    @Override    public String sayHello(String name) throws RemoteException {        return null;    }}

3,利用java自带rmic工具生成sutb存根类(jdk1.5.0_15/bin/rmic)

jdk1.2以后的RMI可以通过反射API可以直接将请求发送给真实类,所以不需要skeleton类了

sutb存根为远程方法类在本地的代理,是在服务端代码的基础上生成的,需要HelloImpl.class文件,由于HelloImpl继承了Hello接口,故Hello.class文件也是不可少的

Test

- - server

- - - - Hello.class

- - - - HelloImpl.class

方式一:

[name@name Test]$ cd /home/name/Test/  [name@name Test]$ rmic server.HelloImpl

方式二:

[name@name Test]$ rmic -classpath /home/name/Test server.HelloImpl

运行成功后将会生成HelloImpl_Stub.class文件

测试:第三部可以省略

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,编写服务端代码

import java.rmi.Naming;import java.rmi.registry.LocateRegistry;/** * Created by zhubo on 2018/1/7. */public class HelloServer {    public static void main(String[] args) {        try {            Hello h = new HelloImpl();            //创建并导出接受指定port请求的本地主机上的Registry实例。            //LocateRegistry.createRegistry(12312);            /** Naming 类提供在对象注册表中存储和获得远程对远程对象引用的方法             *  Naming 类的每个方法都可将某个名称作为其一个参数,             *  该名称是使用以下形式的 URL 格式(没有 scheme 组件)的 java.lang.String:             *  //host:port/name             *  host:注册表所在的主机(远程或本地),省略则默认为本地主机             *  port:是注册表接受调用的端口号,省略则默认为1099,RMI注册表registry使用的著名端口             *  name:是未经注册表解释的简单字符串             */            //Naming.bind("//host:port/name", h);            LocateRegistry.createRegistry(12312);            Naming.bind("rmi://192.168.1.113:12312/Hello", h);            System.out.println("HelloServer启动成功");        }catch (Exception e){            e.printStackTrace();        }    }}

先创建注册表,然后才能在注册表中存储远程对象信息

6,运行服务端(58.164):

Test

- - server

- - - - Hello.class

- - - - HelloImpl.class

- - - - HelloServer.class

[name@name ~]$ java server.HelloServer  HelloServer启动成功

7,编写客户端代码

import java.rmi.Naming;/** * Created by zhubo on 2018/1/7. */public class HelloClient {    public static void main(String[] args) {        try {            Hello h = (Hello) Naming.lookup("rmi://192.168.1.113:12312/Hello");            System.out.println(h.sayHello("zhangsan"));        }catch (Exception e){            e.printStackTrace();        }    }}

8,运行客户端(58.163):

Test

- - client

- - - - HelloClient.class

- - server

- - - - Hello.class

- - - - HelloImpl_Stub.class//服务端生成的存根文件

205838_F96l_3101476.png

 

java RMI的缺点:

1,从代码中也可以看到,代码依赖于ip与端口

2,RMI依赖于Java远程消息交换协议JRMP(Java Remote Messaging Protocol),该协议为java定制,要求服务端与客户端都为java编写

 

 

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 的基础上向前又迈进了一步,即提供分布式对象间的通讯。

本地对象调用

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

ObjectClass objectA = new ObjectClass();    String retn = objectA.Method();

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

RMI就解决了这个问题。

工作原理

08122529_Z7il.jpg

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

要完成以上步骤需要有以下几个步骤: 
1、 生成一个远程接口 
2、 实现远程对象(服务器端程序)
3、 生成占位程序和骨干网(服务器端程序)
4、 编写服务器程序 
5、 编写客户程序 
6、 注册远程对象 
7、 启动远程对象  

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

08122529_wzya.jpg

上图中的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。

1. 定义一个远程接口

/* IHello.java */package mytest;/*  * 在Java中,只要一个类extends了java.rmi.Remote接口,即可成为存在于服务器端的远程对象,  * 供客户端访问并提供一定的服务。JavaDoc描述:Remote 接口用于标识其方法可以从非本地虚拟机上  * 调用的接口。任何远程对象都必须直接或间接实现此接口。只有在“远程接口”  * (扩展 java.rmi.Remote 的接口)中指定的这些方法才可被远程调用。  */import java.rmi.Remote;public interface IHello extends Remote {    /* extends了Remote接口的类或者其他接口中的方法若是声明抛出了RemoteException异常,     * 则表明该方法可被客户端远程访问调用。     */	public String sayHello(String name) throws java.rmi.RemoteException;}

2. 远程接口实现类

/* HelloImpl.java */package mytest;import java.rmi.RemoteException;import java.rmi.server.UnicastRemoteObject;/* * 远程对象必须实现java.rmi.server.UniCastRemoteObject类,这样才能保证客户端访问获得远程对象时, * 该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为“存根”, * 而服务器端本身已存在的远程对象则称之为“骨架”。其实此时的存根是客户端的一个代理,用于与服务器端的通信, * 而骨架也可认为是服务器端的一个代理,用于接收客户端的请求之后调用远程方法来响应客户端的请求。 */ /* java.rmi.server.UnicastRemoteObject构造函数中将生成stub和skeleton */public class HelloImpl extends UnicastRemoteObject implements IHello {    // 这个实现必须有一个显式的构造函数,并且要抛出一个RemoteException异常      protected HelloImpl() throws RemoteException {        super();    }        private static final long serialVersionUID = 4077329331699640331L;    public String sayHello(String name) throws RemoteException {        return "Hello " + name + " ^_^ ";    }}

3. 服务端

/* HelloServer.java */package mytest;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;/* 注册远程对象,向客户端提供远程对象服务  * 远程对象是在远程服务上创建的,你无法确切地知道远程服务器上的对象的名称 * 但是,将远程对象注册到RMI Service之后,客户端就可以通过RMI Service请求 * 到该远程服务对象的stub了,利用stub代理就可以访问远程服务对象了 */ public class HelloServer {    public static void main(String[] args) {        try {            IHello hello = new HelloImpl(); /* 生成stub和skeleton,并返回stub代理引用 */            /* 本地创建并启动RMI Service,被创建的Registry服务将在指定的端口上侦听到来的请求              * 实际上,RMI Service本身也是一个RMI应用,我们也可以从远端获取Registry:             *     public interface Registry extends Remote;             *     public static Registry getRegistry(String host, int port) throws RemoteException;             */            LocateRegistry.createRegistry(1099);            /* 将stub代理绑定到Registry服务的URL上 */            java.rmi.Naming.rebind("rmi://localhost:1099/hello", hello);            System.out.print("Ready");        } catch (Exception e) {            e.printStackTrace();        }    }}

4. 客户端

/* Hello_RMI_Client.java */package mytest;import java.rmi.Naming;/* 客户端向服务端请求远程对象服务 */public class Hello_RMI_Client {    public static void main(String[] args) {        try {            /* 从RMI Registry中请求stub             * 如果RMI Service就在本地机器上,URL就是:rmi://localhost:1099/hello             * 否则,URL就是:rmi://RMIService_IP:1099/hello             */            IHello hello = (IHello) Naming.lookup("rmi://localhost:1099/hello");            /* 通过stub调用远程接口实现 */            System.out.println(hello.sayHello("zhangxianxin"));        } catch (Exception e) {            e.printStackTrace();        }    }}

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

08122529_MBAJ.jpg

RMI应用需要用到的类

08122530_ouH5.jpg

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代码:

public interface Person {          public int getAge() throws Throwable;          public String getName() throws Throwable;      }

2. Stub的实现 

Person_Stub代码:   

import java.io.ObjectOutputStream;      import java.io.ObjectInputStream;      import java.net.Socket;      public class Person_Stub implements Person {          private Socket socket;          public Person_Stub() throws Throwable {              // connect to skeleton              socket = new Socket("computer_name", 9000);          }          public int getAge() throws Throwable {              // pass method name to skeleton              ObjectOutputStream outStream =                  new ObjectOutputStream(socket.getOutputStream());              outStream.writeObject("age");              outStream.flush();              ObjectInputStream inStream =                  new ObjectInputStream(socket.getInputStream());              return inStream.readInt();          }          public String getName() throws Throwable {              // pass method name to skeleton              ObjectOutputStream outStream =                  new ObjectOutputStream(socket.getOutputStream());              outStream.writeObject("name");              outStream.flush();              ObjectInputStream inStream =                  new ObjectInputStream(socket.getInputStream());              return (String)inStream.readObject();          }}

Person_Stub是建立socket连接,并向Skeleton发请求,然后通过Skeleton调用PersonServer的方法,最后接收返回的结果。 

3. 骨架(Skeleton)的实现

Person_Skeleton代码:

import java.io.ObjectOutputStream;      import java.io.ObjectInputStream;      import java.net.Socket;      import java.net.ServerSocket;      public class Person_Skeleton extends Thread {          private PersonServer myServer;          public Person_Skeleton(PersonServer server) {              // get reference of object server              this.myServer = server;          }          public void run() {              try {                  // new socket at port 9000                  ServerSocket serverSocket = new ServerSocket(9000);                  // accept stub's request                  Socket socket = serverSocket.accept();                  while (socket != null) {                      // get stub's request                      ObjectInputStream inStream =                          new ObjectInputStream(socket.getInputStream());                      String method = (String)inStream.readObject();                      // check method name                      if (method.equals("age")) {                          // execute object server's business method                          int age = myServer.getAge();                          ObjectOutputStream outStream =                              new ObjectOutputStream(socket.getOutputStream());                          // return result to stub                          outStream.writeInt(age);                          outStream.flush();                      }                      if(method.equals("name")) {                          // execute object server's business method                          String name = myServer.getName();                          ObjectOutputStream outStream =                              new ObjectOutputStream(socket.getOutputStream());                          // return result to stub                          outStream.writeObject(name);                          outStream.flush();                      }                  }              } catch(Throwable t) {                  t.printStackTrace();                  System.exit(0);              }          }           }

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

4. Person的实现PersonServer类

PersonServer代码:

public class PersonServer implements Person {          private int age;          private String name;          public PersonServer(String name, int age) {              this.age = age;              this.name = name;          }          public int getAge() {              return age;          }          public String getName() {              return name;          }              public static void main(String args []) {              // new object server              PersonServer person = new PersonServer("Richard", 34);              Person_Skeleton skel = new Person_Skeleton(person);              skel.start();          } }

PersonServer中对Person的接口进行了真正的实现,创建PersonServer实例对象,并启动Person_Skeleton服务。

5. Client的实现

PersonClient 代码:

public class PersonClient {          public static void main(String [] args) {              try {                  Person person = new Person_Stub();                  int age = person.getAge();                  String name = person.getName();                  System.out.println(name + " is " + age + " years old");              } catch(Throwable t) {                  t.printStackTrace();              }          }      }

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

 

 

转载于:https://my.oschina.net/LucasZhu/blog/1603666

你可能感兴趣的文章
ThinkPHP 模板变量输出
查看>>
android系统信息(内存、cpu、sd卡、电量、版本)获取
查看>>
Eclipse Debug Android Native Application
查看>>
java动态代理
查看>>
node.js原型继承
查看>>
揭露让Linux与Windows隔阂消失的奥秘(1)
查看>>
我的友情链接
查看>>
Mysql备份和恢复策略
查看>>
linux17-邮件服务器
查看>>
AS开发JNI步骤
查看>>
二分查找,php
查看>>
python面试题-django相关
查看>>
Python——eventlet.greenthread
查看>>
记大众点评之面试经历
查看>>
第三章:基本概念
查看>>
Jersey+mybatis实现web项目第一篇
查看>>
C++形参中const char * 与 char * 的区别
查看>>
espresso 2.0.4 Apple Xcode 4.4.1 coteditor 价格
查看>>
Object-C中emoji与json的问题
查看>>
linux 命令
查看>>