1. 背景

这学期上了《分布式系统》课程,内容主要是基于Java实现分布式计算,所以老师前几节课主要在给我们讲用Java做分布式可能会用到的一些技术。为了方便学习和记录,我将老师讲的内容结合资料做了一些整理,这一篇主要讨论Java远程方法调用(RMI)。

下面的内容主要翻译自Java官方文档。我使用的是Google翻译,翻完了以后如果有不太对的地方就手动调整了一下,难免有一些地方翻译得不是很清楚,欢迎指出,或直接查看官方文档原文。

阅读时间:34 min (9397 字)

2. RMI 概述

  • 定义Java远程方法调用(RMI)系统允许在一个Java虚拟机中运行的对象调用在另一个Java虚拟机中运行的对象的方法。RMI提供了用Java编程语言编写的程序之间的远程通信。

:如果要连接到现有的IDL程序,应该使用Java IDL而不是RMI。

本文简要概述了RMI系统,然后介绍了一个完整的客户端/服务器示例,该示例使用RMI的独特功能在运行时加载和执行用户定义的任务。示例中的服务器实现了一个通用的计算引擎,客户端使用该引擎计算 π \pi π 符号的值。

3. 什么是 RMI 应用

3.1. 定义

  • 定义RMI应用程序通常包含两个独立的程序,一个服务端和一个客户端。

    • 典型的服务端程序会创建一些远程对象,使这些对象的引用可被访问,并等待客户端调用这些对象上的方法。
    • 典型的客户端程序会获得对服务器上一个或多个远程对象的远程引用,然后调用这些对象上的方法。RMI提供了服务器和客户机之间通信和来回传递信息的机制。这样的应用程序有时被称为分布式对象应用程序。
  • 分布式对象应用程序的功能:

    • 定位远程对象。应用程序可以使用各种机制来获取对远程对象的引用。例如,一个应用程序可以用RMI的简单命名工具——RMI注册表来注册它的远程对象。或者,应用程序可以作为其他远程调用的一部分传递和返回远程对象引用。
    • 与远程对象通信。远程对象之间通信的细节由RMI处理。对于程序员来说,远程通信类似于普通的Java方法调用。
    • 加载传递的对象的类定义。因为RMI允许对象来回传递,所以它提供了加载对象的类定义以及传输对象数据的机制。

下面的插图描述了一个RMI分布式应用程序,它使用RMI注册表来获取对远程对象的引用。

  • 服务器调用注册表将名称与远程对象关联(或绑定)。

  • 客户端通过服务器注册表中的名称查找远程对象,然后在其上调用一个方法。

  • 图中还显示了RMI系统使用一个现有的web服务器来加载类定义,从服务器到客户端,从客户端到服务器,在需要的时候加载对象。

<mtext> Figure 1. RMI应用程序 </mtext> \text{Figure 1. RMI应用程序} Figure 1. RMI应用程序

3.2. 动态代码加载的优点

RMI的核心和独特的特性之一是,如果对象的类没有在接收方的Java虚拟机中定义,它就能够下载对象的类的定义。对象的所有类型和行为(以前只能在单个Java虚拟机中使用)都可以传输到另一个(可能是远程的)Java虚拟机。RMI通过对象的实际类传递对象,因此当对象被发送到另一个Java虚拟机时,对象的行为不会改变。该功能允许将新的类型和行为引入远程Java虚拟机,从而动态扩展应用程序的行为。本文中的计算引擎示例使用此功能为分布式程序引入新的行为。

3.3. 远程接口、对象和方法

与任何其他Java应用程序一样,使用Java RMI构建的分布式应用程序由接口和类组成。接口声明方法。这些类实现接口中声明的方法,可能还声明其他方法。在分布式应用程序中,一些实现可能驻留在一些Java虚拟机中,而不是其他虚拟机。具有可以跨Java虚拟机调用的方法的对象称为远程对象。

对象通过实现远程接口而成为远程对象,远程接口具有以下特征:

  • 远程接口扩展了java.rmi.Remote接口。
  • 接口的每个方法在它的throws子句中都声明了java.rmi.RemoteException,以及任何特定于应用程序的异常。

当对象从一个Java虚拟机传递到另一个Java虚拟机时,RMI将远程对象与非远程对象区别对待。RMI不是在接收方的Java虚拟机中复制实现对象,而是为远程对象传递远程存根(Stub)。存根充当远程对象的本地代表或***,对于客户端来说,它基本上是远程引用。客户端调用本地存根上的方法,该方法负责对远程对象执行方法调用。

远程对象的存根(Stub)实现远程对象实现的同一组远程接口。此属性允许将存根转换为远程对象实现的任何接口。但是,只有在远程接口中定义的那些方法可以从接收的Java虚拟机中调用。

图2-3能够更加清晰地描述RMI的交互过程。

<mtext> Figure 2. RMI的交互图 </mtext> \text{Figure 2. RMI的交互图} Figure 2. RMI的交互图

<mtext> Figure 3. 存根和骨干网的具体通信过程 </mtext> \text{Figure 3. 存根和骨干网的具体通信过程} Figure 3. 存根和骨干网的具体通信过程

3.4. 使用RMI创建分布式应用程序

使用RMI来开发一个分布式应用程序包括以下一般步骤:

  1. 设计和实现分布式应用程序的组件
  2. 编译源文件
  3. 使类可以通过网络访问
  4. 启动应用程序

3.4.1. 设计和实现应用程序组件

首先,确定您的应用程序体系结构,包括哪些组件是本地对象,哪些组件可以远程访问。这个步骤包括:

  • 定义远程接口。远程接口指定客户端可以远程调用的方法。客户端面向远程接口编程,而不是面向那些接口的实现类。这些接口的设计包括将用作这些方法的参数和返回值的对象类型的确定。如果这些接口或类中的任何一个还不存在,您也需要定义它们。

  • 实现远程对象。远程对象必须实现一个或多个远程接口。远程对象类可以包括仅在本地可用的其他接口和方法的实现。如果要将任何本地的类作为这些方法的参数或返回值使用,那么这些本地的类也必须实现它们。

  • 实现客户端。使用远程对象的客户端可以在定义远程接口之后的任何时候实现,包括在部署远程对象之后。

3.4.2. 编译源文件

与任何Java程序一样,使用javac编译器编译源文件。源文件包含远程接口的声明、它们的实现、任何其他服务端类和客户端类。

:对于Java平台之前的版本,即标准版5.0,通过使用rmic编译器构建存根类需要一个额外的步骤。然而,这一步已经没有必要了。

3.4.3. 使类可以通过网络访问

在此步骤中,你将使某些类定义可以通过网络访问,例如远程接口及其关联类型的定义,以及需要下载到客户端或服务器的类的定义。类定义通常通过web服务器进行网络访问。

3.4.4. 启动应用程序

启动应用程序包括运行RMI远程对象注册表、服务器和客户端。

本节的其余部分将介绍用于创建计算引擎的步骤。

3.5. 构建一个通用的计算引擎

下面的内容主要介绍一个简单但功能强大的分布式应用程序,即计算引擎。计算引擎是服务器上的一个远程对象,它从客户端获取任务,运行任务并返回任何结果。任务在服务器运行的机器上运行。这种类型的分布式应用程序可以使许多客户端机器能够使用特别强大的机器或具有专门硬件的机器。

计算引擎的新颖之处在于,在编写或启动计算引擎时,不需要定义它运行的任务。可以在任何时候创建新类型的任务,然后将其交给要运行的计算引擎。任务的惟一要求是其类实现特定的接口。完成任务所需的代码可以由RMI系统下载到计算引擎。然后,计算引擎运行任务,使用正在运行计算引擎的机器上的资源。

执行任意任务的能力是由Java平台的动态特性所支持的,它通过RMI扩展到网络。RMI动态地将任务代码加载到计算引擎的Java虚拟机中,并在不知道实现任务的类的情况下运行任务。这种能够动态下载代码的应用程序通常被称为基于行为的应用程序。此类应用程序通常需要完整的启用***的基础设施。对于RMI,这样的应用程序是Java平台上分布式计算的基本机制的一部分。

4. 如何实现一个 RMI 服务器

服务器从客户端接收任务,运行任务并返回任何结果。服务器代码由接口和类组成。接口定义了可以从客户端调用的方法。本质上,接口定义了远程对象的客户端视图。类定义了方法的实现细节。

这一章主要分为两个部分

  1. Designing a Remote Interface

第一部分定义了Compute接口,它提供客户端和服务器之间的连接。通过这一节内容,你还将了解支持这种通信的RMI API

  1. Implementing a Remote Interface

第二部分将讨论如何实现Compute接口的类,从而实现远程对象。该类还提供组成服务器程序的其余代码,包括创建远程对象实例、向RMI注册表注册该对象并设置安全管理器的main方法。

4.1. 设计远程接口

计算引擎的核心是协议,该协议使任务可以提交到计算引擎,计算引擎可以运行这些任务,并将这些任务的结果返回给客户端。该协议在计算引擎支持的接口中表示。 下图说明了此协议的远程通信。

<mtext> Figure 4. 远程通信 </mtext> \text{Figure 4. 远程通信} Figure 4. 远程通信

每个接口都包含一个方法。 计算引擎的远程接口Compute使任务可以提交到引擎。 客户端接口Task定义了计算引擎如何执行提交的任务。

compute.Compute接口定义了可远程访问的部分,即计算引擎本身。 这是Compute接口的源代码:

package compute;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Compute extends Remote {
    <T> T executeTask(Task<T> t) throws RemoteException;
}

通过扩展接口java.rmi.RemoteCompute接口将其自身标识为可以从另一个Java虚拟机调用其方法的接口。 实现此接口的任何对象都可以是远程对象。

作为远程接口的成员,executeTask方法是一种远程方法。 因此,必须将此方法定义为能够抛出java.rmi.RemoteExceptionRMI系统从远程方法调用中抛出此异常,以表明发生通信故障或协议错误。 RemoteException是一个要求被捕获的异常,因此任何调用远程方法的代码都需要通过捕获该异常或在其throws子句中声明该异常来对其进行处理。

计算引擎所需的第二个接口是Task接口,它是Compute接口中executeTask方法的参数类型。 compute.Task接口定义了计算引擎与其需要完成的工作之间的接口,从而提供了开始工作的方式。 这是Task界面的源代码:

package compute;

public interface Task<T> {
    T execute();
}

Task接口定义了一个方法execute,它没有参数,也不会引发异常。 因为该接口未扩展Remote,所以该接口中的方法不需要在其throws子句中列出java.rmi.RemoteException

Task接口具有类型参数T,它表示任务计算的结果类型。 此接口的execute方法返回计算结果,因此其返回类型为T

反过来,Compute接口的executeTask方法返回传递给它的Task实例的执行结果。 因此,executeTask方法具有自己的类型参数T,该参数将其自身的返回类型与传递的Task实例的结果类型相关联。

RMI使用Java对象序列化机制在Java虚拟机之间按值传输对象。 对于被视为可序列化的对象,其类必须实现java.io.Serializable标记接口。 因此,实现Task接口的类也必须实现Serializable,用于任务结果的对象的类也必须实现。

只要任务类型是Task的实现,就可以由Compute对象运行各种任务。实现此接口的类可以包含任务计算所需的任何数据以及计算所需的任何其他方法。

这就是RMI如何使这种简单的计算引擎成为可能。因为RMI可以假定Task对象是用Java编程语言编写的,所以RMI将根据需要将计算引擎先前未知的Task对象的实现下载到计算引擎的Java虚拟机中。此功能使计算引擎的客户端可以定义要在服务器计算机上运行的新任务,而无需在该计算机上显式安装代码。

ComputeEngine类实现的计算引擎实现Compute接口,从而允许通过调用其executeTask方法将不同的任务提交给它。使用任务的execute方法实现运行这些任务,并将结果返回到远程客户端。

4.2. 实现远程接口

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

  • 声明要被实现的远程接口
  • 为每个远程对象定义构造函数
  • 提供远程接口中每个远程方法的实现

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

  • 创建并安装安全管理器
  • 创建和导出一个或多个远程对象
  • 为了引导目的,至少向RMI注册表(或向另一个命名服务,例如可通过Java命名和目录接口访问的服务)注册一个远程对象。

接下来是计算引擎的完整实现。 engine.ComputeEngine类实现远程接口Compute,并且还包括用于设置计算引擎的主要方法。 这是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();
        }
    }
}

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

4.2.1. 声明正在实现的远程接口

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

public class ComputeEngine implements Compute

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

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

4.2.2. 定义远程对象的构造方法

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

public ComputeEngine() {
    super();
}

该构造函数仅调用超类构造函数,它是Object类的无参数构造函数。 尽管这里可以省略,但为了描述清楚还是将其包括在内。

4.2.3. 提供每种远程方法的实现

远程对象的类为远程接口中指定的每个远程方法提供实现。 Compute接口包含一个远程方法executeTask,该方法如下实现:

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

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

4.2.4. 在RMI中传递对象

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

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

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

  • 远程对象本质上是通过引用传递的。 远程对象引用是一个存根,它是一个客户端***,可实现该远程对象实现的完整的远程接口集。
  • 使用对象序列化,本地对象通过副本传递。 默认情况下,将复制所有字段,但标记为静态或瞬态的字段除外。 可以逐级覆盖默认序列化行为。

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

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

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

4.2.5. 实现服务器端的main方法

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

4.2.6. 创建和安装安全管理器

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

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

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

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

4.2.7. 使远程对象可供客户端使用

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

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

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

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

exportObject方法声明它可以引发RemoteException,这是一个经过检查的异常类型。 main方法使用其try / catch块处理此异常。如果未通过这种方式处理异常,则必须在main方法的throws子句中声明RemoteException。如果没有必要的通信资源(例如,请求的端口绑定用于其他目的),则尝试导出远程对象可能会引发RemoteException

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

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

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

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

String name = "Compute";

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

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

此重新绑定调用对本地主机上的RMI注册表进行了远程调用。像任何远程调用一样,此调用可能导致引发RemoteException,该异常由main方法末尾的catch块处理。

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

  • LocateRegistry.getRegistry的无参数重载将对本地主机和默认注册表端口1099上注册表的引用进行综合。如果在1099以外的端口上创建了注册表,则必须使用具有int参数的重载。
  • 在注册表上进行远程调用时,将传递远程对象的存根而不是远程对象本身的副本。诸ComputeEngine实例之类的远程实现对象永远不会离开创建它们的Java虚拟机。因此,当客户端在服务器的远程对象注册表中执行查找时,将返回存根的副本。因此,在这种情况下,远程对象通过(远程)引用而不是通过值有效地传递。
  • 出于安全原因,应用程序只能与在同一主机上运行的注册表绑定,取消绑定或重新绑定远程对象引用。此限制可防止远程客户端删除或覆盖服务器注册表中的任何条目。但是,可以从任何本地或远程主机请求查找。

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

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

5. 如何创建一个 RMI 客户端

计算引擎是一个相对简单的程序:它运行传递给它的任务。 计算引擎的客户端更为复杂。客户端需要调用计算引擎,但还必须定义要由计算引擎执行的任务。

在我们的示例中,两个单独的类构成了客户端。第一个类ComputePi查找并调用Compute对象。第二个类,Pi,实现了Task接口并定义了计算引擎要完成的工作。 Pi类的工作是将 π \pi π 符号的值计算为小数位数。

非远程任务接口定义如下:

package compute;

public interface Task<T> {
    T execute();
}

调用Compute对象的方法的代码必须获得对该对象的引用,创建Task对象,然后请求执行该任务。 任务类Pi的定义在后面示出。Pi对象使用单个参数(结果的所需精度)构造。任务执行的结果是java.math.BigDecimal,表示以指定的精度计算出的 π \pi π 符号。

这是主要客户端类client.ComputePi的源代码:

package client;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.math.BigDecimal;
import compute.Compute;

public class ComputePi {
    public static void main(String args[]) {
        if (System.getSecurityManager() == null) {
            System.setSecurityManager(new SecurityManager());
        }
        try {
            String name = "Compute";
            Registry registry = LocateRegistry.getRegistry(args[0]);
            Compute comp = (Compute) registry.lookup(name);
            Pi task = new Pi(Integer.parseInt(args[1]));
            BigDecimal pi = comp.executeTask(task);
            System.out.println(pi);
        } catch (Exception e) {
            System.err.println("ComputePi exception:");
            e.printStackTrace();
        }
    }
}

ComputeEngine服务器一样,客户端从安装安全管理器开始。此步骤是必需的,因为接收服务器远程对象的存根的过程可能需要从服务器下载类定义。要使RMI下载类,必须启用安全管理器。

安装安全管理器后,客户端将使用ComputeEngine用来绑定其远程对象的相同名称来构建用于查找Compute远程对象的名称。此外,客户端使用LocateRegistry.getRegistry API来合成对服务器主机上注册表的远程引用。第一个命令行参数args [0]的值是运行Compute对象的远程主机的名称。然后,客户端在注册表上调用lookup方法,以在服务器主机的注册表中按名称查找远程对象。使用的LocateRegistry.getRegistry的特定重载(具有单个String参数)返回对命名主机和默认注册表端口1099处的注册表的引用。如果在1099以外的端口创建了注册表,则必须使用具有int参数的重载。

接下来,客户端创建一个新的Pi对象,将解析为整数的第二个命令行参数args [1]的值传递给Pi构造函数。此参数指示在计算中使用的小数位数。最后,客户端调用Compute远程对象的executeTask方法。传递给executeTask调用的对象将返回BigDecimal类型的对象,程序会将其存储在变量result中。最后,程序将打印结果。下图描述了ComputePi客户端,rmiregistryComputeEngine之间的消息流。

<mtext> Figure 5. 消息流 </mtext> \text{Figure 5. 消息流} Figure 5. 消息流

Pi类实现Task接口,并将 π \pi π 符号的值计算为指定的小数位数。对于此示例,实际算法并不重要。重要的是该算法的计算量很大,这意味着您希望在功能强大的服务器上执行该算法。

以下是实现Task接口的类client.Pi的源代码:

package client;

import compute.Task;
import java.io.Serializable;
import java.math.BigDecimal;

public class Pi implements Task<BigDecimal>, Serializable {

    private static final long serialVersionUID = 227L;

    /** constants used in pi computation */
    private static final BigDecimal FOUR = BigDecimal.valueOf(4);

    /** rounding mode to use during pi computation */
    private static final int roundingMode = BigDecimal.ROUND_HALF_EVEN;

    /** digits of precision after the decimal point */
    private final int digits;

    /** * Construct a task to calculate pi to the specified precision. */
    public Pi(int digits) {
        this.digits = digits;
    }

    /** * Calculate pi. */
    public BigDecimal execute() {
        return computePi(digits);
    }

    /** * Compute the value of pi to the specified number of digits after the decimal * point. The value is computed using Machin's formula: * * pi/4 = 4*arctan(1/5) - arctan(1/239) * * and a power series expansion of arctan(x) to sufficient precision. */
    public static BigDecimal computePi(int digits) {
        int scale = digits + 5;
        BigDecimal arctan1_5 = arctan(5, scale);
        BigDecimal arctan1_239 = arctan(239, scale);
        BigDecimal pi = arctan1_5.multiply(FOUR).subtract(arctan1_239).multiply(FOUR);
        return pi.setScale(digits, BigDecimal.ROUND_HALF_UP);
    }

    /** * Compute the value, in radians, of the arctangent of the inverse of the * supplied integer to the specified number of digits after the decimal point. * The value is computed using the power series expansion for the arc tangent: * * arctan(x) = x - (x^3)/3 + (x^5)/5 - (x^7)/7 + (x^9)/9 ... */
    public static BigDecimal arctan(int inverseX, int scale) {
        BigDecimal result, numer, term;
        BigDecimal invX = BigDecimal.valueOf(inverseX);
        BigDecimal invX2 = BigDecimal.valueOf(inverseX * inverseX);

        numer = BigDecimal.ONE.divide(invX, scale, roundingMode);

        result = numer;
        int i = 1;
        do {
            numer = numer.divide(invX2, scale, roundingMode);
            int denom = 2 * i + 1;
            term = numer.divide(BigDecimal.valueOf(denom), scale, roundingMode);
            if ((i % 2) != 0) {
                result = result.subtract(term);
            } else {
                result = result.add(term);
            }
            i++;
        } while (term.compareTo(BigDecimal.ZERO) != 0);
        return result;
    }
}

请注意,所有可序列化类(无论它们是直接实现还是间接实现Serializable接口)都必须声明一个名为serialVersionUID的私有静态最终字段,以确保版本之间的序列化兼容性。如果没有发布该类的先前版本,则该字段的值可以是任何长值,类似于Pi使用的227L,只要该值在以后的版本中始终使用即可。如果已发布该类的先前版本而没有显式serialVersionUID声明,但是与该版本的序列化兼容性很重要,则必须将先前版本的默认隐式计算值用作新版本的显式声明的值。可以针对先前版本运行serialver工具,以确定其默认计算值。

此示例最有趣的功能是,在将Pi对象作为参数传递给executeTask方法之前,Compute实现对象永远不需要Pi类的定义。此时,RMI将用于该类的代码加载到Compute对象的Java虚拟机中,调用execute方法,然后执行任务的代码。结果(在Pi任务的情况下是BigDecimal对象)将返回给调用客户端,在客户端用于打印计算结果。

提供的Task对象计算Pi值的事实与ComputeEngine对象无关。您还可以实现一个任务,例如,通过使用概率算法来生成随机质数。该任务也需要大量计算,因此是传递给ComputeEngine的很好的选择,但是它需要非常不同的代码。当Task对象传递到Compute对象时,也可以下载此代码。就像在需要时引入用于计算 π \pi π 符号的算法的方式一样,将在需要时引入生成随机素数的代码。Compute对象仅知道它接收的每个对象都实现了execute方法。Compute对象不知道,也不需要知道实现的作用。

6. 编译并运行

这一部分每个系统都不太一样,推荐大家直接看官方文档


联系邮箱:curren_wong@163.com

Github:https://github.com/CurrenWong

欢迎转载/Star/Fork,有问题欢迎通过邮箱交流。