來翻譯一篇rmi原理性文章:
成文動機
寫這篇文章是出于我個人的經驗,我是最近才知道java rmi這個東西的。但是立即我對這部分內容產生了濃厚的興趣,尤其是 stubs 和skeletons。讓我特別的感興趣的是RMI的設計者設計的這個框架使得rmi客戶端感覺就好像在調用本地方法一樣,但是實際上是在遠程對象上執行的。隨著我學習的深入,我開始了解了RMIRegistry和更多的東西。對很多的問題我都很困惑,比如RMIRegistry 真正的角色是什么?他是必須存在的么?stub對象是在哪里創建的?(服務端?客戶端?注冊中心?),客戶端怎么知道服務端監聽的端口是哪一個?難道所有的服務端都是使用1099來監聽客戶端的嗎?
這篇文章試圖來回答我之前的這些疑惑。我參考了網上很多關于rmi書籍和文章,但是都不能找到這些疑惑的線索,沒有人告訴我這些事情都是怎么運轉的,每個人都在告訴我怎么寫代碼,如此而已,沒有任何關于底層的工作機制。經過很長時間的研究和實驗,我得到了我想要的答案。所以我想分享我的知識,因為還有很多人可能遇到和我同樣的疑惑。
這篇文章試圖來回答關于RMI原理的幾個問題:
1、誰創建了stubs對象,服務器?客戶端?注冊中心?
2、怎么知道服務器監聽的端口是哪一個?
3、注冊中心對于RMI系統來說是必須的嗎?
4、如果沒有注冊中心,RMI可以運行嗎?
在下面的部分我會詳細解答這些問題,如果你想快速得到這些問題的答案,可以直接去看對應的部分,這里我會一步一步的引導你理解rmi中到底發生了什么
首先,請忘記注冊中心 !!
是的,從現在開始忘掉注冊中心,假設這玩意從現在開始不存在
最開始的場景大概是這樣的:我們有一個server和一個client。server 繼承了 java.rmi.server.UnicastRemoteObject。client和server運行在不同的機器上面
現在我們的需求是這樣的:client想執行一個在遠程機器上server的一個方法。
我們如果做到這一點?java rmi 會處理這些問題,解決方案肯定會涉及到socket網絡編程,因為server運行在遠程機器上,解決這個問題的關鍵點在于
1.客戶端如何從處理網絡連接中解耦開來
2.客戶端如何能就像調用本地方法一樣來調用遠程機器上的方法,因此rmi的開發人員就引入了stub和skeleton模型。
所有與網絡相關的代碼都放在了stub和skeleton中,這樣客戶端和服務端就不需要處理網絡相關的代碼了。stub同樣也實現了和服務端同樣 java.rmi.Remote接口,這樣當client想調用server上方法的時候,就可以調用stub上的相同的方法,但是stub里面只有和網絡相關的處理邏輯,并沒有對應的業務處理邏輯,比如說server上有一個add方法,stub中同樣也有一個add方法,但是stub上的這個add方法并不包含添加的邏輯實現,他僅僅包含如何連接到遠程的skeleton、調用方法的詳細信息、參數、返回值等等。
所以,目前看來大概是這個樣子的:
Client<-->stub<-->[NETWORK]<-->skeleton<-->Server
客戶端和stub對話,stub和skeleton對話,skeleton和server對話,server執行真正的方法,然后把結果原路返回,這樣看來就有4個獨立的部分,也就是說你大概需要4個class了。
在jdk1.2之后,skeleton就被合并到server中了,所以看起來是這個樣子滴:
Client<--->stub<--->[NETWORK]<--->Server_with_skeleton
socket 層的詳細內容
現在,你可以學習在socket層通信是如何完成了的,這部分非常重要!,這部分內容開始變的有點繞,所以,請特別注意這部分內容。
1.server在遠程機器上監聽一個端口,這個端口是jvm或者os在運行時隨機選擇的一個端口。可以說server在遠程機器上在這個端口上導出自己。
2.client并不知道server在哪,以及sever監聽哪個端口,但是他有stub,stub知道所有這些東西,這樣client可以調用stub上他想調用的任何方法。
3.client調用給你stub上的方法
4.stub鏈接server監聽的端口并發送參數,詳細過程如下:
a.client連接server監聽的端口
b.server收到請求并創建一個socket來處理這個鏈接
c.server繼續監聽到來的請求
d.使用雙方協定的歇息,傳送參數和結果
e.協議可以是JRMP或者 iiop
5.方法在遠程server上執行,并發執行結果返回給stub
6.stub返回結果給client,就好像是stub執行了這個方法一樣。
所以整個過程就結束了,但是等一下!回頭再看第2點,說stub知道server在哪,他監聽的端口,這怎么么可能?如果client不知道server的host和port,他怎么能創建一個知道所有這一切的stub對象呢?更何況是在服務端端口是隨機選擇的
啟動的窘境
這是在很多現實生活中最大的問題之一。解決方案在于我們如何通知client這些server端的細節。RMI的設計者們有應對啟動問題的應急措施,那就是RMIRegistry 存在的必要了。RMIRegistry 可以認為是一個服務,它提供了一個hashmap,里面是 public_name, Stub_object 名值對。比如我有一個遠程服務對象叫做 Scientific_Calculator,然后我想把這個服務對外公布為 calc,這樣會在server上創建一個stub對象,燃火把他注冊到RMIRegistry ,這樣client就可以從RMIRegistry 中得到這個stub對象了,你可以使用一個工具類 java.rmi.Naming 來方便的操作注冊和操作。
用一個栗子來說明整個過程
考慮一個計算的應用,它有一個add的方法,你想把這個方法對外發布成一個遠程對象,遠程接口叫做Calc
這就是所有的類,編譯這些類得到class,使用RMI 編譯器生成stub的class,使用.keep選項得到源代碼。
Source code for CalcImpl_Stub.java:
現在看一個這個生成的stub源代碼,注意到他的構造參數需要個RemoteRef類型的對象,這個對象從哪里獲取呢?繼續下面看。
下面先運行程序:
開2個console
然后就可以看到結果:7!!!
這里到底是怎么搞得?
我會給你看這個背后到底發生了什么
1.首先RMIRegistry 運行在server端,RMIRegistry 自身也是一個遠程對象。有一點需要注意的是:所有的遠程對象(繼承了UnicastRemoteObject對象)都會在sever上任意的端口導出自己,因為RMIRegistry 也是一個遠程對象,他也在server上導出自己,只是這個端口是廣為人知的1099,“廣為人知”的意思是所有的client都知道這個端口
2.服務端運行在server上,在UnicastRemoteObject構造函數里面,他把自己導出在server上一個任意端口上,這個端口client是不知道的
3.當你調用Naming.rebind()的時候,會傳入一個CalcImpl 的引用(這里叫做 c)作為第2個參數,Naming 就會構造一個stub對象,詳細如下:
a.Naming會使用getClass來獲取類的名字,這里就是CalcImpl
b.加上后綴_Stub 變成了 CalcImpl _Stub
c.加載CalcImpl_Stub.class到虛擬機中
d.從c中獲取RemoteRef 對象
e.就是這個ref對象中封裝了服務端細節,包括服務端的hostname、port
f.獲取了RemoteRef 對象之后,就可以構造stub對象了。
g.傳遞stud對象到RMIRegistry中進行綁定,即(publicname,stub)
f.RMIRegistry 中內部使用一個hashmap來存儲(publicname,stub)
4.當客戶端使用 Naming.lookup()的時候,會傳入public name 作為參數,RMIRegistry 就會返回stub給客戶端調用
RMIRegistry 是必須的嗎?
No,RMIRegistry 起到的作用只是為了方便client獲取到stub對象,如果還有其他的方法讓client拿到stub就不需要RMIRegistry 了,因為client一旦拿到了stub就不需要RMIRegistry 了
直接在client new一個stub對象不就可以了?
No,stub構造函數中需要一個RemoteRef 對象,這個對象只能在server端獲取。