討論開始以前,咱們先看看網上的一個例子,這個例子我騰抄了一分,沒有用連接的方式,只是爲了讓你們看得方便,若有侵權,我立馬***。java
定義遠程接口:安全
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在監聽:
的確啓動了一個監聽端口。ServerSocket類上添加debug.獲得調用棧以下:
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提及:
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方法。
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
方法,該方法返回的的確是遠程對象,而非遠程對象的存根呀?
問題的緣由在於下面的序列化過程,繼續看調用棧:
看一個重要方法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對象中,序列化到客戶端。