深入了解javaRMI
前言:JavaRMI的使用并不麻烦,让我真正感兴趣的是stubs和skeletons,更让我感兴趣的是RMI框架如何让客户端感觉像在调用本地方法一样去使用远程对象的方法。(阅读本文建议先了解rmi的基本用法,可参考附件demo)
本篇文章重点关注以下问题:
- 什么是stubs和skeletons对象?谁创建了他们?扮演什么角色?
- 客户端如何找到服务端?如何知道服务端发布服务的端口?
- 注册中心是否是必须的?什么作用?
1、回归本质需求
首先忘记注册中心,最开始的使用场景是这样的:服务端Server发布服务,客户端Client需要使用到Server发布的服务(可以理解成api),Server继承了java.rmi.server.UnicastRemoteObject,问题在于Server和Client运行在不同的机器上面,而Client却想执行一个在远程机器上Server发布的方法。
很容易想到,Rmi想解决上述问题肯定会涉及到Socket网络编程,因为Server运行在远程机器上,牵扯到具体实现有两个关键点:
- 客户端Client如何从处理网络连接中解耦开来,从而专注于业务?
- 客户端Client如何像调用本地方法一样来调用远程机器上的方法?
上述问题就是stubs和skeletons存在的意义,正是stubs和skeletons的存在才能让客户端和服务器不需要处理网络相关的方法。stubs同样实现了和服务端同样的java.rmi.server.UnicastRemoteObject接口,这样当Client想调用Server上的方法时,就可以调用stubs上的相同的方法,但是stub上只有和网络相关的处理逻辑,并没有对应的业务处理逻辑。比如说Server上发布了一个add方法,则stub中也同样有一个add方法,但是stub上的这个add方法并不包含业务逻辑部分的实现,它仅仅包含如何连接到远程的skeleton,调用方法的详细信息、参数、返回值等。整个实现架构如下图所示:
简言之,就是客户端Client和stub对话,stub和skeleton对话,skeleton和Server对话,Server执行真正的方法,饭后把结果原路返回。由图中也可以看出,Rmi的功能主要由四个部分组成:Client、stub、skeleton、Server。
2、Socket层详细信息
socket层通信的整个流程:
1.Server在远程机器上发布服务,并监听一个端口。这个端口是JVM或是OS在运行时随机选择的一个端口,可以说Server在远程机器的端口上发布服务。
2.Client调用Server上发布的方法。其实Client并不知道Server在哪,也并不知道Server在监听哪个端口,但是Client有stub,stub知道所有这些东西,这样Client就可以调用stub上他想调用的任何方法;
3、stub链接到Server监听的端口上并发送参数,过程如下:
> Client连接到Server监听的端口;
> Server收到请求并创建一个socket来处理这个链接;
> Server继续监听到来的请求;
> Client和Server使用双发商量的协议来传送参数和结果;(协议可以是JRMP或者 iiop)
4. 方法在远程Server上执行,并将执行结果返回给stub
5. stub返回结果给Client,就好像stub执行了这个方法一样。
但是,仔细揣摩上述过程,似乎仍有漏洞,第二点说stub知道Server在哪,stub知道Server监听的端口,但是Server发布服务的端口是随机的,stub如何知道Server?不知道Server的Ip和Port也就不可能创建一个知道一切的stub。这也引出RMIRegistry注册中心的存在价值。
3、RMIRegistry注册中心的作用
RmiRegistry可以看做是一个服务,它维护了一个hashMap,key为publicName,value为stunObject。比如远程机器上布置一个服务Calculator,并随机在某个端口上发布(此端口客户端无法得知),Server发布服务后会同时在服务端创建一个stub对象,然后把它注册到RmiRegistry,这样注册中心RmiRegistry就知晓了Server和Client之间网络传输的所有条件,当前,前提是Client可以联系到RmiRegistry。庆幸的是,RMIRegistry 发布服务的默认端口是公开的、广为人知的,即1099,当然,也可以自己指定注册服务的端口,这样Client就可以从注册中心RMIRegistry 得到这个stub对象。
4、结合例子阐述整个过程
4.1 服务端发布服务
定义远程服务接口:
/** * 定义一个远程接口,必须继承Remote接口,其中需要远程调用的方法必须抛出RemoteException异常 * @author Administrator */ public interface IHello extends Remote { /** * 简单的返回“Hello World!"字样 * @return 返回“Hello World!"字样 * @throws java.rmi.RemoteException */ public String helloWorld() throws RemoteException; /** * 一个简单的业务方法,根据传入的人名返回相应的问候语 * @param someBodyName 人名 * @return 返回相应的问候语 * @throws java.rmi.RemoteException */ public String sayHelloToSomeBody(String someBodyName) throws RemoteException; }
Server服务端的实现:
/** * 远程的接口的实现 * @author Administrator */ public class HelloImpl extends UnicastRemoteObject implements IHello { private static final long serialVersionUID = -5638936712154214504L; /** * 因为UnicastRemoteObject的构造方法抛出了RemoteException异常,因此这里默认的构造方法必须写,必须声明抛出RemoteException异常 * @throws RemoteException */ public HelloImpl() throws RemoteException {} @Override public String helloWorld() throws RemoteException { return "Hello World!"; } @Override public String sayHelloToSomeBody(String someBodyName) throws RemoteException { return "你好," + someBodyName + "!"; } }
Server发布服务,大致 步骤如下:
- 创建一个远程服务对象,及实现;
- 在指定的端口上发布RmiRegistry服务,用户客户端连接;
- 在注册中心上发布服务;
public static void main(String[] args) { try { // 1. 创建一个远程对象 IHello rhello = new HelloImpl(); // 2. 本地主机上的远程对象注册表Registry的实例,并指定端口为8888,这一步必不可少,缺少注册表创建,则无法绑定对象到远程注册表上 LocateRegistry.createRegistry(8888); // 3. 把远程对象注册到RMI注册服务器上,并命名为RHello //绑定的URL标准格式为:rmi://host:port/name(其中协议名可以省略,下面两种写法都是正确的) //Naming.bind("rmi://localhost:8888/RHello",rhello); Naming.bind("//localhost:8888/RHello",rhello); System.out.println(">>>>>INFO:远程IHello对象绑定成功!"); } catch (RemoteException e) { System.out.println("创建远程对象发生异常!"); e.printStackTrace(); } catch (AlreadyBoundException e) { System.out.println("发生重复绑定对象异常!"); e.printStackTrace(); } catch (MalformedURLException e) { System.out.println("发生URL畸形异常!"); e.printStackTrace(); }
上述为服务端Server发布服务的整个过程。
4.2 客户端使用服务端发布的服务
客户端上定义一个同远程接口上的服务接口(包名应该与服务器发布的接口包名一致)
/** * (包名应该与服务器发布的接口包名一致) * @author Administrator */ public interface IHello extends Serializable { /** * 简单的返回“Hello World!"字样 * @return 返回“Hello World!"字样 * @throws java.rmi.RemoteException */ public String helloWorld() throws RemoteException; /** * 一个简单的业务方法,根据传入的人名返回相应的问候语 * @param someBodyName 人名 * @return 返回相应的问候语 * @throws java.rmi.RemoteException */ public String sayHelloToSomeBody(String someBodyName) throws RemoteException; }客户端上使用服务端发布的服务,步骤如下:
- 向注册中心查找stun;
- 使用服务。
/** * 客户端测试,在客户端调用远程对象上的远程方法,并返回结果。 * @author Administrator */ public class HelloClient { public static void main(String[] args) { try { // 1. 在RMI服务注册表中查找名称为RHello的对象,并调用其上的方法 IHello rhello =(IHello) Naming.lookup("rmi://localhost:8888/RHello"); // 2. 调用服务 System.out.println(rhello.helloWorld()); System.out.println(rhello.sayHelloToSomeBody("熔岩")); } catch (NotBoundException e) { e.printStackTrace(); } catch (MalformedURLException e) { e.printStackTrace(); } catch (RemoteException e) { e.printStackTrace(); } } }
补充说明:之前在多网卡ubantu系统上调试RMI,发现服务端发布绑定的URI并不是我所指定的。如果服务端有多个网卡,它只是使用其中任意一个网卡,所以在多网卡的服务器上开启RMI服务的话得指定所使用的IP。常规办法如下:
- 如果是CS程序,在启动RMI服务前,通过以下代码指定RMI服务使用的IP地址:
System.setProperty("java.rmi.server.hostname","192.168.1.111");
-
如果是WEB程序,添加如下启动参数:
-Djava.rmi.server.hostname=192.168.1.111
以Tomcat为例(Windows)
如果用startup.bat方式启动,则在catalina.bat文件中加入如下语句:
set CATALINA_OPTS=-Djava.rmi.server.hostname=192.168.1.111