Skip to content

9. Java RMI

9.1. 简介

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

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:对象/服务器接口

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

import java.rmi.*;

 // remote interface p
ublic interface interEcho extends Remote{ 
     public String 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.*;

// class implementing remote echo
public class srvEcho extends UnicastRemoteObject implements interEcho{

    // manufacturer
    public srvEcho() throws RemoteException{
        super();
    }// manufacturer end

    // method performing the echo
    public String echo(String msg) throws RemoteException{
        return  "["  + msg + "]";
    }// fine echo
}// end of class

在之前的类中,我们发现:

  1. 执行回显的方法
  2. 一个除了调用父类构造函数外什么也不做的构造函数。它的存在是为了声明该构造函数可能抛出 RemoteException

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

  1. 他们联系目标对象所在机器上的目录服务。该目录服务运行在客户端必须知道的端口上(默认是1099)。客户端向目录请求对象/服务的引用,并提供其名称。如果该名称与目录中的某个对象/服务匹配,目录会向客户端返回一个引用,客户端可以通过该引用与远程对象/服务进行通信。
  2. 从这一刻起,客户端即可像使用本地对象一样使用该远程对象

回到我们的服务器,我们必须创建一个 srvEcho 类型的对象,并将其注册到外部可访问对象的目录中。此注册操作是通过 Naming 类的 rebind 方法来实现的:

Naming.rebind(String nom, Remote obj)

其中

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

obj:远程对象

因此,我们的 srvEcho 类变为如下所示:

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

// class implementing remote echo
public class srvEcho extends UnicastRemoteObject implements interEcho{

    // manufacturer
    public srvEcho() throws RemoteException{
        super();
    }// manufacturer end

    // method performing the echo
    public String echo(String msg) throws RemoteException{
        return  "["  + msg + "]";
    }// fine echo

    // service creation
    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 ");
        }
    }// hand
}// end of class

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

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,并在输入的行是“end”时停止。

这将生成如下客户端:

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

; public class cltEch

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

        // argument verification
        if(arg.length!=1){
            System.err.println("Syntaxe : pg url_service_rmi");
            System.exit(1);
        }

        // client-server dialogue
        String urlService=arg[0];
        BufferedReader in=null;
        String msg=null;
        String reponse=null;
        interEcho serveur=null;

        try{
            // open keyboard flow
            in=new BufferedReader(new InputStreamReader(System.in));
            // service location
            serveur=(interEcho) Naming.lookup(urlService);                
            // loop for reading msg to be sent to echo server
            System.out.print("Message : ");
            msg=in.readLine().toLowerCase().trim();
            while(! msg.equals("fin")){
                // send msg to server and receive response
                reponse=serveur.echo(msg);
                // follow-up
                System.out.println("Réponse serveur : " + reponse);
                // next msg
                System.out.print("Message : ");                
                msg=in.readLine().toLowerCase().trim();
            }// while
            // it's over
            System.exit(0);
        // error management        
        } catch (Exception e){
            System.err.println("Erreur : " + e);
            System.exit(2);
        }// try
    }// hand
}// class                

这个客户端并没有什么特别之处,唯一值得一提的是用于获取服务器引用那条语句:

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

请注意,我们的回显服务是通过以下指令注册在其所在机器的服务目录中的:


            Naming.rebind("srvEcho",serveurEcho);

因此,客户端也使用 Naming 类中的一个方法来获取其想要使用的服务器的引用。所使用的查找方法将请求服务的 URL 作为参数。该 URL 采用标准 URL 的形式:

    rmi://machine:port/nom_service

其中

rmi:可选 - RMI 协议

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

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

service_name:请求的服务注册时使用的名称(本例中为 srvEcho

返回的是远程接口 interEcho 的一个实例。假设客户端和服务器不在同一台机器上,在编译客户端 cltEcho.java 时,文件 interEcho.class(即编译远程接口 interEcho 的结果)必须位于同一目录下;否则,在引用该接口的行上会发生编译错误。

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

为了明确区分服务器端和客户端组件,我们将服务器放在 echo\server 目录中,客户端放在 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 命令提示符窗口中将其在后台启动。注册表启用后,我们可以创建 echo 服务并将其注册到服务注册表中。同样,我们使用 start 命令将其在后台启动:

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

echo 服务器将在一个新的 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.class 和 srvEcho_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类,但因缺少“安全管理器”而失败。我们记得在文档中看到过相关内容。 我们重新深入研究……发现服务器必须创建并安装一个安全管理器,才能向请求加载类的客户端保证这些类是安全的。没有这个安全管理器,类加载就无法进行。这似乎解释了问题:我们的 Linux 客户端向服务器请求了所需的 srvEcho_stub.class,但服务器拒绝了,声称未安装安全管理器。因此,我们将服务器主函数的代码修改如下:

     // service creation 
    public static void main (String arg[]){

         // installation of a security manager Sy
        stem.setSecurityManager(new RMISecurityManager()); 

        // service launch and registration
        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 ");
        }
    }// hand

我们使用 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

看来安全管理器设置过于严格了。我们再次查阅了文档……发现当安全管理器处于活动状态时,启动程序时必须指定其权限。这可以通过以下选项实现:

<mark style="background-color: #ffff00">start j:\\jdk12\\bin\\java -Djava.security.policy=mypolicy srvEcho</mark>

其中

java.security.policy 是一个关键字

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

grant {
    // Allow everything for now
    permission java.security.AllPermission;
};

在此处,该程序拥有全部权限。

让我们重新开始。进入服务器目录并执行以下操作:

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

这次,echo 服务器(但客户端尚未启动)已正确启动。现在您可以尝试以下操作:

  • 先停止回显服务器,然后停止目录服务
  • 在服务器目录以外的目录中重启目录服务
  • 返回服务器所在的目录启动回显服务器——您将收到以下错误:
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

该命令位于一行上。关键字 <mark style="background-color: #ffff00">java.rmi.server.codebase</mark> 用于指定包含服务器所需类的目录的 URL。在此情况下,该 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[]){
         // syntax : cltEcho machine po
        rt // machine: machine where the echo server 
        operates // port: port where the service directory operates on the echo servic

        e machine // argument ve
        rification if(ar
            g.length!=1){ System.err.println("Syntaxe : pg u
            rl_service_rmi"
        )

        ; System.exit(1); } // installation
         of a security manager System.setSecurityManager(n

        ew RMISecurityManager()); 
         // client-server dia
        logue String urlServi
        ce=arg[0]; Buf
        feredReader in=null;
         String msg=null; S

        tring reponse=null; interEcho serveur=null; try{
            ....
        } catch (Exception e){
            ....
        }// try
    }// hand

然后我们按以下步骤进行:

  • 重新编译 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/server/ 中搜索了文件 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://,即使用的协议是 HTTP。目前尚不清楚客户端如何向服务器请求并获取其类。 或许它是通过 *java.rmi.server.codebase* 关键字指定的 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 服务器可访问的位置。在本例中,服务器运行在一台安装了 Microsoft 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. 摘要

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. Echo 服务器运行在 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/server/

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

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

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

9.3.1. 问题

在 JDBC 章节中,我们学习了如何管理关系型数据库。在提供的示例中,应用程序和所用的数据库位于同一台 Windows 机器上。在此,我们建议在 Windows 机器上编写一个 RMI 服务器,以便远程客户端能够访问托管该服务器的机器上的公共 ODBC 数据库。

Image

RMI 客户端可以执行三项操作:

  • 连接到其选定的数据库
  • 发送 SQL 查询
  • 关闭连接

服务器执行客户端的 SQL 查询,并将结果发送回客户端。这是其主要功能,因此我们将它称为 SQL 服务器。

我们将应用之前在 echo 服务器中看到的不同步骤。

9.3.2. 步骤 1:远程接口

远程接口是列出 RMI 客户端可访问的 RMI 服务器方法的接口。我们将使用以下接口:

import java.rmi.*;

 // remote interface p
ublic interface interSQL extends Remote{ 
     public String connect(String pilote, String url, String id, String mdp
        ) throws java.rmi.RemoteExcept
    ion; public String[] executeSQL(String requete, String separa
        teur) throws java.rmi.RemoteE
    xception; public Str
        ing close() throws java.rmi.Re
moteException; }

各种方法的作用如下:

Connect:客户端连接到远程数据库,提供驱动程序、JDBC URL 以及访问数据库所需的用户名密码。服务器返回一个字符串,表示连接结果:

    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服务器的构建。程序中的注释应有助于理解。

// imported packages
import java.rmi.*;
import java.rmi.server.*;
import java.sql.*;
import java.util.*;

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

    // global class data
    private Connection DB;

    // ------------- manufacturer
    public srvSQL() throws RemoteException{
        super();
    }

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

        // connection to url database via driver
        // identification with identity id and password mdp

        String resultat=null;            // result of the method
        try{
            // loading the driver
            Class.forName(pilote);
            // connection request
            DB=DriverManager.getConnection(url,id,mdp);
            // ok
            resultat="200 Connexion réussie";
        } catch (Exception e){
            // error
            resultat="500 Echec de la connexion (" + e + ")";
        }
        // end
        return resultat;
    }            

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

        // executes a SQL query on the DB database
        // and puts the results in an array of strings

        // data required to execute the request
        Statement S=null;
        ResultSet RS=null;
        String[] lignes=null;
        Vector resultats=new Vector();
        String ligne=null;

        try{
            // create query container
            S=DB.createStatement();
            // request execution
            if (! S.execute(requete)){
                // update request
                // returns the number of lines updated
                lignes=new String[1];
                lignes[0]="100 "+S.getUpdateCount();
                return lignes;
            }
            // it was a query request
            // retrieve results
            RS=S.getResultSet();
            // number of Resultset fields
            int nbChamps=RS.getMetaData().getColumnCount();
            // we exploit them
            while(RS.next()){
                // create results line
                ligne="101 ";
                for (int i=1;i<nbChamps;i++)
                    ligne+=RS.getString(i)+separateur;
                ligne+=RS.getString(nbChamps);
                // add to results vector
                resultats.addElement(ligne);
            }// while
            // end of results analysis
            // free up resources
            RS.close();
            S.close();
            // we return the results
            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){
            // error
            lignes=new String[1];
            lignes[0]="500 " + e;
            return lignes;
        }// try-catch
    }// executeSQL

    // --------------- close
    public String close() throws RemoteException {
        // closes database connection
        String resultat=null;
        try{
            DB.close();
            resultat="200 Base fermée";
        } catch (Exception e){
            resultat="500 Erreur à la fermeture de la base ("+e+")";
        }
        // return result
        return resultat;
    }

    // ----------- hand
    public static void main (String[] args){

        // security manager
        System.setSecurityManager(new RMISecurityManager());

        // service launch
        srvSQL serveurSQL=null;
        try{
            // creation
            serveurSQL=new srvSQL();
            // registration
            Naming.rebind("srvSQL",serveurSQL);
            // follow-up
            System.out.println("Serveur SQL prêt");
        } catch (Exception e){
            // error
            System.err.println("Erreur lors du lancement du serveur SQL ("+ e +")");
        }// try-catch
    }// hand

}// class

9.3.4. 编写 RMI 客户端

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

    urlserviceAnnuaire pilote urlBase id mdp separateur

directoryServiceURL:注册了该 SQL 服务器的目录服务的 RMI URL

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

urlBase:待管理数据库的 JDBC URL

id:客户端 ID,若无 ID 则为 null

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

separator: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 数据库中列出的 articles 数据库
id    null
无标识
mdp    null
无密码
separateur    , 
结果中的字段将用逗号分隔

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

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

Java客户端代码如下。注释应足以帮助理解。

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

public class cltSQL {

    // global class data
    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[]){
        // syntax : cltSQL urlServiceAnnuaire separator driver url id mdp
        // urlServiceAnnuaire : url of the directory of services RMI to contact
        // driver: driver to be used for the database to be processed
        // urlBase: jdbc url of the database to be used
        // id: user identity
        // mdp: password
        // separator: string separating fields in query results

        // check number of arguments
        if(arg.length!=6)
            erreur(syntaxe,1);

        // init database connection parameters
        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];        

        // installation of a security manager
        System.setSecurityManager(new RMISecurityManager());

        // client-server dialogue
        String requete=null;
        String reponse=null;
        String[] lignes=null;
        String codeErreur=null;

        try{
            // open keyboard flow
            in=new BufferedReader(new InputStreamReader(System.in));
            // follow-up
            System.out.println("--> Connexion au serveur RMI en cours...");
            // service location
            serveurSQL=(interSQL) Naming.lookup(urlService);
            // follow-up
            System.out.println("--> Connexion à la base de données en cours");
            // initial database connection request
            reponse=serveurSQL.connect(pilote,urlBase,id,mdp);
            // follow-up
            System.out.println("<-- "+reponse);
            // response analysis
            codeErreur=reponse.substring(0,3);
            if(codeErreur.equals("500")) 
                erreur("Abandon sur erreur de connexion à la base",3);
            // loop for reading requests to be sent to the server SQL
            System.out.print("--> Requête : ");
            requete=in.readLine().toLowerCase().trim();
            while(! requete.equals("fin")){
                // send request to server and receive response
                lignes=serveurSQL.executeSQL(requete,separateur);
                // follow-up
                afficheLignes(lignes);
                // following request
                System.out.print("--> Requête : ");                
                requete=in.readLine().toLowerCase().trim();
            }// while
            // follow-up
            System.out.println("--> Fermeture de la connexion à la base de données distante");
            // close the connection
            reponse=serveurSQL.close();
            // follow-up
            System.out.println("<-- " + reponse);
            // end
            System.exit(0);
        // error management        
        } catch (Exception e){
            erreur("Abandon sur erreur : " + e,2);
        }// try
    }// hand

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

    // ------------ error
    private static void erreur(String msg, int exitCode){
        // error msg display
        System.err.println(msg);
        // possible release of resources
        try{
            in.close();
            serveurSQL.close();
        } catch(Exception e){}
        // we leave
        System.exit(exitCode);
    }// error

}// class    

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 {
    // Allow everything for now
    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 ,

--> Connection to server RMI in progress...
--> Current database connection
<-- 200 Successful connection
--> Requête : select nom, stock_actu from articles order by stock_actu desc
<-- 101 vélo.31
<-- 101 test3.13
<-- 101 water skis,13
<-- 101 canoeing,13
<-- 101 panther,11
<-- 101 leopard,11
<-- 101 sperm whale,10
<-- 101 rifle,10
<-- 101 arc,10
--> Query: update articles set stock_actu=stock_actu-1 where stock_actu<=11
<-- 100 5
--> Requête : select nom,stock_actu from articles order by stock_actu asc
<-- 101 sperm whale,9
<-- 101 rifle,9
<-- 101 arc.9
<-- 101 panther,10
<-- 101 leopard,10
<-- 101 test3.13
<-- 101 water skis,13
<-- 101 canoeing,13
<-- 101 vélo.31
--> Query: end
--> Closing the remote database connection
<-- 200 Closed base

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 ,

--> Requête : select nom,stock_actu,stock_mini from articles order by nom
<-- 101 arc,9,8
<-- 101 sperm whale,9,6
<-- 101 canoeing,13.7
<-- 101 test3,13,9
<-- 101 rifle,9,8
<-- 101 leopard,10.7
<-- 101 panther,10.7
<-- 101 water skis,13.8
<-- 101 bicycle,31,8
--> Query: update articles set stock_actu=stock_mini where stock_mini<=7
<-- 100 4
--> Requête : select nom,stock_actu,stock_mini from articles order by nom
<-- 101 arc,9,8
<-- 101 sperm whale,6,6
<-- 101 canoeing,7,7
<-- 101 test3,13,9
<-- 101 rifle,9,8
<-- 101 leopard,7,7
<-- 101 panther,7,7
<-- 101 water skis,13.8
<-- 101 bicycle,31,8
--> Query: end
--> Closing the remote database connection
<-- 200 Closed base

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,服务器会创建一个 DB=DB1 的 Connection 对象。如果第二个客户端请求连接到数据库 B2,服务器会建立一个 DB=DB2 的 Connection 连接,从而中断了第一个客户端与数据库 B1 的连接。

9.4. 练习

9.4.1. 练习 1

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

9.4.2. 练习 2

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