没人比我更懂RMI(上)

前言

参考文章1

参考文章2

每篇鸡汤:

万事开头难,当走出第一步你会发现,其实事情并没有你想象的那么糟糕

文章可能稍微有点长,一定要耐心看完 o.0

RMI概念

本来想把官方对RMI的概念贴在开头的,突然想了想开头就整臭长臭长的文字,谁顶得住。其实总的来说用一句话就可以概括。

——RMI就是负责远程调用对象的。

既然提到了远程调用对象,肯定不是调用自己本地的对象。先给大家举个生活中的例子渗透一下概念:

j

比如你在北京上学,你女神在西安,并且你想通过发送信息给女神告白。

  1. 你想告白,得通过手机发送信息(相当于调用远程方法;但是调用方法之前会先创建Stub(sun.rmi.registry.RegistryImpl_Stub))这个stub就相当于你的手机;也就是说,你并不能直接面对面告诉女神,而手机只是你传递信息的一种媒介,相当于是一个代理。

  2. 然后你给女神里发了个信息,在你刚呼出的一瞬间,手机会将你的呼出信号交给三大运营商(相当于stub会将Remote对象传递给远程引用层(java.rmi.server.RemoteRef)并创建java.rmi.server.RemoteCall(远程调用)对象。)三大运营商会帮你找到你的女神。

  3. 运营商在收到你的信号后会将你这个呼出的动作转换成其他形式的信号进行传输(RemoteCall序列化RMI服务名称Remote对象。)

  4. 然后运营商通过自己架设的线路进行通信(RMI客户端远程引用层传输RemoteCall序列化后的请求信息通过Socket连接的方式传输到RMI服务端远程引用层。)

  5. 在找到对方之后,对方手机会立刻响铃(RMI服务端远程引用层(sun.rmi.server.UnicastServerRef)收到请求会请求传递给Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch);这个Skeleton就相当于女神的手机)

  6. 此时对方手机会将之前用其他形式传输的信号,转变为人能看懂的文字(Skeleton调用RemoteCall反序列化RMI客户端传过来的序列化。)

  7. 对方查看手机(Skeleton)信息,并用手机给你进行回复:“你是个好人”(Skeleton处理客户端请求:bindlistlookuprebindunbind,如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端。)

  8. 你收到信息,并看到是女神给你的回复,非常激动!(RMI客户端反序列化服务端结果,获取远程对象的引用)

  9. 你双击信息打开进一步查看内容(RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。)

  10. 结果看到女神送给你的“好人卡”(RMI客户端反序列化RMI远程方法调用结果。)

在代码实现之前我们还需要明白一个在RMI中扮演着重要角色的”注册中心“,他在服务端创建。

我们知道java远程对象是基于socket来进行传输的,有socket就要有端口。

举个例子:

服务端现在有一个名为A的远程对象。现在你是客户端,你并不知道服务端把这个A开放在对应的哪个端口。就算你知道了A的开放端口。服务其上还有B、C、D……..难道你要记住每个远程对象的端口吗?很明显这是不现实的。

那么这个时候,就要用到我们的注册中心。他的作用就是将自己绑定在服务端的某个端口(默认1099)这样你只需要记住一个端口,他会帮我们记住这100个远程对象对应的开放端口。当客户端进行调用的时候,只需要告诉注册中心,我要找A,那么注册中心就会告诉你A所对应的端口。有点像交换机和路由器的原理。

在了解这个重要概念以后,我们再来接着往下看RMI具体是如何实现的。

RMI具体代码实现如下:

服务端代码:

1
2
3
4
5
6
7
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IRemoteObj extends Remote {
//这个接口定义着你都能调用服务端的什么方法。需要抛出 RemoteException
public String sayHello(String keywords) throws RemoteException;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj {
public RemoteObjImpl() throws RemoteException{
//UnicastRemoteObject.exportObject(this,0); //如果不继承UnicastRemoteObject就需要手动导出
}


@Override
public String sayHello(String keywords){
String upKeywords = keywords.toUpperCase();
System.out.println(upKeywords);
return upKeywords;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
IRemoteObj remoteObj = new RemoteObjImpl(); //创建远程对象
Registry r = LocateRegistry.createRegistry(1099); //创建注册中心
r.bind("remoteObj",remoteObj); //绑定远程对象
System.out.println("server is OK");
}
}

客户端代码:

1
2
3
4
5
6
7
8
9
10
11
12
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IRemoteObj extends Remote {
public String sayHello(String keywords) throws RemoteException;
}

/*
是不是有点眼熟,你可能会有疑问:为什么客户端还要创建一个和服务端相同的接口?
通过共享接口定义,RMI 框架能够在编译时进行类型检查,确保方法调用的正确性。同时,RMI 框架能够自动生成代理对象和存根对象,它们负责处理远程方法的序列化、网络传输和反序列化等细节。

*/
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

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
//写法一:
//获取注册中心
Registry registry = LocateRegistry.getRegistry("192.168.56.1",1099);
//查找远程对象
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");
//调用远程方法
String out = remoteObj.sayHello("hello");
System.out.println(out);
//写法二:
//直接查找远程对象
IRemoteObj remoteObj1 = (IRemoteObj) Naming.lookup("rmi://127.0.0.1:1088/remoteObj");
//调用远程方法
String out1 = remoteObj1.sayHello("hi");
System.out.println(out1);
}
}

首先在服务端创建远程对象的时候”IRemoteObj remoteObj = new RemoteObjImpl();“这一句代码,我们来分析一下他都干了什么

  1. 因为我们继承了UnicastRemoteObject(),所以首先会来到他父类的构造方法处并进行赋值0

i

  1. 随后会将传入的参数赋值给port,相当于给port传入默认值0,如果传入0后续会将远程对象发布在随机的端口上。

image-20230816183938605

  1. 这也就是是我们之前代码中如果继承了这个类就不需要再次调用,如果不继承则需要手动调用

image-20230816184413215

  1. 跟进UnicastServerRef()

image-20230816184541021

  1. 在进行跟进LiveRef()因为他会调用他的构造方法,所以我们直接看构造方法

image-20230816184721936

第一个参数和最后一个参数很好理解,中间的这个参数是什么呢?我们还需要再次跟进

  1. 我们可以看到它的返回值是TCPEndpoint所以很明显这里是处理网络请求的类。

image-20230816184834796

  1. 跟进发现,接收两个参数一个host一个port

image-20230816184951738

  1. 然后我们跟到LiveRef发现他恰好就接收这三个参数,而且从下方参数信息也可以看到它接收到了对应的参数。到这一直是封装封装。而真正处理网络请求的就是TCPTransport,并最后将的结果放入LiveRef

image-20230816185301173

  1. 之后会创建代理,细心的同学已经发现了,为什么服务端创建了stub?stub不是客户端的代理吗?

image-20230816185657657

​ 解答:stub是客户端的代理没错,但是客户端是通过stub来进行远程调用的。他整个的流程是先在服务端创建好stub,然后放在注册中心,客户端创建stub,然后去注册中心找对应的server端stub,找到以后再用server端的stub来操作server端Skeletion。

  1. 之后会创建他的类加载器、接口、调用处理器,而调用处理器里面的值还是之前的LiveRef

image-20230816190430758

  1. 可以看到这里已经把stub的端口从之前的初始值0变成了随机的端口,发布在注册中心,并记录server端。

image-20230816190840205

这里只是简单的跟一下服务端原创对象的创建过程,让大家清楚的看到,服务端在创建对象的时候,实际上都干了什么。具体细节感兴趣的同学可以自己跟一下。

接下来我们再跟一下创建注册中心Registry r = LocateRegistry.createRegistry(1099);看看他都干了什么

  1. 创建注册中心,接收参数端口。

image-20230816191650239

  1. 跟进,发现底层创建了LiveRefUnicastServerRef。服务端创建原创对象的时候也是这样的操作。

image-20230816191957806

  1. 唯一的不同是他创建的这个远程对象是永久的远程对象,而我们之前自定义的远程对象是一个临时的对象。可以看到,后面还是在服务端创建stub给客户端来进行调用。

image-20230816192249999

  1. 可以看到注册中心创建的stub他是由froname创建的,之前创建远程对象的stub实际上是靠动态代理的调用处理器创建的。他们两个的创建方式不一样。

image-20230816192630687

  1. 之后会将创建好的远程对象放进table当中

image-20230816193458856

然后服务端最后一步将远程对象绑定在注册中心这一步比较简单,我们就不跟了。(不是我懒,是今天天气不好,不适合调试)

至此我们把服务端整个RMI的流程简单分析了一遍,现在大家都应该清楚他们每一步到底干了什么。

RMI反序列化

从RMI设计角度来讲,基本分为三层架构模式来实现RMI,分别为RMI服务端RMI客户端RMI注册中心,如下图所示。

image-20230816182716313

他们之间可以相互两两通信。由于RMI在网络传输数据是需要对数据进行序列化和反序列化的。那么这个时候我们其实就可以利用他反序列化从而进行利用。

这里先提一嘴,我下一篇文章在出利用吧,主要不是我懒,是我怕文章太长,使得大家阅读枯燥无味。


没人比我更懂RMI(上)
http://example.com/2023/08/15/没人比我更懂RMI/
作者
Yuanyi
发布于
2023年8月15日
许可协议