RMI (遠程方法)是 Java 平台中建立分布式計算的基礎, 2 年前我剛開始接觸 J2EE 時,怎麼看書都是不得要領,最近這幾天閒著沒事又翻了翻以前沒有看懂的書,突然之間頓 悟了。
一、 簡單的 RMI 示例:
要快速入門,最簡單的方法就是看簡單的例子。下面是我寫的一個簡單的示例:
首先,定義一個接口 IServer ,代碼如下:
IServer.java
1 package rmistudy;
2
3 import java.rmi.Remote;
4
5 public interface IServer extends Remote {
6 public void doSomeThing() throws java.rmi.RemoteException;
7 }
8
9
需要注意的是,這個接口從java.rmi.Remote接口擴展,並且這個接口中定義的方法都需 要拋出java.rmi.RemoteException異常。
接著,我們要根據這個接口來實現自己的服務器對象,所謂服務器對象,就是我們大腦中 想的遠程對象,這個對象中定義的方法都是被別人來調用的。代碼如下:
ServerImp.java
package rmistudy;
import java.rmi. * ;
import java.rmi.server. * ;
public class ServerImp extends UnicastRemoteObject implements IServer {
public ServerImp() throws RemoteException {
super ();
}
public void doSomeThing() throws RemoteException {
System.out.println( " 不帶參數的遠程函數doSomeThing()被調用,該 信息顯示在服務器端。 " );
}
public static void main(String[] args) {
ServerImp server = null ;
try {
server = new ServerImp();
} catch (Exception e) {
System.out.println( " 創建遠程對象失敗: " );
System.out.println(e.getMessage());
System.exit( 0 );
}
try {
java.rmi.Naming.rebind( " //localhost/MyServer " , server);
System.out.println( " 遠程對象綁定成功。 " );
} catch (Exception e) {
System.out.println( " 遠程對象綁定失敗: " );
System.out.println(e.getMessage());
System.exit( 0 );
}
}
}
這個類很容易理解, doSomeThing() 方法只簡單的輸出被調用的信息。唯一的難點就在 main() 函數中,我們通過 java.rmi.Naming.rebind() 把我們的遠程對象注冊到 rmi 注冊 表中,這樣,別人就可以通過 java.rmi.Naming.lookup() 來查找我們的遠程對象。那麼, rmi 注冊表在哪裡呢? J2SDK 的 bin 目錄下有一個程序 rmiregistry ,運行它就可以得到 一個注冊表進程,我們可以通過它來綁定或者查找遠程對象, java.rmi.Naming.rebind 函 數的第一個參數就是要指定注冊表進程的位置,因為我這裡運行在自己的機器上,所以是 //localhost/ ,如果是在別的機器上,可以用 IP 地址代替。
最後,我們寫一個客戶機,來調用這個遠程對象的方法。代碼如下:
Client.java
1 package rmistudy;
2
3 import java.rmi. * ;
4
5 public class Client {
6
7 public static void main(String[] args) {
8 IServer server = null ;
9
10 try {
11 server = (IServer)Naming.lookup( " //localhost/MyServer " );
12 System.out.println( " 查找遠程對象成功。 " );
13 } catch (Exception e) {
14 System.out.println( " 查找遠程對象失敗: " );
15 System.out.println(e.getMessage());
16 System.exit( 0 );
17 }
18
19 try {
20 server.doSomeThing();
21 System.out.println( " 調用doSomeThing()成功。 " );
22 } catch (Exception e) {
23 System.out.println( " 調用doSomeThing()失敗: " );
24 System.out.println(e.getMessage());
25 System.exit( 0 );
26 }
27 }
28 }
29
可以看到,我們的客戶端程序只用到了 IServer 接口,而不需要 ServerImp 類,它只通 過 java.rmi.Naming.lookup() 來查找遠程對象的引用。
下面,我們就可以開始測試我們的程序了。先編譯以上程序,然後:
第一步,要先啟動 Rmi 注冊表,如下:
第二步,使用 rmic 對 ServerImp.class 進行編譯,生成代理類 ServerImp_Stub.class ,如下:
第三步,啟動服務器端程序,如下:
第四步,啟動客戶端程序,我們多調用幾次,如下:
這個時候,我們再看看服務器端是什麼反應:
可以看到,服務器端的方法被調用,在服務器端的控制台上打印出了這樣幾行消息。
下面,我們使用一個簡單的圖表來表示客戶機、服務器和 RMI 注冊表之間的關系,綠色 的數字代表順序:
二、參數傳遞
前面的例子沒有涉及到參數的傳遞。如果我們需要向遠程方法傳遞參數,或者要從遠程方 法接受返回值,是不是有什麼特殊的約定呢?不錯,如果我們要在客戶機和服務器之間傳遞 參數,則該對象要麼是實現Serializable接口的對象,要麼是擴展自UnicastRemoteObject的 對象,這兩種對象是有差別的。
如果參數是實現Serializable接口的對象,則該對象是按值傳遞的,也就是把這整個對象 傳遞到遠程方法中。請看下面的例子,我們定義了一個ISerializableWorker接口,擴展自 Serializable接口,客戶端創建一個SerializableWorkerImp對象wk,並把它傳遞到服務器端 ,服務器端調用wk.work()方法,這個方法在服務器端執行,這就說明了我們成功把這個對象 傳遞到了服務器端。服務器端返回的String對象,也可以成功傳遞到客戶端。
ISerializableWorker.java
package rmistudy;
2
3 import java.io.Serializable;
4
5 public interface ISerializableWorker extends Serializable {
6 public void work();
7 }
SerializableWorkerImp.java
1package rmistudy;
2
3public class SerializableWorkerImp implements ISerializableWorker {
4
5 public void work() {
6 System.out.println("該信息由SerializableWorker對象輸出。");
7 }
8
9}
IServer.java
1package rmistudy;
2
3import java.rmi.Remote;
4import java.rmi.RemoteException;
5
6public interface IServer extends Remote {
7 public void doSomeThing() throws RemoteException;
8 public String doSomeThing(ISerializableWorker wk) throws RemoteException;
9}
ServerImp.java
1package rmistudy;
2
3import java.rmi.*;
4import java.rmi.server.*;
5
6public class ServerImp extends UnicastRemoteObject implements IServer {
7
8 public ServerImp() throws RemoteException {
9 super();
10 }
11
12
13 public void doSomeThing() throws RemoteException {
14
15 System.out.println("不帶參數的遠程函數doSomeThing()被調用,該信息顯示 在服務器端。");
16
17 }
18
19 public String doSomeThing(ISerializableWorker wk) throws RemoteException {
20 wk.work();
21 return new String("調用成功,該信息來自服務器端。");
22 }
23
24 /** *//**
25 * @param args
26 */
27 public static void main(String[] args) {
28 ServerImp server = null;
29
30 try{
31 server = new ServerImp();
32 }catch(Exception e){
33 System.out.println("創建遠程對象失敗:");
34 System.out.println(e.getMessage());
35 System.exit(0);
36 }
37
38 try{
39 java.rmi.Naming.rebind("//localhost/MyServer", server);
40 System.out.println("遠程對象綁定成功。");
41 }catch(Exception e){
42 System.out.println("遠程對象綁定失敗:");
43 System.out.println(e.getMessage());
44 System.exit(0);
45 }
46 }
47
48}
Client.java
1package rmistudy;
2
3import java.rmi.*;
4
5public class Client {
6
7 /** *//**
8 * @param args
9 */
10 public static void main(String[] args) {
11 IServer server = null;
12
13 try{
14 server = (IServer)Naming.lookup("//localhost/MyServer");
15 System.out.println("查找遠程對象成功。");
16 }catch(Exception e){
17 System.out.println("查找遠程對象失敗:");
18 System.out.println(e.getMessage());
19 System.exit(0);
20 }
21
22 try{
23 server.doSomeThing();
24 System.out.println("調用doSomeThing()成功。");
25 String str = server.doSomeThing(new SerializableWorkerImp());
26 System.out.println("調用帶序列化參數的doSomeThing()成功");
27 System.out.println("從服務器端返回的字符串:"+str);
28 }catch(Exception e){
29 System.out.println("調用doSomeThing()失敗:");
30 System.out.println(e.getMessage());
31 System.exit(0);
32 }
33
34 }
35
36}
37
程序的運行方法同前,我就不再羅嗦了。這裡需要注意的是,該示例在單機上運行可以, 但是真的在分布環境下運行就會出錯,畢竟,別人要把一個對象傳遞到你的機器上,怎麼著 你也要放著別人的對象搞破壞吧。最後我們會討論安全問題。
另外一種參數的傳遞方式,就是按照引用傳遞,如果作為參數的對象是擴展自 java.rmi.server.UnicastRemoteObject類的話,那麼該對象傳遞給遠程方法的只是它的引用 。比如,客戶端創建了一個擴展自java.rmi.server.UnicastRemoteObject的對象A,把對象A 傳遞到服務器端,這個時候服務器端得到的只是對象A的引用,如果服務器調用對象A的方法 ,這個方法就會在客戶端執行。
下面的例子說明了這一點,我們定義IRefWorker接口和RefWorkerImp類,在客戶端創建 RefWorkerImp類的對象,把該對象傳遞到服務器端,服務器端調用該對象的方法,你會發現 該方法在客戶端執行。
IRefWorker.java
1package rmistudy;
2
3import java.rmi.Remote;
4import java.rmi.RemoteException;
5
6public interface IRefWorker extends Remote {
7 public void work() throws RemoteException;
8}
RefWorkerImp.java
1package rmistudy;
2
3import java.rmi.RemoteException;
4import java.rmi.server.RMIClientSocketFactory;
5import java.rmi.server.RMIServerSocketFactory;
6import java.rmi.server.UnicastRemoteObject;
7
8public class RefWorkerImp extends UnicastRemoteObject implements IRefWorker {
9
10 public RefWorkerImp() throws RemoteException {
11 super();
12 }
13
14 public void work() throws RemoteException {
15 System.out.println("該方法在服務器端調用,在客戶端執行。");
16 }
17
18}
IServer.java
1package rmistudy;
2
3import java.rmi.Remote;
4import java.rmi.RemoteException;
5
6public interface IServer extends Remote {
7 public void doSomeThing() throws RemoteException;
8 public String doSomeThing(ISerializableWorker wk) throws RemoteException;
9 public void doSomeThing(IRefWorker wk) throws RemoteException;
10}
ServerImp.java
該類中實現接口中定義的方法,和前面的代碼相比,多了如下一行
1public void doSomeThing(IRefWorker wk) throws RemoteException{
2 wk.work();
3 }
Client.java
1package rmistudy;
2
3import java.rmi.*;
4
5public class Client {
6
7 /** *//**
8 * @param args
9 */
10 public static void main(String[] args) {
11 IServer server = null;
12
13 try{
14 server = (IServer)Naming.lookup("//localhost/MyServer");
15 System.out.println("查找遠程對象成功。");
16 }catch(Exception e){
17 System.out.println("查找遠程對象失敗:");
18 System.out.println(e.getMessage());
19 System.exit(0);
20 }
21
22 try{
23 server.doSomeThing();
24 System.out.println("調用doSomeThing()成功。");
25 String str = server.doSomeThing(new SerializableWorkerImp());
26 System.out.println("調用帶序列化參數的doSomeThing()成功");
27 System.out.println("從服務器端返回的字符串:"+str);
28 server.doSomeThing(new RefWorkerImp());
29 System.out.println("調用帶引用參數的doSomeThing()成功");
30 }catch(Exception e){
31 System.out.println("調用doSomeThing()失敗:");
32 System.out.println(e.getMessage());
33 System.exit(0);
34 }
35
36 }
37
38}
程序的運行方法同前,不再重復。
三、安全管理與授權策略
前面提到過,前面的示例代碼,如果真正運行到分布式環境下的話,是會出錯的,原因就 在於安全性問題。J2EE中的安全管理廣泛,我們這裡僅僅只用到授權,比如我們可以只授權 遠程程序訪問某一個文件夾或某一個文件,或者只授權遠程程序訪問網絡等等。
要使用授權,需要一個授權文件,我們新建一個Policy.txt文件,為了簡單起見,我們授 權遠程程序可以訪問所有的本地資源:
1grant{
2 permission java.security.AllPermission "","";
3};
然後,我們需要在服務器端程序中載入安全管理器,我們這裡使用默認的 RMISecurityManager,下面是經過修改了的ServerImp.java中的mian()函數:
1public static void main(String[] args) {
2 ServerImp server = null;
3
4 try{
5 System.setSecurityManager(new RMISecurityManager());
6 }catch(Exception e){
7 System.out.println("加載安全管理器失敗:");
8 System.out.println(e.getMessage());
9 System.exit(0);
10 }
11
12 try{
13 server = new ServerImp();
14 }catch(Exception e){
15 System.out.println("創建遠程對象失敗:");
16 System.out.println(e.getMessage());
17 System.exit(0);
18 }
19
20 try{
21 java.rmi.Naming.rebind("//localhost/MyServer", server);
22 System.out.println("遠程對象綁定成功。");
23 }catch(Exception e){
24 System.out.println("遠程對象綁定失敗:");
25 System.out.println(e.getMessage());
26 System.exit(0);
27 }
28 }
然後,我們需要這樣運行服務器端:
java -Djava.security.policy=Policy.txt rmistudy.ServerImp
給幾個貼圖:
1.運行服務器:
2.運行客戶端:
3.運行客戶端後服務器的反應:
總結
J2EE規范雖然龐大而復雜,但是如果我們分開來學習,也是可以逐步理解的。J2EE包含企 業數據、企業通訊、企業服務、企業Web支持和企業應用程序等方面。而我們的RMI就是企業 通訊中的一種,另外一種就是日益流行起來的Web Service通訊,至於其它通訊架構,我們大 可以到需要的時候再去學習,比如CORBA。
EJB是架構在RMI基礎上的,但是它太復雜,很多時候使用簡單的RMI就可以解決很多問題 ,比如科學領域的分布式計算。大家一定聽說過找外星人的SETI項目,它就是利用全球志願 者的個人PC來進行分布式的運算。在我們中國,大型機還比較缺乏,如果我們的某個實驗室 需要強大的計算能力,大可以向SETI項目學習。而使用RMI,建立分布式計算平台是多麼的簡 單。