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

@@ -238,13 +238,13 @@ function hide_canvas() {
<h3>ChannelPipeline 内部结构</h3>
<p>首先我们要理清楚 ChannelPipeline 的内部结构是什么样子,这样才能理解 ChannelPipeline 的处理流程。ChannelPipeline 作为 Netty 的核心编排组件,负责调度各种类型的 ChannelHandler实际数据的加工处理操作则是由 ChannelHandler 完成的。</p>
<p>ChannelPipeline 可以看作是 ChannelHandler 的容器载体,它是由一组 ChannelHandler 实例组成的,内部通过双向链表将不同的 ChannelHandler 链接在一起,如下图所示。当有 I/O 读写事件触发时ChannelPipeline 会依次调用 ChannelHandler 列表对 Channel 的数据进行拦截和处理。</p>
<p><img src="assets/CgqCHl-dLiiAcORMAAYJnrq5ceE455.png" alt="image.png" /></p>
<p><img src="assets/CgqCHl-dLiiAcORMAAYJnrq5ceE455.png" alt="png" /></p>
<p>由上图可知,每个 Channel 会绑定一个 ChannelPipeline每一个 ChannelPipeline 都包含多个 ChannelHandlerContext所有 ChannelHandlerContext 之间组成了双向链表。又因为每个 ChannelHandler 都对应一个 ChannelHandlerContext所以实际上 ChannelPipeline 维护的是它与 ChannelHandlerContext 的关系。那么你可能会有疑问,为什么这里会多一层 ChannelHandlerContext 的封装呢?</p>
<p>其实这是一种比较常用的编程思想。ChannelHandlerContext 用于保存 ChannelHandler 上下文ChannelHandlerContext 则包含了 ChannelHandler 生命周期的所有事件,如 connect、bind、read、flush、write、close 等。可以试想一下,如果没有 ChannelHandlerContext 的这层封装,那么我们在做 ChannelHandler 之间传递的时候,前置后置的通用逻辑就要在每个 ChannelHandler 里都实现一份。这样虽然能解决问题,但是代码结构的耦合,会非常不优雅。</p>
<p>根据网络数据的流向ChannelPipeline 分为入站 ChannelInboundHandler 和出站 ChannelOutboundHandler 两种处理器。在客户端与服务端通信的过程中,数据从客户端发向服务端的过程叫出站,反之称为入站。数据先由一系列 InboundHandler 处理后入站,然后再由相反方向的 OutboundHandler 处理完成后出站,如下图所示。我们经常使用的解码器 Decoder 就是入站操作,编码器 Encoder 就是出站操作。服务端接收到客户端数据需要先经过 Decoder 入站处理后,再通过 Encoder 出站通知客户端。</p>
<p><img src="assets/Ciqc1F-dLm2APCjcAAPRZBy9s5c466.png" alt="image.png" /></p>
<p><img src="assets/Ciqc1F-dLm2APCjcAAPRZBy9s5c466.png" alt="png" /></p>
<p>接下来我们详细分析下 ChannelPipeline 双向链表的构造ChannelPipeline 的双向链表分别维护了 HeadContext 和 TailContext 的头尾节点。我们自定义的 ChannelHandler 会插入到 Head 和 Tail 之间,这两个节点在 Netty 中已经默认实现了,它们在 ChannelPipeline 中起到了至关重要的作用。首先我们看下 HeadContext 和 TailContext 的继承关系,如下图所示。</p>
<p><img src="assets/Ciqc1F-aW9qADWwSAAndrBdsXyc104.png" alt="image.png" /></p>
<p><img src="assets/Ciqc1F-aW9qADWwSAAndrBdsXyc104.png" alt="png" /></p>
<p>HeadContext 既是 Inbound 处理器,也是 Outbound 处理器。它分别实现了 ChannelInboundHandler 和 ChannelOutboundHandler。网络数据写入操作的入口就是由 HeadContext 节点完成的。HeadContext 作为 Pipeline 的头结点负责读取数据并开始传递 InBound 事件,当数据处理完成后,数据会反方向经过 Outbound 处理器,最终传递到 HeadContext所以 HeadContext 又是处理 Outbound 事件的最后一站。此外 HeadContext 在传递事件之前,还会执行一些前置操作。</p>
<p>TailContext 只实现了 ChannelInboundHandler 接口。它会在 ChannelInboundHandler 调用链路的最后一步执行,主要用于终止 Inbound 事件传播,例如释放 Message 数据资源等。TailContext 节点作为 OutBound 事件传播的第一站,仅仅是将 OutBound 事件传递给上一个节点。</p>
<p>从整个 ChannelPipeline 调用链路来看,如果由 Channel 直接触发事件传播,那么调用链路将贯穿整个 ChannelPipeline。然而也可以在其中某一个 ChannelHandlerContext 触发同样的方法,这样只会从当前的 ChannelHandler 开始执行事件传播,该过程不会从头贯穿到尾,在一定场景下,可以提高程序性能。</p>
@@ -295,7 +295,7 @@ function hide_canvas() {
</table>
<p><strong>2. ChannelOutboundHandler 的事件回调方法与触发时机。</strong></p>
<p>ChannelOutboundHandler 的事件回调方法非常清晰,直接通过 ChannelOutboundHandler 的接口列表可以看到每种操作所对应的回调方法,如下图所示。这里每个回调方法都是在相应操作执行之前触发,在此就不多做赘述了。此外 ChannelOutboundHandler 中绝大部分接口都包含ChannelPromise 参数,以便于在操作完成时能够及时获得通知。</p>
<p><img src="assets/CgqCHl-aW-2AJmXxAAVxQEbkD5w806.png" alt="image" /></p>
<p><img src="assets/CgqCHl-aW-2AJmXxAAVxQEbkD5w806.png" alt="png" /></p>
<h3>事件传播机制</h3>
<p>在上文中我们介绍了 ChannelPipeline 可分为入站 ChannelInboundHandler 和出站 ChannelOutboundHandler 两种处理器,与此对应传输的事件类型可以分为<strong>Inbound 事件</strong><strong>Outbound 事件</strong></p>
<p>我们通过一个代码示例,一起体验下 ChannelPipeline 的事件传播机制。</p>
@@ -342,9 +342,9 @@ public class SampleOutBoundHandler extends ChannelOutboundHandlerAdapter {
}
</code></pre>
<p>通过 Pipeline 的 addLast 方法分别添加了三个 InboundHandler 和 OutboundHandler添加顺序都是 A -&gt; B -&gt; C下图可以表示初始化后 ChannelPipeline 的内部结构。</p>
<p><img src="assets/CgqCHl-dLuOAPXJFAAJ3Qmmho38501.png" alt="image.png" /></p>
<p><img src="assets/CgqCHl-dLuOAPXJFAAJ3Qmmho38501.png" alt="png" /></p>
<p>当客户端向服务端发送请求时,会触发 SampleInBoundHandler 调用链的 channelRead 事件。经过 SampleInBoundHandler 调用链处理完成后,在 SampleInBoundHandlerC 中会调用 writeAndFlush 方法向客户端写回数据,此时会触发 SampleOutBoundHandler 调用链的 write 事件。最后我们看下代码示例的控制台输出:</p>
<p><img src="assets/CgqCHl-aW_yAKkKnAAWUaqNNpiI795.png" alt="image" /></p>
<p><img src="assets/CgqCHl-aW_yAKkKnAAWUaqNNpiI795.png" alt="png" /></p>
<p>由此可见Inbound 事件和 Outbound 事件的传播方向是不一样的。Inbound 事件的传播方向为 Head -&gt; Tail而 Outbound 事件传播方向是 Tail -&gt; Head两者恰恰相反。在 Netty 应用编程中一定要理清楚事件传播的顺序。推荐你在系统设计时模拟客户端和服务端的场景画出 ChannelPipeline 的内部结构图,以避免搞混调用关系。</p>
<h3>异常传播机制</h3>
<p>ChannelPipeline 事件传播的实现采用了经典的责任链模式,调用链路环环相扣。那么如果有一个节点处理逻辑异常会出现什么现象呢?我们通过修改 SampleInBoundHandler 的实现来模拟业务逻辑异常:</p>
@@ -372,7 +372,7 @@ public class SampleOutBoundHandler extends ChannelOutboundHandlerAdapter {
}
</code></pre>
<p>在 channelRead 事件处理中,第一个 A 节点就会抛出 RuntimeException。同时我们重写了 ChannelInboundHandlerAdapter 中的 exceptionCaught 方法,只是在开头加上了控制台输出,方便观察异常传播的行为。下面看一下代码运行的控制台输出结果:</p>
<p><img src="assets/Ciqc1F-aXAiAV52JABzDltoTrWE345.png" alt="image" /></p>
<p><img src="assets/Ciqc1F-aXAiAV52JABzDltoTrWE345.png" alt="png" /></p>
<p>由输出结果可以看出 ctx.fireExceptionCaugh 会将异常按顺序从 Head 节点传播到 Tail 节点。如果用户没有对异常进行拦截处理,最后将由 Tail 节点统一处理,在 TailContext 源码中可以找到具体实现:</p>
<pre><code>protected void onUnhandledInboundException(Throwable cause) {
try {
@@ -388,7 +388,7 @@ public class SampleOutBoundHandler extends ChannelOutboundHandlerAdapter {
<p>虽然 Netty 中 TailContext 提供了兜底的异常处理逻辑,但是在很多场景下,并不能满足我们的需求。假如你需要拦截指定的异常类型,并做出相应的异常处理,应该如何实现呢?我们接着往下看。</p>
<h3>异常处理的最佳实践</h3>
<p>在 Netty 应用开发的过程中,良好的异常处理机制会让排查问题的过程事半功倍。所以推荐用户对异常进行统一拦截,然后根据实际业务场景实现更加完善的异常处理机制。通过异常传播机制的学习,我们应该可以想到最好的方法是在 ChannelPipeline 自定义处理器的末端添加统一的异常处理器,此时 ChannelPipeline 的内部结构如下图所示。</p>
<p><img src="assets/Ciqc1F-dLz2AMj8yAALx2oNWK94344.png" alt="image.png" /></p>
<p><img src="assets/Ciqc1F-dLz2AMj8yAALx2oNWK94344.png" alt="png" /></p>
<p>用户自定义的异常处理器代码示例如下:</p>
<pre><code>public class ExceptionHandler extends ChannelDuplexHandler {
@Override
@@ -400,7 +400,7 @@ public class SampleOutBoundHandler extends ChannelOutboundHandlerAdapter {
}
</code></pre>
<p>加入统一的异常处理器后,可以看到异常已经被优雅地拦截并处理掉了。这也是 Netty 推荐的最佳异常处理实践。</p>
<p><img src="assets/CgqCHl-aXBCAS8QAAAWXhTFjQOE519.png" alt="image" /></p>
<p><img src="assets/CgqCHl-aXBCAS8QAAAWXhTFjQOE519.png" alt="png" /></p>
<h3>总结</h3>
<p>本节课我们深入分析了 Pipeline 的设计原理与事件传播机制。那么课程最初我提出的几个问题你是否已经都找到答案了?我来做个简单的总结:</p>
<ul>