一、概述
RMI全称是Remote Method Invocation(远程方法调用),是专为Java环境设计的远程方法调用机制,远程服务器提供API,客户端根据API提供相应参数即可调用远程方法。
由此可见,使用RMI时会涉及到参数传递和结果返回,参数为对象时,要求对象可以被序列化。目的是为了让两个隔离的java虚拟机,如虚拟机A能够调用到虚拟机B中的对象,而且这些虚拟机可以不存在于同一台主机上。
RMI存在着三个主体:
-
RMI Registry
-
RMI Client
-
RMI Server
RMI中主要的api大致有:
-
java.rmi:提供客户端需要的类、接口和异常;
-
java.rmi.server:提供服务端需要的类、接口和异常;
-
java.rmi.registry:提供注册表的创建以及查找和命名远程对象的类、接口和异常;
二、举例
1、服务端
就服务端而言,需要提供远程对象给与客户端远程调用,所谓远程对象即实现java.rmi.Remote接口的类或者继承了java.rmi.Remote接口的所有接口的远程对象。
一个RMI Server分为三部分:
-
部分1:一个继承了java.rmi.Remote 的接口,其中定义我们要远程调用的函数,比如这里的sayHello()
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface RemoteObj extends Remote {
public String sayHello(String keywords) throws Exception;
}
-
部分2:一个实现了此接口的类,首先有几个关键点:
-
实现方法必须抛出RemoteException异常
-
实现类需要同时继承UnicastRemoteObject类,如果不继承,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法
-
只有在接口中声明的方法才能被调用到
-
import java.rmi.server.UnicastRemoteObject;
public class RemoteObjImpl extends UnicastRemoteObject implements RemoteObj {
public RemoteObjImpl() throws Exception{
}
public String sayHello(String keywords) throws Exception {
String upKeywrods = keywords.toUpperCase();
System.out.println("Server:" + upKeywrods);
return upKeywrods;
}
}
-
部分3:一个主类,用来创建Registry,并将上面的类实例化后绑定到一个地址。就服务端而言其实现的关键在于Naming这个类,利用bind方法将对象绑定一个名。
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws Exception {
RemoteObjImpl remoteObj = new RemoteObjImpl();
Registry registry = LocateRegistry.createRegistry(1099);
// 写法一:通过Naming绑定
// Naming.bind("remoteObj",remoteObj);
// 写法二:通过registry绑定
registry.bind("remoteObj",remoteObj);
}
}
2、客户端
客户端也需要具有接口类
import java.rmi.Remote;
public interface RemoteObj extends Remote {
public String sayHello(String keywords) throws Exception;
}
客户端的操作可变性就很多了,同样是通过Naming类中提供的方法来操作,有如下几种方法:
-
lookup
-
list
-
bind
-
rebind
-
unbind
拿list举例子(可以列出目标上所有绑定的对象):
-
我们可以通过list方法获取到目标server上有哪些绑定的方法
import java.rmi.Naming;
public class HelloRmiClientList {
public static void main(String[] args) throws Exception {
String[] clazz = Naming.list("rmi://127.0.0.1:1099");
for (String s:clazz) {
System.out.println(s);
}
}
}
用lookup举例子(获得某个远程对象):
-
当上面获取到接口的方法时,用lookup可以执行远程方法,代码是在远程服务器器上执行,返回给客户端的为一个Remote对象,可以强转为我们本地的对象进行利用:
-
所以捋一捋这整个过程,首先客户端连接Registry,并在其中寻找Name是Hello的对象,这个对应数据流中的Call消息;然后Registry返回一个序列化的数据,这个就是找到的Name=Hello的对象,这个对应数据流中的ReturnData消息;客户端反序列化该对象,发现该对象是一个远程对象,地址在192.168.135.142:33769 ,于是再与这个地址建立TCP连接;在这个新的连接中,才执行真正远程方法调用,也就是hello() 。
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
public static void main(String[] args) throws Exception {
// 方法一:通过registry的lookup
// Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
// RemoteObj remoteObj = (RemoteObj)registry.lookup("remoteObj");
// System.out.println("Client:" + remoteObj.sayHello("hello"));
// 方法二:通过Naming的lookup
RemoteObj RemoteObj = (RemoteObj)Naming.lookup("rmi://127.0.0.1:1099/remoteObj");
System.out.println("Client:" + RemoteObj.sayHello("hello"));
}
}
Server的窗口:
Client的窗口:
在上述操作后,我们会发现Server输出了Server:HELLO,并且Client端也输出了Client:HELLO。到底怎么回事呢?
其实,在Server启动的时候,Server启动了一个RMI的注册中心,接着把RemoteObjImpl暴露并注册到RMI注册中心,其中存储着RemoteObjImpl的stub数据,包含有RemoteObjImpl所在服务器的ip和port。在Client启动之后,通过连接RMI注册中心,并从其中根据名称查询到了对应的对象(JNDI),并把其数据下载到本地,然后RMI会根据stub存储的信息,也就是Server中RemoteObjImpl实现暴露的ip和port,最后通过JRMP协议发起RMI请求,RMI后,Server输出大写之后的hello并通过JRMP协议把大写之后的hello的序列化数据返回给程序B,程序B对其反序列化后输出。
PS:java.rmi.registry.Registry 和 java.rmi.Naming 类之间的区别
不同之处在于Naming是一个具有静态方法的实用程序类,Registry而是一个远程接口。
请注意,传递给java.rmi.Naming中name的参数是 URL 格式,并包括注册表的位置,而使用java.rmi.registry.Registry,name只是名称。
例如,可以这样调用:
Naming.rebind("//host/objName", myObj);
而使用Registry,您需要注册表对象上的现有句柄,并且调用:
Registry registry = LocateRegistry.getRegistry("host");
registry.rebind("objName", myObj);
所以Naming实际上只是一个方便的类,它可以让您不必Registry手动查找 - 它一步执行注册表查找和重新绑定。
三、逻辑关系
1、基本逻辑关系
RMI Registry就像一个网关,他自己是不会执行远程方法的,但RMI Server可以在上面注册一个Name到对象的绑定关系;RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接RMI Server;最后,远程方法实际上在RMI Server上调用。
在JVM之间通信时,RMI对远程对象和非远程对象的处理方式是不一样的,它并没有直接把远程对象复制一份传递给客户端,而是传递了一个远程对象的Stub,Stub基本上相当于是远程对象的引用或者代理。Stub对开发者是透明的,客户端可以像调用本地方法一样直接通过它来调用远程方法。Stub中包含了远程对象的定位信息,如Socket端口、服务端主机地址等等,并实现了远程调用过程中具体的底层网络通信细节,所以RMI远程调用逻辑是这样的:
从逻辑上来看,数据是在Client和Server之间横向流动的,但是实际上是从Client到Stub,然后从Skeleton到Server这样纵向流动的。
-
1.Server端监听一个端口,这个端口是JVM随机选择的;
-
2.Client端并不知道Server远程对象的通信地址和端口,但是Stub中包含了这些信息,并封装了底层网络操作;
-
3.Client端可以调用Stub上的方法;
-
4.Stub连接到Server端监听的通信端口并提交参数;
-
5.远程Server端上执行具体的方法,并返回结果给Stub;
-
6.Stub返回执行结果给Client端,从Client看来就好像是Stub在本地执行了这个方法一样;
2、Stub如何获取呢
Stub的获取方式有很多,常见的方法是调用某个远程服务上的方法,向远程服务获取存根。但是调用远程方法又必须先有远程对象的Stub,所以这里有个死循环问题。JDK提供了一个RMI注册表(RMIRegistry)来解决这个问题。RMIRegistry也是一个远程对象,默认监听在传说中的1099端口上,可以使用代码启动RMIRegistry,也可以使用rmiregistry命令。
-
要注册远程对象,需要RMI URL和一个远程对象的引用
IHello rhello = newHelloImpl();
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://0.0.0.0:1099/hello", rhello);
LocateRegistry.getRegistry()会使用给定的主机和端口等信息本地创建一个Stub对象作为Registry远程对象的代理,从而启动整个远程调用逻辑。服务端应用程序可以向RMI注册表中注册远程对象,然后客户端向RMI注册表查询某个远程对象名称,来获取该远程对象的Stub。
Registry registry = LocateRegistry.getRegistry("kingx_kali_host",1099);
IHello rhello = (IHello) registry.lookup("hello");
rhello.sayHello("test");
-
使用RMI Registry之后,RMI的调用关系是这样的:
所以其实从客户端角度看,服务端应用是有两个端口的,一个是RMI Registry端口(默认为1099),另一个是远程对象的通信端口(随机分配的)。这个通信细节比较重要,真实利用过程中可能会在这里遇到一些坑。
参考: