前言:隨著一個系統被使用者認可,業務量、請求量不斷上升,那麼單機系統必然就無法滿足了,於是系統就慢慢走向分布式了,隨之而來的是系統之間“溝通”的障礙。一般來說,解決系統之間的通訊可以有兩種方式:即遠程調用和訊息。RMI(Remote Method Invocation)就是遠程調用的一種方式,也是這篇文章主要介紹的。
一、RMI的一個簡單樣本
這個樣本拆分為服務端和用戶端,放在兩個idea項目中,並且通過了單機和雙機兩種環境的測試,是真正意義上的分布式應用。
項目結構
服務端應用: Server
主程式: com.jnu.wwt.entry.Server
服務介面: com.jnu.wwt.service.IOperation
服務實現: com.jnu.wwt.service.impl.OperationImpl
用戶端應用: Client
主程式: com.jnu.wwt.entry.Client
服務介面: com.jnu.wwt.service.IOperation
源碼:
Server.java
/** * Created by wwt on 2016/9/14. */public class Server { public static void main(String args[]) throws Exception{ //以1099作為LocateRegistry接收用戶端請求的連接埠,並註冊服務的映射關係 Registry registry=LocateRegistry.createRegistry(1099); IOperation iOperation=new OperationImpl(); Naming.rebind("rmi://127.0.0.1:1099/Operation",iOperation); System.out.println("service running..."); }}
IOperation.java(服務端和用戶端各需要一份)
/** * 服務端介面必須實現java.rmi.Remote * Created by wwt on 2016/9/14. */public interface IOperation extends Remote{ /** * 遠程介面上的方法必須拋出RemoteException,因為網路通訊是不穩定的,不能吃掉異常 * @param a * @param b * @return */ int add(int a, int b) throws RemoteException;}
OperationImpl.java
/** * Created by wwt on 2016/9/14. */public class OperationImpl extends UnicastRemoteObject implements IOperation{ public OperationImpl() throws RemoteException { super(); } @Override public int add(int a, int b) throws RemoteException{ return a+b; }}
Client.java
/** * Created by wwt on 2016/9/15. */public class Client { public static void main(String args[]) throws Exception{ IOperation iOperation= (IOperation) Naming.lookup("rmi://127.0.0.1:1099/Operation"); System.out.println(iOperation.add(1,1)); }}
運行結果
先運行Server應用,服務就起來了。然後切換到Client應用,點擊運行,Client調用Server的服務,返回結果。
二、RMI做了些什麼
現在我們先忘記Java中有RMI這種東西。假設我們需要自己實現上面例子中的效果,怎麼辦呢。可以想到的步驟是:
編寫服務端服務,並將其通過某個服務機的連接埠暴露出去供用戶端調用。 編寫用戶端程式,用戶端通過指定服務所在的主機和連接埠號碼、將請求封裝並序列化,最終通過網路通訊協定發送到服務端。 服務端解析和還原序列化請求,調用服務端上的服務,將結果序列化並返回給用戶端。 用戶端接收並還原序列化服務端返回的結果,反饋給使用者。
這是大致的流程,我們不難想到,RMI其實也是幫我們封裝了一些細節而通用的部分,比如序列化和還原序列化,串連的建立和釋放等,下面是RMI的具體流程:
這裡涉及到幾個新概念:
Stub和Skeleton:這兩個的身份是一致的,都是作為代理的存在。用戶端的稱作Stub,服務端的稱作Skeleton。要做到對程式員屏蔽遠程方法調用的細節,這兩個代理是必不可少的,包括網路連接等細節。
Registry:顧名思義,可以認為Registry是一個“註冊所”,提供了服務名到服務的映射。如果沒有它,意味著用戶端需要記住每個服務所在的連接埠號碼,這種設計顯然是不優雅的。
三、走進RMI原理之前,先來看看用到的類及其階層和主要的方法。
哪裡看不懂隨時回來看看結構。。。開始了
四、一步步解剖RMI的底層原理
服務端啟動Registry服務
Registry registry=LocateRegistry.createRegistry(1099);
從上面這句代碼入手,追溯下去,可以探索服務端建立了一個RegistryImpl對象,這裡做了一個判斷。如果服務端指定的連接埠號碼是1099並且系統開啟了安全管理器,那麼可以在限定的許可權集內(listen和accept)繞過系統的安全校正。反之則必須進行安全校正。這裡純粹是為了效率起見。真正做的事情在setUp()方法中,繼續看下去。
public RegistryImpl(final int var1) throws RemoteException { if(var1 == 1099 && System.getSecurityManager() != null) { try { AccessController.doPrivileged(new PrivilegedExceptionAction() { public Void run() throws RemoteException { LiveRef var1x = new LiveRef(RegistryImpl.id, var1); RegistryImpl.this.setup(new UnicastServerRef(var1x)); return null; } }, (AccessControlContext)null, new Permission[]{new SocketPermission("localhost:" + var1, "listen,accept")}); } catch (PrivilegedActionException var3) { throw (RemoteException)var3.getException(); } } else { LiveRef var2 = new LiveRef(id, var1); this.setup(new UnicastServerRef(var2)); }}
setUp()方法將指向正在初始化的RegistryImpl對象的遠端參照ref(RemoteRef)賦值為傳入的UnicastServerRef對象,這裡涉及了向上轉型。然後繼續移交UnicastServerRef的exportObject()方法。
private void setup(UnicastServerRef var1) throws RemoteException { this.ref = var1; var1.exportObject(this, (Object)null, true);}
進入UnicastServerRef的exportObject()方法。可以看到,這裡首先為傳入的RegistryImpl建立一個代理,這個代理我們可以推斷出就是後面服務於用戶端的RegistryImpl的Stub對象。然後將UnicastServerRef的skel(skeleton)對象設定為當前RegistryImpl對象。最後用skeleton、stub、UnicastServerRef對象、id和一個boolean值構造了一個Target對象,也就是這個Target對象基本上包含了全部的資訊。調用UnicastServerRef的ref(LiveRef)變數的exportObject()方法。
public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException { Class var4 = var1.getClass(); Remote var5; try { var5 = Util.createProxy(var4, this.getClientRef(), this.forceStubUse); } catch (IllegalArgumentException var7) { throw new ExportException("remote object implements illegal remote interface", var7); } if(var5 instanceof RemoteStub) { this.setSkeleton(var1); } Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3); this.ref.exportObject(var6); this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4); return var5;}
到上面為止,我們看到的都是一些變數的賦值和建立工作,還沒有到串連層,這些引用對象將會被Stub和Skeleton對象使用。接下來就是串連層上的了。追溯LiveRef的exportObject()方法,很容易找到了TCPTransport的exportObject()方法。這個方法做的事情就是將上面構造的Target對象暴露出去。調用TCPTransport的listen()方法,listen()方法建立了一個ServerSocket,並且啟動了一條線程等待用戶端的請求。接著調用父類Transport的exportObject()將Target對象存放進ObjectTable中。
public void exportObject(Target var1) throws RemoteException { synchronized(this) { this.listen(); ++this.exportCount; } boolean var2 = false; boolean var12 = false; try { var12 = true; super.exportObject(var1); var2 = true; var12 = false; } finally { if(var12) { if(!var2) { synchronized(this) { this.decrementExportCount(); } } } } if(!var2) { synchronized(this) { this.decrementExportCount(); } }}
到這裡,我們已經將RegistryImpl對象建立並且起了服務等待用戶端的請求。
用戶端擷取服務端Rgistry代理
IOperation iOperation= (IOperation) Naming.lookup("rmi://127.0.0.1:1099/Operation");
從上面的代碼看起,容易追溯到LocateRegistry的getRegistry()方法。這個方法做的事情是通過傳入的host和port構造RemoteRef對象,並建立了一個本地代理。可以通過Debug功能發現,這個代理對象其實是RegistryImpl_Stub對象。這樣用戶端便有了服務端的RegistryImpl的代理(取決於ignoreStubClasses變數)。但注意此時這個代理其實還沒有和服務端的RegistryImpl對象關聯,畢竟是兩個VM上面的對象,這裡我們也可以猜測,代理和遠端Registry對象之間是通過socket訊息來完成的。
public static Registry getRegistry(String host, int port, RMIClientSocketFactory csf) throws RemoteException{ Registry registry = null; if (port <= 0) port = Registry.REGISTRY_PORT; if (host == null || host.length() == 0) { // If host is blank (as returned by "file:" URL in 1.0.2 used in // java.rmi.Naming), try to convert to real local host name so // that the RegistryImpl's checkAccess will not fail. try { host = java.net.InetAddress.getLocalHost().getHostAddress(); } catch (Exception e) { // If that failed, at least try "" (localhost) anyway... host = ""; } } LiveRef liveRef = new LiveRef(new ObjID(ObjID.REGISTRY_ID), new TCPEndpoint(host, port, csf, null), false); RemoteRef ref = (csf == null) ? new UnicastRef(liveRef) : new UnicastRef2(liveRef); return (Registry) Util.createProxy(RegistryImpl.class, ref, false);}
服務端建立服務物件 從OperationImpl的建構函式看起。調用了父類UnicastRemoteObject的構造方法,追溯到UnicastRemoteObject的私人方法exportObject()。這裡做了一個判斷,判斷服務的實現是不是UnicastRemoteObject的子類,如果是,則直接賦值其ref(RemoteRef)對象為傳入的UnicastServerRef對象。反之則調用UnicastServerRef的exportObject()方法。這裡我們是第一種情況。
private static Remote exportObject(Remote obj, UnicastServerRef sref) throws RemoteException{ // if obj extends UnicastRemoteObject, set its ref. if (obj