JavaSec-从IDEA断点分析RMI通信原理

从IDEA断点分析RMI通信原理

服务注册

此时我们将断点打在RMIServer的创建远程对象这里

①远程对象的发布

开始调试,首先到达的是远程对象的构造函数RemoteObjImpl;此时我们要将这个对象其发布到网络上去,此时我们需要分析的是它是如何被发布到网络上去的

又因为RemoteObjImpl这个类是继承UnicastRemoteObject的,所以接着会到达父类的构造函数;此时的参数port传入了0;此时的过程实际上是把你的远程对象发布到一个随机的端口(传入0相当于一个默认值);需要注意的是此过程不同于注册中心的1099 端口;这个是远程服务的。

此时继续往下看可以看到调用了一个静态函数exportObject()(导出对象)

它就是主要负责将远程服务发布到网络上,如何更好理解 exportObject() 的作用呢?我们可以看到 RemoteObjImpl 这个实现类的构造函数里面,我注销了一句代码

1
2
3
public RemoteObjImpl() throws RemoteException {  
// UnicastRemoteObject.exportObject(this, 0); // 如果不能继承 UnicastRemoteObject 就需要手工导出
}

如果不继承 UnicastRemoteObject 这个类的话,我们就需要手动调用这个函数。

我们来看这个静态函数,第一个参数是 obj 对象,第二个参数是 new UnicastServerRef(port),第二个参数是用来处理网络请求的。继续往下面跟,去到了 UnicastServerRef 的构造函数。

跟进去之后UnicastServerRef的构造函数,我们看到它new了一个LiveRef(port),这个非常重要,它算是一个网络引用的类,跟进看一看。

跟进去之后,先是一个构造函数,先跳进this看一看

此时这个构造函数有三个参数分别是objID, TCPEndpoint.getLocalEndpoint(port), true;而这个TCPEndpoint.getLocalEndpoint(port)又是什么东西呢,我们可以进去看看

此时可以看到其返回值是一个TCPEndpoint;此时我们可以得知TCPEndpoint是一个网络请求的类;这个时候我们可以看一下它的构造函数

此时我们可以看到有两个参数分别是host、port;此时正好对应一个ip一个端口;也就是这个类是一个处理网络请求的类,只要给它一个ip一个端口,它就可以处理网络请求;而此时我们需要注意的是网络引用类LiveRef里面放的就是它。

此时我们继续跟进到LiveRef的构造函数this里面,此时我们会发现一些赋值引用;就是我们刚才说的tcpendpoint、id、islocal

此时我们可以看到tcpendpoint里面传入了ip和端口;此时需要注意的是后面的tcpTransport这个东西,它是真正处理网络请求的部分。所以记住数据是在LiveRef里面即可,并且这一LiveRef至始至终只会存在一个。

回到上文那个地方,继续 f7 进入 super 看一看它的父类 UnicastRef,这里就证明整个创建远程服务的过程只会存在一个LiveRef。一路 f7 到一个静态函数 exportObject(),我们后续的操作过程都与 exportObject() 有关,基本都是在调用它,这一段不是很重要,一路 f7 就好了。直到此处出现Stub

此时可能会有一个疑问,stub不是在客户端吗,为什么此时会出现在服务端呢。因为此时是服务端创建了stub,然后将其放到了注册中心去,然后客户端再从注册中心拿到stub

此时我们来看一看stub的创建,此时继续调试

此时我们直接跟进到createProxy;此时我们来看一下几个参数impClass即为远程对象的类;而下面的clientRef即为被封装的LiveRef

接下来跟进到一个判断语句,此时我们先不管它;紧接着我们就可以看到一个动态代理创建的标准流程;此时的参数loader、interfaces、handler分别对应加载器、远程接口、调用处理器;这个调用处理器里面只有一个值依旧还是一个封装的LiveRef

继续跟进我们就可以发现动态代理已经创建好了

此时继续跟进;到Target这里,Target这里相当于一个总的封装,将所有用的东西放到 Target 里面,我们可以进去看一看Target里面都放了什么。此时可以观察到存放了id、weakImpl(引用,里面存放远程对象)、disp(前面创建的服务端引用)、stub

此时有个细节,服务端引用中(UnicastServerRef)的Liveref和客户端(UnicastRef)的LiveRef都是一样的;他们两中存放的是同一个处理网络请求的引用;也就是说服务端和客户端需要通信,所以存放的是同一个东西。总而言之,target相当于一个总的封装,它把我们之前创建的所有东西都放了进去。

继续向下调试可以发现ref.exportObject(target);;此时它把封装好的target都发布了出去

此时我们继续跟进看看他的发布逻辑;此时一路跟进到这里

接下来就是真正的网络请求部分;先获取 TCPEndpoint,然后我们继续 f8 往后看,直到 server = ep.newServerSocket(); 这里

它创建了一个新的 socket,已经准备好了,等别人来连接,所以之后就开始定义连接之后要做什么

此时我们可以进入AcceptLoop看看;此时如果客户端连接后便会走run的逻辑即为如果连接就执行executeAcceptLoop()

此时我们继续跟进到executeAcceptLoop();此时里面基本都是一些进行网络请求的操作

此时的

1
2
3
4
5
private void executeAcceptLoop() {
if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF, "listening on port " +
getEndpoint().getPort());
}

代码起到的是一个连接处理的效果。

继续向下跟进发现下面的操作是开启线程等待客户端的连接

然后回到 listen 去,一路 f8,观察一下整个流程结束之后Target里面是增加了port

远程对象发布完成之后的记录

第一个语句 target.setExportedTransport(this); 是一个简单的赋值,我们就不看了,看下面的 ObjectTable.putTarget(target);,跟进去,一路 f8,因为都是一些赋值的语句,直到此处。

RMI这里会把所有的信息保存到两个 table里面

此时就是就是将我们创建的远程对象都保存在系统的一个静态的表里面,也就是说你这个远程对象创建好了之后开了一个端口;但是我服务端需要知道你这些信息的;所以我将其保存在一个静态的表里面。个人理解这个有点像日志。

②注册中心的创建+绑定

创建注册中心和创建远程服务是独立的,所以谁先谁后无所谓;此时我们将断点打在Registry r = LocateRegistry.createRegistry(1099);

创建注册中心

首先会经过一个静态方法creatrRegistry

继续往下走会走到了RegistryImpl这个对象;f8 进去,会发现新建了一个 RegistryImpl 对象。这里 122 行,判断 port 是否为注册中心的 port,以及是否开启了 SecurityManager,也就是一系列的安全检查,这部分不是很重要,继续 f8。此时可以发现新创建了一个RegistryImpl对象。

再继续往下,它创建了一个LiveRef,以及创建了一个新的UnicastServerRef

此时这个过程与创建远程对象很类似;我们可以跟进setup看看究竟

跟进去之后依旧是和之前一样先行进行赋值,然后进行exportObject()的调用

此时和发布远程对象的不同的点在于第三个参数的不同;名为 permanent,第一张是 false,第二张是 true,这代表我们创建注册中心这个对象,是一个永久对象,而之前远程对象是一个临时对象。

继续往下到exportObject,此时依旧和发布远程对象一样到了创建stub的阶段。

但是此时的创建stub和发布远程对象那里变有不同之处了。此时我们继续跟进creatProxy();首先会做一个判断

可以跟进 stubClassExists 进行判断,我们看到这个地方,是判断是否能获取到 RegistryImpl_Stub 这个类,换句话说,也就是若 RegistryImpl_Stub 这个类存在,则返回 True,反之 False。我们可以找到 RegistryImpl_Stub 这个类是存在的。

  • 对比发布远程对象那个步骤,创建注册中心是走进到 createStub(remoteClass, clientRef); 进去的,而发布远程对象则是直接创建动态代理的。

此时执行这个方法也很简单,直接通过反射创建这个对象,里面放的就是ref

相比较于发布远程对象中的stub,是一个动态代理,里面存放了ref;现在创建注册中心中的stub是通过foeName反射来创建的,里面依旧存放了ref。两者是一致的

此时继续往下,再服务端定义好之后,就调用setSkeleton()方法,此时我们继续跟进此时发现这里有一个creatSkeleton()

此时的Skeleton是通过反射方法forName()进行创造的

继续往下走,到了Target的地方,此时Target就和远程对象发布中的Target一样了,用于封装之前的数据,但是比发布远程对象多了一个Skeleton

所以此时这段就和之前发布远程对象一样了,此时继续往下走走到了exportObject这里

继续走,直到 super.exportObject(target); 这里,f7 跟进,到里面有一个 putTarget() 方法,它会把封装的数据放进去。

继续往下调试看看放了什么东西进去

绑定

绑定也就是最后一步,bind操作;我们将断点下在r.bind("remoteObj",remoteObj);

一句话形容一下就是 hashTable.put(IP, port)