Remote Method Invocation 入門
Many examples in this document are adapted from Java: How To Program (3rd Ed.), written by Deitel and Deitel, and Thinking in Java (2nd Edition), written by Bruce Eckel. All examples are solely used for educational purposes. Hopefully, I am not violating any copyright issue here. If so, please do email me.Please install JDK 1.3.1_02 or later with Java Plugin to view this page. Also, this page is best viewed with browsers (for examples, Mozilla 0.99 or later, IE 6.x or later) with CSS2 support. This document is provided as is. You are welcomed to use it for non-commercial purpose.
Written by: 國立中興大學資管系呂瑞麟 Eric Jui-Lin Lu
請勿轉貼
看其他教材
目錄
前言
Remote Method Invocation (RMI) 類似於之前所說的 RPC (Remote Procedure Call),希望能讓程式開發人員專注於 business logic 而不必分散心思於 networking 以及 marshalling/unmarshalling 的工作。可是已經有了 RPC 為什麼又要 RMI 呢?這是因為 RPC 只支援傳遞一些簡單的資料型態,並無法傳遞 Java 的 object。 另外,由於 RPC 需要定義介面,但是目前各家的介面定義語言 (Interface Definition Language; IDL) 並不相同,造成運作上 的不便。因此 Sun 提出了 RMI 使得程式之間能夠傳遞複雜的資料 型態,而且 IDL 採用的是 Java 標準。簡單來說,RMI 的運作過程如下:提供 service 的程式先寫好, 然後由 service 的開發者向 registry 註冊。而客戶端需要使用 該服務之前,先向 registry 查詢,然後在取得該服務的參考 (reference) 以後,向 service 提出要求並得到答覆! 有不少的網頁指出,一般來說並不建議使用 IE 來作為載入 RMI applet 的 container。Hello World -- RMI
在這個範例裡面,我們做一件非常簡單的動作。就是由 service 傳回 "Hello World" 給客戶端,並由客戶端列印出來。而且我們 假設你把 service 安裝在 penguin.im.cyut.edu.tw, 而在你自己使用的電腦上(假設為 Win32 的環境)執行客戶端程式。- 先設計一個介面定義程式(放在 penguin 和客戶端)
// // Step 1. Create Interface // // Note: interface must extend java.rmi.Remote // public interface HelloIF extends java.rmi.Remote { // the single method to be implemented: getHello public String getHello() throws java.rmi.RemoteException; }
- 設計一個 service (也就是一般認知的 server 程式),並安裝在 penguin。
// // Step 2. Create Interface Implementation // import java.rmi.*; import java.rmi.server.*; public class HelloImpl extends UnicastRemoteObject implements HelloIF { private String serviceName; public HelloImpl(String s) throws RemoteException { super(); serviceName = s; } public String getHello() throws RemoteException { return "Hello World"; } public static void main( String argv[] ) { System.setSecurityManager(new RMISecurityManager()); try { HelloImpl obj = new HelloImpl("HelloService"); // 你可以更改成你所需要的 hostname,甚至可以另外以 // hostname:port 來設定另一個 port,例如 // "//penguin.im.cyut.edu.tw:2110/HelloService". Naming.rebind("//penguin.im.cyut.edu.tw/HelloService", obj); System.out.println("HelloService registered."); } catch (Exception e) { System.out.println("HelloImpl Err: " + e.getMessage()); e.printStackTrace(); } } }
- 最後,把你所需要的客戶端程式寫出來。
// // Step 3. Create the client // import java.rmi.*; public class HelloClient { public static void main( String argv[] ) { String message = null; try { // 如果不是使用預設的 port,你也必須更改成 hostname:port, ex. // "rmi://penguin.im.cyut.edu.tw:2110/HelloService" HelloIF obj = (HelloIF) Naming.lookup( "rmi://penguin.im.cyut.edu.tw/HelloService"); message = obj.getHello(); System.out.println(message); } catch (Exception e) { System.out.println("HelloService exception: " + e.getMessage()); e.printStackTrace(); } } }
- 請先將程式碼 compile 好:
- server 端:
- javac HelloImpl.java
- rmic HelloImpl 會自動產生 client stub (HelloImpl_Stub.class) 和 server skeleton (HelloImpl_Skel.class)。
- client 端:
- javac HelloClient.java
- 將剛剛 server 所產生的 interface 和 stub (即 HelloIF.class 和 HelloImpl_Stub.class)下載下來使用。
- server 端:
- 執行 registry,請下指令 rmiregistry。執行 rmiregistry 之前你必須注意所有有可能需要被執行的類別(包含 _Stub.class 和 _Skel.class)的路徑都需要定義在 CLASSPATH 內。
- 執行 service 程式來向 registry 註冊。如果你直接執行
java HelloImpl,你會得到 access control 的錯誤訊息,這是因為 JVM
在預設的情形下,只允許有限的動作可以執行(想知道有哪些可以執行,請參考
jdk/jre/lib/security/java.policy)。若要成功執行這個程式,你
必須授權(grant)這些權限給執行的程式。為了達成這個目的,你需要產生一個
.java.policy 檔(請注意檔名最前方的 "."),並將他至於你所使用
的那種系統之下的使用者家目錄 (home directory)。安裝完了以後,請再次
執行程式。(在 Windows 2000/XP 的電腦上,你可以將 .java.policy 放置於
C:\Documents and Settings\<userid>\.java.policy。
Win98 好像放在 C:\Windows 目錄。)
grant { permission java.net.SocketPermission "*:1024-","connect,accept"; permission java.net.SocketPermission "*:80","connect"; };
- 在客戶端執行 java HelloClient
- 練習題
- 請將這個範例改成 applet 版。為了避免困擾,請利用 appletviewer 來測試你的程式。確定無誤以後,請再嘗試將網頁放在 mail,service 放在 penguin, 再用客戶端的 browser(測試過程還是建議利用 appletviewer)來連結。 非常有意思的地方在於,你必須授權給客戶端程式來連結 service,這樣子就 可經由授權的方式來開放 sandbox 的限制。
- 請將這個範例改成 service 傳回 hostname 並加上時間。你可以使用 java.net.InetAddress.getLocalHost().getHostName() 來取得 hostname。
RMI and DB
在這個範例中,我們把 service 安裝於 win32 的電腦上,並由該 service 存取 local 的 access 資料庫內的資料,並把資料回傳給位於 Unix 電腦上的 client。- 介面的定義
// // Create Interface // public interface QueryIF extends java.rmi.Remote { public java.util.Vector getSalary(int salary) throws java.rmi.RemoteException; }
- service 程式:把這個程式安裝於電腦教室的電腦上。要執行這個程式還需要
對 RunPermission、PropertyPermission 等參數作授權的動作。
// // Step 2. Create Interface Implementation // import java.rmi.*; import java.rmi.server.*; import java.sql.*; import java.util.*; public class QueryImpl extends UnicastRemoteObject implements QueryIF { private String serviceName; public QueryImpl(String s) throws RemoteException { super(); serviceName = s; } public Vector getSalary(int salary) throws RemoteException { Connection conn = null; ResultSet rs = null; Vector v = new Vector(); String qs = "select fname, lname, ssn, salary from employee " + "where salary >= " + String.valueOf(salary); try { Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); // 請將下列的 UID, PWD 作適當的改變 conn = DriverManager.getConnection("jdbc:odbc:samples", UID, PWD); Statement st = conn.createStatement(); rs = st.executeQuery(qs); while(rs.next()) for(int i=1; i<=4; i++) v.add(rs.getString(i)); // Clean up rs.close(); st.close(); conn.close(); } catch (SQLException sqle) { System.out.println("QueryImpl SQLException " + sqle.getMessage()); System.exit(0); } catch (Exception e) { System.out.println("QueryImpl Err: " + e.getMessage()); System.exit(1); } return v; } public static void main( String argv[] ) { System.setSecurityManager(new RMISecurityManager()); try { QueryImpl obj = new QueryImpl("QueryService"); // 請將下列的 hostname 作適度的改變 Naming.rebind("//hostname/QueryService", obj); System.out.println("QueryService registered."); } catch (Exception e) { System.out.println("QueryImpl Err: " + e.getMessage()); } } }
- client 程式:你可以把 client 程式安裝在與 service 同一部電腦上,
也可以另尋一部電腦來安裝。為了突顯分散式的環境,我們把 client 程式安裝在
penguin(OS 是 Linux)上。
import java.rmi.*; import java.sql.*; import java.util.*; public class QueryClient { public static void main( String argv[] ) { Vector v = null; try { // 這裡的 hostname 應該改成和 QueryImpl.java 相同的 hostname QueryIF obj = (QueryIF) Naming.lookup( "rmi://hostname/QueryService"); v = obj.getSalary(30000); Enumeration e = v.elements(); StringBuffer sb = new StringBuffer(); int count = 0; while(e.hasMoreElements()) { for(int i=0; i<4 data-blogger-escaped-catch="catch" data-blogger-escaped-e.getmessage="e.getmessage" data-blogger-escaped-e.nextelement="e.nextelement" data-blogger-escaped-e="e" data-blogger-escaped-exception:="exception:" data-blogger-escaped-i="i" data-blogger-escaped-n="n" data-blogger-escaped-pre="pre" data-blogger-escaped-sb.append="sb.append" data-blogger-escaped-sb="sb" data-blogger-escaped-system.out.println="system.out.println" data-blogger-escaped-t="t" data-blogger-escaped-ueryservice="ueryservice" data-blogger-escaped-xception="xception">
- 練習題: 請將這個程式改成 applet 版,使得使用者可以 輸入欲查詢的薪資金額,並把結果呈現出來。如果你想接受挑戰的話,也 可以將這個 applet 程式改成 4-tier 的架構。
- 我們現在來看看介面的定義。目前設計的方式 getSalary() 會回傳 Vector 的類別,可是我們為什麼不直接讓他回傳 ResultSet 呢?答案是「不可以」,這是因為 ResultSet 不是一個可以 Serializable 的類別,而 RMI 要求所有傳遞的資料都必須是 Serializable 的類別。要解決這個問題,目前有兩種:一種 就是我們所用的,也就是在 service 端把資料轉成 Serializable 的 物件,另一種就是使用提供能夠 Serializable 物件的 JDBC 驅動程式,例如 RmiJdbc。
- 練習題:試試看,讓一個 client 跟兩個(或兩個以上)的 services 來連結並完成一個工作!
查詢 services 清單
我們可以寫一個小的程式來顯示註冊在某個 server 的所有服務。- 程式:
import java.rmi.*; public class ListServices { public static void main( String argv[] ) { if(argv.length != 1) { System.out.println("Usage: java ListServices hostname"); System.exit(0); } try { String [] slist = Naming.list("rmi://" + argv[0] + "/"); if(slist.length == 0) System.out.println("Currently, there is no registered services."); else for(int i=0; i<slist.length; i++) System.out.println(slist[i]); } catch (Exception e) { System.out.println("ListServices exception: " + e.getMessage()); } } }
- 練習題: 請將這個範例改成使用者可以查詢某一個服務是否 存在的程式,如果使用者需入 "*" 才代表列印所有服務。
RmiJdbc
RmiJdbc 是什麼呢?依據 RmiJdbc 首頁的定義: RmiJdbc is a client/server (Type 3) JDBC Driver that relies on Java RMI. All JDBC classes (like Connection, ResultSet, etc...) are distributed as RMI objects, so that you can distribute as you like the access to any database supporting the JDBC API. In fact, RmiJdbc is just a bridge to allow remote access to JDBC drivers. 首先,我們說明下列程式的關係。RMI Client <--> (rmiregistry + RmiJdbc Server) <--> DB Server在以下的說明中,我們假設 client 是一部 win32 的電腦,rmiregistry 和 RmiJdbc Server 的電腦是 penguin,而資料庫伺服器就是系上的 SQL Server。執行 RMI Client 之前,必須先在 penguin 執行下列步驟:
- 需下載 RmiJdbc 以及 MS SQL-2000 的 JDBC Driver,並將這幾個 jar 檔案設定在 CLASSPATH。
- 執行 rmiregistry &。注意,由於 rmiregistry 只需要 在一部機器上,因此請先執行 ps -ef | grep rmiregistry 來查看是否 rmiregistry 已經執行。
- 執行 RmiJdbc Server:java org.objectweb.rmijdbc.RJJdbcServer -noreg com.microsoft.jdbc.sqlserver.SQLServerDriver。Note: the JDBC/ODBC bridge is registered by default, no need to add "sun.jdbc.odbc.JdbcOdbcDriver" to the driver list.
- Application 版:
- 程式:
import java.awt.*; import javax.swing.*; import java.sql.*; import java.rmi.*; import java.net.InetAddress; /** * This is a sample program for RmiJdbc client/server jdbc Driver * RmiJdbc relies on Java RMI for jdbc objects distribution */ public class TestClient { public static void main(String[] args) { try { // 非常有意思的用法,利用 showMessageDialog 可以內含其他 // 物件的方式來把 JPasswordField 包到 showMessageDialog // 使得密碼的輸入不會被見到! JLabel name=new JLabel("User Name"); JTextField uname=new JTextField(); JLabel passwd=new JLabel("Password"); JPasswordField pword=new JPasswordField(); Object[] ob={name,uname,passwd,pword}; JOptionPane.showMessageDialog(null, ob); // getPassword() 會傳回 char[] 所以需要被 String() 來轉換 String uid = uname.getText(); String pwd = new String(pword.getPassword()); // Register RmiJdbc Driver in jdbc DriverManager // On some platforms with some java VMs, newInstance() is necessary... Class.forName("org.objectweb.rmijdbc.Driver").newInstance(); // Database URL. 請使用 db 的 url,例如,如果是 jdbc-odbc, // 那麼 url 只是 "jdbc:odbc:samples" String url = "jdbc:microsoft:sqlserver://db_host_ip:1433"; // Database Host. IP number or Internet name, and port number. // RmiJdbc server installs it own in port 1099. You can change this. String rmiHost = new String("//rmi_server_host:1099"); // RmiJdbc URL is of the form: // jdbc:rmi:/// Connection c = DriverManager.getConnection("jdbc:rmi:" + rmiHost + "/" + url, uid, pwd); Statement st = c.createStatement(); ResultSet rs = st.executeQuery("SELECT * FROM bookstores"); ResultSetMetaData md = rs.getMetaData(); while(rs.next()) { System.out.print("\n | "); for(int i=1; i<= md.getColumnCount(); i++) { System.out.print(rs.getString(i) + " | "); } } System.out.println(""); rs.close(); c.close(); } catch(Exception e) { e.printStackTrace(); } System.exit(1); } }
- 在客戶端的電腦也需要安裝 RmiJdbc.jar 才能執行。
- 請試試看將 db 改成 access,把 rmiregistry 和 RmiJdbc Server 也在安裝 db 的電腦上執行起來,並試著從 penguin 來連取 access 的 資料。
- 程式:
- Applet 版:你可以試試看。
- 網頁:使用 archive="RmiJdbc.jar" 的缺點是每一次這個網頁被呼叫
的時候,RmiJdbc.jar 就得被下載一下而浪費時間與頻寬。
<applet code="TestApplet.class" archive="RmiJdbc.jar" width="450" height="250"> </applet>
- 程式:
import java.sql.*; import java.awt.*; import javax.swing.*; public class TestApplet extends JApplet { Connection con; Statement sentence; ResultSet rs; String field1; String field2; JTextArea status; JLabel comment; public void init() { comment = new JLabel("Database Results:"); status = new JTextArea("Launching program...\n", 10, 35); Container c = getContentPane(); c.setLayout(new FlowLayout(FlowLayout.CENTER)); c.add(comment); c.add(new JScrollPane(status)); JLabel name=new JLabel("User Name"); JTextField uname=new JTextField(); JLabel passwd=new JLabel("Password"); JPasswordField pword=new JPasswordField(); Object[] ob={name,uname,passwd,pword}; JOptionPane.showMessageDialog(null, ob); String uid = uname.getText(); String pwd = new String(pword.getPassword()); status.append("Connecting Database.\n"); try { Class.forName("org.objectweb.rmijdbc.Driver").newInstance(); } catch (Exception e) { status.append("Error: " + e); return; } try { String url = "jdbc:microsoft:sqlserver://db_host:1433"; // Database Host. IP number or Internet name, and port number. // RmiJdbc server installs it own in port 1099. You can change this. String rmiHost = new String("//rmi_host:1099"); // Connection.. con = DriverManager.getConnection("jdbc:rmi:" + rmiHost + "/" + url, uid, pwd); sentence = con.createStatement(); try { rs = sentence.executeQuery("SELECT * FROM books"); java.sql.ResultSetMetaData md = rs.getMetaData(); while(rs.next()) { status.append("\n | "); for(int i=1; i<= md.getColumnCount(); i++) { status.append(rs.getString(i) + " | "); } } status.append("\n"); } catch (SQLException e) {}; } catch (Exception e) { status.append("Error: " + e); return; } } // init }
- 請確定你已經把 RmiJdbc.jar 放置於網頁的同一個目錄下。
- compile 完以上的程式以後,建議利用 appletviewer 來測試。你可以 執行 appletviewer http://penguin.im.cyut.edu.tw/~jlu/rmi/rmijdbc.html。
- 網頁:使用 archive="RmiJdbc.jar" 的缺點是每一次這個網頁被呼叫
的時候,RmiJdbc.jar 就得被下載一下而浪費時間與頻寬。
- 網頁也可以不必加入 archive,不過這時候你必須確定客戶端有 RmiJdbc.jar
而且在啟動 appletviewer 或者 browser 的時候,他們能載入 RmiJdbc.jar。
- 將 RmiJdbc.jar 安裝于 JAVA_HOME/jre/lib/ext 目錄內,那麼 你就可以直接啟動 appletviewer。
- 將 RmiJdbc.jar 放置於某一特定目錄(例如 c:\jar),那麼你可以執行 appletviewer -J-cp -J.;c:\jar\RmiJdbc.jar rmijdbc.html。
- 至於 browser 的設定方式我還沒找到,有誰知道?
- 範例: 請試試看將 RMI and DB 那一節的範例改成利用 RmiJdbc 的
Serializable 的物件。
- Interface:
// // Create Interface // public interface QueryIF extends java.rmi.Remote { public java.sql.ResultSet getSalary(int salary) throws java.rmi.RemoteException; }
- QueryImpl:
// // Step 2. Create Interface Implementation // import java.rmi.*; import java.rmi.server.*; import java.sql.*; import java.util.*; import java.io.*; public class QueryImpl extends UnicastRemoteObject implements QueryIF { private String serviceName; private static Connection conn = null; private ResultSet rs = null; public QueryImpl(String s) throws RemoteException { super(); serviceName = s; } public java.sql.ResultSet getSalary(int salary) throws RemoteException { String qs = "select * from bookstores " + "where rank >= " + String.valueOf(salary); try { Statement st = conn.createStatement(); rs = st.executeQuery(qs); } catch (SQLException sqle) { System.out.println("QueryImpl SQLException " + sqle.getMessage()); System.exit(0); } catch (Exception e) { System.out.println("getQuery: QueryImpl Err: " + e.getMessage()); System.exit(1); } return rs; } public static void main( String argv[] ) { System.setSecurityManager(new RMISecurityManager()); // 把 connection 移到這哩,可以降低 connection 的數目 try { Class.forName("org.objectweb.rmijdbc.Driver").newInstance(); // 請將下列的 hostname, db_host:port, UID, PWD 作適當的改變 conn = DriverManager.getConnection( "jdbc:rmi://hostname:1099/" + "jdbc:microsoft:sqlserver://db_host:port", UID, PWD); QueryImpl obj = new QueryImpl("QueryService"); // 請將下列的 hostname 作適度的改變 Naming.rebind("//hostname/QueryService", obj); System.out.println("QueryService registered."); } catch (IOException e) { System.out.println("Load driver error."); System.exit(1); } catch (SQLException e) { System.out.println("DB connection error."); System.exit(2); } catch (Exception e) { System.out.println("main: QueryImpl Err: " + e.getMessage()); } } }
- QueryClient:
import java.rmi.*; import java.sql.*; import java.util.*; public class QueryClient { public static void main( String argv[] ) { ResultSet v = null; try { // 這裡的 hostname 應該改成和 QueryImpl.java 相同的 hostname QueryIF obj = (QueryIF) Naming.lookup( "rmi://hostname/QueryService"); v = obj.getSalary(10); StringBuffer sb = new StringBuffer(); while(v.next()) { for(int i=1; i<4 data-blogger-escaped-catch="catch" data-blogger-escaped-e.getmessage="e.getmessage" data-blogger-escaped-e="e" data-blogger-escaped-exception:="exception:" data-blogger-escaped-i="i" data-blogger-escaped-n="n" data-blogger-escaped-pre="pre" data-blogger-escaped-sb.append="sb.append" data-blogger-escaped-sb="sb" data-blogger-escaped-system.out.println="system.out.println" data-blogger-escaped-t="t" data-blogger-escaped-ueryservice="ueryservice" data-blogger-escaped-v.getstring="v.getstring" data-blogger-escaped-xception="xception">
- Interface:
RMI 的其他議題
- 你可能再學習的過程中會遇到一些問題而不知道怎麼辦,其實你在 初學上的問題非常有可能已經被問過了上千次,所以除了利用搜索引擎來 找尋解答之外,FAQ 也是一個很好的解答來源,RMI 有兩個不錯的 FAQ: 請注意,我們在這裡介紹的是屬於 RMI 1.2 版的溝通方式,這個方式 與 1.1 板有極大的差異。如果你有必要讓 Java Objects 在不同版本間 傳遞,你需要注意要一些相容性的問題。
- RMI 的學習可以讓我們慢慢了解 web services 的運作方式,而且因為
RMI 的簡潔性,讓開發人員的入門學習簡化很多。就算有這些優點,RMI 有沒有
比較重要的缺點?
- 只能傳送 Java Objects,那麼其他的語言所開發出來的 objects 便無法 交換。
- services 只能向同一部電腦上的 rmiregistry 註冊,這樣子大大的降低 的分散式處理的彈性。想想看,如果我們可以「services 向遠端的某一部電腦 註冊」、而且「registry 上找不到的 services 這個 registry 可以自動幫我們 去找到 services 所在的 registry 並把 services 的 reference 回傳」,這樣的 分散式架構不是很棒嗎?感覺上應該可行,可是為什麼不作呢?唉,一句話, 「security」。
- 現代化的資訊環境大多安裝有防火牆,RMI 的 client 和 service 是經由 動態產生的 port 來溝通,因此會被防火牆檔掉。目前有一些替代方案, 你可以查詢 FAQ 來找尋答案,我們不對這個問題作更深入的探討。
- 由於傳送的資料會被 marshalled,因此有時候在 debug 的過程很難了解 問題出在哪裡(想想看你如果面對的是一個 n-tier 的服務架構)。因此如果 傳送的資料是以純文字的方式傳送會更容易 debug。
- 針對 RMI 的問題,比較早期的替代方案有 CORBA 和 COM/DCOM,而目前有 web services。
Written by: 國立中興大學資管系呂瑞麟 Eric Jui-Lin Lu
沒有留言:
張貼留言