Java中的RMI使用方法詳解
1、概述
1.1、簡介
RMI 是 Java 提供的一個完善的簡單易用的遠程方法調用框架,采用客戶/服務器通信方式,在服務器上部署了提供各種服務的遠程對象,客戶端請求訪問服務器上遠程對象的方法,它要求客戶端與服務器端都是 Java 程序。
RMI 框架采用代理來負責客戶與遠程對象之間通過 Socket 進行通信的細節(jié)。RMI 框架為遠程對象分別生成了客戶端代理和服務器端代理。位于客戶端的代理必被稱為存根(Stub),位于服務器端的代理類被稱為骨架(Skeleton)。
1.2、原理
當客戶端調用遠程對象的一個方法時,實際上是調用本地存根對象的相應方法。存根對象與遠程對象具有同樣的接口。存根采用一種與平臺無關的編碼方式,把方法的參數(shù)編碼為字節(jié)序列,這個編碼過程被稱為參數(shù)編組。RMI 主要采用Java 序列化機制進行參數(shù)編組。
存根把以下請求信息發(fā)送給服務器:
- 被訪問的遠程對象的名字
- 被調用的方法的描述
- 編組后的參數(shù)的字節(jié)序列
服務器端接收到客戶端的請求信息,然后由相應的骨架對象來處理這一請求信息,骨架對象執(zhí)行以下操作:
- 反編組參數(shù),即把參數(shù)的字節(jié)序列反編碼為參數(shù)
- 定位要訪問的遠程對象
- 調用遠程對象的相應方法
- 獲取方法調用產生的返回值或者異常,然后對它進行編組
- 把編組后的返回值或者異常發(fā)送給客戶
客戶端的存根接收到服務器發(fā)送過來的編組后的返回值或者異常,再對它進行反編組,就得到調用遠程方法的返回結果
JDK5.0 之后,RMI 框架會在運行時自動為運程對象生成動態(tài)代理類(包括存根和骨架類),從而更徹底地封裝了 RMI 框架的實現(xiàn)細節(jié),簡化了 RMI 框架的使用方式。
RMI交互圖
Stub和Skeleton通信過程
方法調用從客戶對象-經(jīng)-存根(stub)、遠程引用層(Remote Reference Layer)和傳輸層(Transport Layer)向下,傳遞給主機,然后再次經(jīng)傳輸層,向上穿過遠程調用層和骨干網(wǎng)(Skeleton),到達服務器對象。
- 存根:扮演著遠程服務器對象的代理的角色,使該對象可被客戶激活。
- 遠程調用層:處理語義、管理單一或多重對象的通信,決定調用是應發(fā)往一個服務器還是多個。
- 傳輸層:管理實際的連接,并且追蹤可以接受方法調用的遠程對象。
- 骨干網(wǎng):完成對服務器對象實際的方法調用,并獲取返回值。返回值向下經(jīng)遠程引用層、服務器端的傳輸層傳遞回客戶端,再向上經(jīng)傳輸層和遠程調用層返回。最后,存根獲得返回值。
1.3、組成
RMI由3個部分構成:
第一個是rmiregistry(JDK提供的一個可以獨立運行的程序,在bin目錄下)
第二個是server端的程序,對外提供遠程對象
第三個是client端的程序,想要調用遠程對象的方法。
首先,先啟動rmiregistry服務,啟動時可以指定服務監(jiān)聽的端口,也可以使用默認的端口(1099)。其次,server端在本地先實例化一個提供服務的實現(xiàn)類,然后通過RMI提供的Naming/Context/Registry(下面實例用的Registry)等類的bind或rebind方法將剛才實例化好的實現(xiàn)類注冊到rmiregistry上并對外暴露一個名稱。最后,client端通過本地的接口和一個已知的名稱(即rmiregistry暴露出的名稱)再使用RMI提供的Naming/Context/Registry等類的lookup方法從RMIService那拿到實現(xiàn)類。這樣雖然本地沒有這個類的實現(xiàn)類,但所有的方法都在接口里了,便可以實現(xiàn)遠程調用對象的方法了。
1.4、數(shù)據(jù)傳遞
Java程序中引用類型(不包括基本類型)的參數(shù)傳遞是按引用傳遞的,對于在同一個虛擬機中的傳遞時是沒有問題的,因為的參數(shù)的引用對應的是同一個內存空間,在分布式系統(tǒng)中,由于對象不存在于同一個內存空間,虛擬機A的對象引用對于虛擬機B沒有任何意義,那么怎么解決這個問題呢?
第一種:
將引用傳遞更改為值傳遞,也就是將對象序列化為字節(jié),然后使用該字節(jié)的副本在客戶端和服務器之間傳遞,而且一個虛擬機中對該值的修改不會影響到其他主機中的數(shù)據(jù);但是對象的序列化也有一個問題,就是對象的嵌套引用就會造成序列化的嵌套,這必然會導致數(shù)據(jù)量的激增,因此我們需要有選擇進行序列化。
在Java中一個對象如果能夠被序列化,需要滿足下面兩個條件之一:
–是Java的基本類型;
–實現(xiàn)java.io.Serializable接口(String類即實現(xiàn)了該接口);
對于容器類,如果其中的對象是可以序列化的,那么該容器也是可以序列化的;
可序列化的子類也是可以序列化的;
第二種:
使用引用傳遞,每當遠程主機調用本地主機方法時,該調用還要通過本地主機查詢該引用對應的對象,在任何一臺機器上的改變都會影響原始主機上的數(shù)據(jù),因為這個對象是共享的;
RMI中的參數(shù)傳遞和結果返回可以使用的三種機制(取決于數(shù)據(jù)類型):
簡單類型:按值傳遞,直接傳遞數(shù)據(jù)拷貝;?
遠程對象引用(實現(xiàn)了Remote接口):以遠程對象的引用傳遞;?
遠程對象引用(未實現(xiàn)Remote接口):按值傳遞,通過序列化對象傳遞副本,本身不允許序列化的對象不允許傳遞給遠程方法;
在調用遠程對象的方法之前需要一個遠程對象的引用,如何獲得這個遠程對象的引用在RMI中是一個關鍵的問題,如果將遠程對象的發(fā)現(xiàn)類比于IP地址的發(fā)現(xiàn)可能比較好理解一些。
平常我們上網(wǎng)是通過域名來定位一個網(wǎng)站,實際上網(wǎng)絡是通過IP地址來定位網(wǎng)站,因此其中就存在一個映射的過程,域名系統(tǒng)(DNS)就是為了這個目的出現(xiàn)的,在域名系統(tǒng)中通過域名來查找對應的IP地址來訪問對應的服務器。
對應的,IP地址在這里就相當于遠程對象的引用,而DNS則相當于一個注冊表(Registry)
而域名在RMI中就相當于遠程對象的標識符,客戶端通過提供遠程對象的標識符訪問注冊表,來得到遠程對象的引用。這個標識符是類似URL地址格式的,它要滿足的規(guī)范如下:
該名稱是URL形式的,類似于http的URL,schema是rmi;
格式類似于rmi://host:port/name
,host指明注冊表運行的注解,port表明接收調用的端口,name是一個標識該對象的簡單名稱
主機和端口都是可選的,如果省略主機,則默認運行在本地;如果端口也省略,則默認端口是1099;
2、示例
2.1、創(chuàng)建接口
創(chuàng)建一個接口Hello,該接口需要繼承Remote接口,接口所定義的方法需要拋出RemoteException異常:
//遠程接口,該接口需要繼承Remote接口,并且接口中的方法全都要拋出RemoteException異常 public interface Hello extends Remote { public String welcome(String name) throws RemoteException; }
2.2、實現(xiàn)接口類
基于上面定義的接口實現(xiàn)一個類Helloimpl,該實現(xiàn)類需要繼承UnicastRemoteObject類,同樣重載的方法需要拋出RemoteException異常:
/** * 遠程接口實現(xiàn)類,必須繼承UnicastRemoteObject * (繼承RemoteServer->繼承RemoteObject->實現(xiàn)Remote,Serializable), * 只有繼承UnicastRemoteObject類,才表明其可以作為遠程對象,被注冊到注冊表中供客戶端遠程調用 * (補充:客戶端lookup找到的對象,只是該遠程對象的Stub(存根對象), * 而服務端的對象有一個對應的骨架Skeleton(用于接收客戶端stub的請求,以及調用真實的對象)對應, * Stub是遠程對象的客戶端代理,Skeleton是遠程對象的服務端代理, * 他們之間協(xié)作完成客戶端與服務器之間的方法調用時的通信。) */ public class HelloImpl extends UnicastRemoteObject implements Hello { //因為UnicastRemoteObject的構造方法拋出了RemoteException異常, //因此這里默認的構造方法必須寫,也必須聲明拋出RemoteException異常 public HelloImpl() throws RemoteException { } @Override public String welcome(String name) throws RemoteException { return "Hello " + name; } }
如果一個遠程類已經(jīng)繼承了其他類,無法再繼承 UnicastRemoteObiect 類,那么可以在構造方法中調用 UnicastRemoteObject 類的靜態(tài) expotObject 方法,同樣,遠程類的構造方法也必須聲明拋出 RemoteException
public class HelloImpl2 implements Hello { @Override public String welcome(String name) throws RemoteException { return "Hello " + name; } public HelloImpl2() throws RemoteException{ //參數(shù) port 指定監(jiān)聽的端口,如果取值為0,就表示監(jiān)聽任意一個匿名端口 UnicastRemoteObject.exportObject(this, 0); } }
2.3、創(chuàng)建服務端
服務端創(chuàng)建了一個注冊表,并注冊了客戶端需要的對象:
public class Server { public static void main(String[] args) throws RemoteException { //創(chuàng)建對象 Hello hello = new HelloImpl(); // 本地主機上的遠程對象注冊表Registry的實例, // 并指定端口,這一步必不可少(Java默認端口是1099) Registry registry = LocateRegistry.createRegistry(1099); //綁定對象到注冊表,并給它取名為hello registry.rebind("hello", hello); } }
向注冊器注冊遠程對象有三種方式:
//創(chuàng)建遠程對象 HelloService service1 = new HelloServiceImpl("service1"); //方式1:調用 java.i.registry.Registy 接口的 bind 或 rebind 方法 Registry registry = LocateRegistry.createRegistry(1099); registry.rebind("HelloService1", service1); //方式2:調用命名服務類 java.rmi.Naming 的 bind 或 rebind 方法 Naming.rebind("HelloService1", service1); //方式3:調用 JNDI API 的 javax.naming.Context 接口的 bind 或rebind 方法 Context namingContext = new InitialContext(); namingContext.rebind("rmi:HelloService1", service1);
2.4、客戶端調用遠程對象
public class Client { public static void main(String[] args) throws RemoteException, NotBoundException { //獲取到注冊表的代理 Registry registry = LocateRegistry.getRegistry("localhost", 1099); //利用注冊表的代理去查詢遠程注冊表中名為hello的對象 Hello hello = (Hello) registry.lookup("hello"); //調用遠程方法 System.out.println(hello.welcome("tom")); } }
2.5、運行結果
先運行服務端,再運行客戶端:
3、其它
3.1、遠程方法中的參數(shù)與返回值傳遞
當客戶端調用服務器端的遠程對象的方法時,客戶端會向服務器端傳遞參數(shù),服務器端則會向客戶端傳遞返回值。RMI 規(guī)范對參數(shù)以及返回值的傳遞的規(guī)定如下所述:
- 只有基本類型的數(shù)據(jù)、遠程對象以及可序列化的對象才可以被作為參數(shù)或者返回值進行傳遞
- 如果參數(shù)或返回值是一個遠程對象,那么把它的存根對象傳遞到接收方。也就是說接收方得到的是遠程對象的存根對象
- 如果參數(shù)或返回值是可序列化對象,那么直接傳遞該對象的序列化數(shù)據(jù)。也就是說接收方得到的是發(fā)送方的可序列化對象的復制品
- 如果參數(shù)或返回值是基本類型的數(shù)據(jù),那么直接傳遞該數(shù)據(jù)的序列化數(shù)據(jù)。也就是說,接收方得到的是發(fā)送方的基本類型的數(shù)據(jù)的復制品
3.2、分布式垃圾收集
在 Java 虛擬機中,對于一個本地對象,只要不被本地 Java 虛擬機內的任何變量引用,它就會結束生命周期,可以被垃圾回收器回收。而對于一個遠程對象,不僅會被本地 Java 虛擬機內的變量引用,還會被遠程引用
服務器端的一個遠程對象受到三種引用:
- 服務器端的一個本地對象持有它的本地引用
- 這個遠程對象已經(jīng)被注冊到 RMI 注冊器,可以理解為,RMI 注冊器持有它的引用
- 客戶端獲得了這個遠程對象的存根對象,可以理解為,客戶端持有它的遠程引用
RMI 框架采用分布式垃圾收集機制來管理遠程對象的生命周期,當一個遠程對象不受到任何本地引用和遠程引用時,這個遠程對象才會結束生命周期,并且可以被本地 Java 虛擬機的垃圾回收器回收。
服務器端如何知道客戶端持有一個遠程對象的遠程引用呢?當客戶端獲得了一個服務器端的遠程對象的存根后,就會向服務器發(fā)送一條租約通知,告訴服務器自己持有這個遠程對象的引用了??蛻舳藢@個遠程對象有一個租約期限,默認值為 600000ms。當至達了租約期限的一半時間,客戶如果還持有遠程引用,就會再次向服務器發(fā)送租約通知??蛻舳瞬粩嘣诮o定的時間間隔中向服務器發(fā)送租約通知,從而使腸務器知道客戶端一直持有遠程對象的引用。如果在租約到期后,服務器端沒有繼續(xù)收到客戶端的新的租約通知,服務器端就會認為這個客戶已經(jīng)不再持有遠程對象的引用了
3.3、動態(tài)加載
遠程對象一般分布在服務器端,當客戶端試圖調用遠程對象的方法時,如果在客戶端還不存在遠程對象所依賴的類文件,比如遠程方法的參數(shù)和返回值對應的類文件,客戶就會從 java.rmi.server.codebase 系統(tǒng)屬性指定的位貿動態(tài)加載該類文件
同樣,當服務器端訪問客戶端的遠程對象時,如果服務器端不存在相關的類文件,腐務器就會從 java.rmi.server.codebase 屬性指定的位置動態(tài)加載它們
此外,當服務器向 RMI 注冊器注冊遠程對象時,注冊器也會從 java.rmi.server.codebase 屬性指定的位置動態(tài)加載相關的遠程接口的類文件
前面的例子都是在同一個 classpath 下運行服務器程序以及客戶程序的,這些程序都能從本地 classpath 中找到相應的類文件,因此無須從 java.rmi.server.codebase 屬性指定的位置動態(tài)加載類。而在實際應用中,客戶程序與服務器程序運行在不同的主機上,因此當客戶端調用服務器端的遠程對象的方法時,有可能需要從遠程文件系統(tǒng)加載類文件。同樣,當服務器端調用客戶端的遠程對象的方法時,也有可能從遠程文件系統(tǒng)加載類文件
我們可以且把這些需要被加載的類的文件都集中放在網(wǎng)絡上的同一地方,啟動時將java.rmi.server.codebase 設置為指定位置,從而實現(xiàn)動態(tài)加載
start java -Djava.rmi.server.codebase=http://www.javathinker.net/download/
到此這篇關于Java中的RMI使用方法詳解的文章就介紹到這了,更多相關Java RMI使用內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
一篇文章教你將JAVA的RabbitMQz與SpringBoot整合
這篇文章主要介紹了如何將JAVA的RabbitMQz與SpringBoot整合,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2021-09-09SpringSecurity多表多端賬戶登錄的實現(xiàn)
本文主要介紹了SpringSecurity多表多端賬戶登錄的實現(xiàn)2024-05-05IDEA搭建SpringBoot多模塊聚合工程過程詳解(多模塊聚合工程)
這篇文章主要是介紹一下,如何在IDEA開發(fā)工具下,搭建一個基于SpringBoot的多模塊聚合工程項目,本篇文章,將項目模塊細分為幾個子工程模塊,在文中給大家詳細介紹過,對IDEA搭建SpringBoot多模塊聚合工程感興趣的朋友一起看看吧2022-04-04有關ServletConfig與ServletContext的訪問
下面小編就為大家?guī)硪黄嘘PServletConfig與ServletContext的訪問。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-01-01IntelliJ IDEA彈出“IntelliJ IDEA License Activation”的處理方法
這篇文章主要介紹了IntelliJ IDEA彈出“IntelliJ IDEA License Activation”的處理方法,本文給出解決方法,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09