美文网首页
Dubbo——Remoting 层核心接口分析

Dubbo——Remoting 层核心接口分析

作者: 小波同学 | 来源:发表于2021-04-17 23:53 被阅读0次

    前言

    dubbo-remoting 模块提供了多种客户端和服务端通信的功能。在 Dubbo 的整体架构设计图中,我们可以看到最底层红色框选中的部分即为 Remoting 层,其中包括了 Exchange、Transport和Serialize 三个子层次。这里我们要介绍的 dubbo-remoting 模块主要对应 Exchange 和 Transport 两层。

    Dubbo 并没有自己实现一套完整的网络库,而是使用现有的、相对成熟的第三方网络库,例如,Netty、Mina 或是 Grizzly 等 NIO 框架。我们可以根据自己的实际场景和需求修改配置,选择底层使用的 NIO 框架。

    下图展示了 dubbo-remoting 模块的结构,其中每个子模块对应一个第三方 NIO 框架,例如,dubbo-remoting-netty4 子模块使用 Netty4 实现 Dubbo 的远程通信,dubbo-remoting-grizzly 子模块使用 Grizzly 实现 Dubbo 的远程通信。

    dubbo-remoting-zookeeper使用 Apache Curator 实现了与 Zookeeper 的交互。

    dubbo-remoting-api 模块

    Dubbo 的 dubbo-remoting-api 是其他 dubbo-remoting-* 模块的顶层抽象,其他 dubbo-remoting 子模块都是依赖第三方 NIO 库实现 dubbo-remoting-api 模块的,依赖关系如下图所示:

    先来看一下 dubbo-remoting-api 中对整个 Remoting 层的抽象,dubbo-remoting-api 模块的结构如下图所示:


    一般情况下,我们会将功能类似或是相关联的类放到一个包中,所以我们需要先来了解 dubbo-remoting-api 模块中各个包的功能。

    • buffer 包:定义了缓冲区相关的接口、抽象类以及实现类。缓冲区在NIO框架中是一个不可或缺的角色,在各个 NIO 框架中都有自己的缓冲区实现。这里的 buffer 包在更高的层面,抽象了各个 NIO 框架的缓冲区,同时也提供了一些基础实现。

    • exchange 包:抽象了 Request 和 Response 两个概念,并为其添加很多特性。这是整个远程调用非常核心的部分。

    • transport 包:对网络传输层的抽象,但它只负责抽象单向消息的传输,即请求消息由 Client 端发出,Server 端接收;响应消息由 Server 端发出,Client端接收。有很多网络库可以实现网络传输的功能,例如 Netty、Grizzly 等, transport 包是在这些网络库上层的一层抽象。

    • 其他接口:Endpoint、Channel、Transporter、Dispatcher 等顶层接口放到了org.apache.dubbo.remoting 这个包,这些接口是 Dubbo Remoting 的核心接口。

    下面我们就来介绍 Dubbo 是如何抽象这些核心接口的。

    传输层核心接口

    在 Dubbo 中会抽象出一个“端点(Endpoint)”的概念,可以通过一个 ip 和 port 唯一确定一个端点,两个端点之间会创建 TCP 连接,可以双向传输数据。Dubbo 将 Endpoint 之间的 TCP 连接抽象为通道(Channel),将发起请求的 Endpoint 抽象为客户端(Client),将接收请求的 Endpoint 抽象为服务端(Server)。这些抽象出来的概念,也是整个 dubbo-remoting-api 模块的基础。

    Endpoint 接口

    • getXXX():用于获得 Endpoint 本身的一些属性,如Endpoint 的本地地址、关联的 URL 信息以及底层 Channel 关联的 ChannelHandler。
    • send():负责数据发送。
    • close():及 startClose() 用于关闭底层 Channel。
    • isClosed():方法用于检测底层 Channel 是否已关闭。

    Channel

    对 Endpoint 双方连接的抽象,就像传输管道。消息发送端往 Channel 写入消息,接收端从 Channel 读取消息。

    接口的定义:

    • 继承 Endpoint 接口,也具备开关状态以及发送数据能力。
    • 可在 Channel 上附加 KV 属性。

    ChannelHandler

    ChannelHandler 是注册在 Channel 上的消息处理器,在 Netty 中也有类似的抽象。

    在 ChannelHandler 中可以处理 Channel 的连接建立以及连接断开事件,还可以处理读取到的数据、发送的数据以及捕获到的异常。


    需要注意的是:ChannelHandler 接口被 @SPI 注解修饰,表示该接口是一个扩展点。

    @SPI
    public interface ChannelHandler {
    
        /**
         * on channel connected.
         *
         * @param channel channel.
         */
        void connected(Channel channel) throws RemotingException;
    
        /**
         * on channel disconnected.
         *
         * @param channel channel.
         */
        void disconnected(Channel channel) throws RemotingException;
    
        /**
         * on message sent.
         *
         * @param channel channel.
         * @param message message.
         */
        void sent(Channel channel, Object message) throws RemotingException;
    
        /**
         * on message received.
         *
         * @param channel channel.
         * @param message message.
         */
        void received(Channel channel, Object message) throws RemotingException;
    
        /**
         * on exception caught.
         *
         * @param channel   channel.
         * @param exception exception.
         */
        void caught(Channel channel, Throwable exception) throws RemotingException;
    
    }
    

    Netty 中有一类特殊的 ChannelHandler 专门负责实现编解码功能,从而实现字节数据与有意义的消息之间的转换,或是消息之间的相互转换。在dubbo-remoting-api 中也有相似的抽象,如下所示:

    @SPI
    public interface Codec2 {
        @Adaptive({Constants.CODEC_KEY})
        void encode(Channel channel, ChannelBuffer buffer, Object message) 
            throws IOException;
        @Adaptive({Constants.CODEC_KEY})
        Object decode(Channel channel, ChannelBuffer buffer)
            throws IOException;
            
        enum DecodeResult {
            NEED_MORE_INPUT, SKIP_SOME_INPUT
        }
    }
    

    这里需要关注的是 Codec2 接口被 @SPI 接口修饰了,表示该接口是一个扩展接口,同时其 encode() 方法和 decode() 方法都被 @Adaptive 注解修饰,也就会生成适配器类,其中会根据 URL 中的 codec 值确定具体的扩展实现类。

    DecodeResult 这个枚举是在处理 TCP 传输时粘包和拆包使用的,之前简易版本 RPC 也处理过这种问题,例如,当前能读取到的数据不足以构成一个消息时,就会使用 NEED_MORE_INPUT 这个枚举。

    接下来看Client 和 RemotingServer 两个接口,分别抽象了客户端和服务端,两者都继承了 Channel、Resetable 等接口,也就是说两者都具备了读写数据能力。


    Client 和 Server 本身都是 Endpoint,只不过在语义上区分了请求和响应的职责,两者都具备发送的能力,所以都继承了 Endpoint 接口。Client 和 Server 的主要区别是 Client 只能关联一个 Channel,而 Server 可以接收多个 Client 发起的 Channel 连接。所以在 RemotingServer 接口中定义了查询 Channel 的相关方法,如下图所示:

    Dubbo 在 Client 和 Server 之上又封装了一层Transporter 接口,其具体定义如下:

    @SPI("netty")
    public interface Transporter {
    
        @Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY})
        RemotingServer bind(URL url, ChannelHandler handler) throws RemotingException;
    
        @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
        Client connect(URL url, ChannelHandler handler) throws RemotingException;
    
    }
    

    我们看到 Transporter 接口上有 @SPI 注解,它是一个扩展接口,默认使用“netty”这个扩展名,@Adaptive 注解的出现表示动态生成适配器类,会先后根据“server”“transporter”的值确定 RemotingServer 的扩展实现类,先后根据“client”“transporter”的值确定 Client 接口的扩展实现。

    Transporter 接口的实现有哪些呢?如下图所示,针对每个支持的 NIO 库,都有一个 Transporter 接口实现,散落在各个 dubbo-remoting-* 实现模块中。


    这些 Transporter 接口实现返回的 Client 和 RemotingServer 具体是什么呢?如下图所示,返回的是 NIO 库对应的 RemotingServer 实现和 Client 实现。


    Netty、Mina、Grizzly 这个 NIO 库对外接口和使用方式不一样,如果在上层直接依赖了 Netty 或是 Grizzly,就依赖了具体的 NIO 库实现,而不是依赖一个有传输能力的抽象,后续要切换实现的话,就需要修改依赖和接入的相关代码,非常容易改出 Bug。这也不符合设计模式中的开放-封闭原则。

    有了 Transporter 层之后,我们可以通过 Dubbo SPI 修改使用的具体 Transporter 扩展实现,从而切换到不同的 Client 和 RemotingServer 实现,达到底层 NIO 库切换的目的,而且无须修改任何代码。即使有更先进的 NIO 库出现,我们也只需要开发相应的 dubbo-remoting-* 实现模块提供 Transporter、Client、RemotingServer 等核心接口的实现,即可接入,完全符合开放-封闭原则。

    在最后,我们还要看一个类——Transporters,它不是一个接口,而是门面类,其中封装了 Transporter 对象的创建(通过 Dubbo SPI)以及 ChannelHandler 的处理,如下所示:

    public class Transporters {
        private Transporters() {
        // 省略bind()和connect()方法的重载
        public static RemotingServer bind(URL url, 
                ChannelHandler... handlers) throws RemotingException {
            ChannelHandler handler;
            if (handlers.length == 1) {
                handler = handlers[0];
            } else {
                handler = new ChannelHandlerDispatcher(handlers);
            }
            return getTransporter().bind(url, handler);
        }
        public static Client connect(URL url, ChannelHandler... handlers)
               throws RemotingException {
            ChannelHandler handler;
            if (handlers == null || handlers.length == 0) {
                handler = new ChannelHandlerAdapter();
            } else if (handlers.length == 1) {
                handler = handlers[0];
            } else { // ChannelHandlerDispatcher
                handler = new ChannelHandlerDispatcher(handlers);
            }
            return getTransporter().connect(url, handler);
        }
        public static Transporter getTransporter() {
            // 自动生成Transporter适配器并加载
            return ExtensionLoader.getExtensionLoader(Transporter.class)
                .getAdaptiveExtension();
        }
    }
    

    在创建 Client 和 RemotingServer 的时候,可以指定多个 ChannelHandler 绑定到 Channel 来处理其中传输的数据。Transporters.connect() 方法和 bind() 方法中,会将多个 ChannelHandler 封装成一个 ChannelHandlerDispatcher 对象。

    ChannelHandlerDispatcher 也是 ChannelHandler 接口的实现类之一,维护了一个 CopyOnWriteArraySet 集合,它所有的 ChannelHandler 接口实现都会调用其中每个 ChannelHandler 元素的相应方法。另外,ChannelHandlerDispatcher 还提供了增删该 ChannelHandler 集合的相关方法。

    到此为止,Dubbo Transport 层的核心接口就介绍完了,这里简单总结一下:

    • Endpoint 接口抽象了“端点”的概念,这是所有抽象接口的基础。

    • 上层使用方会通过 Transporters 门面类获取到 Transporter 的具体扩展实现,然后通过 Transporter 拿到相应的 Client 和 RemotingServer 实现,就可以建立(或接收)Channel 与远端进行交互了。

    • 无论是 Client 还是 RemotingServer,都会使用 ChannelHandler 处理 Channel 中传输的数据,其中负责编解码的 ChannelHandler 被抽象出为 Codec2 接口。

    Transporter 层整体结构图


    Transporter 层整体结构图

    参考:
    https://javaedge.blog.csdn.net/article/details/109080278

    相关文章

      网友评论

          本文标题:Dubbo——Remoting 层核心接口分析

          本文链接:https://www.haomeiwen.com/subject/totmlltx.html