实现远程interface

本节讨论为计算引擎实现类的任务。通常,实现远程interface的类至少应执行以下操作:

  • 声明正在实现的远程interface

  • 为每个远程对象定义构造函数

  • 提供远程interface中每个远程方法的实现

RMI 服务器程序需要创建初始的远程对象并将其导出到 RMI 运行时,这使它们可用于接收传入的远程调用。此设置过程可以封装在远程对象实现类本身的方法中,也可以完全包含在另一个类中。设置过程应执行以下操作:

  • 创建并安装安全 管理 器

  • 创建和导出一个或多个远程对象

  • 为了引导 Object,至少向 RMI 注册表(或向另一个命名服务,例如可通过 Java 命名和目录interface访问的服务)注册一个远程对象。

接下来是计算引擎的完整实现。 engine.ComputeEngine类实现远程interfaceCompute,并且还包括用于设置计算引擎的main方法。这是ComputeEngine类的源代码:

package engine;

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import compute.Compute;
import compute.Task;

public class ComputeEngine implements Compute {

    public ComputeEngine() {
        super();
    }

    public <T> T executeTask(Task<T> t) {
        return t.execute();
    }

    public static void main(String[] args) {
        if (System.getSecurityManager() == null) {
            System.setSecurityManager(new SecurityManager());
        }
        try {
            String name = "Compute";
            Compute engine = new ComputeEngine();
            Compute stub =
                (Compute) UnicastRemoteObject.exportObject(engine, 0);
            Registry registry = LocateRegistry.getRegistry();
            registry.rebind(name, stub);
            System.out.println("ComputeEngine bound");
        } catch (Exception e) {
            System.err.println("ComputeEngine exception:");
            e.printStackTrace();
        }
    }
}

以下各节讨论计算引擎实现的每个组件。

声明正在实现的远程interface

计算引擎的实现类声明如下:

public class ComputeEngine implements Compute

该声明指出该类实现了Compute远程interface,因此可以用于远程对象。

ComputeEngine类定义一个远程对象实现类,该类实现单个远程interface而没有其他interface。 ComputeEngine类还包含两个只能在本地调用的可执行程序元素。这些元素中的第一个是ComputeEngine实例的构造函数。这些元素的第二个是main方法,该方法用于创建ComputeEngine实例并将其提供给 Client 端。

定义远程对象的构造方法

ComputeEngine类具有一个不带任何参数的构造函数。构造函数的代码如下:

public ComputeEngine() {
    super();
}

该构造函数仅调用超类构造函数,它是Object类的无参数构造函数。尽管即使从ComputeEngine构造函数中省略了超类构造函数,也会将其调用,但为清楚起见将其包括在内。

为每种远程方法提供实现

远程对象的类为远程interface中指定的每个远程方法提供实现。 Computeinterface包含一个远程方法executeTask,其实现方式如下:

public <T> T executeTask(Task<T> t) {
    return t.execute();
}

此方法实现ComputeEngine远程对象与其 Client 端之间的协议。每个 Client 端为ComputeEngine提供Task对象,该对象具有Taskinterface的execute方法的特定实现。 ComputeEngine执行每个 Client 端的任务,并将任务的execute方法的结果直接返回给 Client 端。

在 RMI 中传递对象

远程方法的参数或从远程方法返回的值几乎可以是任何类型,包括本地对象,远程对象和原始数据类型。更准确地说,任何类型的任何实体都可以传入或传出远程方法,只要该实体是作为原始数据类型,远程对象或可序列化对象的类型的实例即可,这意味着它实现了interfacejava.io.Serializable.

一些对象类型不满足这些条件中的任何一个,因此无法传递给远程方法或从远程方法返回。这些对象中的大多数(例如线程或文件 Descriptors)都封装了仅在单个地址空间内有意义的信息。许多核心类(包括软件包java.langjava.util中的类)都实现Serializableinterface。

控制如何传递参数和返回值的规则如下:

  • 远程对象本质上是通过引用传递的。远程对象引用是一个存根,它是一个 Client 端代理,可实现该远程对象实现的完整的远程interface集。

  • 使用对象序列化通过副本传递本地对象。默认情况下,将复制所有字段,但标记为statictransient的字段除外。可以逐级覆盖默认序列化行为。

通过引用传递远程对象意味着通过远程方法调用对对象状态所做的任何更改都将反映在原始远程对象中。传递远程对象时,只有作为远程interface的那些interface可用于接收器。在实现类中定义的方法或在由该类实现的非远程interface中定义的任何方法均不适用于该接收者。

例如,如果要将引用传递给ComputeEngine类的实例,则接收方只能访问计算引擎的executeTask方法。该接收者将看不到ComputeEngine构造函数,其main方法或java.lang.Object的任何方法的实现。

在远程方法调用的参数和返回值中,不是远程对象的对象将按值传递。因此,在接收 Java 虚拟机中创建对象的副本。接收者对对象状态的任何更改仅反映在接收者的副本中,而不反映在发送者的原始实例中。发送者对对象状态的任何更改仅反映在发送者的原始实例中,而不反映在接收者的副本中。

实现服务器的主要方法

ComputeEngine实现中最复杂的方法是main方法。 main方法用于启动ComputeEngine,因此需要进行必要的初始化和内务处理,以使服务器准备好接受来自 Client 端的呼叫。此方法不是远程方法,这意味着无法从其他 Java 虚拟机调用它。因为main方法被声明为static,所以该方法根本不与对象关联,而与ComputeEngine类关联。

创建和安装安全 管理 器

main方法的首要任务是创建并安装安全 管理 器,以保护对系统资源的访问免受 Java 虚拟机中运行的不可信下载代码的访问。安全 管理 器确定下载的代码是否可以访问本地文件系统或可以执行任何其他特权操作。

如果 RMI 程序未安装安全 管理 器,则 RMI 将不会下载作为自变量接收的对象或从远程方法调用返回的值的类(从本地 Classpath 之外)。此限制确保了由下载的代码执行的操作必须遵守安全策略。

这是创建和安装安全 管理 器的代码:

if (System.getSecurityManager() == null) {
    System.setSecurityManager(new SecurityManager());
}

使远程对象可供 Client 端使用

接下来,main方法创建ComputeEngine的实例,并使用以下语句将其导出到 RMI 运行时:

Compute engine = new ComputeEngine();
Compute stub =
    (Compute) UnicastRemoteObject.exportObject(engine, 0);

静态UnicastRemoteObject.exportObject方法导出提供的远程对象,以便它可以从远程 Client 端接收其远程方法的调用。第二个参数int指定用于侦听该对象的传入远程调用请求的 TCP 端口。通常使用零值,该值指定使用匿名端口。然后,实际端口将在运行时由 RMI 或基础 os 选择。但是,也可以使用非零值来指定用于侦听的特定端口。 exportObject调用成功返回后,ComputeEngine远程对象已准备就绪,可以处理传入的远程调用。

exportObject方法返回导出的远程对象的存根。请注意,变量stub的类型必须为Compute,而不是ComputeEngine,因为远程对象的存根仅实现导出的远程对象实现的远程interface。

exportObject方法声明可以抛出RemoteException,这是一个已检查的异常类型。 main方法使用其try/catch块处理此异常。如果未通过这种方式处理异常,则必须在main方法的throws子句中声明RemoteException。如果必需的通信资源不可用,例如,如果请求的端口绑定用于其他 Object,则try导出远程对象可能会抛出RemoteException

在 Client 端可以调用远程对象上的方法之前,Client 端必须首先获取对该远程对象的引用。可以通过在程序中获得任何其他对象引用的方式来获得引用,例如,通过将引用作为方法返回值的一部分或包含此类引用的数据结构的一部分来获取。

系统提供一种特殊类型的远程对象 RMI 注册表,用于查找对其他远程对象的引用。 RMI 注册表是一种简单的远程对象命名服务,使 Client 端能够通过名称获得对远程对象的引用。注册表通常仅用于定位 RMIClient 端需要使用的第一个远程对象。然后,第一个远程对象可能会为查找其他对象提供支持。

java.rmi.registry.Registry远程interface是用于绑定(或注册)和在注册表中查找远程对象的 API。 java.rmi.registry.LocateRegistry类提供了静态方法,用于综合对特定网络地址(主机和端口)上的注册表的远程引用。这些方法将创建包含指定网络地址的远程引用对象,而不执行任何远程通信。 LocateRegistry还提供了用于在当前 Java 虚拟机中创建新注册表的静态方法,尽管本示例未使用这些方法。在 localhost 上的 RMI 注册表中注册了远程对象后,任何主机上的 Client 端都可以按名称查找远程对象,获取其引用,然后在该对象上调用远程方法。注册表可以由主机上运行的所有服务器共享,或者单个服务器进程可以创建和使用其自己的注册表。

ComputeEngine类使用以下语句为对象创建名称:

String name = "Compute";

然后,代码将名称添加到服务器上运行的 RMI 注册表中。稍后使用以下语句完成此步骤:

Registry registry = LocateRegistry.getRegistry();
registry.rebind(name, stub);

rebind调用对 localhost 上的 RMI 注册表进行远程调用。像任何远程调用一样,此调用可能导致引发RemoteException,这由main方法末尾的catch块处理。

请注意以下有关Registry.rebind调用的内容:

  • LocateRegistry.getRegistry的无参数重载将对 localhost 和默认注册表端口 1099 上的注册表的引用进行综合。如果注册表是在 1099 以外的端口上创建的,则必须使用具有int参数的重载。

  • 在注册表上进行远程调用时,将传递远程对象的存根而不是远程对象本身的副本。远程实现对象(例如ComputeEngine的实例)永远不会离开创建它们的 Java 虚拟机。因此,当 Client 端在服务器的远程对象注册表中执行查找时,将返回存根的副本。因此,在这种情况下,远程对象通过(远程)引用而不是通过值有效地传递。

  • 出于安全原因,应用程序只能在同一个主机上运行带有注册表的bindunbindrebind远程对象引用。此限制可防止远程 Client 端删除或覆盖服务器注册表中的任何条目。但是,可以从任何本地或远程主机请求lookup

服务器在本地 RMI 注册表中注册后,将显示一条消息,表明它已准备好开始处理呼叫。然后,main方法完成。不必 await 线程使服务器保持活动状态。只要在本地或远程的另一个 Java 虚拟机中都引用了ComputeEngine对象,就不会关闭ComputeEngine对象或对其进行垃圾回收。因为该程序将对ComputeEngine的引用绑定到注册表中,所以可以从远程 Client 端(注册表本身)访问它。 RMI 系统使ComputeEngine的进程保持运行。 ComputeEngine可用于接受呼叫,直到将其绑定从注册表中删除并且没有远程 Client 端拥有对ComputeEngine对象的远程引用之前,它不会被收回。

ComputeEngine.main方法中的最后一段代码将处理可能出现的任何异常。通过UnicastRemoteObject.exportObject调用或通过注册表rebind调用,可以在代码中引发的唯一已检查异常类型为RemoteException。在这两种情况下,该程序都只能执行打印错误消息后退出的操作。在某些分布式应用程序中,可以从故障中恢复以进行远程调用。例如,应用程序可能try重试该操作,或者选择其他服务器 continue 该操作。