JAVA RMI分布式原理和應用
RMI(Remote Method Invocation)是JAVA早期版本(JDK 1.1)提供的分布式應用解決方案,它作為重要的API被廣泛的應用在EJB中。隨著互聯網應用的發展,分布式處理任務也隨之復雜起 來,WebService也得到普遍的推廣和應用。
在某些方面,例如跨語言平臺的分布式應用,RMI就顯得力不從心了。在實際的應用中,是采用WebService還是傳統的RMI來實現?這是一個需要權衡的問題,兩者的比較如下所述:
1. 比起WebService,它只能使用(調用)由JAVA編寫的遠程服務。而WebService是跨語言平臺的,只要客戶端和服務端雙方都遵守SOAP規範即可;
2. RMI是在TCP協議基礎上傳遞可序列化的java對象(字節數據),而WebService是在HTTP協議基礎上通過XML來傳輸數據的。因此,在同等業務數據量的前提下,RMI的效率要高於WebService。
因此,RMI可以用在業務結構比較簡單,而要求實時高效的分布式應用中。
從設計角度上講,JAVA采用的是三層結構模式來實現RMI。在整個體系結構中,有如下幾個關鍵角色構成了通信雙方:
1.客戶端:
1)樁(StubObject):遠程對象在客戶端上的代理;
2)遠程引用層(RemoteReference Layer):解析並執行遠程引用協議;
3)傳輸層(Transport):發送調用、傳遞遠程方法參數、接收遠程方法執行結果。
2.服務端:
1)骨架(Skeleton):讀取客戶端傳遞的方法參數,調用服務器方的實際對象方法,並接收方法執行後的返回值;
2)遠程引用層(Remote ReferenceLayer):處理遠程引用語法之後向骨架發送遠程方法調用;
3)傳輸層(Transport):監聽客戶端的入站連接,接收並轉發調用到遠程引用層。
3.註冊表(Registry):以URL形式註冊遠程對象,並向客戶端回復對遠程對象的引用。
圖1 RMI三層模型
在實際的應用中,客戶端並沒有真正的和服務端直接對話來進行遠程調用,而是通過本地JVM環境下的樁對象來進行的。
1.遠程調用過程:
1)客戶端從遠程服務器的註冊表中查詢並獲取遠程對象引用。當進行遠程調用時,客戶端首先會與樁對象(Stub Object)進行對話,而這個樁對象將遠程方法所需的參數進行序列化後,傳遞給它下層的遠程引用層(RRL);
2)樁對象與遠程對象具有相同的接口和方法列表,當客戶端調用遠程對象時,實際上是由相應的樁對象代理完成的。遠程引用層在將樁的本地引用轉換為服務器上對象的遠程引用後,再將調用傳遞給傳輸層(Transport),由傳輸層通過TCP協議發送調用;
3)在服務器端,傳輸層監聽入站連接,它一旦接收到客戶端遠程調用後,就將這個引用轉發給其上層的遠程引用層;
4)服務器端的遠程引用層將客戶端發送的遠程應用轉換為本地虛擬機的引用後,再將請求傳遞給骨架(Skeleton);
5)骨架讀取參數,又將請求傳遞給服務器,最後由服務器進行實際的方法調用。
2.結果返回過程:
1)
2)客戶端的傳輸層接收到返回值後,又沿著“傳輸層->遠程引用層->樁”向上傳遞,然後由樁來反序列化這些返回值,並將最終的結果傳遞給客戶端程序。
又從技術的角度上講,有如下幾個主要類或接口扮演著上述三層模型中的關鍵角色:
1)註冊表:java.rmi.Naming和ava.rmi.Registry;
2)骨架:java.rmi.remote.Skeleton
3)樁:java.rmi.server.RemoteStub
4)遠程引用層:java.rmi.server.RemoteRef和sun.rmi.transport.Endpoint;
5)傳輸層:sun.rmi.transport.Transport
作為一般的RMI應用,JAVA為我們隱藏了其中的處理細節,而讓開發者有更多的精力和時間花在實際的應用中。開發RMI的步驟如下所述:
1.服務端:
1)定義Remote子接口,在其內部定義要發布的遠程方法,並且這些方法都要Throws RemoteException;
2)定義遠程對象的實現類,通常有兩種方式:
a. 繼承UnicastRemoteObject或Activatable,並同時實現Remote子接口;
b. 只實現Remote子接口和java.io.Serializable接口。
3)編譯樁(在JAVA 1.5及以後版本中,如果遠程對象實現類繼承了UnicastRemoteObject或Activatable,則無需此步,由JVM自動完成。否則需手工利用rmic工具編譯生成此實現類對應的樁類,並放到和實現類相同的編譯目錄下);
4)啟動服務器:依次完成註冊表的啟動和遠程對象綁定。另外,如果遠程對象實現類在定義時沒有繼承UnicastRemoteObject或Activatable,則必須在服務器端顯示的調用UnicastRemoteObject類中某個重載的exportObject(Remote remote)靜態方法,將此實現類對象導出成為一個真正的遠程對象。
2.客戶端:
1)定義用於接收遠程對象的Remote子接口,只需實現java.rmi.Remote接口即可。但要求必須與服務器端對等的Remote子接口保持一致,即有相同的接口名稱、包路徑和方法列表等。
2)通過符合JRMP規範的URL字符串在註冊表中獲取並強轉成Remote子接口對象;
3)調用這個Remote子接口對象中的某個方法就是為一次遠程方法調用行為。
下面結合一個例子來說明RMI分布式應用的開發步驟。
背景:
1.在服務端和客戶端定義對等Remote子接口(SystemManager)
Java代碼
1 package com.daniele.appdemo.rmi; 2 3 import java.rmi.Remote; 4 import java.rmi.RemoteException; 5 6 import com.daniele.appdemo.test.domain.User; 7 8 /** 9 * <p>系統管理遠程對象接口</p> 10 * @author <a href="mailto:[email protected]">Daniele</a> 11 * @version 1.0.0, 2013-5-21 12 * @see 13 * @since AppDemo1.0.0 14 */ 15 public interface SystemManager extends Remote { 16 17 /** 18 * <p>發布的遠程對象方法一:獲取所有系統環境信息</p> 19 * @author <a href="mailto:[email protected]">Daniele</a> 20 * @return 21 * @throws RemoteException 22 * @since AppDemo1.0.0 23 */ 24 public String getAllSystemMessage() throws RemoteException; 25 26 /** 27 * <p>發布的遠程對象方法二:獲取指定的系統環境信息</p> 28 * @author <a href="mailto:[email protected]">Daniele</a> 29 * @param properties:環境信息對應的屬性名,多個名之間用逗號隔開 30 * @return 31 * @throws RemoteException 32 * @since AppDemo1.0.0 33 */ 34 public String getSystemMessage(String properties) throws RemoteException; 35 }
2.在服務端定義Remote子接口的實現類(SystemManagerImpl),即遠程對象的實際行為邏輯。
Java代碼
1 package com.daniele.appdemo.rmi.server; 2 3 import java.io.Serializable; 4 import java.rmi.RemoteException; 5 import java.rmi.server.RemoteServer; 6 import java.rmi.server.ServerNotActiveException; 7 8 import org.apache.log4j.Logger; 9 10 import com.daniele.appdemo.rmi.SystemManager; 11 import com.daniele.appdemo.test.domain.User; 12 import com.daniele.appdemo.util.SystemUtils; 13 14 /** 15 * <p> 16 * 系統管理遠程對象實現類。有兩種實現方式: 17 * 1.繼承UnicastRemoteObject或Activatable,並同時實現Remote子接口 18 * 2.只實現Remote子接口,這種方式靈活但比較復雜: 19 * 1)要求此實現類必須實現java.io.Serializable接口; 20 * 2)通過這種方式定義的實現類此時還不能叫做遠程對象實現類, 21 * 因為在服務器端綁定遠程對象之前,還需要利用JDK提供的rmic工具 22 * 將此實現類手工編譯生成對應的樁實現類,並放到和它相同的編譯目錄下。 23 * </p> 24 * @author <a href="mailto:[email protected]">Daniele</a> 25 * @version 1.0.0, 2013-5-21 26 * @see 27 * @since AppDemo1.0.0 28 */ 29 public class SystemManagerImpl implements SystemManager, Serializable { 30 31 //public class SystemManagerImpl extends UnicastRemoteObject implements SystemManager { 32 33 private static final long serialVersionUID = 9128780104194876777L; 34 35 private static final Logger logger = Logger.getLogger(SystemManagerImpl.class); 36 37 private static SystemManagerImpl systemManager = null; 38 39 /** 40 * <p>在服務端本地的匿名端口上創建一個用於監聽目的的UnicastRemoteObject對象</p> 41 * @author <a href="mailto:[email protected]">Daniele</a> 42 * @throws RemoteException 43 * @since AppDemo1.0.0 44 */ 45 private SystemManagerImpl() throws RemoteException { 46 super(); 47 // 在控制臺中顯示遠程對象被調用,以及返回結果時產生的日誌 48 RemoteServer.setLog(System.out); 49 } 50 51 public static SystemManagerImpl getInstance() throws RemoteException { 52 if (systemManager == null) { 53 synchronized (SystemManagerImpl.class) { 54 if (systemManager == null) 55 systemManager = new SystemManagerImpl(); 56 } 57 } 58 return systemManager; 59 } 60 61 public String getAllSystemMessage() throws RemoteException { 62 try { 63 /* 64 * getClientHost()方法可以獲取觸發當前遠程方法被調用時的客戶端的主機名。 65 * 在遠程服務端的環境中,如果當前線程實際上沒有運行客戶端希望調用的遠程方法時, 66 * 則會拋出ServerNotActiveException。 67 * 因此,為了盡量避免這個異常的發生,它通常用於遠程方法的內部實現邏輯中, 68 * 以便當此方法真正的被調用時,可以記錄下哪個客戶端在什麽時間調用了這個方法。 69 */ 70 logger.info("Client {" + RemoteServer.getClientHost() + "} invoke method [getAllSystemMessage()]" ); 71 } catch (ServerNotActiveException e) { 72 e.printStackTrace(); 73 } 74 return SystemUtils.formatSystemProperties(); 75 } 76 77 public String getSystemMessage(String properties) throws RemoteException { 78 try { 79 logger.info("Client {" 80 + RemoteServer.getClientHost() 81 + "} invoke method [getAllSystemMessage(String properties)]"); 82 } catch (ServerNotActiveException e) { 83 e.printStackTrace(); 84 } 85 return SystemUtils.formatSystemProperties(properties.split(",")); 86 } 87 88 }
3.由於SystemManagerImpl 不是通過繼承UnicastRemoteObject 或 Activatable來實現的,因此在服務器端需要利用JDK提供的rmic工具編譯生成對應的樁類。否則,此步略過。例如,在Windows環境下打開命令控制臺後
1)進入工程根目錄 :
cd d:\ Development\AppDemo
2)樁編譯:
cmic -classpath WebContent\WEB-INF\classes com.daniele.appdemo.rmi.server.SystemManagerImpl
語法格式為:
cmic -classpath <遠程對象實現類bin目錄的相對路徑> <遠程對象實現類所在的包路徑>.<遠程對象實現類的名稱>
完成後,rmic將在相對於根目錄的com\daniele\appdemo\rmi\server子目錄中自動生成SystemManagerImpl_Stub樁對象類 (即“遠程對象實現類名稱_Stub”) 的編譯文件,此時需要再將此編譯文件拷貝到與遠程對象實現類SystemManagerImpl相同的編譯目錄(\WebContent\WEB-INF\classes\com\daniele\appdemo\rmi\server)中,否則在服務器端發布遠程對象時將會拋出java.rmi.StubNotFoundException。如下圖所示。
圖2 實現類與樁
需要特別註意的是:如果服務端中的遠程對象實現類存在有對應的樁對象類編譯文件,則要求在RMI客戶端的環境中,也必須有這個對等的樁對象類編譯文件,即意味著這個文件在兩端有著相同的包路徑、文件名和內部實現細節。因此,最簡單的做法就是連同整個包(com\daniele\appdemo\rmi\server)在內,將圖2中的SystemManagerImpl_Stub.class文件拷貝到RMI客戶端工程的bin目錄下即可。如下圖。否則,當RMI客戶端調用遠程服務時將會拋出java.rmi.StubNotFoundException。
圖3 RMI客戶端工程下的對等樁文件
4.創建用於發布遠程對象目的用的服務器(SystemManagerServer)
Java代碼
1 package com.daniele.appdemo.rmi.server; 2 3 import java.io.IOException; 4 import java.util.Arrays; 5 import java.rmi.Naming; 6 import java.rmi.registry.LocateRegistry; 7 import java.rmi.server.UnicastRemoteObject; 8 9 import org.apache.log4j.Logger; 10 11 import com.daniele.appdemo.rmi.SystemManager; 12 13 /** 14 * <p> 15 * 系統管理遠程服務器,它主要完成如下任務: 16 * 1.在綁定之前先啟動註冊表服務。 17 * 2.將遠程對象SystemManager綁定到註冊表中,以便讓客戶端能遠程調用這個對象所發布的方法; 18 * </p> 19 * @author <a href="mailto:[email protected]">Daniele</a> 20 * @version 1.0.0, 2013-5-21 21 * @see 22 * @since AppDemo1.0.0 23 */ 24 public class SystemManagerServer { 25 26 private static final Logger logger = Logger.getLogger(SystemManagerServer.class); 27 28 public static void main(String[] args) { 29 try { 30 31 SystemManager systemManager = SystemManagerImpl.getInstance(); 32 33 /* 34 * 如果遠程對象實現類不是通過繼承UnicastRemoteObject或Activatable來定義的, 35 * 則必須在服務器端顯示的調用UnicastRemoteObject類中某個重載的exportObject(Remote remote)靜態方法, 36 * 將此實現類的某個對象導出成為一個真正的遠程對象。否則,此步省略。 37 */ 38 UnicastRemoteObject.exportObject(systemManager); 39 40 int port = 9527; 41 String url = "rmi://localhost:" + port + "/"; 42 String remoteObjectName = "systemManager"; 43 44 /* 45 * 在服務器的指定端口(默認為1099)上啟動RMI註冊表。 46 * 必不可缺的一步,缺少註冊表創建,則無法綁定對象到遠程註冊表上。 47 */ 48 LocateRegistry.createRegistry(port); 49 50 /* 51 * 將指定的遠程對象綁定到註冊表中: 52 * 1.如果端口號不為默認的1099,則綁定時遠程對象名稱中必須包含Schema, 53 * 即"rmi://<host或ip><:port>/"部分,並且Schema裏指定的端口號應與createRegistry()方法參數中的保持一致 54 * 2.如果端口號為RMI默認的1099,則遠程對象名稱中不用包含Schema部分,直接定義名稱即可 55 */ 56 if (port == 1099) 57 Naming.rebind(remoteObjectName, systemManager); 58 else 59 Naming.rebind(url + remoteObjectName, systemManager); 60 logger.info("Success bind remote object " + Arrays.toString(Naming.list(url))); 61 } catch (IOException e) { 62 e.printStackTrace(); 63 } 64 } 65 66 }
5.創建發出遠程調用請求的客戶端(SystemManagerClient)
Java代碼
1 package com.daniele.appdemo.rmi.client; 2 3 import java.io.IOException; 4 import java.rmi.Naming; 5 import java.rmi.NotBoundException; 6 7 import com.daniele.appdemo.rmi.SystemManager; 8 import com.daniele.appdemo.test.domain.User; 9 10 /** 11 * <p>系統管理進行遠程調用的客戶端</p> 12 * @author <a href="mailto:[email protected]">Daniele</a> 13 * @version 1.0.0, 2013-5-21 14 * @see 15 * @since AppDemo1.0.0 16 */ 17 18 public class SystemManagerClient { 19 20 public static void main(String[] args) { 21 /* 22 * RMI URL格式:Schame/<遠程對象名> 23 * 1.Schame部分由"rmi://<server host或IP>[:port]"組成, 24 * 如果遠程對象綁定在服務端的1099端口(默認)上,則port部分可以省略,否則必須指定。 25 * 2.URL最後一個"/"後面的值為遠程對象綁定在服務端註冊表上時定義的遠程對象名, 26 * 即對應Naming.rebind()方法第一個參數值中最後一個"/"後面的值 27 */ 28 String rmi = "rmi://192.168.1.101:9527/systemManager"; 29 try { 30 /* 31 * 根據URL字符串查詢並獲取遠程服務端註冊表中註冊的遠程對象, 32 * 這裏返回的是本地實現了Remote接口的子接口對象(Stub), 33 * 它與服務端中的遠程對象具有相同的接口和方法列表,因而作為在客戶端中遠程對象的一個代理。 34 */ 35 SystemManager systemManager = (SystemManager) Naming.lookup(rmi); 36 37 // System.out.println(systemManager.getAllSystemMessage()); 38 System.out.println(systemManager.getSystemMessage("java.version,os.name")); 39 } catch (IOException e) { 40 e.printStackTrace(); 41 } catch (NotBoundException e) { 42 e.printStackTrace(); 43 } 44 } 45 46 }
完成後,再依次到服務器和客戶端上啟動SystemManagerServer和SystemManagerClient即可。
轉載:http://code727.iteye.com/blog/1874271
JAVA RMI分布式原理和應用