RMI原理揭秘之远程对象

简介:

讨论开始之前,我们先看看网上的一个例子,这个例子我腾抄了一分,没有用链接的方式,只是为了让大家看得方便,如有侵权,我立马***。

  1. 定义远程接口:

  2. 1
    2
    3
    4
    5
    6
    package  com.guojje;
    import  java.rmi.Remote;
    import  java.rmi.RemoteException;
    public  interface  IHello  extends  Remote {
         public  int  helloWorld() throws  RemoteException;
    }

 3. 定义实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package  com.guojje;
import  java.rmi.RemoteException;
import  java.rmi.server.UnicastRemoteObject;
 
public  class  Hello  extends  UnicastRemoteObject  implements  IHello {
     private  static  final  long  serialVersionUID = 1L;
     private  int  index =  0 ;
     protected  Hello()  throws  RemoteException {
     }
     @Override
     public  int  helloWorld(){
         System.out.println( "Hello!" );
         return  ++index;
     }
}

 4.服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package  com.guojje;
import  java.rmi.registry.LocateRegistry;
import  java.rmi.registry.Registry;
 
public  class  HelloServer {
     public  static  void  main(String args[]) {
         try  {
             IHello rhello =  new  Hello();
             Registry registry = LocateRegistry.createRegistry( 8888 );
             registry.bind( "test" , rhello);
             System.out.println( "Remote Hello Object is bound sucessfully!" );
         catch  (Exception e) {
             e.printStackTrace();
         }
     }
}

5.客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package  com.guojje;
import  java.rmi.Naming;
public  class  HelloClient {
     public  static  void  main(String args[]) {
         try  {
             for  ( int  i =  0 ; i <  5 ; i++) {
                 IHello rhello = (IHello) Naming
                         .lookup( "rmi://localhost:8888/test" );
                 System.out.println(rhello.helloWorld());
             }
         catch  (Exception e) {
             e.printStackTrace();
         }
     }
}

6.输出结果:  

1)服务端输出:

 Remote Hello Object is bound sucessfully!
Hello!
Hello!
Hello!
Hello!
Hello!

2)客户端输出:

0
1
2
3
4

7.把实现类更改为不继承UnicastRemoteObject基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package  com.guojje;
import  java.io.Serializable;
import  java.rmi.RemoteException;
 
public  class  Hello  implements  IHello,Serializable {
     private  static  final  long  serialVersionUID = 1L;
     private  int  index =  0 ;
     protected  Hello()  throws  RemoteException {
     }
     @Override
     public  int  helloWorld(){
         System.out.println( "Hello!" );
         return  ++index;
     }
}

8.输出结果:  

1)服务端输出:

 Remote Hello Object is bound sucessfully!

2)客户端输出:

Hello!
1
Hello!
1
Hello!
1
Hello!
1
Hello!
1

这两个用例的输出结果来看,前一个用例index计数器一直在增加,且Hello!输出在服务端。这说明

helloWorld方法执行是在服务端,客户端的每一次对象方法调用,都是对服务端对象的调用。

而后一个用例就不同了。helloWorld方法执行是在客户端,且每次lookup出来的都是新的对象。

我们看一下lookup出来的对象类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package  com.guojje;
import  java.rmi.Naming;
public  class  HelloClient {
     public  static  void  main(String args[]) {
         try  {
             for  ( int  i =  0 ; i <  5 ; i++) {
                 IHello rhello = (IHello) Naming
                         .lookup( "rmi://localhost:8888/test" );
                 System.out.println(rhello.getClass());
                 System.out.println(rhello.helloWorld());
             }
         catch  (Exception e) {
             e.printStackTrace();
         }
     }
}

实现类继承UnicastRemoteObject时,lookup出来的对象类型是$Proxy0,而不继承UnicastRemoteObject的类,对象类型是com.guojje.Hello。

我们把继承UnicastRemoteObject类的对象叫做远程对象,我们lookup出来的对象,只是该远程对象的存根(Stub)对象,而远程对象永远在远方。客户端每一次的方法调用,最后都是仅有的那一个远程对象的方法调用。

没有继承UnicastRemoteObject类的对象,同样可以bind到Registry,但lookup出来了对象却是远程对象

经过序列化,然后到客户端反序列化出来的新的对象,后续的方法调用与远程对象再无关系。

那UnicastRemoteObject类的继承是如何影响这些的呢? 我们来探索一下。

1
2
3
4
5
6
7
8
9
10
package  com.guojje;
public  class  HelloServer {
     public  static  void  main(String args[]) {
         try  {
             new  Hello();
         catch  (Exception e) {
             e.printStackTrace();
         }
     }
}

仅仅new一个远程对象,运行这个程序,我们就发现进程不会退出。它hang在哪儿呢?

jstack查看线程栈,发现有SocketAccept在监听:wKioL1OS0jnDzmo-AAIVRoxVLQ4285.jpg

wKiom1OS0sqxo5iXAABQrHaFVmM834.jpg

的确启动了一个监听端口。ServerSocket类上添加debug.得到调用栈如下:

wKiom1OS0--zrDznAAG61A8Jrp4407.jpg

UnicastRemoteObject基类的构造方法将远程对象发布到一个随机端口上,当然端口也可以指定。

1
2
3
4
5
  protected  UnicastRemoteObject( int  port)  throws  RemoteException
     {
         this .port = port;
         exportObject((Remote)  this , port);
     }
1
2
3
4
5
   public  static  Remote exportObject(Remote obj,  int  port)
         throws  RemoteException
     {
         return  exportObject(obj,  new  UnicastServerRef(port));
     }
1
2
3
4
5
6
7
8
9
10
11
12
  /**
      * Exports the specified object using the specified server ref.
      */
     private  static  Remote exportObject(Remote obj, UnicastServerRef sref)
         throws  RemoteException
     {
         // if obj extends UnicastRemoteObject, set its ref.
         if  (obj  instanceof  UnicastRemoteObject) {
             ((UnicastRemoteObject) obj).ref = sref;
         }
         return  sref.exportObject(obj,  null false );
     }

迎来一个重要的方法(UnicastServerRef.java):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  /**
      * Export this object, create the skeleton and stubs for this
      * dispatcher.  Create a stub based on the type of the impl,
      * initialize it with the appropriate remote reference. Create the
      * target defined by the impl, dispatcher (this) and stub.
      * Export that target via the Ref.
      */
     public  Remote exportObject(Remote impl, Object data,
                                boolean  permanent)
         throws  RemoteException
     {
         Class implClass = impl.getClass();
         Remote stub;
 
         try  {
             stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
         catch  (IllegalArgumentException e) {
             throw  new  ExportException(
                 "remote object implements illegal remote interface" , e);
         }
         if  (stub  instanceof  RemoteStub) {
             setSkeleton(impl);
         }
 
         Target target =
             new  Target(impl,  this , stub, ref.getObjID(), permanent);
         ref.exportObject(target);
         hashToMethod_Map = hashToMethod_Maps.get(implClass);
         return  stub;
     }

这里的stub变量就是我们lookup出来的远程对象的存根对象。而target保留了远程对象的信息集合:

对象,存根,objId等。

接着看TCPTransport.java的exportObject方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
   /**
      * Export the object so that it can accept incoming calls.
      */
     public  void  exportObject(Target target)  throws  RemoteException {
         /*
          * Ensure that a server socket is listening, and count this
          * export while synchronized to prevent the server socket from
          * being closed due to concurrent unexports.
          */
         synchronized (this) {
             listen();
             exportCount++;
         }
 
         /*
          * Try to add the Target to the exported object table; keep
          * counting this export (to keep server socket open) only if
          * that succeeds.
          */
         boolean  ok =  false ;
         try  {
             super .exportObject(target);
             ok =  true ;
         finally  {
             if  (!ok) {
                 synchronized  ( this ) {
                     decrementExportCount();
                 }
             }
         }
     }

listen()启动监听。这里对TcpTransport加了同步,防止多个线程同时执行,同时也防止同一端口启动多次。

一次TcpTransport会有一个监听端口,而一个端口上可能会发部多个远程对象。exportCount计录该TcpTransport发布了多少个对象。

1
super .exportObject(target);
1
2
3
4
5
6
7
/**
      * Export the object so that it can accept incoming calls.
      */
     public  void  exportObject(Target target)  throws  RemoteException {
         target.setExportedTransport( this );
         ObjectTable.putTarget(target);
     }

ObjectTable为静态方法调用,那么所有的远程对象信息(target)都会放到这个对象表中。我们这个target包含了远程对象几乎所有的信息,找到他,就找到远程对象的存根对象。

远程对象的发布,先说这么多。我们再看一下lookup是如何找到远程对象的存根的.

当然先从远程对象的bind说起:

wKioL1OS446jQG1iAAB55G2yZU8637.jpg

RegistryImpl.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  /**
      * Binds the name to the specified remote object.
      * @exception RemoteException If remote operation failed.
      * @exception AlreadyBoundException If name is already bound.
      */
     public  void  bind(String name, Remote obj)
         throws  RemoteException, AlreadyBoundException, AccessException
     {
         checkAccess( "Registry.bind" );
         synchronized  (bindings) {
             Remote curr = bindings.get(name);
             if  (curr !=  null )
                 throw  new  AlreadyBoundException(name);
             bindings.put(name, obj);
         }
     }

bindings里放得确实是远程对象本身,而不是他的存根。这是怎么回事?继续追究lookup方法。

wKiom1OS3yOxvz0jAAHQmvM6xbM210.jpg

RegistryImpl_Skel类是rmic生成所有没有源代码,我们只能从反编译的代码中查看一点信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
case  2 // lookup(String)
     {
         java.lang.String $param_String_1;
         try  {
         java.io.ObjectInput in = call.getInputStream();
         $param_String_1 = (java.lang.String) in.readObject();
         catch  (java.io.IOException e) {
         throw  new  java.rmi.UnmarshalException( "error unmarshalling arguments" , e);
         catch  (java.lang.ClassNotFoundException e) {
         throw  new  java.rmi.UnmarshalException( "error unmarshalling arguments" , e);
         finally  {
         call.releaseInputStream();
         }
         java.rmi.Remote $result = server.lookup($param_String_1);
         try  {
         java.io.ObjectOutput out = call.getResultStream( true );
         out.writeObject($result);
         catch  (java.io.IOException e) {
         throw  new  java.rmi.MarshalException( "error marshalling return" , e);
         }
         break ;
     }

从网络流中读取第一个参数必须是lookup的字符串参数。然后调用服务端RegistryImpl对象lookup

方法,该方法返回的的确是远程对象,而非远程对象的存根呀?

问题的原因在于下面的序列化过程,继续看调用栈:wKiom1OS4PTwzJB8AAJXCHenxJw799.jpg

看一个重要方法MarshalOutputStream.java

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
      * Checks for objects that are instances of java.rmi.Remote
      * that need to be serialized as proxy objects.
      */
     protected  final  Object replaceObject(Object obj)  throws  IOException {
         if  ((obj  instanceof  Remote) && !(obj  instanceof  RemoteStub)) {
             Target target = ObjectTable.getTarget((Remote) obj);
             if  (target !=  null ) {
                 return  target.getStub();
             }
         }
         return  obj;
     }

如果对象是java.rmi.Remote实例,则向对象表中找到该对象的Target,如果存在,则返回其存根对象。

所以返回服务端的变成了远程对象的存根。先到此,后续再探索其方法调用,安全等相关问题。


补充:

其实Registry的实现类RegistryImpl也是个远程对象,这里registry.bind却没有进行远程调用。这是因为我是用LocateRegistry.createRegistry(8888)创建的远程对象,而不是通过LocateRegistry.getRegistry(8888)获取的远程对象,而getRegistry返回的是RegistryImpl的Stub.仅此而已。


另外一次RMI调用,要创建两个连接,一个连接面向注册端口,即上面的8888端口。另一个面向服务端口。在这里这个服务端口是随机的。最后绑定在UnicastServerRef对象中,序列化到客户端。


本文转自 anranran 51CTO博客,原文链接:http://blog.51cto.com/guojuanjun/1423392


相关文章
|
Docker 容器 网络协议
|
2月前
|
网络协议 安全 Java
分布式(基础)-RMI的原理
分布式(基础)-RMI的原理
|
3月前
|
容器
容器镜像解析问题之通过 ImageIndex 接口获取其引用的 Image 或 ImageIndex如何解决
容器镜像解析问题之通过 ImageIndex 接口获取其引用的 Image 或 ImageIndex如何解决
13 0
|
5月前
|
编译器 测试技术 API
浅谈RPC调用远程资源
【6月更文挑战第1天】gRPC是Google开源的微服务通信框架,基于RPC,允许客户端像调用本地对象一样调用远程服务器方法。它使用Protocol Buffers编译器插件生成客户端和服务器代码,并在多个平台上提供高性能。要安装gRPC Go依赖,需执行相应go install命令。
93 7
浅谈RPC调用远程资源
|
安全 Java
Java RMI 反序列化漏洞-远程命令执行
Java RMI 反序列化漏洞-远程命令执行
450 0
|
开发框架 网络协议 Java
[J2EE规范]RMI简单实例
[J2EE规范]RMI简单实例
91 0
|
监控 安全 Java
RMI 的介绍和应用实例 | 学习笔记
快速学习 RMI 的介绍和应用实例
RMI 的介绍和应用实例 | 学习笔记
|
Dubbo Java 应用服务中间件
java远程调用之RMI(终于可以自己写代码控制别人电脑了)
之前在研究生课程当中学了分布式系统这门课,而且还是自己的导师讲的这门课,在课堂上迷迷糊糊的晃悠了一学期,除了听见几个名词,也没太多印象。正好这几天用到远程过程调用,使用的是gRPC,想到之前上课听过,于是把这块的知识从书到教程好好地补充了一下。 本篇文章尽量不啰嗦,它的重要性自己可以私下了解一下。
356 0
java远程调用之RMI(终于可以自己写代码控制别人电脑了)

相关实验场景

更多
下一篇
无影云桌面