RMI原理揭祕之遠程對象

討論開始以前,咱們先看看網上的一個例子,這個例子我騰抄了一分,沒有用連接的方式,只是爲了讓你們看得方便,若有侵權,我立馬***。java

  1. 定義遠程接口:安全

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

 3. 定義實現類:網絡

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.服務端:app

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.客戶端:socket

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.輸出結果: 
ide

1)服務端輸出:this

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

2)客戶端輸出:線程

0
1
2
3
4debug

7.把實現類更改成不繼承UnicastRemoteObject基類

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出來的對象類型:

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類的繼承是如何影響這些的呢? 咱們來探索一下。

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基類的構造方法將遠程對象發佈到一個隨機端口上,固然端口也能夠指定。

 protected UnicastRemoteObject(int port) throws RemoteException
    {
        this.port = port;
        exportObject((Remote) this, port);
    }
  public static Remote exportObject(Remote obj, int port)
        throws RemoteException
    {
        return exportObject(obj, new UnicastServerRef(port));
    }
 /**
     * 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):

 /**
     * 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方法:

  /**
     * 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發佈了多少個對象。

super.exportObject(target);
/**
     * 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:

 /**
     * 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生成全部沒有源代碼,咱們只能從反編譯的代碼中查看一點信息:

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

/**
     * 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對象中,序列化到客戶端。

相關文章
相關標籤/搜索