阅读本篇文章前注意事项

如果你是初次接触RMI,JNDI等概念,请先理解阅读我写的另一篇文章”RMI-JNDI初探”,个人认为整个RMI的运行机制还是非常复杂的,因为他在整个调用的过程中是多线程的,有非常非常多的坑点,本篇文章将以RMI运行过程为大纲,深入剖析其运行原理。

本篇文章运行环境为JDK7U80

[TOC]

第一步:服务端开启注册服务

调试代码:

Server端:

1
Registry registry = LocateRegistry.createRegistry(1099);

Client端:

1
2
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
registry.lookup("hello");

一切准备就绪,跟随Server端的启动进入RMI的调用旅程

跟进后首先便是实例化RegistryImpl对象

进入RegistryImpl类的构造函数,实例化一个LiveRef对象,var2就是端口号1099,此时this为RegistryImpl,调用RegistryImpl的setup方法

setup方法中将var1(unicastServerRef给this.ref),然后调用unicastServerRef的exportObject方法,此方法会将对应对象暴露对应1099端口上,我们接下来跟进这个方法

exportObject中首先就获取var1(RegistryImpl)方法的字节码,明显是准备进行反射实例化RegistryImpl对象

在85行Util.createProxy中创建一个存根对象,继续跟进createProxy

看到第55行调用createStub返回了一个Stub(存根)对象

跟进createStub,很明显通过反射实例化RegistryImpl_Stub对象并将其类型转换为RemoteStub后返回

回到刚刚的UnicastServerRef#exportObject方法中,看右下角Variables窗口,此时var5就是刚刚我们创建的RegistryImpl_Stub对象,在第91行判断var5是否为Stub对象,如果是就创建服务器骨架(skeleton),在第94行处将刚刚我们创造的对象全部和LiveRef(this.ref为LiveRef)的objID打包为一个大的Target对象(var6),将这个大对象继续传递给LiveRef的exportObject方法

继续往下分析,发现是一波套娃调用,此时this.ep代表TCPEndpoint,接着调用TCPEndpoint的exportObject方法

继续套娃,调用TCPTransport方法的exportObject方法跟进

来到TCPTransport#exportObject方法中,151行处的this.listen()将会开启1099端口的监听,这个步骤涉及到其他线程的启动,我们跟进this.listen()

233行this.getEndpoint()获取到本地ip,224行var1.getpor()获取到对应端口,直接便来到第231行,开启一个处于1099端口的监听

我们此时运行231行代码,查看本机监听会发现1099端口已经开启了监听服务了

然后往下,这里创建了一个进程,进程名字叫做TCP-Accept-端口号,此时var2为1099,于是我们便在这一行创建了一个名为TCP-Accept-1099的线程,用来接收客户端请求,然后调用var3.start();开启这个线程

此时已经开启了TCP-Accept-1099线程,我们暂时让主线程先暂停到刚刚的this.listen()后面,跟进TCP-Accept-1099线程,跟进这个excuteAcceptLoop

我们发现在这个里面运行到366行this.serverSocker.accept()线程便暂停等待客户端的请求了,准备接受客户端传来的信息

这个时候我们运行我们的客户端代码,代码继续运转,此时wireshark查看1099端口流量包发现在此刻成功建立TCP链接并且执行了一次有客户端发向服务器的RMI通信

其内容为0x4a524d49(JRMI的16进制),用于java确认此包是JRMI的

回到刚刚的代码继续往下,在371行处又新建了一个进程,进程名为RMI TCP Connection

直接步过第371行代码,线程RMI TCP Connection启动,切换到RMI TCP Connection线程

来到TCPTransport#run,跟进run0()方法

run0方法中开始读取客户端发送的第一个数据包,从第515行开始读取socket的输入流,然后接着一波转换和读取,最后在519行的var6 = var5.readInt()获取到了数据,此时var6为1246907721,它的16进制数据即为0x4a524d49(我们刚刚从wireshark中看到的第一次JRMI通信客户端发给服务器的包的内容也为0x4a524d49),满足537行的if判断,进入537行if内的

来到了处理JRMI协议的逻辑中,此时读取第二个数据包,在第546行byte var15 = var5.readByte();处读取var5的一个字节,var5就是刚刚客户端->服务端的数据包(0x4a524d49),一个字节即为0x4a(75)所以我们会进入case75的代码段,开始构造返回包。在第552行向DataOutputStream中写入78(16进制为0x4e),随后调用var10.flush()发送返回包,后面仍然是一些数据包读取,但是都没有什东西,直接跟进569行TCPTransport.this.handleMessages(var14, true)

客户端收到的返回包

此时又开始读取客户端发送的第三个数据包(这个数据包是Registry.lookup的数据包,后面会讲到),此时wireshark显示并没有再次客户端没有进行发包,意味着客户端再一次传输中发了多个数据包,此时var5是80(16进制0X50),进入case80,进入279行StreamRemoteCall.serviceCall()

JRMI协议方法介绍:

Message: Call Ping DgcAck

Call: 0x50 CallData

Ping: 0x52

DgcAck: 0x54 UniqueIdentifier

Call、Ping 和 DgcAck。Call 将对方法调用进行编码。Pin
g 是一个传输级消息,用于测试远程虚拟机的活动性。DGCAck 是一个对服务器的
分布式垃圾收集器的确认,指示客户机已经接收到服务器返回值中的远程对象。

此处case80(0x50)代表业务方法(Call),如lookup,bind等

case82(0x52)代表心跳包(Ping)

case84(0x54)代表DgcAck包

来到StreamRemoteCall#serviceCall,此时77行var39去读取InputStream中序列化数据的BlockData块,然后在第83行从ObjectTable中获取Target,此时我们已经很久没有见到我们的主线程了,我们回到主线程看一下

可以看到图中框起来的0X50刚刚已经读入了,用来判断行为,此时是call行为,代表将方法调用进行编码,然后0xaced后应该就是序列化的数据了

此时主线程的状态是这样的,刚刚说了我们要从ObjectTable中去获取Target,但是现在主线程还没有将Target放入ObjetcTable中,看到160行代码,调用TCPTransport的父类Transport的exportObject方法,将Target放入ObjectTable

Transport#exportObject如下,可以看到调用ObjectTable.putTarget将target放入ObjectTable,现在我们继续回到我们的RMI TCP Connection线程中

可以看到,此时var5已经从ObjectTable中获取到了Target,继续进入if语句

在if语句内看到第85行,此时调用了UnicastServerRef#dispatch方法

继续跟进this.oldDispatch(this:UnicastServerRef)方法

加载了一个类以及一些操作后从224行进入了最终的RegistryImpl_Skel#dispatch方法

这个里面出现了一些case语句,是用来判断执行了哪些操作,对应关系

0->bind
1->list
2->lookup
3->rebind
4->unbind

此时继续往下直接调用ObjectInputStream#readObject,触发反序列化

所以这里的关系就是先反序列化传输过来的序列化对象,然后再根据对应的操作如bind,lookup进入对应的代码块执行相应操作.

整个流程调用图如下,点击图片查看原图:

RMI-创建注册中心调用流程图

第二步:客户端获取注册中心

调试代码

1
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);

第一步我们创建注册中心创建的是RegistryImpl对象

而我们客户端获取到的注册中心是RegistryImpl_Stub对象

我们跟进getRegistry,在LocateRegistry#getRegistry中调用Util.createProxy创建一个RegistryImpl的存根类实例

Util#createProxy中调用createStub返回一个registryImpl_Stub对象

具体细节可以看到同样是通过反射加载registryImpl_Stub对象并返回

值得注意的一点是这个过程并没有与客户端交互,整个获取注册中心并不是从服务器上获取服务端创建的那个注册中心,而是自己实例化一个注册中心的存根对象registryImpl_Stub,此处其实就是创建一个registryImpl对象的代理registryImpl_Stub,之后客户端与服务端交互通过这个代理对象进行交互,由于这个步骤调用比较简单,所以就不画调用流程图了

第三步:创建远程对象

调试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface HelloService extends Remote {
String sayHello() throws RemoteException;
}
class HelloServiceImpl extends UnicastRemoteObject implements HelloService {
static {
System.out.println("静态代码块");
}
protected HelloServiceImpl() throws RemoteException {
}

public String sayHello() throws RemoteException {
System.out.println("此处在程序A中被调用");
System.out.println(Runtime.getRuntime());
return "程序B接收到返回值!";
}
}

public class demo_server {
public static void main(String[] args) throws Exception{
HelloServiceImpl hello = new HelloServiceImpl();
}
}

整个过程大概可以描述为:创建远程对象会生成这个远程对象的stub和skel,并将其绑定到一个随机端口发布

由于我们的HelloServiceImpl继承自UnicastRemoteObject,来到它的构造方法,跟进this()

继续来到UnicastRemoteObject的含参构造方法,此时this为我们的HelloServiceImpl类,传入它和port=0到exportObject方法中

直接return了exportObject的返回值,obj为HelloServiceImpl对象,这里实例化的UnicastServerRef就是一个target,包含了一些objid,host等,继续跟进

此时sref为LiveRef,调用LiveRef.exportObject并将obj(HelloServiceImpl对象)传入

进入Util.createProxy创建HelloServiceImpl类的代理对象,我们发现这个地方非常熟悉,之前获取注册中心和创建注册中心的时候在这个地方创建注册中心存根实例,而此时已经变成了创建一个HelloServiceImpl的动态代理对象用于被客户端调用,我们跟进看看

现在没有执行第55行的代码创建Stub,而是来到了62行创建HelloServiceImpl的动态代理对象

现在我们回到UnicastServerRef#exportObject,此时var5便是我们刚刚创建的HelloServiceImpl的动态代理对象,和之前一样,之前会在第91行创建服务端var6将一系列数据进行打包成为一个Target对象,我们接着跟进LiveRef#exportObject方法

一波套娃调用直接来到TCPTransport#exportObject

还是这熟悉的地方,进入this.listen就会涉及网络服务,开启线程,开启监听

在231行处随机创建了一个端口的监听31970,同时开启线程TCP ACCEPT 0

来到TCP ACCEPT 0线程,跟进executeAcceptLoop,在366行var1 = this.serverSocket.accept();处阻塞进程,等待请求,到此整个服务被暴露(exportObject方法)在对应端口上

第四步:服务端绑定对象

调试代码:

这个时候我们就需要提供一个远程对象以供绑定了,我这里实现了一个远程对象HelloServiceImpl,并将它绑定到了hello上(registry.bind(“hello”, hello))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface HelloService extends Remote {
String sayHello() throws RemoteException;
}
class HelloServiceImpl extends UnicastRemoteObject implements HelloService {
static {
System.out.println("静态代码块");
}
protected HelloServiceImpl() throws RemoteException {
}

public String sayHello() throws RemoteException {
System.out.println("此处在程序A中被调用");
System.out.println(Runtime.getRuntime());
return "程序B接收到返回值!";
}
}

public class demo_server {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(1099);
HelloServiceImpl hello = new HelloServiceImpl();
registry.bind("hello", hello);
}
}

因为此处我们调用的是LocateRegistry.createRegistry(1099);即获取到的registry是RegistryImpl对象,所以我们调用的bind方法是RegistryImpl#bind方法

在registry.bind处下断点跟进:

可以看到绑定只是将hello(键)和HelloServiceImpl对象(值)关联了起来,放入RegistryImpl的bindings这个hashtable中

第五步:客户端调用lookup方法

调试代码

客户端:

1
2
3
4
5
6
public class demo_client {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
registry.lookup("hello");
}
}

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface HelloService extends Remote {
String sayHello() throws RemoteException;
}
class HelloServiceImpl extends UnicastRemoteObject implements HelloService {
static {
System.out.println("静态代码块");
}
protected HelloServiceImpl() throws RemoteException {
}

public String sayHello() throws RemoteException {
System.out.println("此处在程序A中被调用");
System.out.println(Runtime.getRuntime());
return "程序B接收到返回值!";
}
}

public class demo_server {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(1099);
HelloServiceImpl hello = new HelloServiceImpl();
registry.bind("hello", hello);
}
}

在运行服务端后我们在客户端registry.lookup(“hello”);处断点,lookup里面首先调用的是super.ref.newCall(),super.ref就是UnicastRef,所以就会直接进入下面的UnicastRef#newCall

跟进后首先来到UnicastRef#newCall,在newConnection中与服务端建立链接,包括发起TCP握手包,协议确认包等,我们跟进详细分析一下

在newConnection的结尾createConnection处创建链接,跟进

来看第一部分代码在110行建立新的Socket链接,完成三次TCP握手

三次握手流量包

紧接着刚刚的来看第一部分try部分的代码段,可以看到在119行处向输出流写入75(0x4b)然后调用var3.flush()发送

查看这个返回包,其中内容便是0x4b我们刚刚发送的可以看到这是一个用来识别协议的数据包

第四个包便是服务器,从目标端口可以看出确实是由1099端口(即服务端)返回的并且传输的内容为0x4e,在前面的创建注册中心这一步也有分析道这个包是服务端发过来的

继续往下看,下面就开始读取服务端的返回包了此时var读取了服务端发过来的包.var6=78(0x4e)

包内容如下:可以看到Input Stream Message为0x4e

在后面开始构造客户端->服务端的返回包,var3开始写入各种数据,然后在154行发送

流量包内容,到这里客户端发送了两个数据包,服务器回复了一个数据包

回到UnicastRef#newCall,此时创建了一个StreamRemoteCall对象,在这里面开始构造远程调用流(即lookup的网络包)

跟进StreamRemoteCall的构造函数,发现在38行写入80(0x50),在前面讲创建注册端的时候handleMessage中存在case80的逻辑,0x50表方法调用,构造这个数据包的目的就是发送信息给服务端让服务端去调用方法

回到lookup中,此时var1为我们lookup的字符串’hello’,将其序列化并写入了var3,准备数据等下发包给服务端,继续跟进super.ref.invoke方法(super.ref为UnicastRef),

跟进StreamRemoteCall#excuteCall

在第138行将刚刚我们写入缓存的序列化字符串发送了出去,我们看一下这个过程客户端与服务端的通信过程

整个通信流程一共发了2个JRMI数据包,其中1号数据包是客户端发送的,3号数据包时服务端回复的,我们先来分析一下1号数据包

1处明显就是刚刚写入的80(0x50),2处magic头中0xaced告诉服务器这是一个序列化数据,需要被反序列化3处则是我们序列化后的数据

3号数据包是服务端给我们回复的数据包,我们用过lookup向服务端请求想要寻找一个和字符串hello绑定的远程对象,通过分析3号数据包看看服务端给了我们那些东西

使用SerializationDumper.jar看一下内部结构,注意到TC_PROXYCLASSDESC处发现我们继承了java.rmi.Remote,HelloService,这里的HelloService就是我们自己写的那个绑定到注册中心上的类,下面的TC_BLOCKDATA也存放了一些数据,马上我们将会去读他

现在到这个地方,我们收到了服务器返回给我们的包,那么服务端的包究竟是怎么发过来的呢?在我们分析创建注册端的时候在RegistryImpl_Skel#dispatch中的switch语句会根据客户端发起的请求执行对应的回复,我们来看看这个地方,可以看到在下方的变量列表中param_3就是此时的var3,var3为2表示执行lookup方法,这与我们客户端的行为是对应的,我们重点关注一下case 2部分的代码

这个地方是case2的代码

在上图第80行中根据字符串hello查询到了对应的对象,可以看到此时var1为字符串hello,var3是我们查询到的对象HelloServiceImpl

继续跟进RegistryImpl_Skel#dispatch 83行处,这个地方在准备回复包,写入了两次数据和一次UID,

继续往下在RegistryImpl_Skel#dispatch 242行var2.releaseOutputStream();发送数据,而这个地方发送的数据就是我们刚刚分析的搭载序列化数据的数据包

回到客户端,刚刚我们分析到了StreamRemoteCall#excuteCall,接着往下执行后返回到lookup,在var6.readObject处获取到我们HelloServiceImpl的代理对象

进入var6.readObject,可以看到获取到了我们的代理对象,并将其返回给var23

最后调用super.ref.done,发送一些后续的包如垃圾处理的Dgc包,通知服务端我已经收到了代理对象,客户端可以进行垃圾回收了,来看这个过程的数据包,12号数据包是服务器将序列化对象传回给客户端,然后客户端读取这个包,获得代理对象后首先发了一次存活确认包(14-17),确认存活后客户端向服务端发起垃圾回收信号(第18个包),告诉服务端可以进行垃圾回收了,然后TCP挥手断开链接。

至此,整个通信流程已经分析完毕了,整个流程我画了个调用图,如下:

RMI-SERVER-CLIENT网络调用图registry.lookup通信过程

到这里我不抛出了一个疑问:既然远程调用方法实在服务端上调用然后返回给我们返回值,那么这个返回值必然也是通过网络传输的,我翻遍了ReturnData这个包,用SerializationDumper.jar反反复复翻找也找不到我的返回值在那个地方的,于是我进行了一波探究,我发现在整个调用流程中并不只是存在1099端口的通信

将服务端上远程对象的返回值更改为比较显眼的字符串

在wireshark搜索后发现在另一个tcp流中找到了我们的返回值,此时是运行在3752->3766端口的,我觉得应该是在这个端口上通信来执行远程方法调用的一些网络通信

至此,就基本上了解了RMI的通信交互过程

原文地址:http://www.men9da.cn/2022/03/10/%E9%80%9A%E8%BF%87%E8%B0%83%E8%AF%95%E6%BA%90%E7%A0%81%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E5%88%86%E6%9E%90RMI%E8%BF%90%E8%A1%8C%E6%9C%BA%E5%88%B6/