Java RMI 浅析

RMI

RMI 协议的全称为 Remote Method Invocation (远程方法调用)协议。

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

分布式对象应用需要做到以下几点:

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

下图描述了一个 RMI 分布式应用程序,它使用 RMI 注册表来获取对远程对象的引用。服务器调用注册表以将名称与远程对象关联(或绑定)。客户端在服务器的注册表中通过其名称查找远程对象,然后调用它的方法。该图还显示,RMI 系统使用现有的 Web 服务器在需要时为对象加载类定义,从服务器到客户端以及从客户端到服务器。

rmi-2

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

:rmi的一次调用的tcp报文

image-20220325031558307

注意:由于rmi协议为应用层协议,基于tcp,所以 wireshark 只能通过源/目的端口 1099 识别rmi协议报文。

构建 RMI 应用

img

远程接口、对象和方法

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

一个对象通过实现一个远程接口变得远程,它具有以下特点:

  • 远程接口扩展了接口 java.rmi.Remote
  • 除了任何特定于应用程序的异常之外,接口的每个方法都声明抛出 java.rmi.RemoteException

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

远程对象的存根实现了与远程对象实现的相同的远程接口集。此属性允许将存根强制转换为远程对象实现的任何接口。但是,只有在远程接口中定义的那些方法才能从接收 Java 虚拟机中调用。

步骤

使用 RMI 开发分布式应用程序涉及以下一般步骤:

  1. 设计和实现分布式应用程序的组件。
  2. 编译源。
  3. 使网络可访问远程Class。
  4. 启动应用程序。

首先,确定应用程序架构,包括哪些组件是本地对象,哪些组件可以远程访问。此步骤包括:

  • 定义远程接口。远程接口指定客户端可以远程调用的方法。客户端编程到远程接口,而不是那些接口的实现类。此类接口的设计包括确定将用作这些方法的参数和返回值的对象类型。如果这些接口或类中的任何一个尚不存在,您还需要定义它们。
  • 实现远程对象。远程对象必须实现一个或多个远程接口。远程对象类可能包括仅在本地可用的其他接口和方法的实现。如果要使用任何本地类作为这些方法中的任何一个的参数或返回值,它们也必须实现。
  • 实施客户。使用远程对象的客户端可以在定义远程接口后的任何时间实现,包括部署远程对象之后。
编译java源文件

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

注意: 对于 Java 平台标准版 5.0 之前的版本,需要一个额外的步骤来使用 rmic编译器来构建存根类。但是,现在不再需要此步骤

注意:服务端与客户端的接口声明的方法数量可以不同,但是客户端所用到的方法,服务端必须也同样声明,负责在调用时会抛出异常。

使网络可访问远程Class

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

启动应用程序

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

客户端

客户端通过 LocateRegistry 工具类的静态方法 getRegistry(String url, int port) 来获取 Registry 实例。

这个 Registry 实例实际上Stub实例:类型为:sun.rmi.registry.RegistryImpl_Stub,但是这是客户端自己本来就存在的Stub类型。

客户端通过注册中心 Registry 实例的 lookup 方法获取远程stub的。

:客户端

package client;

import remote.IRemoteEncoder;

import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIClient {
    public void run(int port) throws RemoteException, NotBoundException {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", port, Socket::new);
        Object remoteObj = registry.lookup("base64Encoder");
        System.out.println("remote object class: "+remoteObj.getClass());
        IRemoteEncoder encoder = (IRemoteEncoder)remoteObj;
        System.out.println("encoder class: "+encoder.getClass());
        Object result1 = encoder.encode("Send by Client 1st");
        System.out.println(result1.getClass());
        System.out.println(result1.toString());
        
        System.out.println(encoder.encode("Send by Client 2nd"));
        System.out.println(encoder.encode("Send by Client 3rd"));
    }

    public static void main(String[] args) throws Exception {
//        System.setProperty("java.rmi.server.useCodebaseOnly","true");
        new RMIClient().run(1099);
    }
}

进行一次lookup获取stub实例的网络:

image-20220324193844130

注意

类型实际上是包含了包名在内的,所以服务端、客户端的远程调用接口的声明要完全一致,否则会抛出 java.rmi.UnmarshalException

服务端

java.rmi.Remote 为一个声明接口,所要远程调用的对象/接口必须直接或间接实现/继承于该接口。

package java.rmi;

public interface Remote {}

同时要为调用的远程接口的方法声明中,抛出的异常包含 java.rmi.RemoteException ;如果不声明抛出该异常,则服务端在绑定远程对象到注册中心时会抛出 java.rmi.server.ExportException

Exception in thread "main" java.rmi.server.ExportException: remote object implements illegal remote interface; nested exception is: 
...
UnicastRemoteObject

image-20220325032447458

image-20220325032523289

接口实现类还需要继承 java.rmi.server.UnicastRemoteObject ,用于生成 Stub(存根)和 Skeleton(骨架)。

  • Stub:可以看作远程对象在本地的一个代理,囊括了远程对象的具体信息,客户端可以通过这个代理和服务端进行交互。

    Stub 类实际上是一个动态代理类,类名为 ClassName+_Stub,由于是代理类,其会继承所有的接口 。

    注意:如果已存在该类,则直接使用该类,例如 RegistryImpl_Stub

  • Skeleton:可以看作为服务端的一个代理,用来处理Stub发送过来的请求,然后去调用客户端需要的请求方法,最终将方法执行结果返回给Stub。

    Skeleton 也是一个动态代理类,类名为 ClassName+_Skel

    注意:如果已存在该类,则直接使用该类,例如 RegistryImpl_Skel

代理类并不是只能通过继承的方式来生成,真正起作用的实际上是 UnicastRemoteObject 构造函数中调用的exportObject((Remote) this, port, csf, ssf) 方法,即将该服务器上的远程对象的 Skel 暴露到一个指定接口中。所以直接通过调用静态方法 exportObject 一样可以实现远程调用。

注意:当端口为0时,会随机为该远程对象指派一个端口。

socket的创建

参数 csfrsf 是分别实现了 createSocketcreateServerSocket 方法的接口实例。

指定该参数是为了使用不同安全场景的需要,比如要对传输内容进行加密时,可使用 SSLServerSocket

RMIClientSocketFactory 创建指定 hostportSocket,用于客户端连接远程服务端:

public interface RMIClientSocketFactory {
    public Socket createSocket(String host, int port) throws IOException;
}

MIServerSocketFactory 用于创建指定 portServerSocket,用于接受客户端的连接:

public interface RMIServerSocketFactory {
    public ServerSocket createServerSocket(int port) throws IOException;
}

如下所示,直接使用 lambda表达式应用方法即可(当然也可以不用该参数,会默认创建):

RMIClientSocketFactory csrfImpl = Socket::new;
RMIServerSocketFactory ssrfImpl = ServerSocket::new;

:接口以及实现类定义

定义接口,用于对数据对象进行编码:

package remote;

import java.rmi.Remote;

public interface IRemoteCaller extends Remote {
    public Object encode(Object param);
}

实现类,用于对象的toString 值进行 base64 编码,返回编码后的String:

package remote;

import java.nio.charset.StandardCharsets;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.Base64;

public class RemoteBase64EncoderImpl extends UnicastRemoteObject implements IRemoteEncoder {
    
    public static int count = 0;

    public RemoteBase64EncoderImpl() throws RemoteException {
        super();
        System.out.println("RemoteCallerImpl constructor invoked.");
    }

    @Override
    public Object encode(Object param) {
        System.out.println("call method invoked: "+count++);
        return new String(Base64.getEncoder().encode(param.toString().getBytes(StandardCharsets.UTF_8)));
    }
}

注意:此处的也可以通过 exportObject 来实现远程Stub在服务器上的端口绑定(要注意端口重复绑定):

public class RemoteBase64EncoderImpl implements IRemoteEncoder {
 public RemoteBase64EncoderImpl() throws RemoteException {
     super();
        unicastRemoteObject.exportObject(this,12345); 之后无需继承 UnicastRemoteObject
        System.out.println("RemoteCallerImpl constructor invoked.");
    }

服务端通过创建一个 Registry 实例开启一个RMI服务,并通过 bind 方法将远程接口对象与名称相绑定。

注意

如果不进行 bind 操作,则 Registry 实例没创建 ServerSocket,也就不会被阻塞。

所有的远程接口中的方法必须声明 throws RemoteException,否则在绑定时会抛出 java.rmi.server.ExportException: remote object implements illegal remote interface

源码:Registry 接口,实际上bind方法就是绑定了一个 Remote 接口实例

package java.rmi.registry;

public interface Registry extends Remote {
    int REGISTRY_PORT = 1099;
    Remote lookup(String var1) throws RemoteException, NotBoundException, AccessException;
    void bind(String var1, Remote var2) throws RemoteException, AlreadyBoundException, AccessException;
    void unbind(String var1) throws RemoteException, NotBoundException, AccessException;
    void rebind(String var1, Remote var2) throws RemoteException, AccessException;
    String[] list() throws RemoteException, AccessException;
}

:服务端创建一个 base64Encoder 实例,并与 encoder 名字相绑定

package server;

import remote.RemoteBase64EncoderImpl;

import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Server {
    public void run(int port) throws RemoteException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(1099, Socket::new, ServerSocket::new);
        registry.bind("base64Encoder", new RemoteBase64EncoderImpl());
    }

    public static void main(String[] args) throws AlreadyBoundException, RemoteException {
        new Server().run(1099);
    }
}

启动服务端后,执行客户端的远程调用:

server:

RemoteCallerImpl constructor invoked.
call method invoked: 0
call method invoked: 1
call method invoked: 2    

client:

remote object class: class com.sun.proxy.$Proxy0
encoder class: class com.sun.proxy.$Proxy0
class java.lang.String
U2VuZCBieSBDbGllbnQgMXN0
U2VuZCBieSBDbGllbnQgMm5k
U2VuZCBieSBDbGllbnQgM3Jk

从结果上可以看出,方法的调用完全是在服务端进行的。

而且 lookup 方法返回的并不是 IRemoteEncoder 的实现类,而是一个 com.sun.proxy.$Proxy0 的一个代理类。实际上之后就是通过该代理类实例进行远程方法调用的。

:通过 getDeclaredMethods() 获取的方法,发现这就是继承了 Remote 接口的接口所有的方法:

[public final boolean com.sun.proxy.$Proxy0.equals(java.lang.Object),
 public final java.lang.String com.sun.proxy.$Proxy0.toString(), 
 public final int com.sun.proxy.$Proxy0.hashCode(), 
 public final java.lang.Object com.sun.proxy.$Proxy0.encode(java.lang.Object,java.lang.Object) throws java.rmi.RemoteException]

注意:如果客户端试图通过一个错误的名字获取一个未曾绑定的stub,会抛出 java.rmi.NotBoundException

RMI与序列化

RMI参数对象的传递实际上是通过序列化实现的。

  • 客户端把参数由左向右序列化并发送给服务端;
  • 服务端反序列化参数,并调用相应的方法;
  • 服务端将返回结果序列化并发送给客户端;
  • 客户端反序列化返回结果

注意:在不使用特殊的 RMIServerSocketFactoryRMIClientSocketFactory 的情况下,整个过程都是明文传输的。

服务端新增接口实现类:remote.RemoteObjectEncoderImpl: 作用为在序列化数据时将obj进行base64编码,读取时按照base64解码:

package remote;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class MyObject implements Serializable {
    private static final long serialVersionUID = 1L;

    Object obj;

    public MyObject(Object obj){
        this.obj = obj;
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        Object toSend = new String(Base64.getEncoder().encode(this.obj.toString().getBytes(StandardCharsets.UTF_8)));
        System.out.println("send: "+toSend+", origin: "+this.obj.toString());
        out.writeObject(toSend);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        this.obj = in.readObject();
        System.out.print("receive: "+this.obj+" , type: "+obj.getClass());
        System.out.println(", after base64 decoding: "+new String(Base64.getDecoder().decode(this.obj.toString())));
    }

}

客户端:

public class RMIClient {
    public void run(int port) throws RemoteException, NotBoundException {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", port);
        Object remoteObj = registry.lookup("objectEncoder");
        IRemoteEncoder encoder = (IRemoteEncoder)remoteObj;
        Object result1 = encoder.encode("Send by Client 1st");
    }

    public static void main(String[] args) throws Exception {
//        System.setProperty("java.rmi.server.useCodebaseOnly","true");
        new RMIClient().run(1099);
    }
}

服务端:

public class Server {
    public void run(int port) throws RemoteException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(port);
        registry.bind("objectEncoder", new RemoteObjectEncoderImpl());
    }

    public static void main(String[] args) throws AlreadyBoundException, RemoteException {
        new Server().run(1099);
    }
}

执行结果:

server:

receive: U2VuZCBieSBDbGllbnQgMXN0 , type: class java.lang.String, after base64 decoding: Send by Client 1st
send: cmVtb3RlLk15T2JqZWN0QDRmOWUwYjcz, origin: remote.MyObject@4f9e0b73

client:

send: U2VuZCBieSBDbGllbnQgMXN0, origin: Send by Client 1st
receive: cmVtb3RlLk15T2JqZWN0QDEzYjE2N2U4 , type: class java.lang.String, after base64 decoding: remote.MyObject@13b167e8

参考

posted @ 2022-03-30 01:04  NIShoushun  阅读(344)  评论(0编辑  收藏  举报