当前位置: 首页 > 科技观察

Netty的基本动作——ChannelHandler的优秀实践

时间:2023-03-13 23:53:54 科技观察

今天,我们继续学习Netty逻辑架构的另一个核心组件ChannelHandler和ChannelPipeline。如果说线程模型是Netty的“核心内功”,那么ChannelHandler就是Netty最著名的“武功一招”,也是我们日常使用Netty时接触最多的组件。引用《Netty in action》的一句话,从应用开发者的角度来看,Netty的主要组件是ChannelHandler。因此,Amaru尽量通过图片和代码演示让大家得到最直观的体验。本文预计阅读时间10分钟左右,将重点关注以下问题:什么是ChannelHandler和ChannelPipeline?ChannelHandler的事件传播机制ChannelHandler的异常处理机制ChannelHandler的最佳实践处理逻辑的容器载体,用于处理Netty的输入输出数据。如数据格式转换、异常处理等。ChannelPipeline是ChannelHandler的容器载体,负责以链式形式调度每个注册的ChannelHandler。我们先回顾一下之前介绍的Netty逻辑架构,观察ChannelPipeline和ChannelHandler的位置。从局部放大,可以更清楚的看到ChannelPipeline和ChannelHandler的作用。如上图所示,当在EventLoop中监听到事件时,会处理I/O事件。而这个处理交给了ChannelPipeline,更严格的说是交给了ChannelPipeline中的各个ChannelHandler,按照一定的顺序进行处理。根据数据的流向,Netty将ChannelHandler分为两种,InboundHandler和OutboundHandler。如上图所示,Netty收到数据后,经过几个InboundHandlers的处理,成功接收。如果要输出数据,需要经过几个OutboundHandler进程后发送。例如,我们经常需要对接收到的数据进行解码,这些数据是在专门的decodeInboundHandler中进行处理的。如果要发送数据,往往需要对其进行编码,这是在一个专门的encodeOutBoundHandler中处理的。值得一提的是,虽然我们在使用Netty时直接与ChannelPipeline和ChannelHandler打交道,但是它们之间有一个“隐形”的桥梁,叫做ChannelHandlerContext。ChannelHanderContext顾名思义就是ChannelHandler的上下文,每个ChannelHandler对应一个ChannelHandlerContext。每个ChannelPipeline包含多个ChannelHandlerContext,所有的ChannelHandlerContext组成一个双向链表。如下所示。其中,有两个特殊的ChannelHandlerContext,分别是HeadContext和TailContext,分别代表双向链表的头节点和尾节点。从类图中可以看出,HeadContext同时实现了ChannelInboundHandler和ChannelOutboundHandler。因此,HeadContext在读取数据时作为头节点,向后传递InBound事件。同时作为写数据时的尾节点,处理最终的OutBound事件。TailContext只实现ChannelInboundHandler。它在InBound事件传递的最后,负责处理一些资源释放。在OutBound事件传递的第一个节点中,不做任何处理,只将OutBound事件传递给prev节点。我们自定义的ChannelHandler被插入到这两个头节点和尾节点之间。至此,我们对ChannelHandler和ChannelPipeline有了基本的了解。在实践中,我们如何正确使用ChannelHandler呢?要使用ChannelHandler,首先要了解ChannelHandler的事件传播机制和异常处理机制。2、ChannelHandler的事件传播机制。我们在Netty中提到了两种事件类型,Inbound事件和Outbound事件,分别对应InboundHandler和OutbountHandler。我们在使用Netty进行开发的时候,一定要了解Inbound事件和Outbound事件是如何在ChannelPipeline中进行“事件传播”的,以及InboundHandler和OutboundHandler的注册顺序有什么影响。话不多说,先来个demo来直观感受一下。自定义一个ChannelInboundHandler自定义一个ChannelOutboundHandler,简单组装EchoPipelineServer,特别注意6个handler的注册顺序。然后我们简单的通过命令行访问这个NettyServercurllocalhost:8081可以看到控制台输出如下这样事件传播的顺序就很清楚了:-对于Inbound事件,InboundHandler的处理顺序和注册顺序是一致的-对于Outbound事件,OutboundHandler的处理顺序与注册顺序相反。结合上一节提到的HeadContext和TailContext,我们画一张图更直观的看一下这个ChannelPipeline中的handler构建顺序是怎样的。在上面的ChannelInitializer中,我们根据需要添加了3个InboundHandlers和3个OutboundHandlers。因此,在头节点HeadContext和TailContext之间,有序的形成了一个双向链表。在InboundHandler3中,通过调用ctx.channel.writeAndFlush(msg)方法,消息从TailContext开始,按照OutboundHandler的路径传播到HeadContext。具体可以看DefaultChannelPipeline类中的实现。虽然这是一个双向链表,但无论是Inbound事件还是Outbound事件,在依次访问链表节点时,都会根据事件类型进行过滤。3、ChannelHandler的异常传播机制我们已经了解了ChannelPipeline的链式传递规则。如果双向链表中的任何处理程序抛出异常,我们应该如何处理?3.1InboundHandler异常处理我们修改下例中的TestInboudHandler进行模拟。channelRead方法抛出异常,重写exceptionCaught方法打印当前节点捕获异常,得到输出如下。InboundHander1中虽然抛出了异常,但还是会被三个InboundHandlers捕获一次,依次送往tail节点的Direction,抛出异常。我们还看到Netty给出了最后一个节点没有进行任何异常处理的警告。AnexceptionCaught()事件被触发,它到达了管道的尾部。这通常意味着管道中的最后一个处理程序没有处理异常。3.2OutboundHandler的异常处理OutboundHandler也是这样操作的吗?让我们做一个实验。写操作抛出异常重写exceptionCaught方法(该方法在OutboundHandler中被标记为废弃)重写组装channelPipeline,第二个OutboundHandler抛出异常的输出如下:Huh?异常被吃掉了!!不仅没有进入exceptionCaught方法,也没有抛出其他异常。只是后面的handler的write方法不再执行了,flush方法还是执行了。我们从源码中找出原因。顺着断点,马上找到原因:在AbstractChannelHandlerContext中,OutboundHandler的write方法异常被捕获,然后通知ChannelPromise。后续源码就不展开了,有兴趣的同学打断跟贴,看得更清楚。那么问题来了,如何在OutboundHandler中捕获异常呢?显然,直接添加ChannelPromise的回调。上面代码:在前面提到的ExceptionHandler中,只是重写了write方法,然后注册了一个ChannelPromiseListener。当然这个ExceptionHandler也必须注册到ChannelPipeline中。请注意!这里ExceptionHandler也是加在ChannelPipeline的tail方向的末尾,不是head方向。无论是inboundHandler异常还是outboundHandler异常,都按顺序传递给tail方向。异常是这样捕获的。4.ChannelHandler的最佳实践其实之前已经介绍过ChannelHandler的通用机制,这里简单介绍下两个最佳实践。4.1ChannelHandler中没有耗时处理这一点其实在之前的文章《 深入Netty逻辑架构,从Reactor线程模型开始》中已经提到了。这里作为自定义ChannelHandler的最佳实践,再次强调耗时的处理不是在ChannelHandler中做的。这里有两点。耗时操作不直接在I/O线程中处理。不要将耗时的操作放入EventLoop的任务队列中。由于Netty4的无锁序列化设计,一旦任何耗时操作阻塞了一个EventLoop,这个EventLoop上的每一个channel都会被阻塞。更详细的可以参考之前的文章《 深入Netty逻辑架构,从Reactor线程模型开始》。所以对于比较耗时的操作,我们需要放到自己的业务线程池中进行处理。如果我们需要发送响应,就需要将任务提交到EventLoop的任务队列中执行。给个简单的demo。4.2统一异常处理本文第三节讲解了ChannelHandler的异常传播机制。对于InboundHandler,如果你有handler-specific异常,可以直接在handler中执行exceptionCaught。对于一些常见的异常,可以自定义ExceptionHandler,注册到ChannelPipeline的末尾进行统一拦截。对于OutboudHandler,就是通过自定义ExceptionHandler重写相应的方法,注册ChannelPromise的Listener。同样,ExceptionHandler注册在ChannelPipeline的末尾,用于统一拦截。那么,综上所述,如何添加一个“统一”的异常拦截器呢?CustomExceptionHandler继承ChannelDuplexHandler,在尾节点(ChannelPipeline的最后一个节点)之前注册。对于Inbound事件,我们需要在exceptionCaught()中进行处理。对于Outbound事件,我们需要为OutboundHandler的不同方法(如write、flush)注册ChannelFutureListener事件。异常拦截器的注册位置应该是tail方向的最后一个Handler。注意,除了可以更优雅地处理常见的异常,统一的异常处理也是故障排除的好帮手。比如有时候编解码异常可以在统一处理异常处捕获,快速定位问题。5.让我们简单回顾一下总结。本文介绍什么是ChannelHandler和ChannelPipeline。您能说明什么是InboundChannelHandler、OutboundChannelHandler和ChannelHandlerContext吗?然后详细介绍了ChannelHandler的事件传播机制和异常处理机制。最后讲解ChannelHandler在日常开发中的最佳实践。我希望能有所帮助。