单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类型的输出流。我们看一下这个方法:
我们看一下这个方法返回类的继承结构:
好了,本篇文章就先到这里吧