• Java Remote Method Invocation (RMI)


    一个 RemoteObject 在 export 后在 RMI Server 端持有一个 UnicastServerRef 对象(真正被 export 的 Proxy 对象持有的是 UnicastRef 对象),在 RMI Registry 端保存的是 Proxy 对象,RMI Client 通过 RMI Registry lookup 获取到 Proxy 对象,在 RMI Client 端对 RemoteObject 的操作,最终都通过 RemoteObjectInvocationHandler 委托给 UnicastRef#public Object invoke(Object proxy, Method method, Object[] args)。

    UnicastRef 和 UnicastServerRef

    UnicastRef 提供了invoke 方法,UnicastServerRef提供了 exportObject 方法,在 RMI Client 端,Proxy 远程对象持有一个 UnicastRef 对象,一个 UnicastRef 持有一个 LiveRef 对象;在 RMI Server 端,一个 RemoteObject 持有一个 UnicastServerRef 对象, UnicastServerRef 持有一个 LiveRef 对象(一个 remote object reference = LiveRef = Endpoint(host:port)/ObjID)。LiveRef 持有 Endpoint 对象,Endpoint 对象有两个方法

    /**
     * Returns the transport for incoming connections to this endpoint.
     **/
    Transport getInboundTransport();
    
    /**
     * Returns transport for making connections to remote endpoints.
     **/
    Transport getOutboundTransport();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    Transport getOutboundTransport() 方法在 RMI Client 调用,获取到 Transport 之后,通过 Trasport#getChannel 获取到 TCPChannel 对象,最后通过 TCPChannel#newConnection() 方法获取到 Connection 对象,对于 TCPConnection 来说,一个 TCPConnection 对象持有一个 Socket 对象,及通过 Socket 获取到的 InputStream 和 OutputStream;

    Transport 层的这些对象的构造方法里,只传port的就是给服务器端用的;

    TCPTransport 里的 getChannel 是给 RMI Client 用的?(从调用关系上看,此方法最终是通过 UnicastRef#invoke#newCall#free 三个方法调用,都是在 RMI Client 才会调用的;)其他的都是给 RMI Server 用的?

    exportObject

    那么 exportObject 返回的对象,如何有这些能力的呢,发布对象的时候,我们需要给 UnicastRemoteObject#exportObject 传入远程对象和端口号,
    UnicastRemoteObject 会将传入的信息 new 一个 UnicastServerRef2 对象,然后将发布对象的工作委托给 UnicastServerRef2 对象的 exportObject 方法;
    在 UnicastServerRef2 中,host+port 用来构建 LiveRef 对象(根据 host + port 构建 endpoint + ObjID -> liveref),然后将发布对象的工作委托给 LiveRef;LiveRef委托给Endpoint,Endpoint委托给 transport,transport里创建 ServerSocket;

    为什么拿到了 RMI Server 发布的远程对象的序列化的反序列化对象之后,就能跟 RMI Server 上远程对象通过 Socket 通信了呢?
    export的远程对象并不是开发者直接传入的这个对象,

    RemoteHello remoteHello = new RemoteHelloImpl();
    RemoteHello stub = (RemoteHello) UnicastRemoteObject.exportObject(remoteHello, 4000);
    
    • 1
    • 2

    比如上面这段代码,这个 stub 是 remoteHello(RemoteHelloImpl类的实例),通过JDK动态代理之后的对象,那么动态代理给其加了什么功能呢?

    分析 UnicastServerRef # exportObject 得知,exportObject 返回的对象,是使用 JDK 动态代理创建的对象,从 RemoteObjectInvocationHandler 可以看出,这个代理对象的能力是,将对这个代理对象的方法调用,通过 RemoteObject 持有的 UnicastRef 对象的 invoke 方法启动本地 Socket 与远程对象的交互,从而得到远程对象的执行结果。怎么实现这个目标的?

    构建代理对象时,先构建一个 UnicastRef 对象(客户端的远程对象引用),这时候注意:
    这是 UnicastServerRef 的方法

    protected RemoteRef getClientRef() {
        return new UnicastRef(ref);
    }
    
    • 1
    • 2
    • 3

    这是 UnicastServerRef2 的方法

    protected RemoteRef getClientRef() {
        return new UnicastRef2(ref);
    }
    
    • 1
    • 2
    • 3

    他们在构建 UnicastRef 对象时使用了对应的 UnicastServerRef 对象的 LiveRef 对象(这个对象持有的就是远程的对象的位置信息,endpoint=objid),LiveRef 对象的序列化和返序列化直接调用了其持有的Endpoint 和ObjID的信息,看一下 Endpoint 的信息如下,就是host和port信息;

    /**
     * Write endpoint to output stream.
     */
    public void write(ObjectOutput out) throws IOException {
        if (csf == null) {
            out.writeByte(FORMAT_HOST_PORT);
            out.writeUTF(host);
            out.writeInt(port);
        } else {
            out.writeByte(FORMAT_HOST_PORT_FACTORY);
            out.writeUTF(host);
            out.writeInt(port);
            out.writeObject(csf);
        }
    }
    
    /**
     * Get the endpoint from the input stream.
     * @param in the input stream
     * @exception IOException If id could not be read (due to stream failure)
     */
    public static TCPEndpoint read(ObjectInput in)
        throws IOException, ClassNotFoundException
    {
        String host;
        int port;
        RMIClientSocketFactory csf = null;
    
        byte format = in.readByte();
        switch (format) {
          case FORMAT_HOST_PORT:
            host = in.readUTF();
            port = in.readInt();
            break;
    
          case FORMAT_HOST_PORT_FACTORY:
            host = in.readUTF();
            port = in.readInt();
            csf = (RMIClientSocketFactory) in.readObject();
            if (csf != null && Proxy.isProxyClass(csf.getClass())) {
                throw new IOException("Invalid SocketFactory");
            }
          break;
    
          default:
            throw new IOException("invalid endpoint format");
        }
        return new TCPEndpoint(host, port, csf, null);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    到这里,代理对象,通过持有带有远程对象位置信息(LiveRef)的 UnicastRef 对象,已经可以在未来被某个 RMI Client 获取到的时候,可以反过来 export 此对象的 RMI Server 上的此对象发起 Socket 连接了。RemoteObjectInvocationHandler 将 method.invoke 委托给 UnicastRef#invoke 方法发起远程通信;走时序图里的流程。

    试一下直接使用 UnicastServerRef2 是否可以将多个对象发布到一个端口?

    Registry

    Registry 接口的实现为 RegistryImpl,其通过继承 java.rmi.server.RemoteServer ,间接实现了 java.rmi.Remote 接口,所以 RegistryImpl 是一个远程对象,sun.rmi.registry.RegistryImpl_Stub 和 sun.rmi.registry.RegistryImpl_Skel 为使用 RMIC 工具生成的存根和骨架类。
    Registry 一共由5个方法, bind ,unbind和rebind是 RMI Server 用来注册和解绑远程对象的;list 和 lookup 是 RMI Client 用来查找远程对象的。

    registry bind & lookup

    RMI Server 往 RMI Registry 注册远程对象:

    此段的主要逻辑在 RegistryImpl_Stub#void bind(java.lang.String $param_String_1, java.rmi.Remote $param_Remote_2) 方法中,

    out.writeObject(name);
    out.writeObject(Remote);
    
    • 1
    • 2

    RMI Registry 接收到调用之后的逻辑在 RegistryImpl_Skel@dispatch(java.rmi.Remote obj, java.rmi.server.RemoteCall remoteCall, int opnum, long hash) 方法中,

    name = SharedSecrets.getJavaObjectInputStreamReadString().readString(in);
    Remote = (java.rmi.Remote) in.readObject();
    
    • 1
    • 2

    其从流中读出 name 和 Remote 对象;然后调用 RegistryImpl#bind,将 name->Remote 放到其持有的全局静态变量中:

    private Hashtable bindings
            = new Hashtable<>(101);
    
    • 1
    • 2

    会写 TransportConstants.NormalReturn ,调用结束(这个逻辑在,StreamRemoteCall#getResultStream 方法中)

    RMI Client 从 RMI Registry 寻找远程对象:

    name = SharedSecrets.getJavaObjectInputStreamReadString().readString(in);
    java.rmi.Remote remote = RegistryImpl.lookup($param_String_1);
    try {
        java.io.ObjectOutput out = call.getResultStream(true);
        out.writeObject($result);  注意这里,将整个对象序列化过去;
    } catch (java.io.IOException e) {
        throw new java.rmi.MarshalException("error marshalling return", e);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这三个对象创建的时候是这么个关系;

    ep = new TCPEndpoint(remoteHost, socket.getLocalPort(),
                         endpoint.getClientSocketFactory(),
                         endpoint.getServerSocketFactory());
    ch = new TCPChannel(TCPTransport.this, ep);
    conn = new TCPConnection(ch, socket, bufIn, bufOut);
    
    • 1
    • 2
    • 3
    • 4
    • 5
  • 相关阅读:
    如何使用chatGPT写好Prompt提示词: Few-shots
    毫秒级!千万人脸库快速比对,上亿商品图片检索,背后的极速检索用了什么神器?
    Flutter SDK 自带的 10 个最有用的 Widget
    C++多态
    亚马逊云科技 Lambda 运行selenium
    centos搭建activemq5.16
    Linux虚拟化网络之链路聚合
    串口触摸屏的键盘控制
    94-Java的转换流:解决代码与文件编码不一致读取乱码的问题
    7 判断给定的二叉树是否是二叉排序树---来源刘H同学
  • 原文地址:https://blog.csdn.net/chen517611641/article/details/127607541