RMI机制
简介
RMI(Remote Method Invocation)为远程方法调用,是允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。 这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中,它的底层是由socket和java序列化和反序列化支撑起来的。
它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。RMI全部的宗旨就是尽可能简化远程接口对象的使用。
我们知道远程过程调用(Remote Procedure Call, RPC)可以用于一个进程调用另一个进程(很可能在另一个远程主机上)中的过程,从而提供了过程的分布能力。Java 的 RMI 则在 RPC 的基础上向前又迈进了一步,即提供分布式对象间的通讯。
RMI依赖的通信协议为JRMP(Java Remote Message Protocol ,Java 远程消息交换协议),该协议为Java定制,要求服务端与客户端都为Java编写。这个协议就像HTTP协议一样,规定了客户端和服务端通信要满足的规范。在RMI中对象是通过序列化方式进行编码传输的。
- Client-客户端:客户端调用服务端的方法
- Server-服务端:远程调用方法对象的提供者,也是代码真正执行的地方,执行结束会返回给客户端一个方法执行的结果
- Registry-注册中心:其实本质就是一个map,相当于是字典一样,用于客户端查询要调用的方法的引用(在低版本的JDK中,Server与Registry是可以不在一台服务器上的,而在高版本的JDK中,Server与Registry只能在一台服务器上,否则无法注册成功)
RMI
远程对象调用过程
在JVM之间通信时,RMI对远程对象和非远程对象的处理方式是不一样的,它并没有直接把远程对象复制一份传递给客户端,而是传递了一个远程对象的Stub(存根),Stub相当于远程对象的引用或者代理。Stub对开发者是透明的,客户端可以像调用本地方法一样直接通过它来调用远程方法。Stub中包含了远程对象的定位信息,如Socket端口、服务端主机地址等等,并实现了远程调用过程中具体的底层网络通信细节。而位于服务器端的Skeleton(骨架),能够读取客户端传递的方法参数,调用服务器方的实际对象方法, 并接收方法执行后的返回值。所以RMI远程调用逻辑大致是这样的
从逻辑上来看,数据是在Client和Server之间横向流动的,但是实际上是从Client到Stub,然后从Skeleton到Server这样纵向流动的。
具体的通信流程如下
- Server监听一个端口,这个端口是JVM随机选择的
- Client并不知道Server远程对象的通信地址和端口,但是位于Client的Stub中包含了这些信息,并封装了底层网络操作。Client可以调用Stub上的方法,并且也可以向Stub发送方法参数。
- Stub连接到Server监听的通信端口并提交参数
- Server执行具体的方法,并将结果返回给Stub
- Stub返回执行结果给Client。因此在Clinet看来,就好像是Stub在本地执行了这个方法。
位于Client上的Stub获取到远程Server的通信信息的需要使用RMI Registry了
RMI Registry
RMI Registry的注册
JDK提供了一个RMI注册表(RMI Registry)来解决这个问题。RMI Registry也是一个远程对象,默认监听在1099端口上,可以使用代码启动RMI Registry,也可以使用rmiregistry命令。
要注册远程对象,需要RMI URL和一个远程对象的引用,如:
private void register() throws Exception{ |
上面代码使用 LocateRegistry.createRegistry(port)在本地的某个端口上创建了一个Registry。最后使用Naming.bind()将实例化对象和地址上的hello绑定在一起,作为远程对象的名字。注意这里使用的是rmi://协议。
RMI Registry的使用
注册完RMI Registry以后,我们将要调用的远程对象已经和服务器端的某个地址绑定在了一起。
package learn.rmi; |
LocateRegistry.getRegistry()会使用给定的主机和端口等信息在本地创建一个Stub对象作为Registry远程对象的代理,从而启动整个远程调用逻辑。服务端应用程序可以向RMI注册表中注册远程对象,然后客户端向RMI注册表查询某个远程对象名称,来获取该远程对象的Stub。这里我们使用了registry.lookup()来查询获取注册表中的远程对象。还有另一种写法
使用了RMI Registry后,RMI的调用关系如下
RMI调用流程
从客户端角度看,服务端应用是有两个端口的,一个是RMI Registry端口(默认为1099),另一个是远程对象的通信端口(随机分配的)。更详细的通信过程如下
使用
使用远程方法调用,必然会涉及参数的传递和执行结果的返回。参数或者返回值可以是基本数据类型,当然也有可能是对象的引用。所以这些需要被传输的对象必须可以被序列化,这要求相应的类必须实现java.io.Serializable接口,并且客户端的serialVersionUID字段要与服务器端保持一致。
任何可以被远程调用方法的对象必须继承java.rmi.Remote接口,远程对象的实现类必须继承UnicastRemoteObject类。如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法,如下:
定义远程服务对象接口并继承Remote接口,需要将远程调用的方法定义在接口里面,并必须抛出RemoteException(否则会报错)
package com.yyjccc.jndi.RMI;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface RMIService extends Remote {
public String say() throws RemoteException;
}定义上面接口的实现类并继承UnicastRemoteObject类
package com.yyjccc.jndi.RMI;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class MyService extends UnicastRemoteObject implements RMIService{
protected MyService() throws RemoteException {
super();
}
public String say() {
System.out.println("call say method");
return "hello";
}
// 在没有继承UnicastRemoteObject的时候构造函数也可以写成如下形式
// protected RMIHello() throws RemoteException{
// UnicastRemoteObject.exportObject(this,0);
// }
}定义RMI服务端并将对应的远程对象绑定到注册中心
package com.yyjccc.jndi.RMI;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
public class RMIServer {
public static void main(String[] args) {
try {
LocateRegistry.createRegistry(1099);
Naming.rebind("rmi://127.0.0.1:1099/myObj",new MyService());
} catch (RemoteException | MalformedURLException e) {
throw new RuntimeException(e);
}
}
}定义RMI客户端,lookup获取远程对象并调用方法
package com.yyjccc.jndi.RMI;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
public class RMIClient {
public static void main(String[] args) {
try {
RMIService myService = (RMIService) Naming.lookup("rmi://127.0.0.1:1099/myObj");
myService.say();
System.out.println();
} catch (NotBoundException | MalformedURLException | RemoteException e) {
throw new RuntimeException(e);
}
}
}
客户端本地必须有远程对象的接口,不然无法指定要调用的方法,而且其全限定名必须与服务器上的对象完全相同
JRMP协议
Java远程方法协议(Java Remote Method Protocol,JRMP)是特定于Java技术的、用于查找和引用远程对象的协议。这是运行在Java远程方法调用(RMI)之下、TCP/IP之上的线路层协议。
开启服务端,在开启WireShark抓包,再启动客户端调用,一次完整的RMI通信数据包如下
可以看到JRMP通信协议双方在完成确认后,随后Clinet向Registry发送”Call”信息,Registry回复”ReturnData”
我们看一下Registry的回复内容
这里传输的是服务器的java序列化数据。注意以上的部分。ac ed是Java序列化的魔术头,该数据流往后的部分就是序列化的内容了。
06 3d转换成十进制为3075,这便是Server在本地开放的随机端口。
RMI流程分析
Registry端
可以通过createRegistry()方法来创建一个Registry
Registry registry = LocateRegistry.createRegistry(1099); |
可以看到,在创建Registry是返回的是RegistryImpl对象
跟进setup
继续跟进UnicastServerRef.exportObject():
- 创建stub
加上_stub并进行实例化.RegistryImpl_Stub
- 创建Skeleton,这就是Server端处理 RMI Client 通信请求的具体操作类
因此最终createRegistry()的结果就是返回了一个RegistryImpl对象,并且赋值this.skel=RegistryImpl_Skel。
完整过程如下:
对远程对象的操作有以下5种
- bind
- list
- lookup
- rebind
- unbind
对于Registry端,操作远程对象其实就是操作HashTable,我们来看RegistryImpl中的bind操作
binddings属性其实就是一个HashTable
Registry使用的这张Hash表就类似于一张”路由表”,将name和绑定其上的远程对象联系了起来。
rebind和bind的区别在于后者不会覆盖以前已经绑定的对象
Client端
获取Registry
和使用如下代码效果一样
Registry registry = LocateRegistry.getRegistry(rmi,port); |
上面都是先获取Registry,与Registry端不同的是这里还会做一层封装
得到的是RegistryImpl_Stub对象
操作远程对象
在Client端,对远程对象的操作同样有以下5种
这里我们以RegistryImpl_Stub.bind操作为例进行分析
首先生成了一个RemoteCall通信对象来建立连接,注意参数里的opnum(操作数),上述5个操作中每一个操作都有一个opnum,这里bind操作的opnum为0。参数中的hash其实就是serializeID。
跟进newCall,在此时Client已经通过newConnection()和Server端建立了连接
然后Client通过StreamRemoteCall()提前将ObjID、opnum和serializeID发送给Server端
回到bind中,可以看到使用了writeObject()将我们要发送的name以及Remote远程对象序列化发送了过去
再往下,bind中的invoke和done便是接收处理服务端返回的信息
完整流程
### Server端 我们知道,在RMI过程中,Server端往往是开启两个端口的,一个1099端口用于Registry,另一个是随机端口用于与Client通信。而远程方法最终是在Server端执行的,Server会把执行的结果返回给Client端。我们上面在创建Registry的时候已经生成了一个RegistryImpl_Skel对象,正是这个对象与Client端的RegistryImpl_Stub通信 #### 开启通信端口 在创建我们的远程对象的时候,跟进构造方法到这里逐层跟进(有点套娃)
最后到TCPTransport.exportObject(),开始监听端口,跟进listen()
这里listen()启动了一个新的线程,跟进看看这个线程做了什么
直接在TCPTransport$AcceptLoop#run处打下断点
此时已经从主线程到了RMI TCP Accept-0线程,调用了executeAcceptLoop(),跟进
在执行完this.serverSocket.accept()之后server才开始真正等待Client的连接
与Client通信
紧接上文,我们在executeAcceptLoop()方法中的TCPTransport.connectionThreadPool.execute()方法处下个断点,然后Client向Server发送连接请求,如下
这里创建了一个ConnectionHandler句柄,我们跟进ConnectionHandler句柄的run()方法,在TCPTransport.run()中
跟进run0(),最终到了该方法中的handleMessages(),这是用来传递消息的句柄
跟进,创建了StreamRemoteCall对象,跟进下面的serviceCall()方法
可以看到serviceCall()方法的参数是一个RemoteCall对象。这里就是Client传来的RemoteCall对象,然后对该对象进行各种操作,读取其中的信息。
最终会调用dispatch()方法
然后调用oldDispatch(),最后跟到this.skel.dispatch()方法中。最终在RegistryImpl_Skel.dispatch()中根据Client发来的信息进行各种操作。
最终在RegistryImpl_Skel.dispatch()进行了各种数据的反序列化