9. Java RMI
9.1. 简介
我们已经了解了如何使用名为套接字(sockets)的通信工具来创建网络应用程序。在基于这些工具构建的客户端/服务器应用程序中,客户端与服务器之间的连接就是它们为通信而采用的通信协议。这两个应用程序可以使用不同的语言编写:例如,客户端使用 Java,服务器使用 Perl,或者任何其他组合。 我们确实拥有两个通过双方都熟悉的通信协议连接的独立应用程序。此外,对于 Java 应用程序而言,通过套接字进行的网络访问并非透明的:它必须使用 Socket 类,这是一个专门用于管理这些被称为套接字的通信工具的类。
Java RMI(远程方法调用)允许您创建具有以下特征的网络应用程序:
- 通信两端的客户端/服务器应用程序均为 Java 应用程序
- 客户端可以像使用本地对象一样使用位于服务器上的对象
- 网络层变得透明:应用程序无需关心信息如何从一点传输到另一点。
最后一点是可移植性的关键因素:如果 RMI 应用程序的网络层发生变化,应用程序本身无需重写。只需将 Java 语言中的 RMI 类适配到新的网络层即可。
RMI通信的原理如下:
- 在机器 A 上编写一个标准的 Java 应用程序。它将充当服务器。为此,该应用程序的一些对象将在运行该应用程序的机器 A 上被“发布”,并由此成为服务。
- 在机器 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
在之前的类中,我们发现:
- 执行回显的方法
- 一个除了调用父类构造函数外什么也不做的构造函数。它的存在是为了声明该构造函数可能抛出 RemoteException。
我们将通过 main 方法创建该类的实例。若要使对象/服务可被外部访问,必须将其创建并注册到外部可访问对象目录中。希望访问远程对象的客户端需按以下步骤操作:
- 他们联系目标对象所在机器上的目录服务。该目录服务运行在客户端必须知道的端口上(默认是1099)。客户端向目录请求对象/服务的引用,并提供其名称。如果该名称与目录中的某个对象/服务匹配,目录会向客户端返回一个引用,客户端可以通过该引用与远程对象/服务进行通信。
- 从这一刻起,客户端即可像使用本地对象一样使用该远程对象
回到我们的服务器,我们必须创建一个 srvEcho 类型的对象,并将其注册到外部可访问对象的目录中。此注册操作是通过 Naming 类的 rebind 方法来实现的:
其中
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.class 和 srvEcho.class。
9.2.1.4. 步骤 4:编写客户端
我们编写一个客户端,该客户端将回声服务器的 URL 作为参数,并
- 读取键盘输入的一行内容
- 将其发送至回显服务器
- 显示其发送的响应
- 循环回到步骤 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
这个客户端并没有什么特别之处,唯一值得一提的是用于获取服务器引用那条语句:
请注意,我们的回显服务是通过以下指令注册在其所在机器的服务目录中的:
Naming.rebind("srvEcho",serveurEcho);
因此,客户端也使用 Naming 类中的一个方法来获取其想要使用的服务器的引用。所使用的查找方法将请求服务的 URL 作为参数。该 URL 采用标准 URL 的形式:
其中
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
在客户端目录中,我们发现了以下源文件:
以及在服务器编译过程中生成的 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 类。事实上,我们之前从未听说过这个类。在客户端中,服务器是通过以下语句定位的:
此处,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 系统中,命令为:
将从 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:运行客户端-服务器回显应用程序
现在我们可以运行我们的客户端-服务器应用程序了。起初,客户端和服务器将在同一台机器上运行。首先,我们需要启动服务器应用程序。请记住,这:
- 会创建服务
- 并在回显服务器运行的机器的服务目录中注册该服务
最后一步需要注册服务。可通过以下命令启动该服务:
rmiregistry 即注册表服务。此处,我们通过 start 命令在 Windows 命令提示符窗口中将其在后台启动。注册表启用后,我们可以创建 echo 服务并将其注册到服务注册表中。同样,我们使用 start 命令将其在后台启动:
echo 服务器将在一个新的 DOS 窗口中运行,并显示请求的输出:
接下来只需启动并测试我们的客户端:
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.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~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.class 和 srvEcho_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 是一个定义程序权限的文本文件。此处的文件内容如下:
在此处,该程序拥有全部权限。
让我们重新开始。进入服务器目录并执行以下操作:
- 启动目录服务: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 数据库。

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 以及访问数据库所需的用户名和密码。服务器返回一个字符串,表示连接结果:
executeSQL:客户端请求在其连接的数据库上执行 SQL 查询。它指定了返回结果中字段之间的分隔符。服务器返回一个字符串数组:
(用于数据库更新查询,其中 n 表示更新行的数量)
如果查询引发了错误
如果查询未返回任何结果
如果查询返回了结果。服务器返回的行即为查询结果。
close:客户端关闭与远程数据库的连接。服务器返回一个字符串,表示此次关闭的结果:
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 服务器客户端时,需传入以下参数:
directoryServiceURL:注册了该 SQL 服务器的目录服务的 RMI URL
driver:SQL 服务器必须用于管理数据库的驱动程序
urlBase:待管理数据库的 JDBC URL
id:客户端 ID,若无 ID 则为 null
password:客户端密码,若无密码则为 null
separator:SQL 服务器必须用于分隔查询结果行中字段的字符
以下是一个可能的参数示例:
其中:
SQL 服务器的 RMI 名称
适用于具有 ODBC 接口的数据库的标准驱动程序
用于使用 Windows 计算机公共 ODBC 数据库中列出的 articles 数据库
无标识
无密码
结果中的字段将用逗号分隔
使用上述参数启动后,客户端将执行以下步骤:
- 它们连接到 RMI 服务器 srvSQL,该服务器与客户端位于同一台机器上
- 请求连接到articles数据库
- 它会提示用户输入一个 SQL 查询
- 将其发送至 SQL 服务器
- 它在屏幕上显示服务器返回的结果
- 它再次提示用户在键盘上输入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 计算机上使用服务器和客户端进行测试
- 目录服务在与服务器和客户端不同的目录中启动
- 将以下 mypolicy 文件放置在客户端和服务器目录中
- 启动服务器
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
- 重新启动目录服务
- 使用与上次测试不同的参数重启服务器
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. 结论
这是一个很有趣的应用程序,因为它允许从网络上的任何工作站访问数据库。我们完全可以采用传统方式使用套接字来编写它,这实际上正是数据库章节中某项练习的要求。如果我们要用传统方式编写这个应用程序:
- 客户端和服务器可能使用不同的编程语言编写
- 客户端和服务器将通过交换文本行进行通信,其对话可能类似于以下形式:
其中前两个参数指定服务器的位置,后四个参数则表示所用数据库的连接参数
服务器可能会返回类似以下的内容:
用于请求服务器在连接到客户端的数据库上执行 SQL 查询。分隔符是用于分隔响应行中各字段的字符。
服务器可能会返回类似以下的内容:
,其中 n 表示被更新的行数
如果查询引发了错误
如果查询未返回任何结果
如果查询返回了结果。服务器返回的行即为查询结果。
用于关闭与远程数据库的连接。服务器可能会返回一个字符串,表示此次关闭的结果:
由此可见,如果我们能够使用上述类型的协议构建一个传统应用程序,就可以推导出 RMI 服务器的可能结构。在协议中,我们有一个从客户端发往服务器的消息,其类型为:
那么在 RMI 服务器中,我们可以定义一个方法
该方法对客户端可见,因此必须包含在服务器发布的接口中。
总结一下,请注意我们的服务器目前仅处理一个客户端:按当前实现方式,它无法处理多个客户端。实际上,如果一个客户端连接到数据库 B1,服务器会创建一个 DB=DB1 的 Connection 对象。如果第二个客户端请求连接到数据库 B2,服务器会建立一个 DB=DB2 的 Connection 连接,从而中断了第一个客户端与数据库 B1 的连接。
9.4. 练习
9.4.1. 练习 1
扩展前面的 SQL 服务器,使其能够处理多个客户端。
9.4.2. 练习 2
编写JDBC章节练习中介绍的Java电子商务小程序,使其能够与上一练习中的RMI服务器配合工作。