一、 前言
作为一种优秀的编程语言,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 将提供必要的机制,既可以加载对象的代码又可以传输对象的数据。服务器调用注册服务程序以使名字与远程对象相关联。客户机在服务器注册服务程序中用远程对象的名字查找该远程对象,然后调用它的方法。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 = "//"+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("//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窗口中