RMI 总结之 Socket 一

本文解析了如何在JVM进程间通过RMI框架实现不同对象间的通信,介绍了服务端的ServerSocket监听、TCPTransport的建立连接过程,以及客户端的连接请求、远程方法调用的详细步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

单JVM进程中,对象之间的交流非常简答:A类中new一个B类对象,然后通过这个对象直接调用B类中定义的方法;反过来B类想要调用A类中的方法,同样可以在B类中new一个A类对象,然后用这个对象直接调用A类中定义的方法即可。但这个方法无法使分布在不同JVM进程中的两个对象进行交流。那它们之间就无法交流了吗?相信聪明的你已经找到了使活跃在不同JVM进程中的两个对象进行交流的可靠方法。譬如:spring cloud、dubbo等(将这两个框架解读为让活跃在两个不同JVM进程中的对象之间进行交流的工具的说法实在不妥,希望各位不要见怪)。而最近一直学习的RMI框架跟它们一样,也有让活跃在不同JVM进程中的对象之间进行交流的功能。那RMI是怎么使实现这个功能的,本节及后续章节将围绕这个点不断拓展,直至掀开其真面目为止。

在开始前,先梳理一下思路,要想让两个不同JVM进程中的对象之间进行交流,首先就是让它们之间知道彼此的存在;接着A进程中的A对象通过某种方式连接到B进程中的B对象上;然后A对象向B对象发送要处理的数据,B对象对接收到的数据进行处理,并向A对象反馈处理结果。在RMI中A对象和B对象之间建立连接用到了TCP/IP。而java中进行TCP/IP编程的常见工具是ServerSocket/Socket。所以这个就是本篇要总结的知识点。下面还是根据RMI源码一步一步领悟吧! 

服务端

 之前的篇目分析时有提到过,RMI服务端会启动一个后台进程,监听1099端口,然后等待客户端连接。其入口见下图:

 继续进入,我们会看到最终会调用RegistryImpl#setup(UnicastServerRef)方法。然后该方法又继续调用UnicastServerRef#exportObject(Remote,Object,boolean)方法,该方法会创建一个RegistryImpl_Stub代理(客户端代理)和一个RegistryImpl_Skel代理(服务端代理),紧接着该方法又创建了一个Target对象,接着调用UnicastServerRef.LiveRef#exportObject(Target)方法。这个方法会继续调用LiveRef.Endpoint#exportObject(Target)方法。这个方法又继续调用TCPEndpoint.TCPTransport#export(Target)方法。好了就到这里,我们看一下其源码,如下图所示:

 上图便是TCPTransport#export(Target)的源码,这里我们重点关注红色方框标出的listen()方法,该方法的源码如下图所示:

上图所示方法中的ep.newServerSocket()代码的本质就调用RMIServerSocketFactory的实现类TCPDirectSocketFactory中的createServerSocket(int port)方法创建一个ServerSocket对象,然后jiang 该对象返回给上级调用者(即:TCPTransport#listen())。接着TCPTransport#listen()方法又会创建一个AcceptLoop线程(该线程持有了刚刚创建出来的ServerSocket对象)并启动该线程。下面的图片展示的是AcceptLoop类中的run()的代码:

该方法又掉了本类(AcceptLoop类)中的executeAcceptLoop()方法。这个方法的核心代码如下图所示:

由图可知这段代码就是我们平常写ServerSocket编程时的代码(调用ServerSocket对象的accept()方法等待客户端连接)。

客户端

通过前一小节,我们知道服务端其实就是一个Socket服务。本小节我们将继续跟踪RMI源码以了解客户端是如何与服务端建立连接的。下面这张图是客户端的入口代码:

由红色方框代码下钻,会到达LocateRegistry#getRegistry(String host, int port)处,该方法会继续调用LocateRegistry# getRegistry(String host, int port, RMIClientSocketFactory csf)中,这个方法会首先初始化port和host,然后创建LiveRef和RemoteRef(实际类型为UnicastRef),最后调用Util#createProxy(Class<?> implClass, RemoteRef clientRef, boolean forceStubUse)来创建代理对象,其具体代码如下图所示:

接下来我们一起看一下Util#createProxy(Class<?> implClass, RemoteRef clientRef, boolean forceStubUse)的源码,如下图所示:

这段代码在上一节梳理代理的时候分析过,这里再展示一下,加深一下印象。这里我们主要注意的是createStub(remoteClass, clientRef)处的代码,这段代码会创建一个RegistryImpl_Stub对象,它是一个代理对象(是服务端在客户端的一个代理对象)。这个对象最终会返回到入口处。紧接着入口处会使用这个对象调用lookup()方法,具体代码为:

registry.lookup("RMIService"); // 这里的registry类型为RegistryImpl_Stub

接下来我们一起看一下lookup(java.lang.String $param_String_1)方法的代码,具体如下图所示:

图中蓝色标识的行的主要目的是创建一个Connection对象(Connection只有一个实现类TCPConnection,具体参见下面第二张图片),我们直接进入newConnection()方法中,可以看到如下图所示的代码:

Connection的类结构图:

根据newConnection()方法的代码可以知道,这个方法会首先从缓存中获取连接,如果没有拿到就调用createConnection()方法(TCPConnection)创建一个连接,该方法的代码如下所示:

private Connection createConnection() throws RemoteException {
    Connection conn;
    TCPTransport.tcpLog.log(Log.BRIEF, "create connection");
    Socket sock = ep.newSocket();
    conn = new TCPConnection(this, sock);
    try {
        DataOutputStream out =
            new DataOutputStream(conn.getOutputStream());
        writeTransportHeader(out);
        // choose protocol (single op if not reusable socket)
        if (!conn.isReusable()) {
            out.writeByte(TransportConstants.SingleOpProtocol);
        } else {
            out.writeByte(TransportConstants.StreamProtocol);
            out.flush();
            /*
             * Set socket read timeout to configured value for JRMP
             * connection handshake; this also serves to guard against
             * non-JRMP servers that do not respond (see 4322806).
             */
            int originalSoTimeout = 0;
            try {
                originalSoTimeout = sock.getSoTimeout();
                sock.setSoTimeout(handshakeTimeout);
            } catch (Exception e) {
                // if we fail to set this, ignore and proceed anyway
            }
            DataInputStream in =
                new DataInputStream(conn.getInputStream());
            byte ack = in.readByte();
            if (ack != TransportConstants.ProtocolAck) {
                throw new ConnectIOException(
                    ack == TransportConstants.ProtocolNack ?
                    "JRMP StreamProtocol not supported by server" :
                    "non-JRMP server at remote endpoint");
            }
            String suggestedHost = in.readUTF();
            int    suggestedPort = in.readInt();
            if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) {
                TCPTransport.tcpLog.log(Log.VERBOSE,
                    "server suggested " + suggestedHost + ":" +
                    suggestedPort);
            }
            // set local host name, if unknown
            TCPEndpoint.setLocalHost(suggestedHost);
            // do NOT set the default port, because we don't
            // know if we can't listen YET...
            // write out default endpoint to match protocol
            // (but it serves no purpose)
            TCPEndpoint localEp =
                TCPEndpoint.getLocalEndpoint(0, null, null);
            out.writeUTF(localEp.getHost());
            out.writeInt(localEp.getPort());
            if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) {
                TCPTransport.tcpLog.log(Log.VERBOSE, "using " +
                    localEp.getHost() + ":" + localEp.getPort());
            }
            /*
             * After JRMP handshake, set socket read timeout to value
             * configured for the rest of the lifetime of the
             * connection.  NOTE: this timeout, if configured to a
             * finite duration, places an upper bound on the time
             * that a remote method call is permitted to execute.
             */
            try {
                /*
                 * If socket factory had set a non-zero timeout on its
                 * own, then restore it instead of using the property-
                 * configured value.
                 */
                sock.setSoTimeout((originalSoTimeout != 0 ?
                                   originalSoTimeout :
                                   responseTimeout));
            } catch (Exception e) {
                // if we fail to set this, ignore and proceed anyway
            }
            out.flush();
        }
    } catch (IOException e) {
        try {
            conn.close();
        } catch (Exception ex) {}
        if (e instanceof RemoteException) {
            throw (RemoteException) e;
        } else {
            throw new ConnectIOException(
                "error during JRMP connection establishment", e);
        }
    }
    return conn;
}

接下来我们看一下这段代码吧。该方法中的Socket sock = ep.newSocket();的主要作用是创建一个Socket对象,即我们常见的Socket编程中的客户端对象。ep.newSocket()方法最终调用的是TCPDirectSocketFactory#createSocket()方法,该方法的主要代码如下所示:

public Socket createSocket(String host, int port) throws IOException {
// 这里的port值为1099,host为127.0.0.1(我本地测试的时候是这样的)
    return new Socket(host, port);
}

newSocket()方法在创建完Socket对象后,还会执行下图所示的一些代码:

至此我们创建了一个完整的Socket对象。接着这个对象会被返回到上级调用者,即TCPChannel#createConnection()处。该方法在Socket对象创建完成之后,会立即创建Connection对象,具体代码为:conn = new TCPConnection(this, socket);,通过上面的Connection的类图结构我们知道TCPConnection中有这样几个属性:Socket、Channel、InputStream、Out-putStream。通过TCPConnection的构造方法可知,该构造方法主要作用就是对这几个属性进行赋值。继续回到TCPChannel#createConnection()中,可以看到接着会创建一个DataOutputStream对象(该对象持有的OutputStream来源于Socket的输出流——socket.getOutputStream())。紧接着会向DataOutputStream输出流中写入TransportHeader,其实就是写入一个魔数和版本,具体代码如下所示:

继续回到TCPChannel#createConnection()中,接下来会调用TCPConnection的conn.isReusable()进行一次判断,在跟踪代码的时候,判断结束后直接走了else分支,即下面的代码:

// 向输出流中写入一个数字,个人理解这是一个协议
out.writeByte(TransportConstants.StreamProtocol);
// 然后调用 flush() 方法,这时数据会被传递给服务端(总共三个数据魔数+版本号+协议类型)
out.flush();
// 个人理解如果顺利的话,到这里的时候,客户端与服务端的连接已经建立

/*
 * Set socket read timeout to configured value for JRMP
 * connection handshake; this also serves to guard against
 * non-JRMP servers that do not respond (see 4322806).
 */
int originalSoTimeout = 0;
try {
    originalSoTimeout = sock.getSoTimeout();
// 对socket对象设置soTimeout属性值
    sock.setSoTimeout(handshakeTimeout);
} catch (Exception e) {
    // if we fail to set this, ignore and proceed anyway
}
// 读取服务端返回的数据(从客户端对象Socket中获取InputStream对象)
DataInputStream in = new DataInputStream(conn.getInputStream());
byte ack = in.readByte();
// 拿到服务端给的响应,如果是 ProtocolAck 值,则表示连接已建立,可以正常通信
// 否则断开连接
if (ack != TransportConstants.ProtocolAck) {
    throw new ConnectIOException(
        ack == TransportConstants.ProtocolNack ?
        "JRMP StreamProtocol not supported by server" :
        "non-JRMP server at remote endpoint");
}

String suggestedHost = in.readUTF();
int    suggestedPort = in.readInt();
if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) {
    TCPTransport.tcpLog.log(Log.VERBOSE,
        "server suggested " + suggestedHost + ":" +
        suggestedPort);
}

// set local host name, if unknown
TCPEndpoint.setLocalHost(suggestedHost);
// do NOT set the default port, because we don't
// know if we can't listen YET...

// write out default endpoint to match protocol
// (but it serves no purpose)
TCPEndpoint localEp =
    TCPEndpoint.getLocalEndpoint(0, null, null);
out.writeUTF(localEp.getHost());
out.writeInt(localEp.getPort());
if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) {
    TCPTransport.tcpLog.log(Log.VERBOSE, "using " +
        localEp.getHost() + ":" + localEp.getPort());
}

/*
 * After JRMP handshake, set socket read timeout to value
 * configured for the rest of the lifetime of the
 * connection.  NOTE: this timeout, if configured to a
 * finite duration, places an upper bound on the time
 * that a remote method call is permitted to execute.
 */
try {
    /*
     * If socket factory had set a non-zero timeout on its
     * own, then restore it instead of using the property-
     * configured value.
     */
    sock.setSoTimeout((originalSoTimeout != 0 ?
                       originalSoTimeout :
                       responseTimeout));
} catch (Exception e) {
    // if we fail to set this, ignore and proceed anyway
}

out.flush();

不出意外的话,经过这部分代码之后,客户端便与服务端之间建立起了一条可用的连接。接下来便是正常的通信了。此时再次回到UnicastRef#newCall()方法中,我们可以看到接下来会创建一个RemoteCall对象(实际类型为StreamRemoteCall。这里想再提醒一下脑子迟钝的自己,RemoteCall是一个接口,其实现类只有StreamRemoteCall类),该对象持有了刚创建的Connection对象、ref对象持有的ObjID对象,操作类型(比如上游传递进来的lookup操作,即int类型的数字2),接着会调用marshalCustomCallData()对输出流进行处理,最后将RemoteCall对象返回给上级调用者,即RegistryImpl_Stub的lookup()方法中。这里我们把上面的那张lookup方法的截图再贴一下:

通过上图可知,再创建完StreamRemoteCall对象后,会直接调用该对象的getOutputStream()方法返回一个ObjectOutput类型的输出流。我们看一下这个方法:

我们看一下这个方法返回类的继承结构:

好了,本篇文章就先到这里吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

机器挖掘工

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值