深究Java中的RMI底层原理
原博客地址:http://blog.csdn.net/sinat_34596644/article/details/52599688
前言:随着一个系统被用户认可,业务量、请求量不断上升,那么单机系统必然就无法满足了,于是系统就慢慢走向分布式了,随之而来的是系统之间“沟通”的障碍。一般来说,解决系统之间的通信可以有两种方式:即远程调用和消息。RMI(Remote Method Invocation)就是远程调用的一种方式,也是这篇文章主要介绍的。
一、RMI的一个简单示例
这个示例拆分为服务端和客户端,放在两个idea项目中,并且通过了单机和双机两种环境的测试,是真正意义上的分布式应用。
项目结构
服务端应用: Server
主程序: com.jnu.wwt.entry.Server
服务接口: com.jnu.wwt.service.IOperation
服务实现: com.jnu.wwt.service.impl.OperationImpl
客户端应用: Client
主程序: com.jnu.wwt.entry.Client
服务接口: com.jnu.wwt.service.IOperation
源码:
Server.java
/** * Created by wwt on 2016/9/14. */ public class Server {public static void main(String args[]) throws Exception{//以1099作为LocateRegistry接收客户端请求的端口,并注册服务的映射关系 Registry registry=LocateRegistry.createRegistry(1099); IOperation iOperation=new OperationImpl(); Naming.rebind("rmi://127.0.0.1:1099/Operation",iOperation); System.out.println("service running..."); }}
IOperation.java(服务端和客户端各需要一份)
/** * 服务端接口必须实现java.rmi.Remote * Created by wwt on 2016/9/14. */ public interface IOperation extends Remote{/** * 远程接口上的方法必须抛出RemoteException,因为网络通信是不稳定的,不能吃掉异常 * @param a * @param b * @return */ int add(int a, int b) throws RemoteException; }
OperationImpl.java
/** * Created by wwt on 2016/9/14. */ public class OperationImpl extends UnicastRemoteObject implements IOperation{public OperationImpl() throws RemoteException {super(); }@Override public int add(int a, int b) throws RemoteException{return a+b; }}
Client.java
/** * Created by wwt on 2016/9/15. */ public class Client {public static void main(String args[]) throws Exception{IOperation iOperation= (IOperation) Naming.lookup("rmi://127.0.0.1:1099/Operation"); System.out.println(iOperation.add(1,1)); }}
运行结果
先运行Server应用,服务就起来了。然后切换到Client应用,点击运行,Client调用Server的服务,返回结果。
二、RMI做了些什么
现在我们先忘记Java中有RMI这种东西。假设我们需要自己实现上面例子中的效果,怎么办呢?可以想到的步骤是:
- 编写服务端服务,并将其通过某个服务机的端口暴露出去供客户端调用。
- 编写客户端程序,客户端通过指定服务所在的主机和端口号、将请求封装并序列化,最终通过网络协议发送到服务端。
- 服务端解析和反序列化请求,调用服务端上的服务,将结果序列化并返回给客户端。
- 客户端接收并反序列化服务端返回的结果,反馈给用户。
这是大致的流程,我们不难想到,RMI其实也是帮我们封装了一些细节而通用的部分,比如序列化和反序列化,连接的建立和释放等,下面是RMI的具体流程:
这里涉及到几个新概念:
Stub和Skeleton:这两个的身份是一致的,都是作为代理的存在。客户端的称作Stub,服务端的称作Skeleton。要做到对程序员屏蔽远程方法调用的细节,这两个代理是必不可少的,包括网络连接等细节。
Registry:顾名思义,可以认为Registry是一个“注册所”,提供了服务名到服务的映射。如果没有它,意味着客户端需要记住每个服务所在的端口号,这种设计显然是不优雅的。
三、走进RMI原理之前,先来看看用到的类及其层次结构和主要的方法。
哪里看不懂随时回来看看结构。。。开始了
四、一步步解剖RMI的底层原理
- 服务端启动Registry服务
Registry registry=LocateRegistry.createRegistry(1099);
从上面这句代码入手,追溯下去,可以发现服务端创建了一个RegistryImpl对象,这里做了一个判断。如果服务端指定的端口号是1099并且系统开启了安全管理器,那么可以在限定的权限集内(listen和accept)绕过系统的安全校验。反之则必须进行安全校验。这里纯粹是为了效率起见。真正做的事情在setUp()方法中,继续看下去。
public RegistryImpl(final int var1) throws RemoteException {if(var1 == 1099 && System.getSecurityManager() != null) {try {AccessController.doPrivileged(new PrivilegedExceptionAction() {public Void run() throws RemoteException {LiveRef var1x = new LiveRef(RegistryImpl.id, var1); RegistryImpl.this.setup(new UnicastServerRef(var1x)); return null; }}, (AccessControlContext)null, new Permission[]{new SocketPermission("localhost:" + var1, "listen,accept")}); } catch (PrivilegedActionException var3) {throw (RemoteException)var3.getException(); }} else {LiveRef var2 = new LiveRef(id, var1); this.setup(new UnicastServerRef(var2)); }}
private void setup(UnicastServerRef var1) throws RemoteException {this.ref = var1; var1.exportObject(this, (Object)null, true); }
进入UnicastServerRef的exportObject()方法。可以看到,这里首先为传入的RegistryImpl创建一个代理,这个代理我们可以推断出就是后面服务于客户端的RegistryImpl的Stub对象。然后将UnicastServerRef的skel(skeleton)对象设置为当前RegistryImpl对象。最后用skeleton、stub、UnicastServerRef对象、id和一个boolean值构造了一个Target对象,也就是这个Target对象基本上包含了全部的信息。调用UnicastServerRef的ref(LiveRef)变量的exportObject()方法。
public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException {Class var4 = var1.getClass(); Remote var5; try {var5 = Util.createProxy(var4, this.getClientRef(), this.forceStubUse); } catch (IllegalArgumentException var7) {throw new ExportException("remote object implements illegal remote interface", var7); }if(var5 instanceof RemoteStub) {this.setSkeleton(var1); }Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3); this.ref.exportObject(var6); this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4); return var5; }
到上面为止,我们看到的都是一些变量的赋值和创建工作,还没有到连接层,这些引用对象将会被Stub和Skeleton对象使用。接下来就是连接层上的了。追溯LiveRef的exportObject()方法,很容易找到了TCPTransport的exportObject()方法。这个方法做的事情就是将上面构造的Target对象暴露出去。调用TCPTransport的listen()方法,listen()方法创建了一个ServerSocket,并且启动了一条线程等待客户端的请求。接着调用父类Transport的exportObject()将Target对象存放进ObjectTable中。
public void exportObject(Target var1) throws RemoteException {synchronized(this) {this.listen(); ++this.exportCount; }boolean var2 = false; boolean var12 = false; try {var12 = true; super.exportObject(var1); var2 = true; var12 = false; } finally {if(var12) {if(!var2) {synchronized(this) {this.decrementExportCount(); }}}}if(!var2) {synchronized(this) {this.decrementExportCount(); }}}
到这里,我们已经将RegistryImpl对象创建并且起了服务等待客户端的请求。
- 客户端获取服务端Rgistry代理
IOperation iOperation= (IOperation) Naming.lookup("rmi://127.0.0.1:1099/Operation");
从上面的代码看起,容易追溯到LocateRegistry的getRegistry()方法。这个方法做的事情是通过传入的host和port构造RemoteRef对象,并创建了一个本地代理。可以通过Debug功能发现,这个代理对象其实是RegistryImpl_Stub对象。这样客户端便有了服务端的RegistryImpl的代理(取决于ignoreStubClasses变量)。但注意此时这个代理其实还没有和服务端的RegistryImpl对象关联,毕竟是两个VM上面的对象,这里我们也可以猜测,代理和远程的Registry对象之间是通过socket消息来完成的。
public static Registry getRegistry(String host, int port, RMIClientSocketFactory csf)throws RemoteException {Registry registry = null; if (port <= 0)port = Registry.REGISTRY_PORT; if (host == null || host.length() == 0) {// If host is blank (as returned by "file:" URL in 1.0.2 used in // java.rmi.Naming), try to convert to real local host name so // that the RegistryImpl's checkAccess will not fail. try {host = java.net.InetAddress.getLocalHost().getHostAddress(); } catch (Exception e) {// If that failed, at least try "" (localhost) anyway... host = ""; }} LiveRef liveRef =new LiveRef(new ObjID(ObjID.REGISTRY_ID), new TCPEndpoint(host, port, csf, null), false); RemoteRef ref =(csf == null) ? new UnicastRef(liveRef) : new UnicastRef2(liveRef); return (Registry) Util.createProxy(RegistryImpl.class, ref, false); }
- 服务端创建服务对象
private static Remote exportObject(Remote obj, UnicastServerRef sref)throws RemoteException {// if obj extends UnicastRemoteObject, set its ref. if (obj instanceof UnicastRemoteObject) {((UnicastRemoteObject) obj).ref = sref; }return sref.exportObject(obj, null, false); }
- 将服务实现绑定到服务端的Registry上,使得客户端只需与Registry交互。
Naming.rebind("rmi://127.0.0.1:1099/Operation",iOperation);
从上面这行代码开始看,容易发现Naming的方法全部都是调用的Registry的方法。这里通过host和port找到我们第一步启动的服务端Registry服务对象,追溯到其rebind()方法,可以看到,其实做的事情很是简单,就是把名字和服务实现存进一个Map里面。
public void rebind(String var1, Remote var2) throws RemoteException, AccessException {checkAccess("Registry.rebind"); this.bindings.put(var1, var2); }
- 客户端查找远程服务
接下来就是重头戏了,从下面代码看起。
IOperation iOperation= (IOperation) Naming.lookup("rmi://127.0.0.1:1099/Operation");
追溯下去,获取到远程Registry对象的代理对象之后,调用RegistryImpl_Stub的lookUp()方法。主要代码如下。做的事情是利用上面通过服务端host和port等信息创建的RegistryImpl_stub对象构造RemoteCall调用对象,operations参数中是各个Registry中声明的操作,2指明了是lookUp()操作。接下来分步骤看看...
try {RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L); try {ObjectOutput var3 = var2.getOutputStream(); var3.writeObject(var1); } catch (IOException var18) {throw new MarshalException("error marshalling arguments", var18); }super.ref.invoke(var2); Remote var23; try {ObjectInput var6 = var2.getInputStream(); var23 = (Remote)var6.readObject(); } catch (IOException var15) {throw new UnmarshalException("error unmarshalling return", var15); } catch (ClassNotFoundException var16) {throw new UnmarshalException("error unmarshalling return", var16); } finally {super.ref.done(var2); }return var23; }
调用 RegistryImpl_Stub的ref(RemoteRef)对象的newCall()方法,将RegistryImpl_Stub对象传了进去,不要忘了构造它的时候我们将服务器的主机端口等信息传了进去,也就是我们把服务器相关的信息也传进了newCall()方法。newCall()方法做的事情简单来看就是建立了跟远程RegistryImpl的Skeleton对象的连接。(不要忘了上面我们说到过服务端通过TCPTransport的exportObject()方法等待着客户端的请求)
public RemoteCall newCall(RemoteObject var1, Operation[] var2, int var3, long var4) throws RemoteException {clientRefLog.log(Log.BRIEF, "get connection"); Connection var6 = this.ref.getChannel().newConnection(); try {clientRefLog.log(Log.VERBOSE, "create call context"); if(clientCallLog.isLoggable(Log.VERBOSE)) {this.logClientCall(var1, var2[var3]); }StreamRemoteCall var7 = new StreamRemoteCall(var6, this.ref.getObjID(), var3, var4); try {this.marshalCustomCallData(var7.getOutputStream()); } catch (IOException var9) {throw new MarshalException("error marshaling custom call data"); }return var7; } catch (RemoteException var10) {this.ref.getChannel().free(var6, false); throw var10; } }
连接建立之后自然就是发送请求了。我们知道客户端终究只是拥有Registry对象的代理,而不是真正地位于服务端的Registry对象本身,他们位于不同的虚拟机实例之中,无法直接调用。必然是通过消息进行交互的。看看super.ref.invoke()这里做了什么?容易追溯到StreamRemoteCall的executeCall()方法。看似本地调用,但其实很容易从代码中看出来是通过tcp连接发送消息到服务端。由服务端解析并且处理调用。
try {if(this.out != null) {var2 = this.out.getDGCAckHandler(); }this.releaseOutputStream(); DataInputStream var3 = new DataInputStream(this.conn.getInputStream()); byte var4 = var3.readByte(); if(var4 != 81) {if(Transport.transportLog.isLoggable(Log.BRIEF)) {Transport.transportLog.log(Log.BRIEF, "transport return code invalid: " + var4); }throw new UnmarshalException("Transport return code invalid"); }this.getInputStream(); var1 = this.in.readByte(); this.in.readID(); }
至此,我们已经将客户端的服务查询请求发出了。
- 服务端接收客户端的服务查询请求并返回给客户端结果
try {ObjID var40; try {var40 = ObjID.read(var1.getInputStream()); } catch (IOException var34) {throw new MarshalException("unable to read objID", var34); }Transport var41 = var40.equals(dgcID)?null:this; Target var5 = ObjectTable.getTarget(new ObjectEndpoint(var40, var41)); final Remote var38; if(var5 != null && (var38 = var5.getImpl()) != null) {final Dispatcher var6 = var5.getDispatcher(); var5.incrementCallCount(); boolean var8; try {transportLog.log(Log.VERBOSE, "call dispatcher"); final AccessControlContext var7 = var5.getAccessControlContext(); ClassLoader var42 = var5.getContextClassLoader(); Thread var9 = Thread.currentThread(); ClassLoader var10 = var9.getContextClassLoader(); try {var9.setContextClassLoader(var42); currentTransport.set(this); try {AccessController.doPrivileged(new PrivilegedExceptionAction() {public Void run() throws IOException {Transport.this.checkAcceptPermission(var7); var6.dispatch(var38, var1); return null; }}, var7); return true; } catch (PrivilegedActionException var32) {throw (IOException)var32.getException(); }} finally {var9.setContextClassLoader(var10); currentTransport.set((Object)null); }} catch (IOException var35) {transportLog.log(Log.BRIEF, "exception thrown by dispatcher: ", var35); var8 = false; } finally {var5.decrementCallCount(); }return var8; }throw new NoSuchObjectException("no such object in table"); }
public Remote lookup(String var1) throws RemoteException, NotBoundException {Hashtable var2 = this.bindings; synchronized(this.bindings) {Remote var3 = (Remote)this.bindings.get(var1); if(var3 == null) {throw new NotBoundException(var1); } else {return var3; }} }
- 客户端获取通过lookUp()查询获得的客户端OperationImpl的Stub对象
- 客户端进行真正地远程服务调用
五、看看Skeleton和Stub如何为我们屏蔽底层连接细节
Stub类:
- public class Person_Stub implements Person {
- private Socket socket;
- public Person_Stub() throws Throwable {
- // connect to skeleton
- socket = new Socket("computer_name", 9000);
- }
- public int getAge() throws Throwable {
- // pass method name to skeleton
- ObjectOutputStream outStream =
- new ObjectOutputStream(socket.getOutputStream());
- outStream.writeObject("age");
- outStream.flush();
- ObjectInputStream inStream =
- new ObjectInputStream(socket.getInputStream());
- return inStream.readInt();
- }
- public String getName() throws Throwable {
- // pass method name to skeleton
- ObjectOutputStream outStream =
- new ObjectOutputStream(socket.getOutputStream());
- outStream.writeObject("name");
- outStream.flush();
- ObjectInputStream inStream =
- new ObjectInputStream(socket.getInputStream());
- return (String)inStream.readObject();
- }
- }
可以看到,Stub对象做的事情是建立到服务端Skeleton对象的Socket连接。将客户端的方法调用转换为字符串标识传递给Skeleton对象。并且同步阻塞等待服务端返回结果。
Skeleton类:
- public class Person_Skeleton extends Thread {
- private PersonServer myServer;
- public Person_Skeleton(PersonServer server) {
- // get reference of object server
- this.myServer = server;
- }
- public void run() {
- try {
- // new socket at port 9000
- ServerSocket serverSocket = new ServerSocket(9000);
- // accept stub's request
- Socket socket = serverSocket.accept();
- while (socket != null) {
- // get stub's request
- ObjectInputStream inStream =
- new ObjectInputStream(socket.getInputStream());
- String method = (String)inStream.readObject();
- // check method name
- if (method.equals("age")) {
- // execute object server's business method
- int age = myServer.getAge();
- ObjectOutputStream outStream =
- new ObjectOutputStream(socket.getOutputStream());
- // return result to stub
- outStream.writeInt(age);
- outStream.flush();
- }
- if(method.equals("name")) {
- // execute object server's business method
- String name = myServer.getName();
- ObjectOutputStream outStream =
- new ObjectOutputStream(socket.getOutputStream());
- // return result to stub
- outStream.writeObject(name);
- outStream.flush();
- }
- }
- } catch(Throwable t) {
- t.printStackTrace();
- System.exit(0);
- }
- }
- }
Skeleton对象做的事情是将服务实现传入构造参数,获取客户端通过socket传过来的方法调用字符串标识,将请求转发到具体的服务上面。获取结果之后返回给客户端。
深究Java中的RMI底层原理相关推荐
- 一文带你理解Java中Lock的实现原理
转载自 一文带你理解Java中Lock的实现原理 当多个线程需要访问某个公共资源的时候,我们知道需要通过加锁来保证资源的访问不会出问题.java提供了两种方式来加锁,一种是关键字:synchron ...
- java中的账户冻结原理_java可重入锁(ReentrantLock)的实现原理
前言 相信学过java的人都知道 synchronized 这个关键词,也知道它用于控制多线程对并发资源的安全访问,兴许,你还用过Lock相关的功能,但你可能从来没有想过java中的锁底层的机制是怎么 ...
- 并发编程五:java并发线程池底层原理详解和源码分析
文章目录 java并发线程池底层原理详解和源码分析 线程和线程池性能对比 Executors创建的三种线程池分析 自定义线程池分析 线程池源码分析 继承关系 ThreadPoolExecutor源码分 ...
- 腾讯一面:说一说 MySQL 中索引的底层原理
一.前言 最近有很多读者要我出一些面试题的文章,一般我会给他一个老周整理的电子书,但有些读者反馈回来的面试题我觉得还是蛮经典的,而老周又在写系列的文章,本着对读者负责的态度,我会穿插写几篇我认为比较经 ...
- 揭密 Java方法调用的底层原理
关注公众号"java后端技术全栈" 回复"000"获取优质面试资料 大家好,我是老田,今天来和大家聊聊Java方法调用的底层原理. 我们在日常开发中,其实很少去 ...
- Java并发编程-synchronized底层原理
synchronized底层原理与Monitor密切相关 1.Java对象头 以 32 位虚拟机为例 普通对象 对象的类型,如Student类型,Teacher类型等是由KlassWord来表示的,它 ...
- java中使用rmi进行远程方法调用
java中进行远程方法调用,能支持分布式计算.并且可以实现在server的修改,能反应到各个client. 假如server的ip是:192.168.11.2, server端的代码如下: /*** ...
- java中什么是底层数据结构_JavaScript 对象的底层数据结构是什么
这也是个老生常谈的问题,最近得空也开始总结下这方面的知识点.学过java的同学,都知道java把内存分为两种形式,一种是栈内存,另一种是堆内存.java的基本类型(int,short,long,byt ...
- 【Java】图解 Java 中的数据结构及原理
原文:http://www.javastack.cn/article/2018/data-structure-and-the-principle-diagram/ 最近在整理数据结构方面的知识, 系统 ...
最新文章
- 前景检测算法_3(GMM)
- python中的decode(编码)和encode(解码)
- Java使用AES加密解密
- python自动轨迹绘制_Python——自动轨迹绘制
- 显示 grep 结果的指定行
- mysql load data infile 重写_mysql load data infile 命令的数据导入
- 今天的我叫史努比的飞鸽传书
- 【转】【51CTO 网+】怎样做一款让用户来电的产品
- clip_region_relclip_region
- contos7改分辨率_Centos 7 修改系统屏幕分辨率
- windows7安装Bitvise开启SSH服务
- 安卓bochs模拟linux_bochs下载-bochs模拟器 安卓版v2.5.1-PC6安卓网
- JAVA重写和重载的区别
- php富强民主,鼠标点击网页爱国富强民主特效(附代码)
- JavaWeb宿舍管理系统环境搭建运行教程
- Spring Boot资源获取失败:class path resource cannot be opened because it does not exist
- php加速模块cpan模块,查看perl模块和cpan模块介绍
- Spark系列十七:经典案列使用直连的方式,Kafka,SparkSteaming,Redis
- 点云配准方法原理(NDT、ICP)
- C++面试常见问答题看这三篇文章就够了(上)
热门文章
- clion variable set
- hexo的yelee主题的文章中的超链接颜色修改
- -webkit-border-radius和-moz-border-radius(转)
- haroopad故障
- 详细解释到底啥是共轭先验(用本科知识来解释)
- navicat数据库运行存储过程
- 【机器学习】朴素贝叶斯(Naive Bayes)
- 小米5点位图_5.22首域金融午评:次日02:00 美联储公布5月货币政策会议纪要
- Okhttp同步请求源码分析
- Git使用出错:Couldn‘t reserve space for cygwin‘s heap, Win32