RMI 系列(02)源码分析

目录

  • RMI 系列(02)源码分析

    • 1. 架构
    • 2. 服务注册
      • 2.1 服务发布整体流程
      • 2.2 服务暴露入口 exportObject
      • 2.3 生成本地存根
      • 2.4 服务监听
      • 2.5 ObjectTable 注册与查找
      • 2.6 服务绑定
      • 2.7 总结
    • 3. 服务发现
      • 3.1 注册中心 Stub
      • 3.2 普通服务 Stub

RMI 系列(02)源码分析

1. 架构

RMI 中有三个重要的角色:注册中心(Registry)、客户端(Client)、服务端(Server)。

图1 RMI 架构图

在 RMI 中也要先进行服务注册,客户端从注册中心获取服务。为了屏蔽网络通信的复杂性,RMI 提出了 Stub(客户端存根)和 Skeleton(服务端骨架)两个概念,客户端和服务端的网络信都通过 Stub 和 Skeleton 进行。

图2 RMI 整体调用时序图

总结: 整体还是可以分为三部分,服务注册、服务发现、服务调用。

  1. 服务注册(1 ~ 2)

    • 第一步:创建远程对象包括两部分。一是创建 ServiceImpl 远程对象;二是发布 ServiceImpl 服务。ServiceImpl 继承自 UnicastRemoteObject,在创建时默认会随机绑定一个端口,监听客户端的请求。所以即使可以不注册,直接请求这个端口也可以进行通信。
    • 第二步:向注册中心注册该服务。注意:和其它的注册中心不同,Registry 只能注册本地的服务。
  2. 服务发现(3 ~ 4)
    • 向注册中心查找本地存根,返回给客户端。需要注意的是,Dubbo 先从注册中心获服务的 ip、port 等配置信息,然后在客户端生成 Stub 代理,而 RMI 不一样,已经在服务端保存了 Stub 代理对象,直接通过网络传输直接将 Stub 对象进行序列化与反序列化。
  3. 服务调用(5 ~ 9)
    • 客户端存根和服务器骨架通信,返回结果。

2. 服务注册

首先回顾一下 RMI 服务发布的使用方法:

@Test
public void server() {
    // 1. 服务创建及发布。注意:HelloServiceImpl extends UnicastRemoteObject
    HelloService service = new HelloServiceImpl();
    // 2. 创建注册中心:创建本机 1099 端口上的 RMI 注册表
    Registry registry = LocateRegistry.createRegistry(1099);
    // 3. 服务注册:将服务绑定到注册表中
    registry.bind(name, service);
}

总结: RMI 服务发布有三个流程:

  1. 服务创建及发布:HelloServiceImpl 需要继承自 UnicastRemoteObject,当初始化时会自动将 HelloServiceImpl 任务一个服务发布,绑定一个随机端口。
  2. 创建注册中心:注册中心实际和普通的服务一样,也会将自己作为一个服务发布。
  3. 服务注册:将 service 注册到注册中心。

服务创建及发布和创建注册中心流程完全相同,至于服务注册则是将 service 注册到一个 map 中,非常简单。所以服务的注册主要围绕服务创建及发布展开。

2.1 服务发布整体流程

图3 RMI 服务发布时序图

服务的发布的关键点有以下几个:

  1. 创建本地存根,用于客户端访问。
  2. 启动 socket。
  3. 服务注册与查找。

无论是 HelloServiceImpl 还是 Registry 都是 Remote 的子类,准确的说是 RemoteObject 的子类。RemoteObject 最重要的属性是 RemoteRef ref,RemoteRef 的实现类 UnicastRef,UnicastRef 包含属性 LiveRef ref。LiveRef 类中的 Endpoint、Channel 封装了与网络通信相关的方法。类结构如下:

图4 Remote 和 RemoteRef 类结构


2.2 服务暴露入口 exportObject

HelloServiceImpl 的构造器中调用父类 UnicastRemoteObject,最终调用 exportObject((Remote) this, port)

protected UnicastRemoteObject(int port) throws RemoteException {
    this.port = port;
    exportObject((Remote) this, port);
}
private static Remote exportObject(Remote obj, UnicastServerRef sref)
    throws RemoteException {
    if (obj instanceof UnicastRemoteObject) {
        ((UnicastRemoteObject) obj).ref = sref;
    }
    return sref.exportObject(obj, null, false);
}

Registry createRegistry(int port) 创建注册中心时也会调用 exportObject 方法。

public RegistryImpl(int port) throws RemoteException
    LiveRef lref = new LiveRef(id, port);
    setup(new UnicastServerRef(lref, RegistryImpl::registryFilter));
}
private void setup(UnicastServerRef uref) throws RemoteException {
    ref = uref;
    uref.exportObject(this, null, true);
}

总结: Registry 和 HelloServiceImpl 最终都调用 exportObject 方法,那 exportObject 到底是干什么的呢?从字面上看 exportObject 暴露对象,事实上正如其名,exportObject 打开了一个 ServerSocket,监听客户端的请求。

public Remote exportObject(Remote impl, Object data, boolean permanent)
        throws RemoteException {
    // 1. 创建本地存根,封装网络通信
    Class<?> implClass = impl.getClass();
    Remote stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
    ...
    // 2. 服务暴露,this 是指 RemoteObject 对象
    Target target = new Target(impl, this, stub, ref.getObjID(), permanent);
    ref.exportObject(target);
    return stub;
}

总结: exportObject 核心的方法有两个:一是生成本地存根的代理对象;二是调用 ref.exportObject(target) 启动 socket 服务。

注意:exportObject 时会先将 impl 和 stub 等信息封装到 Target 对象中,最终注册到 ObjectTable。

2.3 生成本地存根

在 Util.createProxy() 方法中创建代理对象。

public static Remote createProxy(Class<?> implClass, RemoteRef clientRef,
        boolean forceStubUse) throws StubNotFoundException {
    Class<?> remoteClass = getRemoteClass(implClass);

    // 1. 是否存在以 _Stub 结尾的类。remoteClass + "_Stub"
    //    forceStubUse 表示当不存在时是否抛出异常
    if (forceStubUse ||
        !(ignoreStubClasses || !stubClassExists(remoteClass))) {
        return createStub(remoteClass, clientRef);
    }

    // 2. jdk 动态代理
    final ClassLoader loader = implClass.getClassLoader();
    final Class<?>[] interfaces = getRemoteInterfaces(implClass);
    final InvocationHandler handler = new RemoteObjectInvocationHandler(clientRef);
    return (Remote) Proxy.newProxyInstance(loader, interfaces, handler);
}

总结: 创建代理对象有两种情况:

  1. 存在以 _Stub 结尾的类(eg: RegistryImpl_Stub)则直接返回,当 forceStubUse=true 时不存在则抛出异常。
  2. JDK 动态代理。RemoteObjectInvocationHandler#invoke 方法实际上直接委托给了 RemoteRef#invoke 方法进行网络通信,具体代码见 UnicastRef#invoke(Remote, Method, Object[], long)

2.4 服务监听

跟踪 LiveRef#exportObject 方法,最终调用 TCPTransport#exportObject 方法。

public void exportObject(Target target) throws RemoteException {
    // 1. 启动网络监听,默认 port=0,即随机启动一个端口
    synchronized (this) {
        listen();
        exportCount++;
    }
    // 2. 将 Target 注册到 ObjectTable
    super.exportObject(target);
}

总结: 最终服务暴露时做了两件事,一是如果 socket 没有启动,启动 socket 监听;二是将 Target 实例注册到 ObjectTable 对象中。

 private void listen() throws RemoteException {
     TCPEndpoint ep = getEndpoint();
     int port = ep.getPort();

     if (server == null) {
             server = ep.newServerSocket();
             Thread t = new NewThreadAction(new AcceptLoop(server),
                            "TCP Accept-" + port, true));
             t.start();
         } catch (IOException e) {
             throw new ExportException("Listen failed on port: " + port, e);
         }
     }
 }

2.5 ObjectTable 注册与查找

ObjectTable 用来管理所有发布的服务实例 Target,ObjectTable 提供了根据 ObjectEndpoint 和 Remote 实例两种方式查找 Target 的方法。先看注册:

private static final Map<ObjectEndpoint,Target> objTable = new HashMap<>();
private static final Map<WeakRef,Target> implTable = new HashMap<>();

// Target 注册
static void putTarget(Target target) throws ExportException {
    ObjectEndpoint oe = target.getObjectEndpoint();
    WeakRef weakImpl = target.getWeakImpl();

    synchronized (tableLock) {
        if (target.getImpl() != null) {
            ...
            objTable.put(oe, target);
            implTable.put(weakImpl, target);
        }
    }
}

那实例查找也就很简单了,之后就可以根据 Target 对象获取本地存根 stub。

static Target getTarget(ObjectEndpoint oe) {
    synchronized (tableLock) {
        return objTable.get(oe);
    }
}
public static Target getTarget(Remote impl) {
    synchronized (tableLock) {
        return implTable.get(new WeakRef(impl));
    }
}

2.6 服务绑定

当服务 HelloService 和 Registry 均已创建并发布后,之后需要将服务绑定到注册中心。这一步就很简单了,代码 registry.bind(name, service)

// 服务名称 -> 实例 impl
private Hashtable<String, Remote> bindings = new Hashtable<>(101);

public void bind(String name, Remote obj)
    throws RemoteException, AlreadyBoundException, AccessException {
    checkAccess("Registry.bind");
    synchronized (bindings) {
        Remote curr = bindings.get(name);
        if (curr != null)
            throw new AlreadyBoundException(name);
        bindings.put(name, obj);
    }
}

总结: service 绑定到注册中心实际就很简单了,将服务名称和实例保存到 map 中即可。查找时可以通过 name 查找到 impl,再通过 impl 在 ObjectTable 中查找到对应的 Target。

2.7 总结

服务暴露主要完成两件事:一是服务端生成本地存根 stub,并包装成 Target 对象,最终注册到 ObjectTable 中;二是启动 ServerSocket 绑定端口,监听客户端的请求。 又可以分为普通服务暴露和注册中心暴露,两种服务暴露过程完全相同。

  1. 普通服务暴露(HelloService):默认绑定随机端口。使用 HelloServicempl 实例,根据动态代理生成本地存储 stub,RemoteObjectInvocationHandler#invoke 最终调用 UnicastRef#invoke(Remote, Method, Object[], long) 方法。
  2. 注册中心暴露(Registry):LocateRegistry.createRegistry(port) 需要指定绑定端口,默认 1099。使用 RegistryImpl 实例,本地存根使用 RegistryImpl_Stub。

3. 服务发现

@Test
public void client() {
    String name = HelloService.class.getName();
    // 获取注册表
    Registry registry = LocateRegistry.getRegistry("localhost", 1099);
    // 查找对应的服务
    HelloService service = (HelloService) registry.lookup(name);
}

总结: RMI 服务发现核心步骤两步:一是获取注册中心 registry;二是根据注册中心获取服务的代理类 service。registry 和 service 都是通过 Util.createProxy() 方法生成的代理类,不过这两个代理类的生成时机完全不同,registry 是在客户端生成的代理类,service 是在服务端生成的代理类。

3.1 注册中心 Stub

public static Registry getRegistry(String host, int port, RMIClientSocketFactory csf) {
    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);
}

由于默认存在 RegistryImpl_Stub,所以直接返回 RegistryImpl_Stub 的实例。

public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
    RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);
    ObjectOutput var3 = var2.getOutputStream();
    var3.writeObject(var1);

    super.ref.invoke(var2);
    ObjectInput var6 = var2.getInputStream();
    Remote var23 = (Remote)var6.readObject();
    super.ref.done(var2);
    return var23;
}

总结: LocateRegistry.getRegistry 获取注册中心时,在客户端直接生成代理对象 RegistryImpl_Stub,RegistryImpl_Stub 实际调用 RemoteRef 的 invoke 方法进行网络通信。

3.2 普通服务 Stub

和 RegistryImpl_Stub 不同,普通服务是在服务端生成本地存根 Stub。在服务注册的阶段,我们提到服务暴露时会将服务实例及其生成的 Stub 包装成 Target,并最终注册到 ObjectTable 上。那客户端 registry.lookup(name) 是如何最终查找到对应服务的 Stub 中的呢?

首先客户端调用 registry.lookup(name) 时,会通过网络通信最终调用到 RegistryImpl#lookup 方法,查找到对应的 Remote 实例,之后将这个实例返回给客户端。

但是这个 Socket 输出流是被 MarshalOutputStream 包装过的,在输出对应时会将 Remote 替换为 Stub 对象。也就是说客户端直接可以拿到代理后的对象,反序列后进行网络通信,不需要在客户端生成代理对象。代码如下:

protected final Object replaceObject(Object obj) throws IOException {
    if ((obj instanceof Remote) && !(obj instanceof RemoteStub)) {
        Target target = ObjectTable.getTarget((Remote) obj);
        if (target != null) {
            return target.getStub();
        }
    }
    return obj;
}

总结: registry.lookup(name) 获取服务端生成的代理对象 stub。这个 stub 代理对象调用 UnicastRef#invoke(Remote, Method, Object[], long) 方法进行网络通信。

注意: 如果该服务没有暴露,则 target=null,也就是直接将服务端注册的实例而不是存根 Stub 返回,所以在客户端必须有该类的实现,否则反序列反时会抛出异常。不过,不暴露服务这种情况好像并没有什么意义。

Exception in thread "main" java.rmi.UnmarshalException: error unmarshalling return; nested exception is:
    java.lang.ClassNotFoundException: com.binarylei.rmi.helloword.service.HelloServiceImpl (no security manager: RMI class loader disabled)
    at sun.rmi.registry.RegistryImpl_Stub.lookup(Unknown Source)
    at com.binarylei.rmi.helloword.ClientTest.main(ClientTest.java:21)
Caused by: java.lang.ClassNotFoundException: com.binarylei.rmi.helloword.service.HelloServiceImpl (no security manager: RMI class loader disabled)
    at sun.rmi.server.LoaderHandler.loadClass(LoaderHandler.java:396)
    at sun.rmi.server.LoaderHandler.loadClass(LoaderHandler.java:186)
    at java.rmi.server.RMIClassLoader$2.loadClass(RMIClassLoader.java:637)
    at java.rmi.server.RMIClassLoader.loadClass(RMIClassLoader.java:264)
    at sun.rmi.server.MarshalInputStream.resolveClass(MarshalInputStream.java:219)
    ... 2 more


每天用心记录一点点。内容也许不重要,但习惯很重要!

原文地址:https://www.cnblogs.com/binarylei/p/12115986.html

时间: 2024-10-03 01:53:24

RMI 系列(02)源码分析的相关文章

jQuery 2.0.3 源码分析 事件绑定 - bind/live/delegate/on

转:http://www.cnblogs.com/aaronjs/p/3440647.html?winzoom=1 事件(Event)是JavaScript应用跳动的心脏,通过使用JavaScript ,你可以监听特定事件的发生,并规定让某些事件发生以对这些事件做出响应 事件的基础就不重复讲解了,本来是定位源码分析实现的, 所以需要有一定的基础才行 为了下一步更好的理解内部的实现,所以首先得清楚的认识到事件接口的划分 网上资料遍地都是,但是作为一个jQuery系列的源码分析,我还是很有必要在重新

java io系列02之 ByteArrayInputStream的简介,源码分析和示例(包括InputStream)

我们以ByteArrayInputStream,拉开对字节类型的“输入流”的学习序幕.本章,我们会先对ByteArrayInputStream进行介绍,然后深入了解一下它的源码,最后通过示例来掌握它的用法. 转载请注明出处:http://www.cnblogs.com/skywang12345/p/io_02.html ByteArrayInputStream 介绍 ByteArrayInputStream 是字节数组输入流.它继承于InputStream.它包含一个内部缓冲区,该缓冲区包含从流

Eureka 系列(02)客户端源码分析

Eureka 系列(02)客户端源码分析 [TOC] 在上一篇 Eureka 系列(01)最简使用姿态 中对 Eureka 的简单用法做了一个讲解,本节分析一下 EurekaClient 的实现 DiscoveryClient.本文的源码是基于 Eureka-1.9.8. 1)服务注册(发送注册请求到注册中心) 2)服务发现(本质就是获取调用服务名所对应的服务提供者实例信息,包括IP.port等) 3)服务续约(本质就是发送当前应用的心跳请求到注册中心) 4)服务下线(本质就是发送取消注册的HT

Eureka 系列(02)服务发现源码分析

Eureka 系列(02)服务发现源码分析 [TOC] 在上一篇文章 Eureka 系列(02)客户端源码分析 中对客户端服务发现与 Eureka 一致性协议: Eureka 是 AP 模型 消息广播: Eureka源码解析 https://blog.csdn.net/u012394095/article/category/9279158 https://blog.csdn.net/u011834741/article/details/54694045 Eureka 集群发现 https://w

java io系列03之 ByteArrayOutputStream的简介,源码分析和示例(包括OutputStream)

前面学习ByteArrayInputStream,了解了“输入流”.接下来,我们学习与ByteArrayInputStream相对应的输出流,即ByteArrayOutputStream.本章,我们会先对ByteArrayOutputStream进行介绍,在了解了它的源码之后,再通过示例来掌握如何使用它. 转载请注明出处:http://www.cnblogs.com/skywang12345/p/io_03.html ByteArrayOutputStream 介绍 ByteArrayOutpu

jquery2源码分析系列目录

学习jquery的源码对于提高前端的能力很有帮助,下面的系列是我在网上看到的对jquery2的源码的分析.等有时间了好好研究下.我们知道jquery2开始就不支持IE6-8了,从jquery2的源码中可以学到很多w3c新的标准( 如html5,css3,ECMAScript).原文地址是:http://www.cnblogs.com/aaronjs/p/3279314.html 关于1.x.x版的jquery源码分析系列,本博客也转载了一个地址http://www.cnblogs.com/jav

jQuery1.6源码分析系列

原文地址:http://www.cnblogs.com/nuysoft/archive/2011/11/14/2248023.html jQuery源码分析(版本1.6.1) 目录 00 前言开光 01 总体架构 02 正则表达式-RegExp-常用正则表达式 03 构造jQuery对象-源码结构和核心函数 03 构造jQuery对象-工具函数 04 选择器 Sizzle-工作原理 04 选择器 Sizzle-设计思路 04 选择器 Sizzle-从左向右的余热 04 选择器 Sizzle-块分

java io系列04之 管道(PipedOutputStream和PipedInputStream)的简介,源码分析和示例

本章,我们对java 管道进行学习. 转载请注明出处:http://www.cnblogs.com/skywang12345/p/io_04.html java 管道介绍 在java中,PipedOutputStream和PipedInputStream分别是管道输出流和管道输入流.它们的作用是让多线程可以通过管道进行线程间的通讯.在使用管道通信时,必须将PipedOutputStream和PipedInputStream配套使用.使 用管道通信时,大致的流程是:我们在线程A中向PipedOut

[转]jQuery源码分析系列

文章转自:jQuery源码分析系列-Aaron 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://github.com/JsAaron/jQuery 正在编写的书 - jQuery架构设计与实现 本人在慕课网的教程(完结) jQuery源码解析(架构与依赖模块) 64课时 jQuery源码解析(DOM与核心模块)64课时 jQuery源码分析目录(完结) jQuery源码分析系列(01) : 整体架构 jQuery源码分析系列(