Skip to content

9. JAVA RMI

9.1. 引言

我们已经了解了如何使用名为 sockets 的通信工具来创建网络应用程序。在基于这些工具构建的客户端/服务器应用程序中,客户端与服务器之间的连接就是它们为进行通信而采用的通信协议。 这两个应用程序可以使用不同的语言编写:例如客户端使用 Java,服务器使用 Perl,或者任何其他组合。 这确实是两个通过双方共同了解的通信协议连接的独立应用程序。此外,对于 Java 应用程序而言,通过套接字访问网络并非透明操作:它必须使用 Socket 类,该类是专门为管理 sockets 这类通信工具而创建的。

JAVA RMI(远程方法调用)可用于创建具有以下特征的网络应用程序:

  1. 客户端/服务器应用程序是指通信两端均为Java应用程序的系统
  2. 客户端可以像使用本地对象一样使用服务器上的对象
  3. 网络层变得透明:应用程序无需关心信息如何从一点传输到另一点。

最后一点是可移植性的关键:如果 RMI 应用程序的网络层发生变更,应用程序本身无需重写。只需将 Java 语言中的 RMI 类适配到新的网络层即可。

RMI通信的基本原理如下:

  1. 在机器A上编写一个经典的Java应用程序。该应用程序将充当服务器。为此,其部分对象将在应用程序运行的机器A上进行“发布”,从而成为服务。
  2. 在机器B上编写了一个经典的Java应用程序,该应用程序将充当客户端。它将能够访问在机器A上发布的对象/服务,也就是说,通过远程引用,它能够像操作本地对象一样操作这些对象。为此,它需要了解其想要访问的远程对象的结构(方法和属性)。

9.2. 通过实例学习

RMI接口背后的理论并不简单。为了更清晰地理解,我们将逐步演示如何编写一个使用Java的RMI包的客户端/服务器应用程序。 我们选取一个在许多关于 RMI 的书籍中常见的应用程序:客户端调用远程对象的一个方法,该方法随后向其返回一个字符串。这里我们介绍一个微小的变体:服务器将客户端发送的内容原样回显。 本书此前已介绍过此类基于套接字的应用程序。

9.2.1. 服务器应用程序

9.2.1.1. 步骤 1:对象/服务器的接口

远程对象是类的一个实例,该类必须实现 Remote 包中定义的 java.rmi 接口。可远程访问的对象方法是那些在 Remote 接口派生接口中声明的方法:

import java.rmi.*;

 // 远程接口 public interf
ace interEcho extends Remote{ public Stri
    ng echo(String msg) throws java.rmi.RemoteException; }

因此,此处声明了一个名为 interEcho 的接口,该接口将方法 echo 声明为可远程访问。该方法可能会抛出 RemoteException 类的异常,该类汇总了所有与网络相关的错误。

9.2.1.2. 步骤 2:编写服务器对象

在接下来的步骤中,我们将定义实现前述远程接口的类。该类必须继承自 UnicastRemoteObject 类,该类提供了支持远程方法调用的方法。

import java.rmi.*;
import java.rmi.server.*;
import java.net.*;

// 实现远程回显的类
public class srvEcho extends UnicastRemoteObject implements interEcho{

    // 构造函数
    public srvEcho() throws RemoteException{
        super();
    }// 构造函数结束

    // 实现回显的方法
    public String echo(String msg) throws RemoteException{
        return  "["  + msg + "]";
    }// 回显结束
}// 类结束

在上述类中,我们发现:

  1. 用于回显的方法
  2. 一个构造函数,其作用仅是调用父类的构造函数。该构造函数的存在是为了声明它可能抛出类型为 RemoteException 的异常。

我们将通过 main 方法创建该类的实例。要使对象/服务对外可访问,必须先创建该对象并将其注册到外部可访问对象目录中。客户端若要访问远程对象,其操作流程如下:

  1. 它会向目标对象所在机器的目录服务发起请求。该目录服务运行在客户端必须知道的端口上(默认端口为1099)。 客户端向目录服务请求指定名称的对象/服务的引用。如果该名称属于目录中的对象/服务,目录服务将向客户端返回一个引用,客户端可通过该引用与远程对象/服务进行交互。
  2. 从这一刻起,客户端即可像操作本地对象一样使用该远程对象

回到我们的服务器,我们需要创建一个类型为 srvEcho 的对象,并将其注册到外部可访问对象目录中。此注册操作通过 Naming 类的 rebind 方法完成:

Naming.rebind(String nom, Remote obj)

其中

名称:将与远程对象关联的名称

obj:远程对象

因此,我们的 srvEcho 类变为如下形式:

import java.rmi.*;
import java.rmi.server.*;
import java.net.*;

// 实现远程回显的类
public class srvEcho extends UnicastRemoteObject implements interEcho{

    // 构造函数
    public srvEcho() throws RemoteException{
        super();
    }// 构造函数结束

    // 实现回显的方法
    public String echo(String msg) throws RemoteException{
        return  "["  + msg + "]";
    }// 回显结束

    // 服务创建
    public static void main (String arg[]){
        try{
            srvEcho serveurEcho=new srvEcho();
            Naming.rebind("srvEcho",serveurEcho);
            System.out.println("Serveur d’écho prêt");
        } catch (Exception e){
            System.err.println(" Erreur "  + e + "  lors du lancement du serveur d’écho ");
        }
    }// main
}// 类结束

阅读前面的程序时,可能会觉得它在创建并注册回显服务后会立即停止。 事实并非如此。由于类 srvEcho 继承自类 UnicastRemoteObject,因此创建的对象将无限期运行:它会在一个匿名端口上监听客户端的请求,即由系统根据具体情况自动选择的端口。 服务创建是异步的:在示例中,方法 main 创建服务后继续执行:它确实会显示“回显服务器已就绪”。

9.2.1.3. 步骤 3:编译服务器应用程序

到目前为止,我们可以编译我们的服务器了。我们将接口 interEcho 对应的文件 interEcho.java 以及类 srvEcho 对应的文件 srvEcho.java 进行编译。 我们得到了相应的 .class 文件:interEcho.classsrvEcho.class

9.2.1.4. 步骤 4:编写客户端

编写一个客户端程序,向其传递回显服务器的 URL 作为参数,该程序

  1. 读取键盘输入的一行内容
  2. 将其发送至回显服务器
  3. 显示服务器返回的响应
  4. 循环回到步骤1,并在输入“fin”时停止。

由此得到以下客户端代码:

import java.rmi.*;
 import java.io.*

; public class cltEch

    o { public static void main(String a
        rg[]){ // 语法:cltEcho URLService

        // 参数验证
        if(arg.length!=1){
            System.err.println("Syntaxe : pg url_service_rmi");
            System.exit(1);
        }

        // 客户端-服务器对话
        String urlService=arg[0];
        BufferedReader in=null;
        String msg=null;
        String reponse=null;
        interEcho serveur=null;

        try{
            // 打开键盘流
            in=new BufferedReader(new InputStreamReader(System.in));
            // 服务定位
            serveur=(interEcho) Naming.lookup(urlService);                
            // 读取待发送至回显服务器的消息循环
            System.out.print("Message : ");
            msg=in.readLine().toLowerCase().trim();
            while(! msg.equals("fin")){
                // 向服务器发送消息并接收响应
                reponse=serveur.echo(msg);
                // 后续处理
                System.out.println("Réponse serveur : " + reponse);
                // 下一条消息
                System.out.print("Message : ");                
                msg=in.readLine().toLowerCase().trim();
            }// while
            // 结束
            System.exit(0);
        // 错误处理        
        } catch (Exception e){
            System.err.println("Erreur : " + e);
            System.exit(2);
        }// try
    }// main
}// 类                

该客户端没有什么特别之处,除了那条请求服务器参考信息的指令:

            serveur=(interEcho) Naming.lookup(urlService);

我们记得,我们的回显服务已通过以下指令注册到所在机器的服务目录中:


            Naming.rebind("srvEcho",serveurEcho);

因此,该客户端同样使用 Naming 类的某个方法来获取其想要使用的服务器的引用。所使用的 lookup 方法接受请求服务的 URL 作为参数。该 URL 采用标准格式:

    rmi://机器:端口/nom_service

其中

rmi:可选 - RMI协议

machine:运行回显服务器的机器名称或 IP 地址——可选,默认值为 localhost

port:该机器目录服务的监听端口 - 可选,默认值为 1099

nom_service:所请求服务的注册名称(本例中为 srvEcho

获取到的是一份远程接口 interEcho 的实例。假设客户端和服务器不在同一台机器上,在编译客户端 cltEcho.java 时, 必须在同一目录下放置文件 interEcho.class(即远程接口 interEcho 的编译结果),否则在引用该接口的代码行上会报编译错误。

9.2.1.5. 步骤 5:生成客户端-服务器应用程序所需的 .class 文件

为了清楚区分服务器端和客户端的内容,我们将服务器放置在目录 echo\serveur 中,客户端放置在目录 echo\client 中。

服务器目录包含以下源文件:

E:\data\java\RMI\echo\serveur>dir *.java

INTERE~1 JAV           158  09/03/99  15:06 interEcho.java
SRVECH~1 JAV           759  09/03/99  15:07 srvEcho.java

编译完这两个源文件后,得到以下 .class 文件:

E:\data\java\RMI\echo\serveur>dir *.class

SRVECH~1 CLA         1 129  09/03/99  15:58 srvEcho.class
INTERE~1 CLA           256  09/03/99  15:58 interEcho.class

在客户端目录中,可以找到以下源文件:

E:\data\java\RMI\echo\client>dir *.java

CLTECH~1 JAV         1 427  09/03/99  16:08 cltEcho.java

以及在服务器编译时生成的文件 interEcho.class

E:\data\java\RMI\echo\client>dir *.class

INTERE~1 CLA           256  09/03/99  15:59 interEcho.class

源文件编译完成后,会生成以下 .class 文件:

E:\data\java\RMI\echo\client>dir *.class

CLTECH~1 CLA         1 506  09/03/99  16:08 cltEcho.class
INTERE~1 CLA           256  09/03/99  15:59 interEcho.class

如果尝试运行客户端 cltEcho,将出现以下错误:

E:\data\java\RMI\echo\client>j:\jdk12\bin\java cltEcho rmi://localhost/srvEcho
Erreur : java.rmi.UnmarshalException: error unmarshalling return; nested exception is:
        java.lang.ClassNotFoundException: srvEcho_Stub

如果尝试运行服务器 srvEcho,会出现以下错误:

E:\data\java\RMI\echo\serveur>j:\jdk12\bin\java srvEcho
Erreur java.rmi.StubNotFoundException: Stub class not found: srvEcho_Stub; nested exception is:
        java.lang.ClassNotFoundException: srvEcho_Stub  lors du lancement du serveur d’écho

在这两种情况下,Java虚拟机都提示未找到类srvEcho_stub。事实上,我们从未听说过这个类。在客户端,服务器的定位是通过以下语句完成的:

            serveur=(interEcho) Naming.lookup(urlService);                

此处,urlservice 即字符串 rmi://localhost/srvEcho,其中

Rmi:RMI协议

Localhost:服务器运行的机器——此处即客户端所在的同一台机器。语法通常为 machine:port。若未指定端口,则默认使用端口 1099。监听该端口的是服务器的目录服务。

srvEcho:这是所请求的特定服务名称

编译时未报告任何错误。只需确保远程接口中的文件 interEcho.class 可用即可。

在运行时,如果请求的服务是 srvEcho,虚拟机将要求存在 srvEcho_stub.class 文件;一般而言,对于 X 服务,则需要 X_stub.class 文件。 该文件仅在运行时需要,而非客户端编译时。服务器端的情况也是如此。那么,这个文件究竟是什么呢?

在服务器端,存在名为 srvEcho.class 的类,这就是我们的远程对象/服务。 客户端即使不需要该类,仍需某种形式的本地映像才能与其通信。实际上,客户端并非直接向远程对象发送请求,而是将其请求发送至位于同一台机器上的本地映像 srvEcho_stub.class。 该本地映像 srvEcho_stub.class 与位于服务器上的同类映像(srvEcho_stub.class)进行交互。 该映像是通过名为 rmic 的 Java 工具,基于服务器上的 .class 文件生成的。在 Windows 系统中,命令为:

E:\data\java\RMI\echo\serveur>j:\jdk12\bin\rmic srvEcho

将基于文件 srvEcho.class 生成另外两个文件 .class

E:\data\java\RMI\echo\serveur>dir *.class

SRVECH~2 CLA         3 264  09/03/99  16:57 srvEcho_Stub.class
SRVECH~3 CLA         1 736  09/03/99  16:57 srvEcho_Skel.class

这里确实有客户端和服务器端在运行时所需的 srvEcho_stub.class 文件。此外还有一个 srvEcho_Skel.class 文件,目前尚不清楚其作用。 我们将文件 srvEcho_stub.class 复制到客户端和服务器端目录中,并删除文件 srvEcho_Skel.class。因此,我们现在拥有以下文件:

服务器端:

E:\data\java\RMI\echo\serveur>dir *.class

SRVECH~1 CLA         1 129  09/03/99  15:58 srvEcho.class
INTERE~1 CLA           256  09/03/99  15:58 interEcho.class
SRVECH~1 CLA         3 264  09/03/99  16:01 srvEcho_Stub.class

客户端:

E:\data\java\RMI\echo\client>dir *.class

CLTECH~1 CLA         1 506  09/03/99  16:08 cltEcho.class
INTERE~1 CLA           256  09/03/99  15:59 interEcho.class
SRVECH~1 CLA         3 264  09/03/99  16:01 srvEcho_Stub.class

9.2.1.6. 步骤 6:运行回显客户端-服务器应用程序

我们已准备好运行我们的客户端-服务器应用程序。起初,客户端和服务器将在同一台机器上运行。首先需要启动我们的服务器应用程序。请记住,该应用程序:

  • 创建服务
  • 将其注册到回显服务器所在机器的服务目录中

最后一点需要目录服务。该服务通过以下命令启动:

start j:\jdk12\bin\rmiregistry

rmiregistry 是目录服务。此处通过 start 命令在 Windows 的 DOS 窗口中将其作为后台任务启动。目录服务启动后,即可创建回显服务并将其注册到服务目录中。同样,它也是通过 start 命令作为后台任务启动的:

E:\data\java\RMI\echo\serveur>start j:\jdk12\bin\java srvEcho

回显服务器在新的 DOS 窗口中运行,并按要求显示如下内容:

Serveur d’écho prêt

接下来,我们只需启动并测试我们的客户端:

E:\data\java\RMI\echo\client>j:\jdk12\bin\java cltEcho rmi://localhost/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin

9.2.1.7. 客户端和服务器位于两台不同的机器上

在前面的示例中,客户端和服务器位于同一台机器上。现在我们将它们分别部署在不同的机器上:

  • 服务器位于一台Windows机器上
  • 客户端位于一台 Linux 机器上

服务器与之前一样在Windows机器上启动。在Linux机器上,我们已将客户端的.class文件传输过去:

shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir
total 9
drwxr-xr-x   2 serge    admin        1024 Mar 10 10:02 .
drwxr-xr-x   4 serge    admin        1024 Mar 10 10:01 ..
-rw-r--r--   1 serge    admin        1506 Mar 10 10:02 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class
-rw-r--r--   1 serge    admin        3264 Mar 10 10:02 srvEcho_Stub.class

客户端已启动:

$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : msg1
Erreur : java.rmi.ServerException: RemoteException occurred in server thread; nested exception is: 
      java.rmi.UnmarshalException: error unmarshalling call header; nested exception is: 
      java.rmi.UnmarshalException: skeleton class not found but required for client version

因此出现了一个错误:Java虚拟机似乎正在请求文件srvEcho_skel.class,该文件是由工具rmic生成的,但此前尚未被使用。我们重新创建该文件,并将其传输到Linux机器上:

shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir
total 11
drwxr-xr-x   2 serge    admin        1024 Mar 10 10:17 .
drwxr-xr-x   4 serge    admin        1024 Mar 10 10:01 ..
-rw-r--r--   1 serge    admin        1506 Mar 10 10:02 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class
-rw-r--r--   1 serge    admin        1736 Mar 10 10:17 srvEcho_Skel.class
-rw-r--r--   1 serge    admin        3264 Mar 10 10:02 srvEcho_Stub.class

出现了和之前一样的错误……于是我们仔细思考,并重新阅读了关于 RMI 的文档。最终我们推测,也许是服务器本身需要那个著名的 srvEcho_Skel.class 文件。 于是,我们在Windows机器上重新启动了服务器,确保srvEcho_Stub.classsrvEcho_Skel.class这两个文件已存在,同时:

E:\data\java\RMI\echo\serveur>dir *.class

SRVECH~1 CLA         1 129  09/03/99  15:58 srvEcho.class
INTERE~1 CLA           256  09/03/99  15:58 interEcho.class
SRVECH~2 CLA         3 264  10/03/99   9:05 srvEcho_Stub.class
SRVECH~3 CLA         1 736  10/03/99   9:05 srvEcho_Skel.class

E:\data\java\RMI\echo\serveur>start j:\jdk12\bin\java srvEcho

然后在Linux机器上,我们再次测试客户端,这次成功了:

shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r--   1 serge    admin        1506 Mar 10 10:02 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class
-rw-r--r--   1 serge    admin        3264 Mar 10 10:02 srvEcho_Stub.class

shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin

由此可推断,服务器端必须存在两个文件:srvEcho_Stub.classsrvEcho_Skel.class。 在客户端,迄今为止仅需 srvEcho_Stub.class 文件。当客户端和服务器位于同一台 Windows 机器上时,该文件被证明是必不可少的。在 Linux 环境下,我们将其移除以进行测试……

shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r--   1 serge    admin        1506 Mar 10 10:02 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class

shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
*** Security Exception: No security manager, stub class loader disabled ***
java.rmi.RMISecurityException: security.No security manager, stub class loader disabled
        at sun.rmi.server.RMIClassLoader.getClassLoader(RMIClassLoader.java:84)
        at sun.rmi.server.MarshalInputStream.resolveClass(MarshalInputStream.java:88)
        at java.io.ObjectInputStream.inputClassDescriptor(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.inputObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:105)
        at java.rmi.Naming.lookup(Naming.java:60)
        at cltEcho.main(cltEcho.java:28)
Erreur : java.rmi.UnexpectedException: Unexpected exception; nested exception is: 
        java.rmi.RMISecurityException: security.No security manager, stub class loader disabled

这里出现了一个有趣的错误,似乎表明Java虚拟机试图加载那个著名的stub类,但因缺少“安全管理器”而失败。我们记得在文档中看到过相关内容。 我们重新查阅了文档……发现服务器必须创建并安装一个安全管理器(security manager),以确保向客户端保证所请求加载的类是安全的。如果没有这个安全管理器,就无法加载这些类。 这似乎说得通:我们的Linux客户端向服务器请求了所需的srvEcho_stub.class类,但服务器以“未安装任何安全管理器”为由拒绝了该请求。因此,我们将服务器端main函数的代码修改如下:

     // 服务创建 public stati
    c void main (String arg[]){

         // 安装安全管理器 System.setSecurityManager(new
         RMISecurityManager()); 

        // 启动并注册服务
        try{
            srvEcho serveurEcho=new srvEcho();
            Naming.rebind("srvEcho",serveurEcho);
            System.out.println("Serveur d’écho prêt");
        } catch (Exception e){
            System.err.println(" Erreur "  + e + "  lors du lancement du serveur d’écho ");
        }
    }// 主

使用工具 rmic 进行编译,生成文件 srvEcho_stub.classsrvEcho_Skel.class。 启动目录服务(rmiregistry)后启动服务器,却出现了一个以前从未出现过的错误!

Erreur java.security.AccessControlException: access denied (java.net.SocketPermission 127.0.0.1:1099 connect,resolve)  lors du lancement du serveur d’écho

看来安全管理器的工作做得太过彻底了。我们重新查阅了文档……发现当安全管理器处于活动状态时,在启动程序时必须明确其权限。这可以通过以下选项实现:

start j:\jdk12\bin\java -Djava.security.policy=mypolicy srvEcho

其中

java.security.policy 是一个关键字

mypolicy 是一个定义程序权限的文本文件。此处的文件内容如下:

grant {
    // 暂时允许所有操作
    permission java.security.AllPermission;
};

该程序在此拥有所有权限。

重新开始。进入服务器目录,依次执行:

  • 启动目录服务:start j:\jdk12\bin\rmiregistry
  • 启动服务器: start j:\jdk12\bin\java -Djava.security.policy=mypolicy srvEcho

这次,回显服务器(客户端尚未启动)成功启动。现在您可以进行以下实验:

  • 先停止回显服务器,然后停止目录服务
  • 在与服务器目录不同的目录下重新启动目录服务
  • 返回服务器所在的目录启动回显服务器——您将收到以下错误:
Erreur java.rmi.ServerException: RemoteException occurred in server thread; nes
ted exception is:
java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is:
java.lang.ClassNotFoundException: srvEcho_Stub  lors du lancement du serveur d’écho

由此可知,目录服务启动的目录位置至关重要。在此情况下,Java 未能找到 srvEcho_stub.class,是因为目录服务并非从服务器目录中启动的。在启动服务器时,可以指定服务器所需类所在的目录:

start j:\jdk12\bin\java 
-Djava.security.policy=mypolicy 
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/ 
srvEcho

该命令应写在单行中。关键字 java.rmi.server.codebase 用于指定包含服务器所需类的目录路径。 此处的 URL 指定了 file 协议(即访问本地文件的协议)以及服务器上包含 .class 文件的目录。因此,如果按以下步骤操作:

  • 停止目录服务
  • 从服务器目录以外的其他目录重新启动目录服务
  • 在服务器目录中,使用以下命令(仅一行)启动该服务:
 start j:\jdk12\bin\java 
-Djava.security.policy=mypolicy 
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/ 
srvEcho

此时服务器已成功启动。接下来可以转到客户端。进行测试:

shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r--   1 serge    admin        1506 Mar 10 14:28 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class

shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
*** Security Exception: No security manager, stub class loader disabled ***
java.rmi.RMISecurityException: security.No security manager, stub class loader disabled
        at sun.rmi.server.RMIClassLoader.getClassLoader(RMIClassLoader.java:84)
        at sun.rmi.server.MarshalInputStream.resolveClass(MarshalInputStream.java:88)
        at java.io.ObjectInputStream.inputClassDescriptor(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.inputObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:105)
        at java.rmi.Naming.lookup(Naming.java:60)
        at cltEcho.main(cltEcho.java:31)
Erreur : java.rmi.UnexpectedException: Unexpected exception; nested exception is: 
        java.rmi.RMISecurityException: security.No security manager, stub class loader disabled

我们得到了相同的错误提示,指出缺少安全管理器。我们认为可能是操作失误,其实是客户端需要自行创建安全管理器。于是,我们保留了服务器端的安全管理器,同时为客户端也创建了一个。客户端cltEcho.java的main函数因此变为:

public static void main(String arg[]){
         // 语法:cltEcho 机器端口 // 机器:回显服
        务器运行的机器 // 端口:回显服务机器上服务目录运行的端口 // 参数验证 
         if(arg.length!=1){ System.err.println("Syntaxe : pg url_service_rmi"); Sy

        stem.exit(1); } // 安装安
        全管理器 System.setS
            ecurityManager(new RMISecurityManager()); // 客
            户端-服务器对话 Stri
        n

        g urlService=arg[0]; BufferedReader in=null
        ; String msg=null; String reponse=null; interE

        cho serveur=null; try
        {






            ....
        } catch (Exception e){
            ....
        }// 尝试
    }// main

接下来按以下步骤操作:

  • 重新编译 cltEcho.java
  • 将 .class 文件传输到 Linux 机器上
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r--   1 serge    admin        1506 Mar 10 14:28 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class
  • 启动客户端
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho

java.io.FileNotFoundException: /e:/data/java/rmi/echo/serveur/srvEcho_Stub.class
        at java.io.FileInputStream.<init>(FileInputStream.java)
        at sun.net.www.protocol.file.FileURLConnection.connect(FileURLConnection.java:150)
        at sun.net.www.protocol.file.FileURLConnection.getInputStream(FileURLConnection.java:170)
        at sun.applet.AppletClassLoader.loadClass(AppletClassLoader.java:119)
        at sun.applet.AppletClassLoader.findClass(AppletClassLoader.java:496)
        at sun.applet.AppletClassLoader.loadClass(AppletClassLoader.java:199)
        at sun.rmi.server.RMIClassLoader.loadClass(RMIClassLoader.java:159)
        at sun.rmi.server.MarshalInputStream.resolveClass(MarshalInputStream.java:97)
        at java.io.ObjectInputStream.inputClassDescriptor(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.inputObject(ObjectInputStream.java)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
        at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:105)
        at java.rmi.Naming.lookup(Naming.java:60)
        at cltEcho.main(cltEcho.java:31)
File not found when looking for: srvEcho_Stub
Erreur : java.rmi.UnmarshalException: Return value class not found; nested exception is: 
        java.lang.ClassNotFoundException: srvEcho_Stub

尽管表面上看起来没有进展,但实际上我们已经有所突破:错误信息已经发生了变化。我们可以看到,客户端曾向服务器请求类 srvEcho_Stub.class,但服务器未能找到该类。因此,如果客户端想要向服务器请求类,就必须拥有一个安全管理器。

若查看前一个错误,可见系统曾在目录 e:/data/java/rmi/echo/serveur/ 中搜索文件 srvEcho_Stub.class 但未找到。然而该文件确实就在该位置。 若进一步查看与该错误相关的方法列表,会发现其中包含 sun.net.www.protocol.file.FileURLConnection.getInputStream 这一项。客户端似乎使用 FileURLConnection 类型的对象打开了一个流。我们推测这与服务器启动方式有关:

start j:\jdk12\bin\java 
-Djava.security.policy=mypolicy 
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/ 
srvEcho

该错误信息似乎提到了关键字 java.rmi.server.codebase 的值。 重新查阅文档后发现,在给出的示例中,该关键字的值始终为:http://……,c.a.d,且使用的协议是HTTP。目前尚不清楚客户端是如何向服务器请求并获取这些类的。 也许客户端是通过在启动服务器时指定的关键字 java.rmi.server.codebase、URL 及其对应的 URL 来请求这些类的。因此,我们决定使用以下新命令启动服务器:

start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho

现在协议已变为 http。必须将 .class 文件移动到存储类文件的机器上,且该位置需可被 http 服务器访问。 在本例中,服务器运行在一台搭载微软 PWS HTTP 服务器的 Windows 机器上。该服务器的根目录为 d:\Inetpub\wwwroot。因此,操作步骤如下:

  • 创建目录 d:\Inetpub\wwwroot\rmi\echo
  • 将服务器的 .class 文件以及 mypolicy 文件放置其中
  • 若尚未启动,请启动Web服务器
  • 重新启动目录服务(rmiregistry)
  • 使用以下命令重启服务器
start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho
  • 在 Linux 机器上,启动客户端:
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r--   1 serge    admin        1622 Mar 10 14:37 cltEcho.class
-rw-r--r--   1 serge    admin         256 Mar 10 10:02 interEcho.class

shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin

呼!成功了。客户端确实成功获取了那个著名的srvEcho_Stub.class文件

这一切给了我们一些灵感,我们不禁想知道,运行在服务器Windows系统上的客户端,是否也能在没有srvEcho_Stub.class文件的情况下正常工作我们进入客户端所在的目录,如果发现srvEcho_Stub.class文件就将其删除,然后像在Linux系统中一样启动客户端:

E:\data\java\RMI\echo\client>dir *.class

CLTECH~1 CLA         1 622  10/03/99  14:12 cltEcho.class
INTERE~1 CLA           256  09/03/99  15:59 interEcho.class

E:\data\java\RMI\echo\client>j:\jdk12\bin\java -Djava.security.policy=mypolicy cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho

Message : nouveau message
Réponse serveur : [nouveau message]
Message : fin

9.2.1.8. Résumé

Windows 服务器端:

  • 服务器配备了一个安全管理器
  • 其启动时使用了以下选项:start j:\jdk12\bin\java -Djava.security.policy=mypolicy

-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho

客户端(Linux 或 Windows)

  • 客户端拥有一个安全管理器
  • 在 Linux 上,它由 java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho 启动
  • 在 Windows 上,由 j:\jdk12\bin\java -Djava.security.policy=mypolicy 启动 cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho

9.2.1.9. Linux 上的回显服务器,Windows 和 Linux 上的客户端

现在将服务器迁移到 Linux 机器上,并测试 Linux 和 Windows 客户端。操作步骤如下:

  • 将服务器上的 .class 文件转移到 Linux 机器上
shiva[serge]:/home/admin/serge/WWW/rmi/echo/serveur#
$ dir
total 11
drwxr-xr-x   2 serge    admin        1024 Mar 10 16:15 .
drwxr-xr-x   3 serge    admin        1024 Mar 10 16:09 ..
-rw-r--r--   1 serge    admin         256 Mar 10 16:09 interEcho.class
-rw-r--r--   1 serge    admin        1245 Mar 10 16:09 srvEcho.class
-rw-r--r--   1 serge    admin        1736 Mar 10 16:09 srvEcho_Skel.class
-rw-r--r--   1 serge    admin        3264 Mar 10 16:09 srvEcho_Stub.class
  • 由于客户端将请求 srvEcho_Stub.class 类,因此为服务器类选择的目录必须是 Linux 机器上的 HTTP 服务器可访问的目录。 此处该目录下的URL位于http://shiva.istia.univ-angers.fr/~serge/rmi/echo/serveur
  • 目录服务以后台任务方式启动:/usr/local/bin/jdk/rmiregistry &
  • 服务器作为后台任务启动:/usr/local/bin/jdk/bin/java

-Djava.rmi.server.codebase=http://shiva.istia.univ-angers.fr/~serge/rmi/echo/serveur/

srvEcho &

现在可以测试客户端了。先测试Windows客户端。

  • 进入Windows机器上的客户端目录
  • 通过以下命令启动客户端:
E:\data\java\RMI\echo\client>j:\jdk12\bin\java -Djava.security.policy=mypolicy cltEcho rmi://shiva.istia.univ-angers.fr/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : fin

测试 Linux 客户端:

shiva[serge]:/home/admin/serge/java/rmi/echo/client#
$ java cltEcho srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin

请注意,对于与回显服务器运行在同一台机器上的 Linux 客户端,在请求服务的 URL 中无需指定机器。

9.3. 第二个示例:运行在Windows机器上的SQL服务器

9.3.1. 问题

我们在第JDBC章中介绍了如何管理关系型数据库。 在所展示的示例中,应用程序和所使用的数据库位于同一台 Windows 机器上。本文将介绍如何在 Windows 机器上编写一个 RMI 服务器,该服务器可让远程客户端访问服务器所在主机上的公共 ODBC 数据库。

Image

RMI客户端可执行3项操作:

  • 连接到其选择的数据库
  • 发送 SQL 请求
  • 关闭连接

服务器执行客户端的 SQL 请求并向其发送结果。这是其核心功能,因此我们将它称为 SQL 服务器。

我们将之前在回显服务器上看到的各个步骤应用到这里。

9.3.2. 步骤 1:远程接口

远程接口是列出 RMI 服务器方法的接口,这些方法将对 RMI 客户端开放。我们将使用以下接口:

import java.rmi.*;

 // 远程接口 public interf
ace interSQL extends Remote{ public Stri
    ng connect(String pilote, String url, String id, String mdp) throws j
        ava.rmi.RemoteException; public
     String[] executeSQL(String requete, String separateur) thr
        ows java.rmi.RemoteException; p
    ublic String close() 
         throws java.rmi.RemoteExceptio
n; }

各方法的作用如下:

Connect:客户端连接到远程数据库,并提供其 piloteurl、JDBC 以及用户名 id 和密码 mdp 来访问该数据库。服务器返回一个字符串,表示连接结果:

    200 - Connexion réussie
    500 - Echec de la connexion

executeSQL:客户端请求在其连接的数据库上执行查询 SQL。它指定了返回结果中字段分隔符的字符。服务器返回一个字符串数组:

    100 n

用于数据库更新请求,其中 n 表示被更新的行数

    500 msg d’erreur

若查询引发错误

    501 Pas de résultats

如果查询未返回任何结果

    101 ligne1
    101 ligne2
    101 ...

如果查询产生了结果。服务器返回的这些行即为查询的结果行。

close:客户端关闭与远程数据库的连接。服务器返回一个字符串,指示此次关闭的结果:

    200 Base fermée
    500 Erreur lors de la fermeture de la base (msg d’erreur)

9.3.3. 步骤 2:编写服务器端

以下是服务器 SQL 的 Java 源代码。要理解该代码,需要掌握 JDBC 中的数据库管理以及 RMI 中的服务器构建。程序中的注释应有助于理解。

// 导入的包
import java.rmi.*;
import java.rmi.server.*;
import java.sql.*;
import java.util.*;

// 类 srvSQL
public class srvSQL extends UnicastRemoteObject implements interSQL{

    // 类的全局数据
    private Connection DB;

    // ------------- 构造函数
    public srvSQL() throws RemoteException{
        super();
    }

    // --------------- 连接
    public String connect(String pilote, String url, String id,
        String mdp) throws RemoteException{

        // 通过驱动程序连接到数据库 URL
        // 使用用户名 id 和密码 mdp 进行身份验证

        String resultat=null;            // 方法返回结果
        try{
            // 加载驱动程序
            Class.forName(pilote);
            // 连接请求
            DB=DriverManager.getConnection(url,id,mdp);
            // 成功
            resultat="200 Connexion réussie";
        } catch (Exception e){
            // 错误
            resultat="500 Echec de la connexion (" + e + ")";
        }
        // 结束
        return resultat;
    }            

    // ------------- executeSQL
    public String[] executeSQL(String requete, String separateur)
        throws RemoteException{

        // 在数据库 DB 上执行查询 SQL
        // 并将结果放入字符串数组中

        // 执行查询所需的数据
        Statement S=null;
        ResultSet RS=null;
        String[] lignes=null;
        Vector resultats=new Vector();
        String ligne=null;

        try{
            // 创建查询容器
            S=DB.createStatement();
            // 执行查询
            if (! S.execute(requete)){
                // 更新查询
                // 返回更新行的数量
                lignes=new String[1];
                lignes[0]="100 "+S.getUpdateCount();
                return lignes;
            }
            // 这是一条查询请求
            // 获取结果
            RS=S.getResultSet();
            // Resultset中的字段数
            int nbChamps=RS.getMetaData().getColumnCount();
            // 对结果集进行处理
            while(RS.next()){
                // 创建结果行
                ligne="101 ";
                for (int i=1;i<nbChamps;i++)
                    ligne+=RS.getString(i)+separateur;
                ligne+=RS.getString(nbChamps);
                // 将结果添加到结果向量中
                resultats.addElement(ligne);
            }// while
            // 结束结果处理
            // 释放资源
            RS.close();
            S.close();
            // 返回结果
            int nbLignes=resultats.size();
            if (nbLignes==0){
                lignes=new String[1];
                lignes[0]="501 Pas de résultats";
            } else {
                lignes=new String[resultats.size()];
                for(int i=0;i<lignes.length;i++)
                    lignes[i]=(String) resultats.elementAt(i);
            }//if
            return lignes;
        } catch (Exception e){
            // 错误
            lignes=new String[1];
            lignes[0]="500 " + e;
            return lignes;
        }// try-catch
    }// executeSQL

    // --------------- 关闭
    public String close() throws RemoteException {
        // 关闭与数据库的连接
        String resultat=null;
        try{
            DB.close();
            resultat="200 Base fermée";
        } catch (Exception e){
            resultat="500 Erreur à la fermeture de la base ("+e+")";
        }
        // 返回结果
        return resultat;
    }

    // ----------- 主程序
    public static void main (String[] args){

        // 安全管理器
        System.setSecurityManager(new RMISecurityManager());

        // 启动服务
        srvSQL serveurSQL=null;
        try{
            // 创建
            serveurSQL=new srvSQL();
            // 记录
            Naming.rebind("srvSQL",serveurSQL);
            // 跟踪
            System.out.println("Serveur SQL prêt");
        } catch (Exception e){
            // 错误
            System.err.println("Erreur lors du lancement du serveur SQL ("+ e +")");
        }// try-catch
    }// main

}// 类

9.3.4. RMI客户端代码

调用 RMI 服务器客户端时,需使用以下参数:

    urlserviceAnnuaire pilote urlBase id mdp separateur

urlserviceAnnuaire:目录服务器的 URL,该服务器已注册 SQL

驱动程序:服务器 SQL 用于管理数据库的驱动程序

urlBase:待管理数据库的 URL(JDBC)

id:客户端标识,若无标识则为 null

mdp:客户端密码,若无密码则为 null

分隔符:SQL 服务器用于分隔查询结果行中各字段的字符

以下是一个可能的参数示例:

    srvSQL sun.jdbc.odbc.JdbcOdbcDriver jdbc:odbc:articles null null ,

其中:

urlserviceAnnuaire    srvSQL

SQL服务器的RMI名称

pilote     sun.jdbc.odbc.JdbcOdbcDriver

带ODBC接口的数据库常用驱动程序

urlBase    jdbc:odbc:articles

用于使用 Windows 机器上公共数据库列表 ODBC 中声明的物料数据库

id    null

无身份

mdp    null

无需密码

separateur    , 

结果字段将以逗号分隔

使用上述参数启动后,客户端将执行以下步骤:

  • 连接到服务器 RMI srvSQL,即位于客户端同一台机器上的服务器 RMI
  • 请求连接到物料数据库
connect(‘’sun.jdbc.odbc.JdbcOdbcDriver’’, ‘‘jdbc:odbc:articles’’, ’’’’, ’’’’)
  • 要求用户通过键盘输入查询语句 SQL
  • 将其发送至服务器 SQL
executeSQL(requete, ’’,’’);
  • 它在屏幕上显示服务器返回的结果
  • 再次提示用户通过键盘输入请求 SQL。当请求结束时,程序将停止运行。

客户端的 Java 代码如下。注释应足以说明其含义。

import java.rmi.*;
import java.io.*;

public class cltSQL {

    // 类的全局数据
    private static String syntaxe =
        "syntaxe : cltSQL urlServiceAnnuaire pilote urlBase id mdp separateur";
    private static BufferedReader in=null;
    private static interSQL serveurSQL=null;

    public static void main(String arg[]){
        // 语法:cltSQL urlServiceAnnuaire 分隔符 驱动程序 url id 密码
        // urlServiceAnnuaire:待联系的服务目录网址 RMI
        // 驱动程序:用于操作数据库的驱动程序
        // urlBase:待处理数据库的JDBC URL
        // id:用户ID
        // mdp:其密码
        // 分隔符:查询结果中字段的分隔字符串

        // 参数数量验证
        if(arg.length!=6)
            erreur(syntaxe,1);

        // 初始化数据库连接参数
        String urlService=arg[0];        
        String pilote=arg[1];
        String urlBase=arg[2];
        String id, mdp, separateur;
        if(arg[3].equals("null")) id=""; else id=arg[3];
        if(arg[4].equals("null")) mdp=""; else mdp=arg[4];        
        if(arg[5].equals("null")) separateur=" "; else separateur=arg[5];        

        // 安装安全管理器
        System.setSecurityManager(new RMISecurityManager());

        // 客户端-服务器通信
        String requete=null;
        String reponse=null;
        String[] lignes=null;
        String codeErreur=null;

        try{
            // 打开键盘流
            in=new BufferedReader(new InputStreamReader(System.in));
            // 跟踪
            System.out.println("--> Connexion au serveur RMI en cours...");
            // 服务定位
            serveurSQL=(interSQL) Naming.lookup(urlService);
            // 跟踪
            System.out.println("--> Connexion à la base de données en cours");
            // 初始数据库连接请求
            reponse=serveurSQL.connect(pilote,urlBase,id,mdp);
            // 跟踪
            System.out.println("<-- "+reponse);
            // 响应分析
            codeErreur=reponse.substring(0,3);
            if(codeErreur.equals("500")) 
                erreur("Abandon sur erreur de connexion à la base",3);
            // 读取待发送至 SQL 服务器的请求循环
            System.out.print("--> Requête : ");
            requete=in.readLine().toLowerCase().trim();
            while(! requete.equals("fin")){
                // 向服务器发送请求并接收响应
                lignes=serveurSQL.executeSQL(requete,separateur);
                // 跟踪
                afficheLignes(lignes);
                // 下一个请求
                System.out.print("--> Requête : ");                
                requete=in.readLine().toLowerCase().trim();
            }// while
            // 跟踪
            System.out.println("--> Fermeture de la connexion à la base de données distante");
            // 结束连接
            reponse=serveurSQL.close();
            // 后续
            System.out.println("<-- " + reponse);
            // 结束
            System.exit(0);
        // 错误处理        
        } catch (Exception e){
            erreur("Abandon sur erreur : " + e,2);
        }// 尝试
    }// main

    // ----------- AfficheLignes
    private static void afficheLignes(String[] lignes){
        for (int i=0;i<lignes.length;i++)
            System.out.println("<-- " + lignes[i]);
    }// afficheLignes

    // ------------ 错误
    private static void erreur(String msg, int exitCode){
        // 显示错误信息
        System.err.println(msg);
        // 可能释放资源
        try{
            in.close();
            serveurSQL.close();
        } catch(Exception e){}
        // 退出
        System.exit(exitCode);
    }// 错误

}// 类    

9.3.5. 步骤 3:创建 .class 文件

  • 服务器已编译
E:\data\java\RMI\sql\serveur>j:\jdk12\bin\javac interSQL.java

E:\data\java\RMI\sql\serveur>j:\jdk12\bin\javac srvSQL.java

E:\data\java\RMI\sql\serveur>dir *.class

INTERS~1 CLA           451  12/03/99  17:54 interSQL.class
SRVSQL~1 CLA         3 238  12/03/99  17:54 srvSQL.class
  • 已创建Stub和Skel文件
E:\data\java\RMI\sql\serveur>j:\jdk12\bin\rmic srvSQL

E:\data\java\RMI\sql\serveur>dir *.class


INTERS~1 CLA           451  12/03/99  17:54 interSQL.class
SRVSQL~1 CLA         3 238  12/03/99  17:54 srvSQL.class
SRVSQL~2 CLA         4 491  12/03/99  17:56 srvSQL_Stub.class
SRVSQL~3 CLA         2 414  12/03/99  17:56 srvSQL_Skel.class
  • 将文件 interSQL.class、srvSQL_Stub.class、srvSQL_Skel.class 传输到客户目录中
E:\data\java\RMI\sql\client>dir

CLTSQL~1 JAV         3 486  11/03/99  11:39 cltSQL.java
INTERS~1 CLA           451  11/03/99  10:55 interSQL.class
SRVSQL~1 CLA         4 491  11/03/99  13:19 srvSQL_Stub.class
SRVSQL~2 CLA         2 414  11/03/99  13:19 srvSQL_Skel.class
  • 编译客户端
E:\data\java\RMI\sql\client>j:\jdk12\bin\javac cltSQL.java

E:\data\java\RMI\sql\client>dir *.class

INTERS~1 CLA           451  11/03/99  10:55 interSQL.class
CLTSQL~1 CLA         2 839  12/03/99  18:00 cltSQL.class
SRVSQL~1 CLA         4 491  11/03/99  13:19 srvSQL_Stub.class
SRVSQL~2 CLA         2 414  11/03/99  13:19 srvSQL_Skel.class

9.3.6. 步骤 4:在同一台 Windows 机器上进行服务器与客户端测试

  • 目录服务在与服务器及客户端不同的目录中启动
F:\>start j:\jdk12\bin\rmiregistry
  • 将以下 mypolicy 文件放置在客户端和服务器目录中
grant {
    // 暂时允许所有操作
    permission java.security.AllPermission;
};
  • 启动服务器
E:\data\java\RMI\sql\serveur>start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/sql/serveur/ srvSQL
  • 启动客户端
E:\data\java\RMI\sql\client>j:\jdk12\bin\java -Djava.security.policy=mypolicy cltSQL srvSQL 
sun.jdbc.odbc.JdbcOdbcDriver jdbc:odbc:articles null null ,

--> Connexion au serveur RMI en cours...
--> Connexion à la base de données en cours
<-- 200 Connexion réussie
--> 查询:select 名称, stock_actu from 文章 order by stock_actu desc
<-- 101 vélo,31
<-- 101 essai3,13
<-- 101 skis nautiques,13
<-- 101 canoé,13
<-- 101 panthère,11
<-- 101 léopard,11
<-- 101 cachalot,10
<-- 101 fusil,10
<-- 101 arc,10
--> 查询:update articles set stock_actu=stock_actu-1 where stock_actu<=11
<-- 100 5
--> 查询:select 名称,stock_actu from 文章 排序按 stock_actu 升序
<-- 101 cachalot,9
<-- 101 fusil,9
<-- 101 arc,9
<-- 101 panthère,10
<-- 101 léopard,10
<-- 101 essai3,13
<-- 101 skis nautiques,13
<-- 101 canoé,13
<-- 101 vélo,31
--> Requête : fin
--> Fermeture de la connexion à la base de données distante
<-- 200 Base fermée

9.3.7. 步骤 5:在 Windows 机器上运行服务器,在 Linux 机器上运行客户端进行测试

  • 如有必要,停止服务器和目录服务
  • 将客户端的 .class 文件传输到 Linux 机器上
shiva[serge]:/home/admin/serge/java/rmi/sql/client#
$ dir *.class
-rw-r--r--   1 serge    admin        2839 Mar 11 14:37 cltSQL.class
-rw-r--r--   1 serge    admin         451 Mar 11 14:37 interSQL.class
  • 将服务器文件放置在Windows机器上HTTP服务器可访问的目录中
D:\Inetpub\wwwroot\rmi\sql>dir

INTERS~1 CLA           451  11/03/99  10:55 interSQL.class
SRVSQL~1 CLA         3 238  11/03/99  13:19 srvSQL.class
SRVSQL~2 CLA         4 491  11/03/99  13:19 srvSQL_Stub.class
SRVSQL~3 CLA         2 414  11/03/99  13:19 srvSQL_Skel.class
MYPOLICY                81  08/06/98  15:01 mypolicy
  • 重新启动目录服务
F:\>start j:\jdk12\bin\rmiregistry
  • 使用与上次测试不同的参数重启服务器
D:\Inetpub\wwwroot\rmi\sql>start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/sql/ srvSQL
  • 在Linux机器上启动客户端
/usr/local/bin/jdk/bin/java cltSQL rmi://tahe.istia.univ-angers.fr/srvSQL sun.jdbc.odbc.JdbcOdbcDriver jdbc:odbc:articles null null ,

--> 查询:select 名称,stock_actu,stock_mini from 文章 排序按 名称
<-- 101 arc,9,8
<-- 101 cachalot,9,6
<-- 101 canoé,13,7
<-- 101 essai3,13,9
<-- 101 fusil,9,8
<-- 101 léopard,10,7
<-- 101 panthère,10,7
<-- 101 skis nautiques,13,8
<-- 101 vélo,31,8
--> 查询:update articles set stock_actu=stock_mini where stock_mini<=7
<-- 100 4
--> 查询:select 名称,stock_actu,stock_mini from 文章 排序按 名称
<-- 101 arc,9,8
<-- 101 cachalot,6,6
<-- 101 canoé,7,7
<-- 101 essai3,13,9
<-- 101 fusil,9,8
<-- 101 léopard,7,7
<-- 101 panthère,7,7
<-- 101 skis nautiques,13,8
<-- 101 vélo,31,8
--> Requête : fin
--> Fermeture de la connexion à la base de données distante
<-- 200 Base fermée

9.3.8. 结论

这是一个很有趣的应用程序,因为它允许从网络中的任意一台计算机访问数据库。我们完全可以采用传统方式使用套接字来编写它,这也是数据库章节中某道习题的要求。如果采用传统方式编写该应用程序:

  • 将会有一个客户端和一个服务器,它们可能使用不同的语言编写
  • 客户端和服务器将通过交换文本行进行通信,对话可能类似于:
client : connect machine port pilote urlBase id mdp

其中前两个参数指定服务器的位置,后四个参数表示要使用的数据库的连接参数

服务器可能会返回类似以下的内容:

200 - Connexion réussie
500 - Echec de la connexion
client : executeSQL requete, separateur

用于请求服务器在连接到客户端的数据库上执行查询 SQL。分隔符是响应行中字段的分隔字符。

服务器可能会返回类似以下内容

    100 n

(针对数据库更新请求,其中 n 表示被更新的行数)

    500 msg d’erreur

如果请求引发了错误

    501 Pas de résultats

如果查询未返回任何结果

    101 ligne1
    101 ligne2
    101 ...

如果查询产生了结果。服务器返回的这些行即为查询的结果行。

client : close

用于关闭与远程数据库的连接。服务器可能会返回一个字符串,指示此次关闭的结果:

    200 Base fermée
    500 Erreur lors de la fermeture de la base (msg d’erreur)

由此可见,如果能够基于上述类型的协议构建传统应用程序,便可据此推导出 RMI 服务器的可能结构。在协议中,当出现客户端向服务器发送类似以下语句时:

    commande param1 param2 ... paramq

那么在 RMI 服务器内部,可能会存在一个方法

    String commande(param1,param2,..., paramq)

,而该方法作为客户端可访问的接口,必须包含在服务器发布的接口中。

最后需要指出的是,我们的服务器目前仅支持一个客户端:按当前实现方式,它无法同时处理多个客户端。 实际上,如果一个客户端连接到数据库 B1,服务器会创建一个对象 Connection DB=DB1。 如果第二个客户端请求连接到数据库 B2, 服务器会将其记录为 Connection DB=DB2,从而中断第一个客户端与 B1 数据库的连接。

9.4. 练习

9.4.1. 练习 1

扩展前面的服务器 SQL,使其能够管理多个客户端。

9.4.2. 练习 2

编写本章练习中介绍的电子商务 Java 小程序 JDBC,使其能够与上一练习中的服务器 RMI 配合工作。