Dubbo笔记 ⑤ : 服务发布流程 - Protocol#export

一、前言

本系列为个人Dubbo学习笔记,内容基于《深度剖析Apache Dubbo 核心技术内幕》, 过程参考官方源码分析文章,仅用于个人笔记记录。本文分析基于Dubbo2.7.0版本,由于个人理解的局限性,若文中不免出现错误,感谢指正。

系列文章地址:Dubbo源码分析:全集整理


Dubbo笔记④ : 服务发布流程 - doExportUrlsFor1Protocol 中介绍过服务发布时 Protocol 存在三种情况:

  1. 本地服务导出:其内部根据URL 中Protocol类型为 injvm,会选择InjvmProtocol
  2. 远程服务导出 & 有注册中心:其内部根据URL 中 Protocol 类型为 registry,会选择RegistryProtocol
  3. 远程服务导出 & 没有注册中心:根据服务协议头类型判断,我们这里假设是 dubbo ,则会选择 DubboProtocol

所以在本文中会介绍 RegistryProtocol#export 、 InjvmProtocol#export 、DubboProtocol#export 三个方法的执行过程。


这里我们可以看一下 Protocol#export 的流程图。其中 XxxProtocolWrapper 指的是QosProtocolWrapper、ProtocolListenerWrapper、ProtocolFilterWrapper。
在这里插入图片描述


由于 Dubbo 2.7 版本的节点监听方式和 Dubbo 2.6有所不同,在本文中注重介绍 Dubbo 2.6 版本的监听方式,对于 Dubbo 2.7 版本的监听方式,如有需要详参:Dubbo笔记衍生篇⑩:2.7 版本的动态配置监听

二、RegistryProtocol#export

当服务的暴露时有注册中心参与时,会调用 RegistryProtocol 来进行服务暴露。RegistryProtocol 可以认为并不是一个真正的协议,他是这些实际的协议(dubbo . rmi)包装者,这样客户端的请求在一开始如果没有服务端的信息,会先从注册中心拉取服务的注册信息,然后再和服务端直连。简单的说 RegistryProtocol 中完成的是与注册中心交互,而并非服务间的交互

RegistryProtocol 通过 URL 的 registry:// 协议头标识, DubboProtocol通过 URL 的dubbo://协议头标识,在ServiceConfig或者RefrenceConfig中基于扩展点自适应机制会寻找对应的Protocol进行发布与引用

Registry类型Invoker不会被 Filter拦截,实际的协议Invoker才会被拦截, 因为ProtocolFilterWrapper 在构造 Dubbo Filter 时判断如果 URL 协议类型是 registry 则不构建过滤器链。如有需要,详看此文 :Dubbo笔记衍生篇③:ProtocolWrapper


下面以 Dubbo协议为例,ZK为注册中心,提供者服务暴露流程大致如下:

  1. 当服务进行发布时,会调用 Protocol#export 来进行服务暴露,需要注意的是由于 Dubbo SPI 机制此时的 Protocol 并非是某个具体的实现,而是 Protocol$AdaptiveProtocol$Adaptive 是 Dubbo SPI 动态生成的一个 Protocol 适配器类,会根据协议内容来选择合适的 Protocol 实现类。因为此次服务暴露以 ZK 作为注册中心,所以URL当前协议为registry。Protocol$Adaptive 根据 registry:// 获取到的对应的 Protocol 实现类 RegistryProtocol。但是由于由于Protocol 存在三个包装类 QosProtocolWrapperProtocolListenerWrapperProtocolFilterWrapperProtocol$Adaptive 会先调用三个包装类,之后再调用 RegistryProtocol。
  2. Dubbo 提供了 Protocol 的自动包装类,这里会先调用其包装类(QosProtocolWrapperProtocolListenerWrapperProtocolFilterWrapper),三个包装类在协议为registry的时候并没有做什么处理(仅仅 QosProtocolWrapper完成了 Dubbo QOS 功能的处理)。
  3. 在包装类执行结束后,由于此时是ZK 作为注册中心,所以协议类型是registry,会选择使用RegistryProtocol作为处理类,因此此时会调用 RegistryProtocol#export()。
  4. RegistryProtocol#export()首先会解析URL信息,准备监听服务,在 2.7 版本的中RegistryProtocol#overrideUrlWithConfig 完成服务监听,之后开始交由DubboProtocol#export进行服务导出。
  5. DubboProtocol#export进行服务导出前仍会先经过ProtocolWrapper,此时由于协议类型不是 Registry而是Dubbo,所以包装类会生效。具体功能为ProtocolListenerWrapper增加了服务操作时候的监听功能, ProtocolFilterWrapper 完成了 Dubbo 拦截链的实现,即对Invoker进行了拦截链的包装。
  6. 调用 DubboProtocol#export,此时会开启Netty服务,并监听端口,经历了这一步,服务提供者的机器已经具备服务处理能力。
  7. DubboProtocol#export 执行结束后,还需要将服务注册到注册中心的提供者节点,这一步是通过RegistryProtocol来完成,RegistryProtocol在zk上创建服务节点,同时对服务自身进行监听,当通过监控中心服务配置进行更新时,如果配置有变动将更新数据缓存重新发布服务。(消费者监听了提供者节点、配置节点、路由节点等,当服务重新发布时消费者可以感知到,并更新本地的提供者信息。)

即整个流程为 :

Protocol$Adaptive#export -> XXXProtocolWrapper#export -> RegistryProtocol#export
	-> Protocol$Adaptive#export -> XXXProtocolWrapper#export -> DubboProtocol#export

关于ProtocolWrapper的内容我们在衍生篇中有过介绍,所以这里跳过 ProtocolWrapper,直接来看RegistryProtocol#export 的详细实现:

	// 原始 URL (originInvoker.getUrl() ) :registry://localhost:2181/org.apache.dubbo.registry.RegistryService?application=Api-provider&dubbo=2.0.2&export=dubbo%3A%2F%2F192.168.111.1%3A20880%2Fapi.DemoService%3Fanyhost%3Dtrue%26application%3DApi-provider%26bind.ip%3D192.168.111.1%26bind.port%3D20880%26dubbo%3D2.0.2%26generic%3Dfalse%26group%3Ddubbo%26interface%3Dapi.DemoService%26methods%3DsayHelloForAsync%2CsayHello%2CsayMsg%2CsayHelloForAsyncByContext%26pid%3D11912%26register%3Dtrue%26release%3D2.7.0%26revision%3D1.0.0%26sayHello.retries%3D3%26side%3Dprovider%26timestamp%3D1616821309244%26version%3D1.0.0&pid=11912&registry=zookeeper&release=2.7.0&timestamp=1616821309240
    @Override
    public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
    	====================1. URL解析 ==================
    	// 1.1 获取注册中心信息 URL 
        URL registryUrl = getRegistryUrl(originInvoker);
        // url to export locally
        // 1.2 获取 需要暴露的 服务URL信息 : 通过 riginInvoker.getUrl().getParameterAndDecoded(EXPORT_KEY) 来获取
        URL providerUrl = getProviderUrl(originInvoker);

        // 1.3 订阅override数据
        // 提供者订阅时,会影响 同一JVM即暴露服务,又引用同一服务的的场景,
        // 因为subscribed以服务名为缓存的key,导致订阅信息覆盖。
        final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
        // 根据 overrideSubscribeUrl  生成 OverrideListener,并缓存到 overrideListeners 中
        // OverrideListener 是监听器实例
        final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
        overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
        // 2.7 版本新增,使用 ServiceConfigurationListener 和  ProviderConfigurationListener 来监听服务
        providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
        //export invoker
        ====================2. 服务暴露 ==================
        // 进行服务暴露,这里会调用真正协议的 export 方法
        final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);
		====================3. 服务注册 ==================
        // url to registry
        // 获取注册中心实例 : 根据调用者的地址获取注册表的实例
        final Registry registry = getRegistry(originInvoker);
        // 获取 当前注册的服务提供者 URL,如果开启了简化 URL,则返回的是简化的URL
        final URL registeredProviderUrl = getRegisteredProviderUrl(providerUrl, registryUrl);
       // ProviderInvokerWrapper 中保存了当前注册服务的一些信息(originInvoker、registryUrl、registeredProviderUrl 以及是否注册)
        ProviderInvokerWrapper<T> providerInvokerWrapper = ProviderConsumerRegTable.registerProvider(originInvoker,
                registryUrl, registeredProviderUrl);
        //to judge if we need to delay publish
        // 判断服务是否需要延迟发布,延迟发布则不会立即进行注册,否则进行注册
        boolean register = registeredProviderUrl.getParameter("register", true);
        if (register) {
        	// 调用远端注册中心的register方法进行服务注册
        	// 此时如果有消费者订阅了该服务,则推送消息让消费者引用此服务
        	// 注册中心缓存了所有提供者注册的服务以供消费者发现。
            register(registryUrl, registeredProviderUrl);
            // 将当前服务置为已注册
            providerInvokerWrapper.setReg(true);
        }
		====================4. 服务订阅 ==================
        // Deprecated! Subscribe to override rules in 2.6.x or before.
        // 不推荐使用!订阅以覆盖2.6.x或更早版本中的规则。
        // 提供者向注册中心订阅所有注册服务,当注册中心有此服务的覆盖配置注册进来时,推送消息给提供者,重新暴露服务,这由管理页面完成。
        // org.apache.dubbo.registry.support.FailbackRegistry#subscribe
        // 向注册中心进行订阅 override 数据
        registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
		======================================
        exporter.setRegisterUrl(registeredProviderUrl);
        exporter.setSubscribeUrl(overrideSubscribeUrl);
        //Ensure that a new exporter instance is returned every time export
        //  保证每次export都返回一个新的exporter实例
        // 返回暴露后的Exporter给上层ServiceConfig进行缓存,便于后期撤销暴露。
        return new DestroyableExporter<>(exporter);
    }

上面的逻辑大体可以分为三个部分:

  1. URL 解析:这一部分主要是对URL解析,获取关于注册中心,待发布服务等相关信息
  2. 服务暴露:当URL解析结束后,便开始进行服务暴露,此时RegistryProtocol 会调用真正的协议(默认是DubboProtocol)来进行服务的暴露,开启服务端口,绑定当前服务信息。
  3. 服务注册:经历上面两步后,服务已经暴露,但是并未在注册中心上注册,此时需要在注册中心上进行服务注册(比如此时会在zk上创建该服务的节点或者添加到提供者列表)。
  4. 服务订阅:服务发布成功后,除了依赖该服务的消费者,服务自身也会订阅自身服务。当服务进行通过动态配置时,会监听到该变化,并进行配置覆盖,重新发布服务,并通知其他消费者。

下面我们根据上面四个阶段进行详细解析:

1. URL解析

下面我们按照代码注释顺序来逐一分析

1.1 获取注册中心URL

该部分代码如下:

   		// 1.1 获取注册中心信息 URL 
       URL registryUrl = getRegistryUrl(originInvoker);

RegistryProtocol#getRegistryUrl 方法用来从 Invoker 中 获取注册中心 URL。其实现如下:

	// org.apache.dubbo.registry.integration.RegistryProtocol#getRegistryUrl
    private URL getRegistryUrl(Invoker<?> originInvoker) {
        URL registryUrl = originInvoker.getUrl();
        // 如果协议 是 register  则说明当前URL 保存的了 注册中心的信息, 
        // 通过 registryUrl.getParameter("registry") 来获取注册中心的类型(如redis、zookeeper。这里为zookeeper)并替换协议类型,返回注册中心的信息。
        if (REGISTRY_PROTOCOL.equals(registryUrl.getProtocol())) {
            String protocol = registryUrl.getParameter(REGISTRY_KEY, DEFAULT_DIRECTORY);
            // 将 协议替换为 注册中心 真实协议。
            registryUrl = registryUrl.setProtocol(protocol).removeParameter(REGISTRY_KEY);
        }
        return registryUrl;
    }

Dubbo笔记 ③ : 服务发布流程 - ServiceConfig#export 一文 的 2.1. loadRegistries(true); 在加载注册中心的URL 时 会原始 URL 的协议类型从 zookeeper 替换成 registry,并且在URL 中添加 registry=zookeeper 用于保存注册中心的协议,而对于多种多样的注册中心(如 zk,nacos,redis), RegistryProtocol 会根据注册中心的实际协议类型来选择合适的 Registry 实现类来完成操作。

如下简化版URL :

registry://localhost:2181/org.apache.dubbo.registry.RegistryService?application=spring-dubbo-provider&dubbo=2.0.2&pid=10404&registry=zookeeper&release=2.7.0&timestamp=1628842310884
  1. registry:// 可以让 Dubbo知晓这是一个注册中心 URL,会选择 RegistryProtocol 来处理流程
  2. registry=zookeeper 会让 Dubbo知道,当前服务选择使用zookeeper 协议来发布服务,即会选择 ZookeeperRegistry 来进行服务注册。

1.2 获取服务URL

该部分代码实现如下:

      // 1.2 获取 需要暴露的 服务URL信息 : 通过 riginInvoker.getUrl().getParameterAndDecoded(EXPORT_KEY) 来获取
      URL providerUrl = getProviderUrl(originInvoker);

RegistryProtocol#getProviderUrl 方法 用来获取需要暴露的服务信息,这里Dubbo笔记 ④ : 服务发布流程 - doExportUrlsFor1Protocol2.2.1 生成代理类章节 提及到,对于存在注册中心的情况,origin URL的结构一般为

registry://...&export=....&...

其中export 属性保存的则为暴露服务的URL。所以这里获取 URL 中的 “export” 属性来获取服务导出信息。下面获取的providerUrl 则表明该服务需要通过dubbo协议发布

	// org.apache.dubbo.registry.integration.RegistryProtocol#getProviderUrl
	// 获取服务提供者的URL 信息	
   	private URL getProviderUrl(final Invoker<?> originInvoker) {
   	  // 从原始URL 中获取 export 属性的value,这里保存的是需要导出的服务信息
       String export = originInvoker.getUrl().getParameterAndDecoded(EXPORT_KEY);
       if (export == null || export.length() == 0) {
           throw new IllegalArgumentException("The registry export url is null! registry: " + originInvoker.getUrl());
       }
       // String 转化为 URL
       return URL.valueOf(export);
    }

1.3 获取订阅URL

该部分代码实现如下:

	// 生成了服务提供者监听的 URL 信息
    final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
	// 给当前 Invoker 注册监听器。当有信息更新时进行通知
	final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
	// 2.7 版本新增,保存 URL 和监听器的映射关系,当 ProviderConfigurationListener 监听触发时通知所有监听器
    overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
    // 获取动态配置信息进行合并
    providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);

这里主要是进行 URL 的准备工作,包括

  1. 获取监听信息生成URL overrideSubscribeUrl。
  2. 创建当前服务的监听器 overrideSubscribeListener,用于监听overrideSubscribeUrl,在 4. 服务订阅 中完成了服务订阅,该服务订阅为 2.6 版本的订阅方式。 Dubbo 为了兼容性保留了 2.6 版本的监听方式。
  3. RegistryProtocol#overrideUrlWithConfig 是 2.7版本新增逻辑,完成了2.7版本的动态配置获取和监听。

这里我们注意:

  1. RegistryProtocol#getSubscribedOverrideUrl 的实现如下,该方法生成了服务提供者监听的 URL 信息:

        private URL getSubscribedOverrideUrl(URL registeredProviderUrl) {
        	// 1. protocol 修改为 provider。表明当前服务监听的是应用于服务提供者端的配置(消费者端则为 consumer);
        	// 2. 添加 category configurators。声明监听的节点为 configurators  节点(消费者还会监听 routers、provider 等节点)。
            return registeredProviderUrl.setProtocol(PROVIDER_PROTOCOL)
                    .addParameters(CATEGORY_KEY, CONFIGURATORS_CATEGORY, CHECK_KEY, String.valueOf(false));
        }
    

    Dubbo提供了动态配置的功能,当服务发布后,用户可以通过管理中心来进行动态配置,提供者可以感知这些配置的变化并更新到本地,如果需要将服务合并配置后重新发布。如当我们使用dubbo-admin 进行动态配置时,dubbo-admin 会在注册中心或配置中心创建配置节点(Dubbo 2.6 没有配置中心,会在注册中心上创建,而 Dubbo 2.7 则是在配置中心上创建),提供者只需要监听这些节点的变化即可。监听的首要工作是生成一个 URL 代表监听的节点信息,即本步的工作。

  2. RegistryProtocol#overrideUrlWithConfig 的实现如下, 该方法完成了配置同步 并且添加了服务监听。

        private URL overrideUrlWithConfig(URL providerUrl, OverrideListener listener) {
        	// 1. 获取应用级别现有配置规则,并在导出之前覆盖提供程序URL。
            providerUrl = providerConfigurationListener.overrideUrl(providerUrl);
            // 2. 初始化服务级别监听器,并保存到 serviceConfigurationListeners 中,完成了对当前服务的监听。
            ServiceConfigurationListener serviceConfigurationListener = new ServiceConfigurationListener(providerUrl, listener);
            serviceConfigurationListeners.put(providerUrl.getServiceKey(), serviceConfigurationListener);
            // 3. 获取服务级别现有配置规则,并在导出之前覆盖提供程序URL。
            return serviceConfigurationListener.overrideUrl(providerUrl);
        }
    

    需要注意,这里存在两个监听器 ServiceConfigurationListener 和 ProviderConfigurationListener ,这两个监听器 是 Dubbo 2.7 之后增加,其功能如下:

    • ProviderConfigurationListener : 应用级别监听器,因为一个程序只能有一个应用名,所以这里之需要一个 ProviderConfigurationListener 实例即可。
    • ServiceConfigurationListener :服务级别监听器。因为一个程序可以提供多个 Dubbo 接口服务,所以这使用 serviceConfigurationListeners 来保存不同接口的监听器。

    关于介绍,使用哪一篇,配置中心还是监听方式

2. 服务暴露

在上面对 URL 处理结束后,我们获取到了三个URL

  1. registryUrl :注册中心 URL
  2. providerUrl :暴露服务的URL
  3. overrideSubscribeUrl :服务监听节点 URL

接下来便开始进行服务暴露,其暴露是通过 doLocalExport(originInvoker, providerUrl); 方法实现的,这里会调用 DubboProtocol#export (我们使用 Dubbo协议)来进行真正的服务暴露。其具体实现如下:

    private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker, URL providerUrl) {
    	//  将 originInvoker 转换为 缓存key,从 bounds 缓存中尝试获取
        String key = getCacheKey(originInvoker);
        ExporterChangeableWrapper<T> exporter = (ExporterChangeableWrapper<T>) bounds.get(key);
        // 如果当前服务没发布过,则开始创建
        if (exporter == null) {
            synchronized (bounds) {
            	// DCL 双端检索机制
                exporter = (ExporterChangeableWrapper<T>) bounds.get(key);
                if (exporter == null) {
					// Invoker 委托
                    final Invoker<?> invokerDelegete = new InvokerDelegate<T>(originInvoker, providerUrl);
                    // Invoker 转换为 Exporter,
                    // 这里注意: protocol.export(invokerDelegete) 中 protocol 在 RegistryProtocol 在创建的时候依赖注入的,其实现还是Protocol适配器
                    exporter = new ExporterChangeableWrapper<T>((Exporter<T>) protocol.export(invokerDelegete), originInvoker);
                    bounds.put(key, exporter);
                }
            }
        }
        return exporter;
    }

上面可以看到,如果当前服务并没有暴露过(即缓存中不存在),则会通过protocol.export(invokerDelegete) 进行服务暴露。因为在 RegistryProtocol 通过 Dubbo SPI 初始化时存在 扩展点之间的依赖注入 的功能(SPI 的依赖注入即:即当一个 SPI 的实例进行创建时,如果其内部存在某个 SPI 接口的 set 方法,则会被反射注入。这部分内容Dubbo笔记衍生篇②:Dubbo SPI 原理2.2. injectExtension 章节中有过分析), 所以此时的 protocol是 Protocol$Adaptive 类型,所以,此时会再走一遍下面的流程:

Protocol$Adaptive#export() => QosProtocolWrapper#export()  => ProtocolListenerWrapper #export() => ProtocolFilterWrapper#export()  => XxxProtocol#export()

即以Dubbo协议类型为例,整个流程大致如下:
在这里插入图片描述

这里需要注意(在Dubbo笔记衍生篇③:ProtocolWrapper 中有过详细介绍):

  • 在第一次执行包装流程时,由于协议类型是Registry,所以包装类并没有进行任何干涉流程的处理,而是直接将请求透传。
  • 在第二次执行包装流程时由于协议类型是Dubbo才会真正进行包装过程。

根据上面的流程图,在第二次包装结束后,则会调用 DubboProtocol#export 方法,该部分我们在下文讲解。

3. 服务注册

当上一步执行结束后,此时的服务已经开启,接下来我们需要在注册中心上保存相应的信息,如zk会建立相应的节点。下面我们来看一下服务注册相关的代码:

        // url to registry
        // 获取注册中心实例 ,通过 RegistryFactory 来获取,也是 SPI 接口。
        // 我们是用zk作为注册中心,所以这里获取的实例是 ZookeeperRegistry
        final Registry registry = getRegistry(originInvoker);
        // 获取 当前注册的服务提供者 URL,如果开启了简化 URL,则返回的是简化的URL
        final URL registeredProviderUrl = getRegisteredProviderUrl(providerUrl, registryUrl);
       // ProviderInvokerWrapper 中保存了当前注册服务的一些信息(originInvoker、registryUrl、registeredProviderUrl 以及是否注册)
       // 如当服务重新发布时就会通过 ProviderInvokerWrapper  判断当前服务是否已经注册
        ProviderInvokerWrapper<T> providerInvokerWrapper = ProviderConsumerRegTable.registerProvider(originInvoker,
                registryUrl, registeredProviderUrl);
        //to judge if we need to delay publish
        // 判断服务是否需要延迟发布,延迟发布则不会立即进行注册,否则进行注册
        boolean register = registeredProviderUrl.getParameter("register", true);
        if (register) {
        	// 调用远端注册中心的register方法进行服务注册
        	// 此时如果有消费者订阅了该服务,则推送消息让消费者引用此服务
        	// 注册中心缓存了所有提供者注册的服务以供消费者发现。
            register(registryUrl, registeredProviderUrl);
            // 将当前服务置为已注册。当服务重新发布时会判断服务是否已经注册,已经注册的服务才能重新发布
            providerInvokerWrapper.setReg(true);
        }

我们主要关注 register(registryUrl, registeredProviderUrl); 方法: 该方法完成了服务的注册,即解析出当前URL中需要暴露的服务,并在ZK 上创建服务节点。其实现如下:

    public void register(URL registryUrl, URL registeredProviderUrl) {
    	// SPI  方式获取 Registry 实例,这里获取的是 ZookeeperRegistry
        Registry registry = registryFactory.getRegistry(registryUrl);
        registry.register(registeredProviderUrl);
    }

这里默认实现 Registry 的默认实现是 ZookeeperRegistry,即这里调用的是 ZookeeperRegistry#register (FailbackRegistry#register)方法,下面我们来看看其详细实现:

	// org.apache.dubbo.registry.support.FailbackRegistry#register
 	@Override
    public void register(URL url) {
    	// org.apache.dubbo.registry.support.AbstractRegistry#register 
    	// 将 url添加到 org.apache.dubbo.registry.support.AbstractRegistry#registered 中
        super.register(url);
        // 开始注册,从失败列表删除
        removeFailedRegistered(url);
        removeFailedUnregistered(url);
        try {
            // Sending a registration request to the server side
            // 这里调用  org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#doRegister
            doRegister(url);
        } catch (Exception e) {
            Throwable t = e;
            // ... 失败处理
            // 添加到注册失败列表,并定期重试
            addFailedRegistered(url);
        }
    }

这里的逻辑还是比较清楚,我们这里关注 doRegister(url) ,其具体实现 ZookeeperRegistry#doRegister 的如下 :

	// org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#doRegister
    @Override
    public void doRegister(URL url) {
        try {
            zkClient.create(toUrlPath(url), url.getParameter(Constants.DYNAMIC_KEY, true));
        } catch (Throwable e) {
            throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
        }
    }

可以看到,这里直接调用了 zkClient#create开始创建了 zk 的节点信息。 (这里的ZkClient 是Dubbo的实现类,有CuratorZookeeperClientZkclientZookeeperClient,默认ZkclientZookeeperClient)


关于 zkClient.create 通过递归的方式创建了节点,详细实现这里就不再追溯。这里以com.kingfish.service.impl.DemoService 服务为例,其最后的创建结构如图(其中URL中并非仅仅IP端口信息,这里仅为了方便描述,此时还没有创建configurators 节点,configurators 节点在下一步才创建):
在这里插入图片描述
zk中的实际结构如下:
在这里插入图片描述


至此,RegistryProtocol#doLocalExport 开启了服务,这一步在zk 上创建了该服务的节点,也即是说说消费者已经可以通过zk获取到当前服务作为提供者,并进行服务调用。


4. 服务订阅

在此之前,服务已经完成了服务端口绑定和服务注册的功能,此时的服务已经可以对外提供功能。但是有些时候我们希望可以动态配置一些参数,而 Dubbo提供了控制台功能,我们可以动态的配置一些参数,提供者和消费者可以即时感知并做出响应的处理。该功能则是依赖于此处的服务订阅功能,Dubbo 服务在启动后,无论是消费者还是提供者都会监听部分节点信息(在zk上映射的节点(服务提供者为 configurators 节点,服务消费者为providers,configurators、routers 节点),当 控制台修改动态配置时会修改配置节点的信息,服务通过监听器感知到节点变化后自身更新配置。

需要注意的是: 这一步的是 Dubbo 2.6 版本的逻辑,Dubbo 2.7 以下版本为了兼容并未删除该部分逻辑, 在 Dubbo 2.7 及以上版本, 则是通过 ServiceConfigurationListener 来监听动态配置的节点。关于 2.7 版本的监听方式,如有需要详参: Dubbo笔记衍生篇⑩:2.7 版本的动态配置监听


下面我们来看详细过程,服务订阅我们只关注下面的代码:

// 这里的 registry 实现是 ZookeeperRegistry 
 registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);

我们先看一下 ZookeeperRegistry#subscribe 的两个入参:

  1. overrideSubscribeUrl : 代表需要订阅的URL 节点,当该URL所对应的节点发生变化时,会通知当前服务。其简化结构如下,其中 category 代表要监听的节点:

    provider://192.168.111.1:9998/com.kingfish.service.impl.DemoService?&category=configurators
    
  2. overrideSubscribeListener : 当监听的节点发生变化时,会回调该监听器。

ZookeeperRegistry#subscribe 的具体 实现如下:

	// org.apache.dubbo.registry.support.FailbackRegistry#subscribe
    @Override
    public void subscribe(URL url, NotifyListener listener) {
    	// 调用 org.apache.dubbo.registry.support.AbstractRegistry#subscribe 
    	// 将 url 和 listener 添加到 org.apache.dubbo.registry.support.AbstractRegistry#subscribed (ConcurrentMap<URL, Set<NotifyListener>> subscribed)中
        super.subscribe(url, listener);
        // 移除订阅失败的url
        removeFailedSubscribed(url, listener);
        try {
            // Sending a subscription request to the server side
            // 进行订阅,这里调用的是 org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#doSubscribe
            // 这一步结束后,zk 上在服务下多个一个 configurators节点
            // 如 : /dubbo/com.kingfish.service.impl.DemoService/configurators
            doSubscribe(url, listener);
        } catch (Exception e) {
          	// ... 异常处理

            // Record a failed registration request to a failed list, retry regularly
            // 添加到订阅失败列表,定时重试
            addFailedSubscribed(url, listener);
        }
    }

关键逻辑在 doSubscribe(url, listener); 中,其具体实现在ZookeeperRegistry#doSubscribe 中。

4.1 ZookeeperRegistry#doSubscribe

ZookeeperRegistry#doSubscribe 的实现如下:
由于我们这里指定了ServiceInterfacecom.kingfish.service.impl.DemoService,所以会走else 分支,其实两个分支实现也基本相同。

	@Override
    public void doSubscribe(final URL url, final NotifyListener listener) {
        try {
        	// 1. 如果url 指定的服务接口为 *, 则监听所有,直接监听root 节点 ,即zk 中的 dubbo 节点
            if (Constants.ANY_VALUE.equals(url.getServiceInterface())) {
            	// 获取 root 节点(dubbo)
                String root = toRootPath();
                // 初始化监听器集合
                ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
                if (listeners == null) {
                    zkListeners.putIfAbsent(url, new ConcurrentHashMap<NotifyListener, ChildListener>());
                    listeners = zkListeners.get(url);
                }
                ChildListener zkListener = listeners.get(listener);
                if (zkListener == null) {
                	// 添加节点监听器,这里和else分支不同
                    listeners.putIfAbsent(listener, new ChildListener() {
                        @Override
                        public void childChanged(String parentPath, List<String> currentChilds) {
                        	// 如果有节点更新,则使用anyServices进行缓存,并进行服务订阅
                            for (String child : currentChilds) {
                                child = URL.decode(child);
                                if (!anyServices.contains(child)) {
                                    anyServices.add(child);
                                    subscribe(url.setPath(child).addParameters(Constants.INTERFACE_KEY, child,
                                            Constants.CHECK_KEY, String.valueOf(false)), listener);
                                }
                            }
                        }
                    });
                    zkListener = listeners.get(listener);
                }
                // 在zk 上创建dubbo节点
                zkClient.create(root, false);
                // 监听root节点,监听器为zkListener
                List<String> services = zkClient.addChildListener(root, zkListener);
                if (services != null && !services.isEmpty()) {
                    for (String service : services) {
                        service = URL.decode(service);
                        anyServices.add(service);
                        subscribe(url.setPath(service).addParameters(Constants.INTERFACE_KEY, service,
                                Constants.CHECK_KEY, String.valueOf(false)), listener);
                    }
                }
            } else {
                List<URL> urls = new ArrayList<URL>();
                // 2. 将 URL 分类遍历 
                // toCategoriesPath 是获取分类路径
                // 如 :/dubbo/com.kingfish.service.impl.DemoService/configurators、/dubbo/com.kingfish.service.impl.DemoService/providers、/dubbo/com.kingfish.service.impl.DemoService/routers
                // 这里获取的是 /dubbo/com.kingfish.service.impl.DemoService/configurators
                for (String path : toCategoriesPath(url)) {
                	// 3. 监听器设置
                	// 获取当前 URL 的监听器Map
                    ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
                    if (listeners == null) {
                    	// 如果当前 URl还没有监听器,则创建一个 Map
                        zkListeners.putIfAbsent(url, new ConcurrentHashMap<NotifyListener, ChildListener>());
                        listeners = zkListeners.get(url);
                    }
                    // 获取当前 NotifyListener 对应的节点监听器
                    ChildListener zkListener = listeners.get(listener);
                    if (zkListener == null) {
                    	// 如果没有NotifyListener  对应的 ChildListener ,则添加一个
                    	// 这里设置了 ChildListener 的回调方法
                        listeners.putIfAbsent(listener, new ChildListener() {
                            @Override
                            public void childChanged(String parentPath, List<String> currentChilds) {
                            	// 设置回调
                                ZookeeperRegistry.this.notify(url, listener, toUrlsWithEmpty(url, parentPath, currentChilds));
                            }
                        });
                        // 获取当前 NotifyListener 对应的 节点监听器
                        zkListener = listeners.get(listener);
                    }
                    //  创建/dubbo/com.kingfish.service.impl.DemoService/configurators 节点
                    zkClient.create(path, false);
                    // 添加节点监听器。使用 zkListener 监听 path,返回值为 path 的子节点
                    List<String> children = zkClient.addChildListener(path, zkListener);
                    if (children != null) {
                    	// 将 children  转换为 url,其中 toUrlsWithEmpty是为了避免出现 null,如果无法转换,则返回一个 empty:// 协议头的 URL
                        urls.addAll(toUrlsWithEmpty(url, path, children));
                    }
                }
          		// 主动调用通知事件
                notify(url, listener, urls);
            }
        } catch (Throwable e) {
            throw new RpcException("Failed to subscribe " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
        }
    }

我们按照代码注释顺序开始分析:

4.1.1 根节点的监听

如果url 指定的服务接口为 *, 则监听所有,直接监听root 节点 ,即zk 中的 dubbo 节点。我们一般会具体监听到某个服务,所以不会走到这里。

4.1.2 URL 分类遍历

这里会调用 ZookeeperRegistry#toCategoriesPath 对 URL 按照类别进行划分。ZookeeperRegistry#toCategoriesPath 的实现如下:

    private String[] toCategoriesPath(URL url) {
        String[] categories;
        // 如果 url 中的 category 属性为 *,则认为当前 URL 监听所有分类的节点。
        // 包括providers、consumers、routers、configurators
        if (Constants.ANY_VALUE.equals(url.getParameter(Constants.CATEGORY_KEY))) {
            categories = new String[]{Constants.PROVIDERS_CATEGORY, Constants.CONSUMERS_CATEGORY,
                    Constants.ROUTERS_CATEGORY, Constants.CONFIGURATORS_CATEGORY};
        } else {
        	// 否认则获取category  属性,默认为 providers
            categories = url.getParameter(Constants.CATEGORY_KEY, new String[]{Constants.DEFAULT_CATEGORY});
        }
        // 对 categories  进一步处理,拼接上 servicepath
        String[] paths = new String[categories.length];
        for (int i = 0; i < categories.length; i++) {
            paths[i] = toServicePath(url) + Constants.PATH_SEPARATOR + categories[i];
        }
        return paths;
    }

我们这里以消费者 URL举例:

consumer://192.168.111.1:9998/com.kingfish.service.impl.DemoService?&category=providers,configurators,routers

则这里会被划分为下面三个节点,在后面的流程,消费者会监听这三个节点的变化:

// 提供者列表
/dubbo/com.kingfish.service.impl.DemoService/providers
// 动态配置信息
/dubbo/com.kingfish.service.impl.DemoService/configurators
// 路由信息
/dubbo/com.kingfish.service.impl.DemoService/routers

同理,此时的提供者的URL 结构为:

provider://192.168.111.1:9998/com.kingfish.service.impl.DemoService?&category=configurators

注:提供者只会监听 /dubbo/com.kingfish.service.impl.DemoService/configurators 节点 ,因为对于提供者来说只需要关心动态配置的内容,并不需要关注有多个提供者提供服务和路由策略。

所以 ZookeeperRegistry#toCategoriesPath 的返回结果 path 为 /dubbo/com.kingfish.service.impl.DemoService/configurators,服务提供者将监听该节点。

4.1.3 监听器的设置

上面的代码注释比较清楚:

  1. 通过 zkListeners.get(url) 判断是否有监听器监听当前URL,如果没有,则创建一个空的Map。
  2. 通过 listeners.get(listener) 判断是否有使用当前 listener 作为监听器监听当前URL。如果没有则创建一个对应的监听。添加的监听器会在节点更新时调用 ZookeeperRegistry#notify
  3. 通过 zkClient.create(path, false); 在zk 上创建对应的监听节点。
  4. 通过 zkClient.addChildListener(path, zkListener), 添加该节点的监听,此时的path 正是 4.1.2 URL 分类遍历 中分类后的节点路径。
  5. 通过 notify(url, listener, urls); 主动调用通知事件。

我们这里看一下 zkListeners 的定义如下:

 private final ConcurrentMap<URL, ConcurrentMap<NotifyListener, ChildListener>> zkListeners = new ConcurrentHashMap<URL, ConcurrentMap<NotifyListener, ChildListener>>();

一个 URL 可以被多个 NotifyListener监听,但是一个 URL 可以划分为多个分类,每个分类都需要一个 ChildListener 来监听节点变化,并且需要有一个 NotifyListener 来执行节点变化后的操作。如下URL,我们指定 Consumer NotifyListener 来监听 该URL:

URL = consumer://192.168.111.1:9998/com.kingfish.service.impl.DemoService?&category=providers,configurators,routers

该 URL 会被划分为 如下三个类型,也就是说我们需要监听下面三个节点,此时会设置三个 ChildListener 来监听,当节点变化时会触发操作:即交由当前指定的 Consumer NotifyListener来处理 :

/dubbo/com.kingfish.service.impl.DemoService/providers
/dubbo/com.kingfish.service.impl.DemoService/configurators
/dubbo/com.kingfish.service.impl.DemoService/routers

到这里,我们分析了服务订阅的整个流程,一言蔽之:服务提供者会订阅 zk 上的 /dubbo/com.kingfish.service.impl.DemoService/configurators 节点,当节点更新时会触发监听器的监听方法。

但是我们尚未分析Dubbo在感知到节点更新后做了何种处理。下面我们一起来分析。

4.2 ZookeeperRegistry#notify

在上面的代码我们可以看到,我们知道,节点更新时会调用 ZookeeperRegistry#notify 来处理,但实际上该方法的调用有两次,如下:
在这里插入图片描述
这里的调用场景如下:

  1. 当节点更新后的回调执行。这没什么好说的,管理端进行了动态配置后更新配置节点,提供者需要根据动态配置来选择是否重新发布服务。
  2. 当服务处理完发布订阅流程后会主动调用一次。这一步的目的是为了同步configurators 子节点的配置信息。即当服务发布完成后到这一步结束前,此时并未完成配置的监听操作,也就是说如果在此区间进行动态配置,提供者因为监听逻辑没有完成是无法感知的,所以需要当完成配置监听后主动调用一次来同步配置。当执行这一步时,根据 urls 的信息判断本地配置信息是否需要更新,如果需要更新则更新,并重新发布服务。也即是说,如果配置子节点不为空(则urls 不为空),服务发布会经历 :服务发布 -> 配置更新 -> 服务重新发布

ZookeeperRegistry#notify 入参的含义:

  • url :当前暴露的服务信息,保存当前服务提供者的信息
  • listener :configurators 子节点 的回调监听器实例,当 configurators 子节点更新时会回调该监听器
  • urls :configurators 的子节点 转换而来的 Url。如果信息不合法,则转化为 empty 协议的 url,在后续处理会被过滤掉。

在这里 ZookeeperRegistry.this.notifynotify(url, listener, urls); 的调用顺序都如下:

1. org.apache.dubbo.registry.support.FailbackRegistry#notify
2. org.apache.dubbo.registry.support.FailbackRegistry#doNotify
3. org.apache.dubbo.registry.support.AbstractRegistry#notify(org.apache.dubbo.common.URL, org.apache.dubbo.registry.NotifyListener, java.util.List<org.apache.dubbo.common.URL>)
4. org.apache.dubbo.registry.integration.RegistryProtocol.OverrideListener#notify

其中 第三步 AbstractRegistry#notify 的实现如下:

	
    protected void notify(URL url, NotifyListener listener, List<URL> urls) {
	    //... 入参校验
	    // key 为 类别( provider、configurators、routers ),value 为所属url
        Map<String, List<URL>> result = new HashMap<String, List<URL>>();
        // 按照url类别进行分组,如 Provider、configurators、routers 
        for (URL u : urls) {
        	// u 为 被监听的 URl, urls 为监听的节点转化为的 URl。这里判断节点 URL 是否可用于 u
        	// 即当前变更的配置u 是否适用于当前服务url
            if (UrlUtils.isMatch(url, u)) {
            	// 按照 category 类别 划分 url
                String category = u.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
                List<URL> categoryList = result.get(category);
                if (categoryList == null) {
                    categoryList = new ArrayList<URL>();
                    result.put(category, categoryList);
                }
                categoryList.add(u);
            }
        }
        // 如果没有使用的url 则直接返回
        // result 的 key 为 category,value 为对应分组的 URl 列表
        if (result.size() == 0) {
            return;
        }
        // key 为 类别, value 为 对应类别的 节点URL
        Map<String, List<URL>> categoryNotified = notified.get(url);
        if (categoryNotified == null) {
            notified.putIfAbsent(url, new ConcurrentHashMap<String, List<URL>>());
            categoryNotified = notified.get(url);
        }
        // 这里需要注意的是,这里回调是按照类别进行回调,即 如果 provider、configurators、routers 则 provider 子节点回调一次,configurators子节点回调一次,routers 子节点回调一次
        for (Map.Entry<String, List<URL>> entry : result.entrySet()) {
            String category = entry.getKey();
            List<URL> categoryList = entry.getValue();
            categoryNotified.put(category, categoryList);
            saveProperties(url);
            // 进行服务通知,这里调用是 RegistryProtocol.OverrideListener#notify
            listener.notify(categoryList);
        }
    }

很显然我们这里需要分析一下 RegistryProtocol.OverrideListener#notify 的具体实现,如下:

        @Override
        public synchronized void notify(List<URL> urls) {
			// 获取 可以用于当前服务配置
            List<URL> matchedUrls = getMatchedUrls(urls, subscribeUrl.addParameter(CATEGORY_KEY,
                    CONFIGURATORS_CATEGORY));
            // No matching results
            // 没有可用于当前服务的配置则返回。
            if (matchedUrls.isEmpty()) {
                return;
            }
			// 挑选出 配置节点URL  并转换为 configurators, 后面会通过 configurators进行合并
            this.configurators = Configurator.toConfigurators(classifyUrls(matchedUrls, UrlUtils::isConfigurator))
                    .orElse(configurators);
			// 在必要的情况下覆盖配置,此时对configurators 的数据进行合并,并缓存到服务本地
            doOverrideIfNecessary();
        }
		
        public synchronized void doOverrideIfNecessary() {
            final Invoker<?> invoker;
            if (originInvoker instanceof InvokerDelegate) {
                invoker = ((InvokerDelegate<?>) originInvoker).getInvoker();
            } else {
                invoker = originInvoker;
            }
            //The origin invoker
            // 获取 原始 URL
            URL originUrl = RegistryProtocol.this.getProviderUrl(invoker);
            String key = getCacheKey(originInvoker);
            ExporterChangeableWrapper<?> exporter = bounds.get(key);
            if (exporter == null) {
                logger.warn(new IllegalStateException("error state, exporter should not be null"));
                return;
            }
            //The current, may have been merged many times
            // 获取当前 URL,当前URL,可能已经合并过很多次,所以并不一定等同于 originUrl 
            URL currentUrl = exporter.getInvoker().getUrl();
            //Merged with this configuration
            // Dubbo 2.6 版本逻辑 使用originUrl 与当前配置合并后,产生新  url
            URL newUrl = getConfigedInvokerUrl(configurators, originUrl);
            // dubbo 2.7.0 版本新增,也是配置合并,合并服务级别配置和应用级别配置
            newUrl = getConfigedInvokerUrl(serviceConfigurationListeners.get(originUrl.getServiceKey())
                    .getConfigurators(), newUrl);
            newUrl = getConfigedInvokerUrl(providerConfigurationListener.getConfigurators(), newUrl);
            // currentUrl != newUrl, 则说明配置有变动,则重新发布服务
            if (!currentUrl.equals(newUrl)) {
            	// 重新发布服务
                RegistryProtocol.this.reExport(originInvoker, newUrl);
				...
            }
        }

至此,提供者的发布流程基本结束。


三、DubboProtocol#export

经过上面的分析,我们知道了 DubboProtocol#export 的调用场景有两个:

  1. 远程服务发布 & 没有注册中心。此时服务发布会直接调用 DubboProtocol#export。
  2. 远程服务发布 & 有注册中心。此时服务发布会调用 RegistryProtocol#export ,而RegistryProtocol#export 会调用 DubboProtocol#export 来完成具体的服务发布。

DubboProtocol 是 Dubbo协议下服务发布的过程实现,DubboProtocol 在服务发布时,开启了Netty 服务,进行端口监听。

下面我们来看 DubboProtocol#export 的详细实现:

    @Override
    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        URL url = invoker.getUrl();
		/*********** 1. 前置工作 **********/
        // export service.
        // 获取需要暴露的服务的key,结构为 协议/服务接口:版本:端口号
        // 如这里解析出来的key为 : dubbo/api.DemoService:1.0.0:20880
        String key = serviceKey(url);
        // 封装成 Export。exporterMap 中保存的是本机暴露的服务接口列表,在服务调用 Exporter#unexport 时会将服务从exporterMap 中移除。
        DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
        // exporterMap 中保存了当前应用发布的服务
        exporterMap.put(key, exporter);

        //export an stub service for dispatching event
         // 本地存根相关代码
        Boolean isStubSupportEvent = url.getParameter(Constants.STUB_EVENT_KEY, Constants.DEFAULT_STUB_EVENT);
        Boolean isCallbackservice = url.getParameter(Constants.IS_CALLBACK_SERVICE, false);
        if (isStubSupportEvent && !isCallbackservice) {
            String stubServiceMethods = url.getParameter(Constants.STUB_EVENT_METHODS_KEY);
            if (stubServiceMethods == null || stubServiceMethods.length() == 0) {
                if (logger.isWarnEnabled()) {
                    logger.warn(new IllegalStateException("consumer [" + url.getParameter(Constants.INTERFACE_KEY) +
                            "], has set stubproxy support event ,but no stub methods founded."));
                }
            } else {
                stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods);
            }
        }
        /*********** 2. 开启服务 **********/
		// 开启服务,这里默认的是 Netty。 同一个机器的不同接口服务导出只会开启一个NettyServer
        openServer(url);
        /*********** 3. 序列化优化 **********/
        // 对序列化进行优化,可以通过 optimizer 参数指定优化器类的全路径类名
        optimizeSerialization(url);
        return exporter;
    }
	

1. 前置工作

前置工作并没有做什么复杂工作,主要就做了两件事:

  1. 将将要暴露的服务保存到 exporterMap 中。我们这里需要注意 exporterMap,其声明如下:

        protected final Map<String, Exporter<?>> exporterMap = new ConcurrentHashMap<String, Exporter<?>>();
    

    exporterMap key 为 服务唯一key(服务接口 + 服务分组 + 服务版本号确定唯一服务),value 为 服务Exporter。即 exporterMap 缓存了当前机器上暴露的服务信息。而此时对于 一个 Exporter 来说,其结构应为 :
    在这里插入图片描述

    当服务消费者进行调用时, 提供者会根据调用服务信息获取serviceKey 通过 exporterMap 来获取 服务Exporter,再通过 Exporter 获取 Invoker 来进行方法调用。这些我们在讲到消费者的调用过程时会详细分析。

  2. 本地存根和和回调服务的配置检查,并将其保存到 stubServiceMethodsMap 中。

2. 开启服务

这里通过 DubboProtocol#openServer 方法开启了服务。默认情况下,如果服务提供者机器尚未开启过服务,该方法会在服务提供者机器上开启Netty 服务。需要注意的是,服务并不会重复开启,如果 服务提供者的 ip:port 已经创建了服务,则不会重复创建。DubboProtocol#openServer 的 具体实现如下:

	// 开启服务,
	// org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol#openServer
    private void openServer(URL url) {
        // find server.
        // 获取服务提供者机器的 ip:port, 并将其作为服务器实例的 key,用于标识当前的服务器实例。
        // 这里的端口是 dubbo 服务 端口
        String key = url.getAddress();
        //client can export a service which's only for server to invoke
        // 判断是否是服务提供者,服务提供者才会启动监听
        boolean isServer = url.getParameter(Constants.IS_SERVER_KEY, true);
        if (isServer) {
        	// 从缓存中获取服务,如果 服务提供者的 ip:port 已经创建了服务,则不会重复创建
            ExchangeServer server = serverMap.get(key);
            if (server == null) {
                synchronized (this) {
                    server = serverMap.get(key);
                    if (server == null) {
                    	// 通过  createServer(url) 创建服务
                        serverMap.put(key, createServer(url));
                    }
                }
            } else {
                // server supports reset, use together with override
                server.reset(url);
            }
        }
    }

可以看到,关键的操作在 createServer(URL url) 中。

2.1 createServer(URL url)

DubboProtocol#createServer 完成了服务的创建过程,在服务提供者上开启了端口监听。

DubboProtocol#createServer的返回值实例类型为 HeaderExchangeServer,内部包含一个NettyServer,NettyServer 包含属性 ChannelHandler,用来处理服务的具体消息,ChannelHandler 在传递过程中层层包裹,最终如下图中所示(部分委托类并未画出):

在这里插入图片描述


下面我们来看一下 DubboProtocol#createServer 的具体实现

	// org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol#createServer
    private ExchangeServer createServer(URL url) {
        // send readonly event when server closes, it's enabled by default
        /*******  1. 参数设置  ********/
        // 1.1 默认启用服务器关闭时发送只读事件
        url = url.addParameterIfAbsent(Constants.CHANNEL_READONLYEVENT_SENT_KEY, Boolean.TRUE.toString());
        // enable heartbeat by default
        // 1.2 默认启用心跳
        url = url.addParameterIfAbsent(Constants.HEARTBEAT_KEY, String.valueOf(Constants.DEFAULT_HEARTBEAT));
        // 1.3 获取传输协议,默认为 Netty
        String str = url.getParameter(Constants.SERVER_KEY, Constants.DEFAULT_REMOTING_SERVER);
		// 根据 Constants.SERVER_KEY(server) 校验对应的SPI 实现接口
        if (str != null && str.length() > 0 && !ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str)) {
            throw new RpcException("Unsupported server type: " + str + ", url: " + url);
        }
		// 1.4 设置编解码器为 dubbo
        url = url.addParameter(Constants.CODEC_KEY, DubboCodec.NAME);
        /*******  2. 开启服务端口  ********/
        ExchangeServer server;
        try {
        	// 2.1 绑定ip端口,开启服务,这里server 默认是NettyServer,需要注意的是这里传入的requestHandler 是最终处理消息的处理器
            server = Exchangers.bind(url, requestHandler);
        } catch (RemotingException e) {
            throw new RpcException("Fail to start server(url: " + url + ") " + e.getMessage(), e);
        }
        // 2.2 校验客户端的传输协议
        str = url.getParameter(Constants.CLIENT_KEY);
        if (str != null && str.length() > 0) {
            Set<String> supportedTypes = ExtensionLoader.getExtensionLoader(Transporter.class).getSupportedExtensions();
            if (!supportedTypes.contains(str)) {
                throw new RpcException("Unsupported client type: " + str);
            }
        }
        return server;
    }

我们下面按照代码注释顺序来分析。

2.1.1 基础参数设置

这里可以分为下面几步:

  1. 该参数控制服务器关闭时发送只读事件,默认启动。 即服务在关闭时会主动想其他连接服务发送只读消息。调用时机如下:在服务关闭时会调用 HeaderExchangeServer#close 而其中会调用 HeaderExchangeServer#sendChannelReadOnlyEvent 方法发送,其实现如下:

    private void sendChannelReadOnlyEvent() {
    	// 设置消息内容
        Request request = new Request();
        request.setEvent(Request.READONLY_EVENT);
        request.setTwoWay(false);
        request.setVersion(Version.getProtocolVersion());
    	// 获取连接通路
        Collection<Channel> channels = getChannels();
        for (Channel channel : channels) {
            try {
            	// 如果还保持连接
                if (channel.isConnected()) {
                	// 发送只读事件
                    channel.send(request, getUrl().getParameter(Constants.CHANNEL_READONLYEVENT_SENT_KEY, true));
                }
            } catch (RemotingException e) {
                logger.warn("send cannot write message error.", e);
            }
        }
    }
    
  2. 该参数控是否启用心跳连接。调用时机如下:在 HeaderExchangeServer#HeaderExchangeServer 构造函数时 调用 startHeartbeatTimer 方法启动心跳和重连的任务。由 HeartbeatTimerTask 和 ReconnectTimerTask 来执行具体任务。

       public HeaderExchangeServer(Server server) {
            if (server == null) {
                throw new IllegalArgumentException("server == null");
            }
            this.server = server;
            this.heartbeat = server.getUrl().getParameter(Constants.HEARTBEAT_KEY, 0);
            this.heartbeatTimeout = server.getUrl().getParameter(Constants.HEARTBEAT_TIMEOUT_KEY, heartbeat * 3);
            if (heartbeatTimeout < heartbeat * 2) {
                throw new IllegalStateException("heartbeatTimeout < heartbeatInterval * 2");
            }
    		// 启动定时器
            startHeartbeatTimer();
        }
    
  3. 校验传输协议服务端的传输协议 Transporter。这里是为了确保Dubbo存在客户端指定的传输协议类型。默认为 Netty。

  4. 添加 DubboCodec 为指定编码器。

2.1.2 绑定服务端口

Dubbo 通过 Exchangers.bind(url, requestHandler) 来绑定Dubbo 服务端口,我们这里需要注意一下这里的 requestHandler 是 DubboProtocol 中的匿名 ExchangeHandlerAdapter 实现类,调用消息最终会交由该类来处理。

Exchangers.bind(url, requestHandler) 代码调用顺序较为复杂,具体如下:

	 public static ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException {
	    ...
	    // 如果 codec 没有默认值,则添加 exchange。但是在基础参数设置中我们已经指定了编码器为 dubbo
        url = url.addParameterIfAbsent(Constants.CODEC_KEY, "exchange");
        // 1. getExchanger(url) : 获取url 中的 exchanger 属性来获取到 Exchanger,默认是Header
        // 2. bind(url, handler):第一步中默认是HeaderExchanger,所以这里实际上是 HeaderExchanger#bind(url, handler)
        return getExchanger(url).bind(url, handler);
    }
  1. Exchangers.bind(url, requestHandler) 实现如下:getExchanger(url) : 通过SPI 获取 Exchanger,默认实现是 HeaderExchanger。所以这里bind(url, handler) 调用的是 HeaderExchanger#bind

    // Exchangers#getExchanger 实现如下:
    public static Exchanger getExchanger(URL url) {
           String type = url.getParameter(Constants.EXCHANGER_KEY, Constants.DEFAULT_EXCHANGER);
           return getExchanger(type);
       }
    	// 通过SPI 获取Exchange
       public static Exchanger getExchanger(String type) {
           return ExtensionLoader.getExtensionLoader(Exchanger.class).getExtension(type);
       }
    
  2. HeaderExchanger#bind 实现如下:

    	// HeaderExchanger#bind 
        @Override
        public ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException {
            return new HeaderExchangeServer(Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler))));
        }
        
    

    其中 Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler))) 实现如下:

        public static Server bind(URL url, ChannelHandler... handlers) throws RemotingException {
        	...
            ChannelHandler handler;
            // 如果有多个 Handlers ,则使用 ChannelHandlerDispatcher 来分发处理。
            if (handlers.length == 1) {
                handler = handlers[0];
            } else {
                handler = new ChannelHandlerDispatcher(handlers);
            }
    
            // 1. getTransporter() 通过SPI 获取到了默认的 Transporter实现 NettyTransporter。
            // 2. .bind(url, handler) 的实现是 NettyTransporter.bind(url, handler),完成了绑定了当前服务.所以此处调用的是NettyTransporter#bind
            return getTransporter().bind(url, handler);
        }
    

    由于 在 2.1.1 基础参数设置 中我们指定了传输协议 server 的值为 netty。 getTransporter() 通过SPI 获取到了默认的 Transporter实现 NettyTransporter,所以此处调用的是NettyTransporter#bind,其详细实现如下:

     	@Override
        public Server bind(URL url, ChannelHandler listener) throws RemotingException {
            return new NettyServer(url, listener);
        }
    

    而在 Dubbo2.7 中提供了Netty3.x 和Netty4.x版本的实现,实现上流程大体相同。
    在这里插入图片描述

    我们这里看的是Netty4版本 即 org.apache.dubbo.remoting.transport.netty4.NettyServer

    关于 NettyServer 内部的具体实现,篇幅所限,另开新篇:Dubbo笔记⑥ : 服务发布流程 - NettyServer

3. 序列化优化

DubboProtocol#optimizeSerialization 实现如下:

    private void optimizeSerialization(URL url) throws RpcException {
    	// 获取指定的序列化方案
        String className = url.getParameter(Constants.OPTIMIZER_KEY, "");
        if (StringUtils.isEmpty(className) || optimizers.contains(className)) {
            return;
        }

        try {
        	// 反射创建序列化类的实例,并检验其正确定
            Class clazz = Thread.currentThread().getContextClassLoader().loadClass(className);
            if (!SerializationOptimizer.class.isAssignableFrom(clazz)) {
                throw new RpcException("The serialization optimizer " + className + " isn't an instance of " + SerializationOptimizer.class.getName());
            }

            SerializationOptimizer optimizer = (SerializationOptimizer) clazz.newInstance();

            if (optimizer.getSerializableClasses() == null) {
                return;
            }

            for (Class c : optimizer.getSerializableClasses()) {
                SerializableClassRegistry.registerClass(c);
            }
			// 将指定的序列化类添加到序列化集合中。
            optimizers.add(className);
        } 
        ....
    }

四、InjvmProtocol#export

对于远程服务来说,提供者和调用者一般不在同一个环境中,通过网络进行调用,如下
在这里插入图片描述
除此之外,Dubbo还提供了一种本地服务暴露和引用的方式,在同一个JVM 进行中发布和调用同一个服务时,这种方式可以避免一次远程调用,而直接在JVM 内进行通信。
在这里插入图片描述

在之前我们讲到本地服务暴露使用的协议是 Injvm,Injvm 是一个伪协议,不开启端口,不发起远程调用,仅与Jvm 内直接关联。Dubbo笔记④ : 服务发布流程 - doExportUrlsFor1Protocol 中我们提到Dubbo 默认情况下是会同时暴露远程服务和本地服务的, 而在ServiceConfig#doExportUrlsFor1Protocol中通过exportLocal 方法对客 InjvmProtocol 参数进行配置并且进行了服务暴露,其代码如下:

	// ServiceConfig#exportLocal
    private void exportLocal(URL url) {
        if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
            URL local = URL.valueOf(url.toFullString())
            		// 设置协议为  injvm
                    .setProtocol(Constants.LOCAL_PROTOCOL)
                    // 设置host为127.0.0.1
                    .setHost(LOCALHOST)
                    // 设置端口为0
                    .setPort(0);
             // 进行服务暴露,该方法下面会讲解
            Exporter<?> exporter = protocol.export(
                    proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
            // 添加到暴露的服务集合中
            exporters.add(exporter);
            logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry");
        }
    }

本地服务的暴露过程相对来说比较简单。这里我们直接来看 InjvmProtocol#export 的实现:


    private static InjvmProtocol INSTANCE;

    public InjvmProtocol() {
        INSTANCE = this;
    }
    
	protected final Map<String, Exporter<?>> exporterMap = new ConcurrentHashMap<String, Exporter<?>>();
	    
    @Override
    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        return new InjvmExporter<T>(invoker, invoker.getUrl().getServiceKey(), exporterMap);
    }

这里我们需要注意两点 :

  1. 在 InjvmProtocol 的构造函数中,使用了静态变量 INSTANCE 来保存 来保存InjvmProtocol 自身实例。由于Dubbo的 SPI 会缓存每个扩展接口实现的Class对象,所以在整个JVM内对于每个扩展接口来说只会存在一个InjvmProtocol 实例,并且其中INSTANCE 保存的就是这个实例对象。

  2. 在 InjvmExporter 构造函数中将自身放入到了InjvmProtocol 管理的 exporterMap 中。即 exporterMap 中保存了已经暴露的本地服务。

        InjvmExporter(Invoker<T> invoker, String key, Map<String, Exporter<?>> exporterMap) {
            super(invoker);
            // 这里的key为服务keu,如:dubbo/com.kingfish.service.impl.DemoService:1.0.0
            this.key = key;
            this.exporterMap = exporterMap;
            exporterMap.put(key, this);
        }
    

综上,我们知道,在 JVM 中, InjvmProtocol 是单例的,InjvmProtocol#INSTANCE指向唯一实例,并且所有的本地暴露服务都被保存在 InjvmProtocol#exporterMap 中。

在消费者启动的时候,会通过 ReferenceConfig#createProxy 创建引用代理。其实现如下(这里我们暂时只关注本地服务引用的过程,远程服务的引用暂不关注):

	private static final Protocol refprotocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

    @SuppressWarnings({"unchecked", "rawtypes", "deprecation"})
    private T createProxy(Map<String, String> map) {
        URL tmpUrl = new URL("temp", "localhost", 0, map);
        // 判断是否是 本地引用
        final boolean isJvmRefer;
        // 如果没有指定
        if (isInjvm() == null) {
            if (url != null && url.length() > 0) { // if a url is specified, don't do local reference
                isJvmRefer = false;
            } else {
                // by default, reference local service if there is
                // 判断是否是本地引用,InjvmProtocol.getInjvmProtocol() 返回的就是 InjvmProtocol#INSTANCE实例。
                isJvmRefer = InjvmProtocol.getInjvmProtocol().isInjvmRefer(tmpUrl);
            }
        } else {
        	// 使用指定参数
            isJvmRefer = isInjvm();
        }
		// 如果是本地引用
        if (isJvmRefer) {
        	// 创建一个本地URL,其中host为127.0.0.1,port 为 0,协议为 injvm,这些参数与服务导出时的一致
            URL url = new URL(Constants.LOCAL_PROTOCOL, NetUtils.LOCALHOST, 0, interfaceClass.getName()).addParameters(map);
            // 由于此时url 的协议类型为 Injvm,refprotocol  通过 SPI机制获取到 InjvmProtocol,从而获取到InjvmInvoker
            invoker = refprotocol.refer(interfaceClass, url);
        } else {
          ... 
        }
        // create service proxy
        return (T) proxyFactory.getProxy(invoker);
    }

可以看到,如果是本地引用,则会创建一个本地URL,其中host为127.0.0.1,port 为 0,协议为 injvm,这些参数与服务导出时的一致。随后会调用 refprotocol.refer(interfaceClass, url); 方法来进行服务引用,这里我们来看一下 refprotocol.refer(interfaceClass, url); 的实现 InjvmProtocol#refer

    @Override
    public <T> Invoker<T> refer(Class<T> serviceType, URL url) throws RpcException {
    	// InjvmProtocol 将自身管理的 exporterMap 交由InjvmInvoker,而 exporterMap 中保存了本地暴露的服务,从而实现了本地服务的引用。
        return new InjvmInvoker<T>(serviceType, url, url.getServiceKey(), exporterMap);
    }

这里可以看到, InjvmProtocol#refer 将 exporterMap 传递给了 InjvmInvoker,在InjvmInvoker 调用服务的时候,便可以从 exporterMap 中获取到本地服务。(由于是本地引用,所以消费者和提供者公用同一个 JVM,所以提供者中的 InjvmProtocol 和消费者中的 InjvmProtocol 是同一个实例。)


以上:内容部分参考
《深度剖析Apache Dubbo 核心技术内幕》
https://dubbo.apache.org/zh/docs/v2.7/dev/source/export-service/
https://blog.csdn.net/jadebai/article/details/80684849
https://blog.csdn.net/luoyang_java/article/details/86613015
https://segmentfault.com/a/1190000017315041
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫吻鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值