一、 前言
作為一種優秀的編程語言,Java在許多方面具有突出的優越性。其中,RMI技術充分展現了Java卓越的分布式計算能力,而JNI技術則體現了Java結合異種編程語言的強大能力。人們常說,RMI是“從Java到Java”,這種說法忽視了這樣一個事實:Java可利用JNI技術很容易地與原有系統連接。JNI+RMI的技術解決方案極大地延伸了Java的分布式功能。
本文的寫作是基于這樣一種實際需要:在實際運行環境當中,我們需要將一臺Linux網絡工作站產生的信息實時動態的顯示在遠程監控者的機器上,以便他及時對該Linux工作站上發生的情況進行處理。比如, Linux工作站上運行著網絡入侵檢測系統,檢測到的入侵信息必須實時地提供給遠程監控者,以便及時作出響應;再比如, Linux工作站上運行著網絡管理系統,產生的有關網絡流量和網絡性能的數據也必須及時提供給網絡管理人員。
而Linux平臺下的軟件,大多數是用C語言編寫的,這是一筆難以舍棄的財富。而且C語言與底層系統的緊密結合以及C的運行速度,是Java所不具備。這就為JNI+RMI技術提供了廣闊的舞臺。
二、 JNI技術
Java以其跨平臺的特性得到廣泛應用,其代碼可以一次編譯多處執行。但正是這種特性 給它帶來了一定的局限性,一些與平臺相關的功能就不能很好地支持。幸運的是Java提供了JNI技術——完備的C語言接口,讓我們可以利用C語言的強大功能來彌補Java的不足。很容易發現JNI在Java和本地應用程序之間起著膠水的作用。圖1將描述JNI是如何將應用程序的C語言部分和Java部分連接在一起的。圖1來源于Sun公司的Java指南。
三、 RMI技術
1、 運行原理
RMI 應用程序通常包括兩個獨立的程序:服務器程序和客戶機程序。典型的服務器應用程序將創建多個遠程對象,使這些遠程對象能夠被引用,然后等待客戶機調用那些遠程對象上的方法。而典型的客戶機程序則從服務器中得到一個或多個遠程對象的引用,然后調用遠程對象的方法。RMI 為服務器和客戶機進行通訊和信息傳遞提供了一種機制。這樣的應用程序有時被稱為分布式對象應用程序。分布式對象應用程序需要:(1)定位遠程對象。它既可用 RMI 的簡單命名工具 rmiregistry 來注冊它的遠程對象,也可將遠程對象引用作為常規操作的一部分來進行傳遞和返回。(2)與遠程對象通訊。遠程對象間通訊的細節由 RMI 處理,對于程序員來說,遠程通訊看起來就象標準的 Java 方法調用。(3)給作為參數或返回值傳遞的對象加載類字節碼。因為 RMI 允許調用程序將純 Java 對象傳給遠程對象,所以 RMI 將提供必要的機制,既可以加載對象的代碼又可以傳輸對象的數據。服務器調用注冊服務程序以使名字與遠程對象相關聯??蛻魴C在服務器注冊服務程序中用遠程對象的名字查找該遠程對象,然后調用它的方法。RMI 能用 Java系統支持的任何 URL 協議(例如 HTTP、FTP、file 等)加載類字節碼。
下圖描繪了一個RMI分布式應用程序 ,它通過registry得到一個遠程對象的引用。服務器調用registry將遠程對象連接(或者綁定)到一個名字上??蛻舳嗽诜掌鞯?registry上根據那個名字查找遠程對象,并且調用名字上的一個方法。圖2同時顯示出每當需要時,RMI系統使用一個Web服務器來裝載類字節碼,從服務器到客戶端并且從顧客到服務器。圖2來源于Sun公司的Java指南。
2、 雙向RMI技術
前面講到通常情況下在RMI應用程序中,是服務器程序提供遠程方法給客戶機程序調用。但是某種情況下,RMI應用程序同時也需要客戶機程序提供遠程方法給服務器程序調用。也就是說,某種情況下,RMI應用程序的兩個部分同時具有服務器和客戶機的功能。
比如,你的遠程客戶希望可以通過瀏覽器實時查看Linux平臺下程序運行的產生的信息。那么從技術上說:遠程客戶機器上運行的Applet程序是RMI服務器程序,而Linux平臺下程序是RMI客戶機程序。Linux平臺下程序通過RMI調用Applet程序的相關方法,將信息實時推送到遠程客戶機器。這是一個非常典型的RMI應用。
但是這里出現了一個問題:遠程客戶機器的IP地址沒有辦法確定,遠程客戶應該可以從任何連接到Internet的機器上訪問到這些信息。如果不知道IP地址,Linux平臺下程序不可能通過URL訪問遠程Applet程序中的RMI方法。
因此這里必須使用雙向RMI技術。首先由遠程Applet程序通過URL調用Linux平臺下程序的RMI方法,建立起RMI連接。然后,再由Linux平臺下程序調用遠程Applet程序中RMI來推送信息。
四、 程序實例
關于Linux平臺下C程序的輸出,可以是網絡入侵檢測系統,網絡管理系統,或者其他系統。這里為了簡化程序,用一個簡單的C程序代替。該程序將你在Linux平臺下輸入的字符串,回顯到遠端客戶瀏覽器上。
1、 編寫Java文件CreatMessage.java
里面包含一些native的函數,這些函數就是將在C中要實現的。另外程序將聲明一個調用RMI方法的Java私有方法,該方法將在C程序中被調用。源程序如下:
class CreatMessage{
MessageServerImpl ms;
//聲明一個本地方法接口函數
public native void creatmsa();
//聲明一個Java方法,該方法將在C程序中被調用
private void pushMessage(String message){
//新建一個RMI遠程對象,并對其賦值
Message msa = new Message();
msa.MessageString = message;
// notifyEvent方法將調用RMI方法notifiedEvent(Message msa)
try{
ms.notifyEvent(msa);
} catch(Exception e){
System.out.println("notifyEvent Exception:"+e.getMessage());
}//catch end
}
//構造函數負責傳遞MessageServerImpl對象的實例
public CreatMessage(MessageServerImpl msserver){
ms = msserver;
}
}
2、 編譯CreatMessage.java文件,生成C語言頭文件CreatMessage.h
用Javac CreatMessage.java命令編譯Java源文件,生成CreatMessage.class,再用Javah CreatMessage命令生成C語言頭文件CreatMessage.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class CreatMessage */
#ifndef _Included_CreatMessage
#define _Included_CreatMessage
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: CreatMessage
* Method: creatmsa
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_CreatMessage_creatmsa
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
這是由javah命令生成的文件,不需要也不允許進行任何修改。我們注意到這里有一段程序 “JNIEXPORT void JNICALL Java_CreatMessage_creatmsa (JNIEnv *, jobject);”。Java_CreatMessage_creatmsa 提供了CreatMessage.class中本地方法的具體實現,當你編寫本地方法的實現程序時,你將使用相同的方法簽名。如果CreatMessage.class含有其他的本地方法,這些方法的簽名同樣會在頭文件中出現。
3、 編寫creatmsa.c,實現本地方法
首先我們將如何得到Java方法的簽名呢?有兩種方法,一種是依據生成規則手工推算,另外Java 類文件反匯編程序工具javap ,可以幫助你消除手工推算方法簽名時發生的錯誤。你能使用javap工具為指定的類打印出成員變量和方法簽名:javap -s -p CreatMessage。
要在C程序中調用Java方法,需要完成下面三個步驟:
(1) 在C程序中調用JNI方法GetObjectClass,它將返回該Java對象的Java類對象。
(2) 接著調用JNI方法GetMethodID,它將在一個給定的類文件中查找Java方法。該查找是基于方法名字以及方法簽名。如果該方法不存在,GetMethodID將返回0。程序將立即返回,并拋出NoSuchMethodError。
(3) 將調用JNI方法CallVoidMethod。該方法將激活一個返回值為void的實例方法。你必須將object, method ID以及該方法的參數傳遞給CallVoidMethod。
creatmsa.c源程序如下:
#include <stdio.h>
#include <jni.h>
#include "CreatMessage.h"
static jclass cls ;
static jmethodID mid ;
JNIEXPORT void JNICALL Java_CreatMessage_creatmsa(JNIEnv *env, jobject obj)
{
char buff[128];
jstring message;
//將用戶輸入字符串,賦值給buff
scanf("%s", buff);
//將buff的值轉換成UTF格式,并賦給message
message = (*env)->NewStringUTF(env,buff);
cls = (*env)->GetObjectClass(env, obj);
mid = (*env)->GetMethodID(env, cls, "pushMessage", "(Ljava/lang/String;)V");
if (mid == 0){
printf("GetMethodID error");
return;
}
(*env)->CallVoidMethod(env, obj, mid, message);
}
4、 編譯creatmsa.c,生成libcreatmsa.so
在Linux平臺下編譯creatmsa.c,要格外注意路徑問題。最經常出現的錯誤就是因為路徑設置不對,而引起的無法找到編譯所需要的文件。本文中使用jdk1.3.1的目錄為/url/local/j2sdk1.3.1,所有例子程序都存放在/rmitest目錄下。因此本文路徑設置為:
classpath = /rmitest:
/url/local/j2sdk1.3.1/include:
/url/local/j2sdk1.3.1/jre/lib/i386
export classpath
LD_LIBRARY_PATH = /rmitest:
/url/local/j2sdk1.3.1/jre/lib/i386:
/url/local/j2sdk1.3.1/jre/lib/i386/native_threads:
/url/local/j2sdk1.3.1/jre/lib/i386/classic:
$LD_LIBRARY_PATH
export LD_LIBRARY_PATH
設置完路徑以后,就可以開始編譯程序了,使用命令:
gcc -I//url/local/j2sdk1.3.1/include
-I//url/local/j2sdk1.3.1/include/linux
-shared
creatmsa.c -o libcreatmsa.so
5、 Message.java
在RMI分布式應用系統中,服務器與客戶機之間傳遞的Java對象必須是可序列化的對象。不可序列化的對象不能在對象流中進行傳遞。因此,我們必須生成一個Java類以傳遞參數。這個類定義的很簡單,在實際運用中,可以根據需要增加內容。
import java.io.Serializable;
import java.io.*;
public class Message implements Serializable
{
String MessageString;
}
6、 MessageServer.java
定義兩個RMI接口。它們將在MessageServerImpl.java程序中實現。
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface MessageServer extends Remote {
void newClient(MessageClient mc) throws RemoteException;
void removeClient(MessageClient mc) throws RemoteException;
}
7、 MessageServerImpl.java
實現在MessageServer.java程序中定義的RMI接口。
import java.rmi.*;
import java.rmi.server.*;
import java.util.Vector;
public class MessageServerImpl extends UnicastRemoteObject implements MessageServer
{
Vector clients=new Vector();
public MessageServerImpl() throws RemoteException {
super();
}
public void newClient(MessageClient mc) throws RemoteException {
clients.addElement(mc);
}
public void removeClient(MessageClient mc) throws RemoteException {
clients.removeElement(mc);
}
public void notifyEvent(Message msa) throws RemoteException {
Vector tc=(Vector)clients.clone();
for (int i=0;i<tc.size();i++){
try {
((MessageClient)(tc.elementAt(i))).notifiedEvent(msa);
} catch(Exception e){
removeClient((MessageClient)(tc.elementAt(i)));
}
}
}
}
8、 MessageClient.java
定義兩個RMI接口。它們將在MessageClientImpl.java程序中實現。
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface MessageClient extends Remote {
void notifiedEvent(Message msa) throws RemoteException;
}
9、 MessageClientImpl.java
實現在MessageClient.java程序中定義的RMI接口。
import java.rmi.*;
import java.rmi.server.*;
import javax.swing.*;
import java.awt.*;
public class MessageClientImpl extends UnicastRemoteObject implements MessageClient
{
JTextArea jt;
static String ipAddr="255.255.255.255";
public void notifiedEvent(Message msa) throws RemoteException {
jt.insert(msa.MessageString+"\n",0);
}
public MessageClientImpl(JTextArea jtext) throws RemoteException {
super();//調用其父類的構造函數UnicastRemoteObject
jt=jtext;
}
public void init() throws RemoteException{
try {
String name = "http://"+ipAddr+":1099/MessageServer";
MessageServer fs = (MessageServer)Naming.lookup(name);
fs.newClient(this);
} catch(Exception e){
System.err.println("MessageClient exception: " + e.getMessage());
}
}
public void setIPaddr(String str)
{
ipAddr = str;
}
}
10、 編譯生成Stub文件
利用命令rmic -v1.2 MessageClientImpl和命令rmic -v1.2 MessageServerImpl
分別生成MessageClientImpl_Stub.class文件和MessageServerImpl_Stub.class文件
11、 StartServer.java
編寫StartServer.java文件,初始化RMI運行環境如啟動rmigegistry,設置RMISecurityManager,綁定RMI對象等等。并加栽C程序生成的動態連接庫文件libcreatmsa.so。
import java.rmi.*;
import java.rmi.RemoteException;
import java.rmi.RMISecurityManager;
import java.rmi.registry.LocateRegistry;
public class StartServer{
static {
System.loadLibrary("creatmsa");/*actual name is "libcreatmsa.so"*/
}
public static void main(String args[])
{
if(System.getSecurityManager() == null)
System.setSecurityManager(new RMISecurityManager());
try{
LocateRegistry.createRegistry(1099);
//注冊IDS處理服務器
MessageServerImpl msObj = new MessageServerImpl();
Naming.rebind("http://127.0.0.1:1099/MessageServer",msObj);
//啟動c程序
CreatMessage cm = new CreatMessage(msObj);
cm.creatmsa();
} catch (Exception e){
System.out.println("registe error: " + e.getMessage());
}
}
}
12、 MessageEcho.java
編寫遠程客戶端的Applet程序,調用RMI方法,與服務器建立連接。并一個Panel上顯示Linux平臺下程序產生的數據。
import java.awt.*;
import java.applet.Applet;
import javax.swing.*;
import java.rmi.RMISecurityManager;
public class MessageEcho extends JApplet{
Container contentPane = getContentPane();
JPanel panel = new JPanel(new BorderLayout());
JPanel alarmPanel;//顯示Linux平臺下程序產生的數據的模板
//顯示Linux平臺下程序產生的數據的區域
final JTextArea alarmList=new JTextArea(8,40);
//運行Linux平臺下程序的機器的IP地址
String ipServer;
public void init() {
ipServer = getParameter("AppServer"); //此項在test.html中賦值
alarmPanel=new JPanel();
alarmList.insert("\nSome message here"+"\n",0);
alarmPanel.setLayout(null);
alarmList.setLineWrap(true);
alarmList.setWrapStyleWord(true);
JScrollPane areaScrollPane = new JScrollPane(alarmList);
areaScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
areaScrollPane.setPreferredSize(new Dimension(200, 120));
alarmPanel.add(areaScrollPane);
areaScrollPane.setBounds(0,40,200,120);
panel.add(alarmPanel,BorderLayout.CENTER);
contentPane.add(panel);
try{
MessageClientImpl mci=new MessageClientImpl(alarmList);
mci.setIPaddr(ipServer);
mci.init();
} catch (Exception e) {
System.err.println("alarmListener exception: " + e.getMessage());
e.printStackTrace();
}
}
}
13、 Test.html
在HTML文件中調用Applet的類文件。其中AppServer是Linux工作站的IP地址。
<HTML>
<BODY>
<COMMENT>
<EMBED width="750" height="400"
align="baseline" code="MessageEcho.class" codebase="."
AppServer="202.114.33.87" ><!--應用服務器地址,即Linux工作站的IP地址-->
</EMBED>
</BODY>
</HTML>
14、 修改java.policy文件
為了允許客戶程序同RMI注冊程序和服務器對象連接,你需要提供一個策略文件(policy file)。策略文件是非常復雜的問題。這里只提供一個修改的樣本,它賦予程序絕大多數的操作權限。這樣使你調試類似程序時候非常方便,但是也請注意這種做法給你的計算機帶來的潛在危險。希望進一步了解安全策略文件的讀者可以看看參考文獻8。
java.policy文件內容如下:
grant {
permission java.security.AllPermission;
};
因為本文中使用的是雙向RMI,所以你必須同時修改兩臺機器的策略文件。在RedHat7.1中需要將java.policy文件拷貝到/url/local/j2sdk1.3.1/jre/lib/security目錄,在Windows2000中是c:\j2sdk1.3.1\jre\lib\security目錄。當然你需要根據你機器上的java目錄做相應調整。
五、 實現環境以及運行步驟
一臺機器操作系統為RedHat7.1,文件有:MessageServer.class,MessageServerImpl.class,MessageServerImpl_Stub.class,Message.class,MessageClient.class,MessageClientImpl_Stub.class,libcreatmsa.so,所有文件均在同一目錄下面。
一臺機器操作系統為Windows2000,文件有:MessageClient.class,MessageClientImpl.class,MessageClientImpl_Stub.class,Message.class,MessageServer.class,MessageServerImpl_Stub.class,MessageEcho.class,test.html,所有文件均在同一目錄下面。
1、 首先修改兩臺機器上的java.policy文件;
2、 然后在RedHat7.1的機器上設置路徑,即運行上文提到的相應命令;
3、 然后在RedHat7.1的機器上運行命令:java StartServer;
4、 然后在Windows2000的機器上運行命令:appletviewer test.html;
5、 接著在RedHat7.1的機器上隨意輸入一字符傳后敲回車鍵,該字符串將回顯在Windows2000機器的Appletviewer窗口中