Java RMI簡介(rmiregistry)


Java RMI簡介
Java RMI用於不同虛擬機之間的通信,這些虛擬機可以在不同的主機上、也可以在同一個主機上;一個虛擬機中的對象調用另一個虛擬上中的對象的方法,只不過是允許被遠程調用的對象要通過一些標志加以標識。這樣做的特點如下:

優點:避免重復造輪子;
缺點:調用過程很慢,而且該過程是不可靠的,容易發生不可預料的錯誤,比如網絡錯誤等;
在RMI中的核心是遠程對象(remote object),除了對象本身所在的虛擬機,其他虛擬機也可以調用此對象的方法,而且這些虛擬機可以不在同一個主機上。每個遠程對象都要實現一個或者多個遠程接口來標識自己,聲明了可以被外部系統或者應用調用的方法(當然也有一些方法是不想讓人訪問的)。

1.1 RMI的通信模型
從方法調用角度來看,RMI要解決的問題,是讓客戶端對遠程方法的調用可以相當於對本地方法的調用而屏蔽其中關於遠程通信的內容,即使在遠程上,也和在本地上是一樣的。

從客戶端-服務器模型來看,客戶端程序直接調用服務端,兩者之間是通過JRMP( Java Remote Method Protocol)協議通信,這個協議類似於HTTP協議,規定了客戶端和服務端通信要滿足的規范。

但是實際上,客戶端只與代表遠程主機中對象的Stub對象進行通信,絲毫不知道Server的存在。客戶端只是調用Stub對象中的本地方法,Stub對象是一個本地對象,它實現了遠程對象向外暴露的接口,也就是說它的方法和遠程對象暴露的方法的簽名是相同的。客戶端認為它是調用遠程對象的方法,實際上是調用Stub對象中的方法。可以理解為Stub對象是遠程對象在本地的一個代理,當客戶端調用方法的時候,Stub對象會將調用通過網絡傳遞給遠程對象。

在java 1.2之前,與Stub對象直接對話的是Skeleton對象,在Stub對象將調用傳遞給Skeleton的過程中,其實這個過程是通過JRMP協議實現轉化的,通過這個協議將調用從一個虛擬機轉到另一個虛擬機。在Java 1.2之后,與Stub對象直接對話的是Server程序,不再是Skeleton對象了。

所以從邏輯上來看,數據是在Client和Server之間橫向流動的,但是實際上是從Client到Stub,然后從Skeleton到Server這樣縱向流動的。

 

1.2 重要的問題
1.2.1 數據的傳遞問題
我們都知道在Java程序中引用類型(不包括基本類型)的參數傳遞是按引用傳遞的,對於在同一個虛擬機中的傳遞時是沒有問題的,因為的參數的引用對應的是同一個內存空間,但是對於分布式系統中,由於對象不再存在於同一個內存空間,虛擬機A的對象引用對於虛擬機B沒有任何意義,那么怎么解決這個問題呢?

第一種:將引用傳遞更改為值傳遞,也就是將對象序列化為字節,然后使用該字節的副本在客戶端和服務器之間傳遞,而且一個虛擬機中對該值的修改不會影響到其他主機中的數據;但是對象的序列化也有一個問題,就是對象的嵌套引用就會造成序列化的嵌套,這必然會導致數據量的激增,因此我們需要有選擇進行序列化,在Java中一個對象如果能夠被序列化,需要滿足下面兩個條件之一:
是Java的基本類型;
實現java.io.Serializable接口(String類即實現了該接口);
對於容器類,如果其中的對象是可以序列化的,那么該容器也是可以序列化的;
可序列化的子類也是可以序列化的;
第二種:仍然使用引用傳遞,每當遠程主機調用本地主機方法時,該調用還要通過本地主機查詢該引用對應的對象,在任何一台機器上的改變都會影響原始主機上的數據,因為這個對象是共享的;
RMI中的參數傳遞和結果返回可以使用的三種機制(取決於數據類型):

簡單類型:按值傳遞,直接傳遞數據拷貝;
遠程對象引用(實現了Remote接口):以遠程對象的引用傳遞;
遠程對象引用(未實現Remote接口):按值傳遞,通過序列化對象傳遞副本,本身不允許序列化的對象不允許傳遞給遠程方法;
1.2.2 遠程對象的發現問題
在調用遠程對象的方法之前需要一個遠程對象的引用,如何獲得這個遠程對象的引用在RMI中是一個關鍵的問題,如果將遠程對象的發現類比於IP地址的發現可能比較好理解一些。

在我們日常使用網絡時,基本上都是通過域名來定位一個網站,但是實際上網絡是通過IP地址來定位網站的,因此其中就需要一個映射的過程,域名系統(DNS)就是為了這個目的出現的,在域名系統中通過域名來查找對應的IP地址來訪問對應的服務器。那么對應的,IP地址在這里就相當於遠程對象的引用,而DNS則相當於一個注冊表(Registry)。而域名在RMI中就相當於遠程對象的標識符,客戶端通過提供遠程對象的標識符訪問注冊表,來得到遠程對象的引用。這個標識符是類似URL地址格式的,它要滿足的規范如下:

該名稱是URL形式的,類似於http的URL,schema是rmi;
格式類似於rmi://host:port/name,host指明注冊表運行的注解,port表明接收調用的端口,name是一個標識該對象的簡單名稱。
主機和端口都是可選的,如果省略主機,則默認運行在本地;如果端口也省略,則默認端口是1099;
二、編程實現
2.1 基本內容
實現RMI所需的API幾乎都在:

java.rmi:提供客戶端需要的類、接口和異常;
java.rmi.server:提供服務端需要的類、接口和異常;
java.rmi.registry:提供注冊表的創建以及查找和命名遠程對象的類、接口和異常;
其實在RMI中的客戶端和服務端並沒有絕對的界限,與Web應用中的客戶端和服務器還是有區別的。這兩者其實是平等的,客戶端可以為服務端提供遠程調用的方法,這時候,原來的客戶端就是服務器端。

2.2 基本實現之一(注冊表單獨運行)
2.2.1 構建服務器端
什么是遠程對象?首先從名稱上來看,遠程對象是存在於服務端以供客戶端調用。那么什么對象可以被客戶端進行遠程調用?這個問題從編程的角度來看,實現了java.rmi.Remote接口的類或者繼承了java.rmi.Remote接口的所有接口都是遠程對象。這些繼承或者實現了該接口的類或者接口中定義了客戶端可以訪問的方法。這個遠程對象中可能有很多個方法,但是只有在遠程接口中聲明的方法才能從遠程調用,其他的公共方法只能在本地虛擬機中使用。

實現過程中的注意事項:

子接口的每個方法都必須聲明拋出java.rmi.RemoteException異常,該異常是使用RMI時可能拋出的大多數異常的父類。
子接口的實現類應該直接或者間接繼承java.rmi.server.UnicastRemoteObject類,該類提供了很多支持RMI的方法,具體來說,這些方法可以通過JRMP協議導出一個遠程對象的引用,並通過動態代理構建一個可以和遠程對象交互的Stub對象。具體的實現看如下的例子。
首先遠程接口如下:

public interface UserHandler extends Remote {
String getUserName(int id) throws RemoteException;
int getUserCount() throws RemoteException;
User getUserByName(String name) throws RemoteException;
}
1
2
3
4
5
遠程接口的實現類如下:

public class UserHandlerImpl extends UnicastRemoteObject implements UserHandler {
// 該構造期必須存在,因為集繼承了UnicastRemoteObject類,其構造器要拋出RemoteException
public UserHandlerImpl() throws RemoteException {
super();
}

@Override
public String getUserName(int id) throws RemoteException {
return "lmy86263";
}
@Override
public int getUserCount() throws RemoteException{
return 1;
}
@Override
public User getUserByName(String name) throws RemoteException{
return new User("lmy86263", 1);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
為了測試在使用RMI的序列化的問題,這里特別設置了一個引用類型User:

public class User implements Serializable {
// 該字段必須存在
private static final long serialVersionUID = 42L;
// setter和getter可以沒有
String name;
int id;

public User(String name, int id) {
this.name = name;
this.id = id;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
在Java 1.4及 以前的版本中需要手動建立Stub對象,通過運行rmic命令來生成遠程對象實現類的Stub對象,但是在Java 1.5之后可以通過動態代理來完成,不再需要這個過程了。

運行該遠程對象的服務器代碼如下:

UserHandler userHandler = null;
try {
userHandler = new UserHandlerImpl();
Naming.rebind("user", userHandler);
System.out.println(" rmi server is ready ...");
} catch (Exception e) {
e.printStackTrace();
}
1
2
3
4
5
6
7
8
這里面的核心代碼為Naming.rebind("user", userHandler) ,通過一個名稱映射到該遠程對象的引用,客戶端通過該名稱獲取該遠程對象的引用。

在遠程對象中有三個方法:getUserName(int id) 和getUserCount()的參數和返回結果都是基本類型,因此是默認序列化的,但是對於getUserByName(String name)方法,返回的結果是一個引用類型,因此會涉及到序列化與反序列的問題,對於User類,必須滿足以下條件:

必須實現java.io.Serializable接口;

其中必須有serialVersionUID字段,格式如下:

private static final long serialVersionUID = 42L;
1
如果沒有該字段,則默認該類會隨機生成一個整數,且在客戶端和服務器生成的整數不相同,則會拋出異常如下:

 

而且在服務器和客戶端這個字段必須保持一致才能進行反序列化,如果兩端都有該字段,但是數據不一致,則會拋出異常如下:

 

這個類在服務器和客戶端都必須可用;

在序列化的時候,如果在字段前加入了transient關鍵字,則該數據不會被序列化;

2.2.2 構建注冊表
注冊表其實不用寫任何代碼,在你的JAVA_HOME下bin目錄下有一個rmiregistry.exe程序,需要在你的程序的classpath下運行該程序。

在啟動服務器的時候,實際上需要運行兩個服務器:

一個是遠程對象本身;
一個是允許客戶端下載遠程對象引用的注冊表;
由於遠程對象需要與注冊表對話,所以必須首先啟動注冊表程序。當注冊表程序沒有啟動的時候,如果強行啟動遠程對象服務器時,會拋出如下錯誤:

 

確保遠程對象類可以被注冊表程序發現,當遠程對象類沒有被注冊表程序發現時,則會發現如下錯誤:

 

如果是使用maven管理工程,則在target/classes目錄中啟動該程序。

這說明注冊表程序時運行在一個單獨的進程中的,它作為一個第三方的組件,來協調客戶端和服務器之間的通信,但是與它們兩個之間是完全解決解耦的。

rmiregistry.exe默認情況下是監聽1099端口,如果已經該端口已經被使用了,可以通過命令

rmiregistry 1020
1
指定其他的端口來運行。

可以通過start rmiregistry命令在后台運行

運行完注冊表程序后,就可以運行遠程對象所在的服務器,以便接受客戶端的連接。

2.2.3 構建客戶端
客戶端的代碼如下:

try {
UserHandler handler = (UserHandler) Naming.lookup("user");
int count = handler.getUserCount();
String name = handler.getUserName(1);
System.out.println("name: " + name);
System.out.println("count: " + count);
System.out.println("user: " + handler.getUserByName("lmy86263"));
} catch (NotBoundException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
在上邊的代碼中通過Naming.lookup(...)獲取該遠程對象的引用。這個方法通過一個指定的名稱來獲取,該名稱必須與遠程對象服務器綁定的名稱一致。可以通過Naming.list(...)方法列出所有可用的遠程對象。

在使用客戶端連接服務器調用遠程方法的時候,需要注意的問題如下:

UserHandler類在客戶端本地必須可用,不然無法指定要調用的方法,而且其全限定名必須與服務器上的對象完全相同,不然拋出如下異常:

 

從注冊表中獲取的對象引用已經失去類型信息,需要強制轉化為遠程對象類型。這樣運行客戶端的時候才能獲得相應的響應;

如果在方法中使用到了引用類型,比如這里的User,那么該類型的全限定名也必須與服務器的相同,如果不相同則會拋出如下異常:

 

客戶端的引用類型的serialVersionUID字段要與服務器端的對象保持一致;

在客戶端的User對象如下:

public class User implements Serializable {
// 與客戶端的serialVersionUID字段數據一致
private static final long serialVersionUID = 42L;
// setter和getter可以沒有
String name;
int id;

@Override
public String toString() {
return "User{" + "name='" + name + '\'' + ", id=" + id + '}';
}
}
1
2
3
4
5
6
7
8
9
10
11
12
2.3 基本實現之二(服務端運行注冊表程序)
對於實現二,和實現一的主要區別在注冊表程序的運行,不再是通過rmiregistry.exe單獨運行,而是通過編程來實現,而遠程接口以及其實現類與實現一完全相同。

這里注冊表的實現是通過java.rmi.registry包中的Registry接口和以及其實現類LocateRegistry來完成的。如果你詳細查看JDK的源碼的話,就會發現其實我們之前使用的java.rmi.Naming類中的方法實際上都是間接通過Registry和LocateRegistry實現的。

其中獲取根據主機和端口獲取注冊表引用的源碼如下:

/**
* Returns a registry reference obtained from information in the URL.
*/
private static Registry getRegistry(ParsedNamingURL parsed) throws RemoteException {
return LocateRegistry.getRegistry(parsed.host, parsed.port);
}
1
2
3
4
5
6
而且Naming中的方法和Registry中是一一對應的。

而如果要創建一個注冊表,這里要使用的是LocateRegistry,該類中只要兩類方法:

創建本地注冊表並且獲取該注冊表的引用;

createRegistry(int port)

createRegistry(int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf)

直接獲取注冊表引用,該注冊表可以是本地運行的,也可以是遠程運行的,這類方法是不能夠創建注冊表的,只能等注冊表程序運行起來之后,和它進行通信來獲取引用,否則拋出異常如下:

 

其中的方法如下:

getRegistry()
getRegistry(int port)
getRegistry(String host)
getRegistry(String host, int port)
getRegistry(String host, int port, RMIClientSocketFactory csf)
由於是可能從遠程主機獲取注冊表引用,因此可能需要指定Socket套接字來和遠程主機進行溝通,在這個過程中也有可能因為各種原因造成調用過程失敗;

運行遠程對象的服務器代碼如下:

UserHandler userHandler = null;
Registry registry = null;
try {
registry = LocateRegistry.createRegistry(1099);
userHandler = new UserHandlerImpl();
registry.rebind("user", userHandler);
System.out.println(" rmi server is ready ...");
} catch (RemoteException e) {
e.printStackTrace();
}
1
2
3
4
5
6
7
8
9
10
除此之外,其他服務器端和客戶端的代碼與實現一完全相同。

關於RMI的實際使用,其中一種方式可以參考相關文章第6個,通過和ZooKeeper結合使用RMI 。

相關文章:

What is a serialVersionUID and why should I use it?

Java 序列化的高級認識

java.rmi.Naming和java.rmi.registry.LocateRegistry的區別

RMI教程:入門與編譯方法

Java深度歷險(十)——Java對象序列化與RMI

使用 RMI + ZooKeeper 實現遠程調用框架


————————————————
版權聲明:本文為CSDN博主「lmy86263」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/lmy86263/article/details/72594760


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM