9. JAVA RMI
9.1. 引言
我们已经了解了如何使用名为 sockets 的通信工具来创建网络应用程序。在基于这些工具构建的客户端/服务器应用程序中,客户端与服务器之间的连接就是它们为进行通信而采用的通信协议。 这两个应用程序可以使用不同的语言编写:例如客户端使用 Java,服务器使用 Perl,或者任何其他组合。 这确实是两个通过双方共同了解的通信协议连接的独立应用程序。此外,对于 Java 应用程序而言,通过套接字访问网络并非透明操作:它必须使用 Socket 类,该类是专门为管理 sockets 这类通信工具而创建的。
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:对象/服务器的接口
远程对象是类的一个实例,该类必须实现 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 + "]";
}// 回显结束
}// 类结束
在上述类中,我们发现:
- 用于回显的方法
- 一个构造函数,其作用仅是调用父类的构造函数。该构造函数的存在是为了声明它可能抛出类型为 RemoteException 的异常。
我们将通过 main 方法创建该类的实例。要使对象/服务对外可访问,必须先创建该对象并将其注册到外部可访问对象目录中。客户端若要访问远程对象,其操作流程如下:
- 它会向目标对象所在机器的目录服务发起请求。该目录服务运行在客户端必须知道的端口上(默认端口为1099)。 客户端向目录服务请求指定名称的对象/服务的引用。如果该名称属于目录中的对象/服务,目录服务将向客户端返回一个引用,客户端可通过该引用与远程对象/服务进行交互。
- 从这一刻起,客户端即可像操作本地对象一样使用该远程对象
回到我们的服务器,我们需要创建一个类型为 srvEcho 的对象,并将其注册到外部可访问对象目录中。此注册操作通过 Naming 类的 rebind 方法完成:
其中
名称:将与远程对象关联的名称
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.class 和 srvEcho.class。
9.2.1.4. 步骤 4:编写客户端
编写一个客户端程序,向其传递回显服务器的 URL 作为参数,该程序
- 读取键盘输入的一行内容
- 将其发送至回显服务器
- 显示服务器返回的响应
- 循环回到步骤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
}// 类
该客户端没有什么特别之处,除了那条请求服务器参考信息的指令:
我们记得,我们的回显服务已通过以下指令注册到所在机器的服务目录中:
Naming.rebind("srvEcho",serveurEcho);
因此,该客户端同样使用 Naming 类的某个方法来获取其想要使用的服务器的引用。所使用的 lookup 方法接受请求服务的 URL 作为参数。该 URL 采用标准格式:
其中
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
在客户端目录中,可以找到以下源文件:
以及在服务器编译时生成的文件 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 的 DOS 窗口中将其作为后台任务启动。目录服务启动后,即可创建回显服务并将其注册到服务目录中。同样,它也是通过 start 命令作为后台任务启动的:
回显服务器在新的 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类,但因缺少“安全管理器”而失败。我们记得在文档中看到过相关内容。 我们重新查阅了文档……发现服务器必须创建并安装一个安全管理器(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.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
看来安全管理器的工作做得太过彻底了。我们重新查阅了文档……发现当安全管理器处于活动状态时,在启动程序时必须明确其权限。这可以通过以下选项实现:
start j:\jdk12\bin\java -Djava.security.policy=mypolicy srvEcho
其中
java.security.policy 是一个关键字
mypolicy 是一个定义程序权限的文本文件。此处的文件内容如下:
该程序在此拥有所有权限。
重新开始。进入服务器目录,依次执行:
- 启动目录服务: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 数据库。

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