接上文《架构设计:系统间通信(8)——通信管理与RMI 上篇》。之前说过,JDK中的RMI框架在JDK1.1、JDK1.2、JDK1.5、JDK1.6+几个版本中做了较大的调整。以下我们讨论的RMI工作原理都是基于JDK1.6+版本的。

3、JAVA RMI 工作原理

通过上面的两组代码,我们大概知道了RMI框架是如何使用的。下面我们来讲解一下RMI的基本原理。本人翻阅网上的众多RMI资料基本上代码都是一大抄(甚至变量名、语法错误都一样),还有很多资料存在误导读者的情况。下图描述了整个RMI框架的几个核心概念:

3-1、Registry和Stub、Skeleton的关系

  • 一定要说明,在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(如果没有就会报错)。

  • 为了验证上文描述的调用过程,我们编写一个简单的程序验证一下Registry、Stub和Skeleton的关系。在下面的实验程序中,我们将Registry、Stub、Skeleton分布于三个独立的JVM上。然后当Remote Client在Registry上成功查询到Remote Service的Stub后,将Registry JVM终止掉。验证一下Stub-Skeleton交互过程会不会受到影响。

以下是一个独立运行的Registry程序(注意,它只负责注册、查询工作):

 package testRMI;

 import java.rmi.registry.LocateRegistry;

 /**
* 就是一个RMI注册仓库,不负责其它事情。<br>
* 创建仓库后,锁住WAITOBJECT对象,保证整个程序不会退出
* @author yinwenjie
*/
public class SingleRegistry { private static final Object WAITOBJECT = new Object(); public static void main(String[] args) throws Exception {
LocateRegistry.createRegistry(1099);
synchronized (WAITOBJECT) {
WAITOBJECT.wait();
}
}
}

RMI 服务接口的定义还是 RemoteServiceInterface,里面的方法定义和上文一样没有发生变化(RemoteServiceInterface接口的实现和上文一样,没有改变。为了节约篇幅这里就不下贴出代码了):

 package testRMI;

 import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List; import testRMI.entity.UserInfo; public interface RemoteServiceInterface extends Remote {
/**
* 这个RMI接口负责查询目前已经注册的所有用户信息
*/
public List<UserInfo> queryAllUserinfo() throws RemoteException;
}

下面的代码是另外一个运行的JVM,它负责提供RMI Remote Service的注册功能。实际上就是具体的服务提供者。代码如下:

 package testRMI;

 import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry; public class RemoteRegistryUnicastMain {
public static void main(String[] args) throws Exception {
/*
* 我们通过LocateRegistry的get方法,寻找一个存在于远程JVM上的RMI注册表
* */
Registry registry = LocateRegistry.getRegistry("192.168.61.1", 1099); // 以下是向远程RMI注册表(绑定/重绑定)RMI Server的Stub。
// 同样的远程RMI注册表的JVM-classpath下,一定要有这个RMI Server的Stub
RemoteUnicastServiceImpl remoteService = new RemoteUnicastServiceImpl(); /*
*
* 注册的服务仓库存在于192.168.61.1这个IP上
* 使用注册表registry进行绑定或者重绑定时,不需要写完整的RMI URL
* */
registry.bind("queryAllUserinfo" , remoteService);
}
}

最后一个JVM是RMI Client。一共有两件工作需要RMI Client完成:首先查询Registry注册表服务,获得Remote Service的Stub,然后通过Stub代理进行正式的请求。为了保证实验程序的连续性,在获取Stub后,对Remote Service进行不间断的连续请求。

 package testRMI;

 import java.rmi.Naming;
import java.util.List; import testRMI.entity.UserInfo; /**
* 客户端调用RMI测试
* @author yinwenjie
*
*/
public class RemoteClient { private static Object WAITOBJECT = new Object(); public static void main(String[] args) throws Exception {
// 您看,这里使用的是java名称服务技术进行的RMI接口查找。
RemoteServiceInterface remoteServiceInterface = (RemoteServiceInterface)Naming.lookup("rmi://192.168.61.1/queryAllUserinfo"); // 保证连续请求,好观察SingleRegistry关闭后的请求情况
for(;;) {
List<UserInfo> users = remoteServiceInterface.queryAllUserinfo();
System.out.println("users.size() = " +users.size());
synchronized (RemoteClient.WAITOBJECT) {
RemoteClient.WAITOBJECT.wait(1000);
}
}
}
}

以下是记录的测试情况,首先我们分别启动SingleRegistry和RemoteRegistryUnicastMain。可以看见,Remote Service在远程Registry上注册成功,并处于工作状态:

请注意上面的截图中,RemoteRegistryUnicastMain Application只有一个线程在工作。这个和我们随后讲到的RMI-Service的线程管理方式有关。接下来我们启动RMI Client:

特别注意Client向RMI Registry查询得到remoteServiceInterface实例,这并不是RemoteServiceInterface接口的具体实现,而是RMI Stub代理。其中的endpoint端点指向RMI Service的具体服务提供位置(这个位置由RMI Service上的Skeleton提供代理服务)。

接下来开始持续调用。在这里过程中,我们关闭RMI Registry服务:

显然SingleRegistry停止工作并没有影响RemoteClient发起的调用

3-2、Remote-Service线程管理

在上文中的演示我们看到了RemoteRegistryUnicastMain处理请求时,使用了线程池。这是JDK1.5到JDK1.6+版本中RMI框架的做的一个改进。包括JDK1.5在内,之前的版本都采用新建线程的方式来处理请求;在JDK1.6版本之后,改用了线程池,并且线程池的大小是可以调整的:

  • sun.rmi.transport.tcp.maxConnectionThreads:连接池的大小,默认为无限制。无限的大小肯定是有问题,按照Linux单进程可打开的最大文件数限制,建议的设置值为65535(生产环境)。如果同一时间连接池中的线程数量达到了最大值,那么后续的Client请求将会报错。测试环境/开发环境是否设置这个值,就没有那么重要了。

  • sun.rmi.transport.tcp.threadKeepAliveTime:如果当线程池中有闲置的线程资源的话,那么这个闲置线程资源多久被注销(单位毫秒),默认的设置是1分钟。

如果您使用的是linux或者window的命令控制台执行的话,您可以通过类似如下语句进行参数设置:

java -Dsun.rmi.transport.tcp.maxConnectionThreads=2 -Dsun.rmi.transport.tcp.threadKeepAliveTime=1000 testRMI.RemoteRegistryUnicastMain

如果您使用的是eclipse控制台进行执行的话,可以通过Debug configuration -> Arguments对话框进行设置,如下图所示:

3-3、Registry和Naming

在这两篇文章所列举的代码中,Registry和Naming都有出现过,在代码的注释中也提醒大家要注意这两者的使用方法。Registry和Naming都可以进行RMI服务的bind/rebind/unbind,都可以用lookup方法查询RMI服务。

Naming实际上是对Registry的封装。使用完整的URL方式对已注册的服务名进行查找。我们通过Naming类中lookup方法的源代码对Naming的工作方式进行说明。以下是Naming类中lookup方法的源代码:

 /**
* Returns a reference, a stub, for the remote object associated
* with the specified <code>name</code>.
*
* @param name a name in URL format (without the scheme component)
* @return a reference for a remote object formatted URL
* @since JDK1.1
*/
public static Remote lookup(String name)
throws NotBoundException,
java.net.MalformedURLException,
RemoteException {
ParsedNamingURL parsed = parseURL(name);
Registry registry = getRegistry(parsed); if (parsed.name == null)
return registry;
return registry.lookup(parsed.name);
}

在这个静态方法中,首先对传入的URL信息进行解析(parseURL(name)私有方法),解析完成后,将返回ParsedNamingURL私有类的一个对象parsed。我们来看看ParsedNamingURL的定义:

 /**
* Simple class to enable multiple URL return values.
*/
private static class ParsedNamingURL {
String host;
int port;
String name; ParsedNamingURL(String host, int port, String name) {
this.host = host;
this.port = port;
this.name = name;
}
}

如果解析过程中,发现URL格式问题,那么将会抛出异常;如果成功解析,则调用Registry的lookup方法。RMI Naming服务所使用的完整URL格式为:

rmi://host:port/name

其中rmi为固定格式 ,host是RMI Registry注册表所在服务地址;port为Registry注册表访问端口,默认端口为1099;name表示RMI Remote Service的注册名,在之前的代码中RMI Remote Service注册名是queryAllUserinfo。

Naming名字服务只提供RMI服务注册信息的查询,您不能使用Naming创建一个注册服务

3-3、UnicastRemoteObject和Activatable

在上篇文章中初次讲解RMI工作原理的时候,我提到“图中呈现的是JDK1.5版本中,RMI框架其中一种运行方式”。也就是说,除了这种工作模式外RMI还有其他的工作模式?

是的,在JDK1.2版本中,由Ann Wollrath执笔加入了一种新的RMI工作方式。即通过RMI“活化”模式,将Remote Service的真实提供者移植到RMI Registry注册表所在的JVM上。

要使用这种工作模式的Remote Service实现不再继承UnicastRemoteObject类,而需要继承Activatable类(其他的业务代码不需要改变):

 package testRMI;

 import java.rmi.MarshalledObject;
import java.rmi.RemoteException;
import java.rmi.activation.Activatable;
import java.rmi.activation.ActivationException;
import java.rmi.activation.ActivationID;
import java.util.ArrayList;
import java.util.List; import testRMI.entity.UserInfo; /**
* RMI Service的一个具体实现。注意这里我们继承的是Activatable父类
* @author yinwenjie
*/
public class RemoteActivationServiceImpl extends Activatable implements RemoteServiceInterface { /**
* 这个构造函数式必须的,不然客户端会报:<br>
* java.lang.NoSuchMethodException: testRMI.RemoteActivationServiceImpl.<init>(java.rmi.activation.ActivationID, java.rmi.MarshalledObject) 异常<br>
* 在这个方法里面,您可以调用任何一个Activatable父级的构造函数。这里我们调用的是最简单的一个
* @throws RemoteException
* @throws ActivationException
*/
public RemoteActivationServiceImpl(ActivationID id, MarshalledObject<?> data) throws RemoteException, ActivationException {
super("file://E:\\testworkspace\\testBSocket\\target\\classes", data, false , );
} /**
*
*/
private static final long serialVersionUID = 6797720945876437472L; /* (non-Javadoc)
* @see testRMI.RemoteServiceInterface#queryAllUserinfo()
*/
@Override
public List<UserInfo> queryAllUserinfo() throws RemoteException {
List<UserInfo> users = new ArrayList<UserInfo>(); UserInfo user1 = new UserInfo();
user1.setUserAge();
user1.setUserDesc("userDesc1");
user1.setUserName("userName1");
user1.setUserSex(true);
users.add(user1); UserInfo user2 = new UserInfo();
user2.setUserAge();
user2.setUserDesc("userDesc2");
user2.setUserName("userName2");
user2.setUserSex(false);
users.add(user2);
return users;
}
}

向RMI Registry注册表进行注册的方式也需要进行更改:

 package testRMI;

 import java.rmi.MarshalledObject;
import java.rmi.RMISecurityManager;
import java.rmi.activation.Activatable;
import java.rmi.activation.ActivationDesc;
import java.rmi.activation.ActivationGroup;
import java.rmi.activation.ActivationGroupDesc;
import java.rmi.activation.ActivationGroupID;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Properties; import testRMI.entity.UserInfo; public class RemoteActivationMain { public static void main(String[] args) throws Exception {
System.setSecurityManager(new RMISecurityManager());
System.setProperty("java.rmi.activation.port",""); // 设置各种参数
Properties props = new Properties();
// 最重要的参数是给rmi 激活服务相应的权限
// all.policy文件的配置在后文中说明
props.put("java.security.policy", "E:/testworkspace/testBSocket/src/all.policy"); /*
* 这里对ActivationDesc构造函数中的参数进行一下说明:
* groupID:就是在registerGroup时系统生成的ActivationGroupID对象
* className:这里就是testRMI.RemoteActivationServiceImpl具体的RemoteServiceInterface接口
* location:相当于classpath。这样注册程序才知道如何序列化类.
* 实际上之前我们建立RemoteActivationServiceImpl的构造函数时,实际上已经location写死了
* 这里只是为了讲解desc和RemoteActivationServiceImpl中构造函数的关系,又进行了一次参数指定
*
* MarshalledObject:一种序列化和反序列化方式,参见:
* http://download.oracle.com/technetwork/java/javase/6/docs/zh/api/java/rmi/MarshalledObject.html
* */
ActivationGroupDesc groupdesc = new ActivationGroupDesc(props, null);
ActivationGroupID groupid = ActivationGroup.getSystem().registerGroup(groupdesc);
UserInfo userinfo = new UserInfo();
ActivationDesc desc = new ActivationDesc(groupid, "testRMI.RemoteActivationServiceImpl", "file://E:\\testworkspace\\testBSocket\\target\\classes", new MarshalledObject<UserInfo>(userinfo)); /*
* 是不是与UnicastRemoteObject的注册方式不一样啊。
* 之所以比较复杂,是因为这个Remote Object将会序列化到RMI Registry所在的JVM进行运行。
* 并适时被“激活”运行
* */
RemoteServiceInterface server = (RemoteServiceInterface)Activatable.register(desc);
/*
* 和执行UnicastRemoteObject注册的情况不同,这里绑定Remote Object成功后,这个JVM将会退出
* 因为之后的RMI调用将和它没有一点关系了。
* */
//Naming.bind("rmi://192.168.61.1:1099/queryAllUserinfo",server);
Registry registry = LocateRegistry.getRegistry("192.168.61.1",);
registry.bind("queryAllUserinfo", server);
}
}

还需要做一个进行JVM权限描述文件all.policy:

grant { 
permission java.security.AllPermission “”, “”; 
};

当然这是比较简单的描述(主要是为了偷懒,哈哈)。您还可以使用JDK安装目录下自带的权限描述文件,当然要进行一些修改。文件存放路径为$JAVA_HOME\jre\lib\security\java.policy,里面有一些权限信息要根据您自己的实际情况进行修改。

之后我们启动rmib——一个支持RMI“活化”模式的注册表程序:

rmid -J-Djava.security.policy=E:\testworkspace\testBSocket\src\all.policy -port 1099 -J-Djava.rmi.server.codebase=file:/E:/testworkspace/testBSocket/target/classes/

如果您使用的是Linux系统的话,命令形式如下:

export CLASSPATH=$CLASSPATH:/root/java/classes/

rmid -J-Djava.security.policy=/usr/jdk1.7.0_71/all.policy -port 1099

客户端的代码和运行方式是不需要更改的。直接运行之前的RMI Client程序就可以看到效果了。这里注意以下几个问题;

  • 使用RMI的“活化”工作模式,原有的RMI Remote Service真实服务提供者就不再需要对RMI Client的调用进行响应了。所以您会看到在RemoteActivationMain完成注册工作后,RemoteActivationMain所在的JVM会退出,而不会像之前的RemoteUnicastMain一样,在完成注册后一直等待。如下图所示:

  • 另外,Client请求到的Stub,其中的endPoint不再指向Remote Service的真实服务提供者了,而是指向Registry注册服务提供者。这是因为Registry将负责真实服务的实现。如下图所示:

请注意Client获取的Stub的endPoint指向。

3、RMI——一种特殊的RPC服务实现

通过这两篇文章的讲解,我们将RMI的基本使用、工作原理、工作模式向您进行了简要的介绍。之所以介绍RMI,是因为要通过介绍RMI引出一种重要的系统间通讯管理框架RPC。从下一篇文章开始,我们将进入RPC规范的讲解,并介绍几款主流的RPC框架的使用和工作原理。

当然通过两篇文章对RMI技术的介绍,并不能将RMI技术的全部内容都囊括进来,后面有时间我将回头继续向您介绍RMI技术的特性。感谢您持续关注我的博客。

来源:http://blog.csdn.net/yinwenjie

系统间通信(9)——通信管理与RMI 下篇的更多相关文章

  1. 系统间通信(8)——通信管理与RMI 上篇

    1.概述 在概述了数据描述格式的基本知识.IO通信模型的基本知识后.我们终于可以进入这个系列博文的重点:系统间通信管理.在这个章节我将通过对RMI的详细介绍,引出一个重要的系统间通信的管理规范RPC, ...

  2. WebService与RMI(远程调用方式实现系统间通信)

    前言 本文是<分布式java应用基础与实践>读书笔记:另外参考了此博客,感觉讲的挺好的,尤其是其中如下内容: 另外,消息方式实现系统间通信本文不涉及.RMI则只采用spring RMI框架 ...

  3. 系统间通信(10)——RPC的基本概念

    1.概述 经过了详细的信息格式.网络IO模型的讲解,并且通过JAVA RMI的讲解进行了预热.从这篇文章开始我们将进入这个系列博文的另一个重点知识体系的讲解:RPC.在后续的几篇文章中,我们首先讲解R ...

  4. 系统间通信——RPC架构设计

    架构设计:系统间通信(10)——RPC的基本概念 1.概述经过了详细的信息格式.网络IO模型的讲解,并且通过JAVA RMI的讲解进行了预热.从这篇文章开始我们将进入这个系列博文的另一个重点知识体系的 ...

  5. 架构设计:系统间通信(34)——被神化的ESB(上)

    1.概述 从本篇文章开始,我们将花一到两篇的篇幅介绍ESB(企业服务总线)技术的基本概念,为读者们理清多个和ESB技术有关名词.我们还将在其中为读者阐述什么情况下应该使用ESB技术.接下来,为了加深读 ...

  6. 系统间通信(5)——IO通信模型和JAVA实践 下篇

    7.异步IO 上面两篇文章中,我们分别讲解了阻塞式同步IO.非阻塞式同步IO.多路复用IO 这三种IO模型,以及JAVA对于这三种IO模型的支持.重点说明了IO模型是由操作系统提供支持,且这三种IO模 ...

  7. JMS解决系统间通信问题

    近期在给公司项目做二次重构,将原来庞大的系统拆分成几个小系统.系统与系统之间通过接口调用,系统间通信有非常多方式,如系统间通信接口做成请求controller,只是这样不方便也不安全,经常使用的方式是 ...

  8. 系统间通信(3)——IO通信模型和JAVA实践 上篇

    来源:http://blog.csdn.net/yinwenjie 1.全文提要 系统间通信本来是一个很大的概念,我们首先重通信模型开始讲解.在理解了四种通信模型的工作特点和区别后,对于我们后文介绍搭 ...

  9. 分布式架构从零开始========》【基于Java自身技术实现消息方式的系统间通信】

    基于Java自身包实现消息方式的系统间通信的方式有:TCP/IP+BIO,TCP/IP+NIO,UDP/IP+BIO,UDP/IP+NIO.下面就这4种类型一一做个详细的介绍: 一.TCP/IP+BI ...

随机推荐

  1. PAT 1021. 个位数统计 (15)

    给定一个k位整数N = dk-1*10k-1 + ... + d1*101 + d0 (0<=di<=9, i=0,...,k-1, dk-1>0),请编写程序统计每种不同的个位数字 ...

  2. two sample ttest & paired ttst

    来源:http://www.pinzhi.org/thread-1023-1-1.html 成对t检验Paired Test是对来自同一总体的样本,在不同条件影响下获取的2组样本进行分析,以评价不同条 ...

  3. 045医疗项目-模块四:采购单模块—采购单提交(Dao,Service,Action三层)

    我们之前做的就是采购单的编辑,在采购单里面添加了药品,然后我们这篇文章要做的就是说提交这个采购单. 当我们创建完成采购单,确定采购单不再修改,需要提交采购单,由监管单位进行审核. 我们在提交这个采购单 ...

  4. HTML <map> 设置图热点

    需要在一张图片中,设置一个区域为热点就用到了<map> 定义一个客户端图像映射.图像映射(image-map)指带有可点击区域的一幅图像. <img src="planet ...

  5. 十大经典排序算法总结——JavaScrip版

    首先,对于评述算法优劣术语的说明: 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面:即排序后2个相等键值的顺序和排序之前它们的顺序相同 不稳定:如果a原本在b的前面,而a=b,排序之后a ...

  6. 使用spring boot+mybatis+mysql 构建RESTful Service

    开发目标 开发两个RESTful Service Method Url Description GET /article/findAll POST /article/insert 主要使用到的技术 j ...

  7. [转]使用URLDecoder和URLEncoder对中文进行处理

    一 URLEncoder HTML 格式编码的实用工具类.该类包含了将 String 转换为 application/x-www-form-urlencoded MIME 格式的静态方法.有关 HTM ...

  8. 如果动态设置json对象的key

    项目中要求动态设置json的key属性,如果按照一般的json设置方法是不行的.假如你把一个key设置为一个变量的话,那么最后js解析出来的就是key为这个变量名而不是这个变量的值. 解决:通过使用 ...

  9. 跟我学习Storm_Storm主要特点

    Storm拥有低延迟.高性能.分布式.可扩展.容错等特性,可以保证消息不丢失,消息处理严格有序.Storm的主要特点如下所示: 简单的编程模型.类似于MapReduce降低了并行批处理复杂性,Stor ...

  10. 还记得高中的向量吗?leetcode 335. Self Crossing(判断线段相交)

    传统解法 题目来自 leetcode 335. Self Crossing. 题意非常简单,有一个点,一开始位于 (0, 0) 位置,然后有规律地往上,左,下,右方向移动一定的距离,判断是否会相交(s ...