什么是RPC
RPC(Remote Procedure Call,远程过程调用),一般用来实现部署在不同机器上的系统之间的方法调用,使得程序能够像访问本地系统资源一样,通过网络传输去访问远端系统资源;对于客户端来说, 传输层使用什么协议,序列化、反序列化都是透明的
了解 Java RMI
RMI 全称是 remote method invocation – 远程方法调用,一种用于远程过程调用的应用程序编程接口,是纯 java 的网络分布式应用系统的核心解决方案之一。
RMI 目前使用 Java 远程消息交换协议 JRMP(Java Remote Messageing Protocol)进行通信,由于 JRMP 是专为 Java对象制定的,是分布式应用系统的百分之百纯 java 解决方案,用 Java RMI 开发的应用系统可以部署在任何支持 JRE的平台上,缺点是,由于 JRMP 是专门为 java 对象指定的,因此 RMI 对于非 JAVA 语言开发的应用系统的支持不足,不能与非 JAVA 语言书写的对象进行通信
Java RMI 代码实践
详见代码
远程对象必须实现 UnicastRemoteObject,这样才能保证客户端访问获得远程对象时,该远程对象会把自身的一个拷贝以 Socket 形式传输给客户端,客户端获得的拷贝称为“stub” , 而 服 务 器 端 本 身 已 经 存 在 的 远 程 对 象 成 为“skeleton”,此时客户端的 stub 是客户端的一个代理,用于与服务器端进行通信,而 skeleton 是服务端的一个代理,用于接收客户端的请求之后调用远程方法来响应客户端的请求
Java RMI 源码分析
远程对象发布
远程引用层
一步步解读源码
发布远程对象
发布远程对象,看到上面的类图可以知道,这个地方会发布两个远程对象,一个是 RegistryImpl、另外一个是我们自己写的 RMI 实现类对象;
从 HelloServiceImpl 的 构 造 函 数 看 起 。 调 用 了 父 类UnicastRemoteObject 的 构 造 方 法 , 追 溯 到
UnicastRemoteObject 的私有方法 exportObject()。这里做 了 一 个 判 断 , 判 断 服 务 的 实 现 是 不 是UnicastRemoteObject 的子类,如果是,则直接赋值其 ref(RemoteRef)对象为传入的 UnicastServerRef 对象。反之则调用 UnicastServerRef 的 exportObject()方法。IHelloService helloService=new HelloServiceImpl();因为 HelloServiceImpl 继承了 UnicastRemoteObject,所以在服务启动的时候,会通过 UnicastRemoteObject 的构造方法把该对象进行发布
服务端启动 Registry 服务
从上面这段代码入手,开始往下看。可以发现服务端创建了一个 RegistryImpl 对象,这里做了一个判断,如果服务端指定的端口是 1099 并且系统开启了安全管理器,那么就可以在限定的权限集内绕过系统的安全校验。这里纯粹是为 了 提 高 效 率 ,真 正 的 逻 辑 在 this.setup(new UnicastServerRef())这个方法里面
setup 方法将指向正在初始化的 RegistryImpl 对象的远程引用 ref(RemoteRef)赋值为传入的 UnicastServerRef 对象,这里涉及到向上转型,然后继续执行 UnicastServerRef 的exportObject 方法
进入 UnicastServerRef 的 exportObject()方法。可以看到,这里首先为传入的 RegistryImpl 创建一个代理,这个代理我们可以推断出就是后面服务于客户端的 RegistryImpl 的Stub(RegistryImpl_Stub)对象。然后将 UnicastServerRef的 skel(skeleton)对象设置为当前RegistryImpl 对象。最后用 skeleton、stub、UnicastServerRef 对象、id 和一个boolean 值构造了一个 Target 对象,也就是这个 Target 对象基本上包含了全部的信息,等待 TCP 调用。调用UnicastServerRef 的 ref(LiveRef)变量的 exportObject()方法。
【var1=RegistryImpl ; var 2 = null ; var3=true】LiveRef 与 TCP 通信的类
到上面为止,我们看到的都是一些变量的赋值和创建工作,还没有到连接层,这些引用对象将会被 Stub 和 Skeleton
对象使用。接下来就是连接层上的了。追溯 LiveRef 的exportObject()方法,很容易找到了 TCPTransport 的exportObject()方法。这个方法做的事情就是将上面构造的Target 对象暴露出去。调用 TCPTransport 的 listen()方法,listen()方法创建了一个 ServerSocket,并且启动了一条线程 等 待客 户端的 请 求。 接着调 用 父类 Transport 的exportObject()将 Target 对象存放进 ObjectTable 中。
到这里,我们已经将 RegistryImpl 对象创建并且起了服务等待客户端的请求。
客户端获取服务端 Registry 代理
从 上面的 代码看 起,容 易追溯 到 LocateRegistry 的getRegistry()方法。这个方法做的事情是通过传入的 host和 port 构造 RemoteRef 对象,并创建了一个本地代理。这个代理对象其实是 RegistryImpl_Stub 对象。这样客户端便 有 了 服 务 端 的 RegistryImpl 的 代 理 ( 取 决 于ignoreStubClasses 变量)。但注意此时这个代理其实还没有和服务端的 RegistryImpl 对象关联,毕竟是两个 VM 上面的对象,这里我们也可以猜测,代理和远程的 Registry对象之间是通过 socket 消息来完成的。
调用 RegistryImpl_Stub 的 ref(RemoteRef)对象的newCall()方法,将 RegistryImpl_Stub 对象传了进去,不要忘了构造它的时候我们将服务器的主机端口等信息传了进去,也就是我们把服务器相关的信息也传进了 newCall()方法。newCall()方法做的事情简单来看就是建立了跟远程RegistryImpl 的 Skeleton 对象的连接。(不要忘了上面我们说到过服务端通过 TCPTransport 的 exportObject()方法等待着客户端的请求)
连接建立之后自然就是发送请求了。我们知道客户端终究只是拥有 Registry 对象的代理,而不是真正地位于服务端的 Registry 对象本身,他们位于不同的虚拟机实例之中,无法直接调用。必然是通过消息进行交互的。看看super.ref.invoke() 这 里 做 了 什 么 ? 容 易 追 溯 到StreamRemoteCall 的 executeCall()方法。看似本地调用,但其实很容易从代码中看出来是通过 tcp 连接发送消息到服务端。由服务端解析并且处理调用。
至此,我们已经将客户端的服务查询请求发出了。服务端接收客户端的服务查询请求并返回给客户端结果这里我们继续跟踪 server 端代码的服务发布代码,一步步往上面翻。按照下图顺序
LiveRef.class
TCPEndpoint.class
TCPTransport.class
在 TCP 协议层发起 socket 监听,并采用多线程循环接收请求:TCPTransport.AcceptLoop(this.server)
继续通过线程池来处理 socket 接收到的请求
下面这个 run0 方法里面做了一些判断,具体的功能是干嘛不太清楚,我猜想是对不同的协议来做处理。我们的这个案例中,会走到如下的代码中来。最终调用TCPTransport.this.handleMessages(var14, true);
这个地方也做了判断,你们如果不知道怎么走的话,直接在这里加断点就知道。这里会走到 case 80 的段落,执行serviceCall()这个方法
一步一步我们找到了 Transport 的 serviceCall()方法,这个方 法 是 关 键 。 瞻 仰 一 下 主 要 的 代 码 , 到
ObjectTable.getTarget()为止做的事情是从 socket 流中获取 ObjId,并通过 ObjId 和 Transport 对象获取 Target 对象,这里的 Target 对象已经是服务端的对象。再借由 Target的派发器 Dispatcher,传入参数服务实现和请求对象RemoteCall,将请求派发给服务端那个真正提供服务的RegistryImpl 的 lookUp()方法,这就是 Skeleton 移交给具体实现的过程了,Skeleton 负责底层的操作。
所以客户端通过
先会创建一个 RegistryImpl_Stub 的代理类,通过这个代理类进行 socket 网络请求,将 lookup 发送到服务端,服务端通过接收到请求以后,通过服务端的 RegistryImpl_Stub(Skeleton),执行 RegistryImpl 的 lookUp。而服务端的RegistryImpl 返回的就是服务端的 HeloServiceImpl 的实现类
客 户 端 获 取 通 过 lookUp() 查 询 获 得 的 客 户 端HelloServiceImpl 的 Stub 对象
客 户 端 通 过 Lookup 查 询 获 得 的 是 客 户 端HelloServiceImpl 的 Stub 对象(这一块我们看不到,因为这块由 Skeleton 为我们屏蔽了),然后后续的处理仍然是通过 HelloServiceImpl_Stub 代理对象通过 socket 网络请求到服务端,通过服务端的
HelloServiceImpl_Stub(Skeleton) 进行代理,将请求通过Dispatcher 转发到对应的服务端方法获得结果以后再次通
过 socket 把结果返回到客户端;
RMI 做了什么
根据上面的源码阅读,实际上我们看到的应该是有两个代理类,一个是 RegistryImpl 的 代 理 类 和 我 们HelloServiceImpl 的代理类。
通信原理
我们大概知道了 RMI 框架是如何使用的。下面我们来讲解一下 RMI 的基本原理
一定要说明,在 RMI Client 实施正式的 RMI 调用前,它必须通过 LocateRegistry 或者 Naming 方式到 RMI 注册表寻找要调用的 RMI 注册信息。找到 RMI 事务注册信息后,Client 会从 RMI 注册表获取这个 RMI Remote Service 的Stub 信息。这个过程成功后,RMI Client 才能开始正式的调用过程。
另外要说明的是 RMI Client 正式调用过程,也不是由 RMI Client 直接访问 Remote Service,而是由客户端获取的Stub 作为 RMI Client 的代理访问 Remote Service 的代理Skeleton,如上图所示的顺序。也就是说真实的请求调用是在 Stub-Skeleton 之间进行的。
Registry 并不参与具体的 Stub-Skeleton 的调用过程,只负责记录“哪个服务名”使用哪一个 Stub,并在 Remote Client 询问它时将这个 Stub 拿给 Client(如果没有就会报错)。