This commit is contained in:
by931
2022-09-06 22:30:37 +08:00
parent 66970f3e38
commit 3d6528675a
796 changed files with 3382 additions and 3382 deletions

View File

@@ -291,17 +291,17 @@ function hide_canvas() {
<p id="tip" align="center"></p>
<div><h1>17 Dubbo Remoting 层核心接口分析:这居然是一套兼容所有 NIO 框架的设计?</h1>
<p>在本专栏的第二部分,我们深入介绍了 Dubbo 注册中心的相关实现,下面我们开始介绍 dubbo-remoting 模块,该模块提供了多种客户端和服务端通信的功能。在 Dubbo 的整体架构设计图中,我们可以看到最底层红色框选中的部分即为 Remoting 层,其中包括了 Exchange、Transport和Serialize 三个子层次。这里我们要介绍的 dubbo-remoting 模块主要对应 Exchange 和 Transport 两层。</p>
<p><img src="assets/CgqCHl9ptP2ADxEXAAuW94W_upc465.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CgqCHl9ptP2ADxEXAAuW94W_upc465.png" alt="png" /></p>
<p>Dubbo 整体架构设计图</p>
<p>Dubbo 并没有自己实现一套完整的网络库而是使用现有的、相对成熟的第三方网络库例如Netty、Mina 或是 Grizzly 等 NIO 框架。我们可以根据自己的实际场景和需求修改配置,选择底层使用的 NIO 框架。</p>
<p>下图展示了 dubbo-remoting 模块的结构,其中每个子模块对应一个第三方 NIO 框架例如dubbo-remoting-netty4 子模块使用 Netty4 实现 Dubbo 的远程通信dubbo-remoting-grizzly 子模块使用 Grizzly 实现 Dubbo 的远程通信。</p>
<p><img src="assets/Ciqc1F9ptRqAJLQnAABcIxQfCkc811.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Ciqc1F9ptRqAJLQnAABcIxQfCkc811.png" alt="png" /></p>
<p>其中的 dubbo-remoting-zookeeper我们在前面第 15 课时介绍基于 Zookeeper 的注册中心实现时已经讲解过了,它使用 Apache Curator 实现了与 Zookeeper 的交互。</p>
<h3>dubbo-remoting-api 模块</h3>
<p>需要注意的是,<strong>Dubbo 的 dubbo-remoting-api 是其他 dubbo-remoting-* 模块的顶层抽象,其他 dubbo-remoting 子模块都是依赖第三方 NIO 库实现 dubbo-remoting-api 模块的</strong>,依赖关系如下图所示:</p>
<p><img src="assets/CgqCHl9ptY2ADzl8AAEVDPN3HVo908.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CgqCHl9ptY2ADzl8AAEVDPN3HVo908.png" alt="png" /></p>
<p>我们先来看一下 dubbo-remoting-api 中对整个 Remoting 层的抽象dubbo-remoting-api 模块的结构如下图所示:</p>
<p><img src="assets/Ciqc1F9ptduASJsQAACrkCpgiGg477.png" alt="Drawing 3.png" /></p>
<p><img src="assets/Ciqc1F9ptduASJsQAACrkCpgiGg477.png" alt="png" /></p>
<p>一般情况下,我们会将功能类似或是相关联的类放到一个包中,所以我们需要先来了解 dubbo-remoting-api 模块中各个包的功能。</p>
<ul>
<li>buffer 包定义了缓冲区相关的接口、抽象类以及实现类。缓冲区在NIO框架中是一个不可或缺的角色在各个 NIO 框架中都有自己的缓冲区实现。这里的 buffer 包在更高的层面,抽象了各个 NIO 框架的缓冲区,同时也提供了一些基础实现。</li>
@@ -313,14 +313,14 @@ function hide_canvas() {
<h3>传输层核心接口</h3>
<p>在 Dubbo 中会抽象出一个“<strong>端点Endpoint</strong>”的概念,我们可以通过一个 ip 和 port 唯一确定一个端点,两个端点之间会创建 TCP 连接可以双向传输数据。Dubbo 将 Endpoint 之间的 TCP 连接抽象为<strong>通道Channel</strong>,将发起请求的 Endpoint 抽象为<strong>客户端Client</strong>,将接收请求的 Endpoint 抽象为<strong>服务端Server</strong>。这些抽象出来的概念,也是整个 dubbo-remoting-api 模块的基础,下面我们会逐个进行介绍。</p>
<p>Dubbo 中<strong>Endpoint 接口</strong>的定义如下:</p>
<p><img src="assets/CgqCHl9pteqACl0cAABxWeZ6ox0288.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CgqCHl9pteqACl0cAABxWeZ6ox0288.png" alt="png" /></p>
<p>如上图所示,这里的 get*() 方法是获得 Endpoint 本身的一些属性,其中包括获取 Endpoint 的本地地址、关联的 URL 信息以及底层 Channel 关联的 ChannelHandler。send() 方法负责数据发送,两个重载的区别在后面介绍 Endpoint 实现的时候我们再详细说明。最后两个 close() 方法的重载以及 startClose() 方法用于关闭底层 Channel isClosed() 方法用于检测底层 Channel 是否已关闭。</p>
<p>Channel 是对两个 Endpoint 连接的抽象,好比连接两个位置的传送带,两个 Endpoint 传输的消息就好比传送带上的货物,消息发送端会往 Channel 写入消息,而接收端会从 Channel 读取消息。这与第 10 课时介绍的 Netty 中的 Channel 基本一致。</p>
<p><img src="assets/CgqCHl9ptsaAeodMAACTIzdsI8g890.png" alt="Lark20200922-162359.png" /></p>
<p><img src="assets/CgqCHl9ptsaAeodMAACTIzdsI8g890.png" alt="png" /></p>
<p>下面是<strong>Channel 接口</strong>的定义,我们可以看出两点:一个是 Channel 接口继承了 Endpoint 接口,也具备开关状态以及发送数据的能力;另一个是可以在 Channel 上附加 KV 属性。</p>
<p><img src="assets/Ciqc1F9ptfKAeNrwAADvN7mxisw072.png" alt="Drawing 5.png" /></p>
<p><img src="assets/Ciqc1F9ptfKAeNrwAADvN7mxisw072.png" alt="png" /></p>
<p><strong>ChannelHandler 是注册在 Channel 上的消息处理器</strong>,在 Netty 中也有类似的抽象,相信你对此应该不会陌生。下图展示了 ChannelHandler 接口的定义,在 ChannelHandler 中可以处理 Channel 的连接建立以及连接断开事件,还可以处理读取到的数据、发送的数据以及捕获到的异常。从这些方法的命名可以看到,它们都是动词的过去式,说明相应事件已经发生过了。</p>
<p><img src="assets/CgqCHl9ptf-AM7HwAABIy1ahqFw153.png" alt="Drawing 6.png" /></p>
<p><img src="assets/CgqCHl9ptf-AM7HwAABIy1ahqFw153.png" alt="png" /></p>
<p>需要注意的是ChannelHandler 接口被 @SPI 注解修饰,表示该接口是一个扩展点。</p>
<p>在前面课时介绍 Netty 的时候,我们提到过有一类特殊的 ChannelHandler 专门负责实现编解码功能从而实现字节数据与有意义的消息之间的转换或是消息之间的相互转换。在dubbo-remoting-api 中也有相似的抽象,如下所示:</p>
<pre><code>@SPI
@@ -339,9 +339,9 @@ public interface Codec2 {
<p>这里需要关注的是 Codec2 接口被 @SPI 接口修饰了,表示该接口是一个扩展接口,同时其 encode() 方法和 decode() 方法都被 @Adaptive 注解修饰,也就会生成适配器类,其中会根据 URL 中的 codec 值确定具体的扩展实现类。</p>
<p>DecodeResult 这个枚举是在处理 TCP 传输时粘包和拆包使用的,之前简易版本 RPC 也处理过这种问题,例如,当前能读取到的数据不足以构成一个消息时,就会使用 NEED_MORE_INPUT 这个枚举。</p>
<p>接下来看<strong>Client 和 RemotingServer 两个接口</strong>,分别抽象了客户端和服务端,两者都继承了 Channel、Resetable 等接口,也就是说两者都具备了读写数据能力。</p>
<p><img src="assets/CgqCHl9ptgaAPRDbAAA7kgy1X5k082.png" alt="Drawing 7.png" /></p>
<p><img src="assets/CgqCHl9ptgaAPRDbAAA7kgy1X5k082.png" alt="png" /></p>
<p>Client 和 Server 本身都是 Endpoint只不过在语义上区分了请求和响应的职责两者都具备发送的能力所以都继承了 Endpoint 接口。Client 和 Server 的主要区别是 Client 只能关联一个 Channel而 Server 可以接收多个 Client 发起的 Channel 连接。所以在 RemotingServer 接口中定义了查询 Channel 的相关方法,如下图所示:</p>
<p><img src="assets/Ciqc1F9pthSAPWv0AAA0yX1lW-Y033.png" alt="Drawing 8.png" /></p>
<p><img src="assets/Ciqc1F9pthSAPWv0AAA0yX1lW-Y033.png" alt="png" /></p>
<p>Dubbo 在 Client 和 Server 之上又封装了一层<strong>Transporter 接口</strong>,其具体定义如下:</p>
<pre><code>@SPI(&quot;netty&quot;)
public interface Transporter {
@@ -355,10 +355,10 @@ public interface Transporter {
</code></pre>
<p>我们看到 Transporter 接口上有 @SPI 注解它是一个扩展接口默认使用“netty”这个扩展名@Adaptive 注解的出现表示动态生成适配器类会先后根据“server”“transporter”的值确定 RemotingServer 的扩展实现类先后根据“client”“transporter”的值确定 Client 接口的扩展实现。</p>
<p>Transporter 接口的实现有哪些呢?如下图所示,针对每个支持的 NIO 库,都有一个 Transporter 接口实现,散落在各个 dubbo-remoting-* 实现模块中。</p>
<p><img src="assets/CgqCHl9pthuAFNMOAABRJaJXls0493.png" alt="Drawing 9.png" /></p>
<p><img src="assets/CgqCHl9pthuAFNMOAABRJaJXls0493.png" alt="png" /></p>
<p>这些 Transporter 接口实现返回的 Client 和 RemotingServer 具体是什么呢?如下图所示,返回的是 NIO 库对应的 RemotingServer 实现和 Client 实现。</p>
<p><img src="assets/Ciqc1F9ptiCAHkUSAADCSKg5KhY994.png" alt="Drawing 10.png" />
<img src="assets/CgqCHl9pti-AHj3DAACwPfuEgm8435.png" alt="Drawing 11.png" /></p>
<p><img src="assets/Ciqc1F9ptiCAHkUSAADCSKg5KhY994.png" alt="png" />
<img src="assets/CgqCHl9pti-AHj3DAACwPfuEgm8435.png" alt="png" /></p>
<p>相信看到这里,你应该已经发现 Transporter 这一层抽象出来的接口,与 Netty 的核心接口是非常相似的。那为什么要单独抽象出 Transporter层而不是像简易版 RPC 框架那样,直接让上层使用 Netty 呢?</p>
<p>其实这个问题的答案也呼之欲出了Netty、Mina、Grizzly 这个 NIO 库对外接口和使用方式不一样,如果在上层直接依赖了 Netty 或是 Grizzly就依赖了具体的 NIO 库实现,而不是依赖一个有传输能力的抽象,后续要切换实现的话,就需要修改依赖和接入的相关代码,非常容易改出 Bug。这也不符合设计模式中的开放-封闭原则。</p>
<p>有了 Transporter 层之后,我们可以通过 Dubbo SPI 修改使用的具体 Transporter 扩展实现,从而切换到不同的 Client 和 RemotingServer 实现,达到底层 NIO 库切换的目的,而且无须修改任何代码。即使有更先进的 NIO 库出现,我们也只需要开发相应的 dubbo-remoting-* 实现模块提供 Transporter、Client、RemotingServer 等核心接口的实现,即可接入,完全符合开放-封闭原则。</p>
@@ -404,7 +404,7 @@ public interface Transporter {
<li>无论是 Client 还是 RemotingServer都会使用 ChannelHandler 处理 Channel 中传输的数据,其中负责编解码的 ChannelHandler 被抽象出为 Codec2 接口。</li>
</ul>
<p>整个架构如下图所示,与 Netty 的架构非常类似。</p>
<p><img src="assets/CgqCHl9ptlyABsjpAAGGk7pFIzQ293.png" alt="Lark20200922-162354.png" /></p>
<p><img src="assets/CgqCHl9ptlyABsjpAAGGk7pFIzQ293.png" alt="png" /></p>
<p>Transporter 层整体结构图</p>
<h3>总结</h3>
<p>本课时我们首先介绍了 dubbo-remoting 模块在 Dubbo 架构中的位置,以及 dubbo-remoting 模块的结构。接下来分析了 dubbo-remoting 模块中各个子模块之间的依赖关系,并重点介绍了 dubbo-remoting-api 子模块中各个包的核心功能。最后我们还深入分析了整个 Transport 层的核心接口,以及这些接口抽象出来的 Transporter 架构。</p>