Netty基础招式——ChannelHandler的最佳实践
本文是Netty系列第7篇
上一篇文章我们深入学习了Netty逻辑架构中的核心组件EventLoop和EventLoopGroup,掌握了Netty的线程模型,并且介绍了Netty4线程模型中的无锁串行化设计。
今天,我们继续学习Netty逻辑架构中的另一个核心组件ChannelHandler和ChannelPipeline。
如果说线程模型是Netty的 “核心内功”,那么ChannelHandler就是Netty最著名的 “武功招式”,是我们日常使用Netty时接触最多的组件。
引用《Netty in action》中的一句话
From the appliaction developer's standpoint, the primary component of Netty is the ChannelHandler.
所以,阿丸尽可能通过 图 和 代码demo,来让大家获得最直观的使用体验。
本文预计阅读时间约 10分钟,将重点围绕以下几个问题展开:
- 什么是ChannelHandler和ChannelPipeline?
- ChannelHandler的事件传播机制
- ChannelHandler的异常处理机制
- ChannelHandler的最佳实践
1. 什么是ChannelHandler和ChannelPipeline?
ChannelHandler是一个包含所有应用处理逻辑的容器载体,用来对Netty的输入输出数据进行加工处理。
比如数据格式转换、异常处理等
ChannelPipeline 则是 ChannelHandler 的容器载体,负责以链式的形式调度各个注册的ChannelHandler。
我们回顾下之前介绍过的Netty逻辑架构,观察下ChannelPipeline和ChannelHandler的位置。
再从局部放大,可以更加明确地看到ChannelPipeline和ChannelHandler的作用。
如上图所示,当EventLoop中监听到事件后,会对I/O事件进行处理。而这个处理,就是交给ChannelPipeline进行,更严格地说,是交给ChannelPipeline中的各个ChannelHandler按照一定的顺序进行处理。
根据数据的流向,Netty把ChannelHandler分为2类,InboundHandler和OutboundHandler。
如上图所示,Netty接收到数据后,经过若干 InboundHandler 处理后接收成功。如果要输出数据,就需要经过若干个 OutboundHandler 处理完成后发送。
比如,我们经常需要对接收到的数据进行解码,就是在某一个专门decode的InboundHandler中处理的。如果要发送数据,往往需要编码,就是在某一个专门encode的OutBoundHandler中处理的。
值得一提的是,虽然我们在使用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 的注册顺序。
然后我们通过命令行简单访问一下这个Netty Server
curl localhost:8081
可以看到控制台的如下输出
这样就清楚了事件传播顺序:
- 对于Inbound事件,InboundHandler的处理顺序是和注册顺序一致
- 对于Outbound事件,OutboundHandler的处理顺序和注册顺序相反
结合上一节说的HeadContext和TailContext,我们画个图来更直观地看一下这个ChannelPipeline中的handler构建顺序是怎样的。
在上面的ChannelInitializer中,我们按需添加了3个InboundHandler和3个OutboundHandler。所以,在头节点HeadContext和TailContext之间,有序构成了双向链表。
而InboundHandler3中,通过调用 ctx.channel.writeAndFlush( msg ) 方法,将消息从TailContext开始,依据OutboundHandler的路径向HeadContext方向传播出去。具体可以看下DefaultChannelPipeline类中的实现
虽然这里是双向链表,但是无论是Inbound事件还是Outbound事件,在按序访问链表节点时,会根据事件类型进行过滤。
3. ChannelHandler的异常传播机制
我们已经了解了ChannelPipeline的链式传递规则,如果双向链表中任意一个handler抛出了异常,那么应该怎么处理呢?
3.1 InboundHandler的异常处理
我们修改下示例中的TestInboudHandler进行模拟。
- channelRead方法中抛出异常
- 重写exceptionCaught方法,打印当前节点捕获异常情况
得到输出如下
可以看到,虽然在InboundHander1中抛出了异常,但是仍然会被3个InboundHandler都捕获一次,并按序向tail节点方向传递,然后抛出异常。
我们也看到了,Netty给出了会警告,在最后的节点没有进行异常处理。
An exceptionCaught() event was fired, and it reached at the tail of the pipeline.
It usually means the last handler in the pipeline did not handle the exception.
3.2 OutboundHandler的异常处理
OutboundHandler也是这么操作吗?
我们来做个实验。
- 在write操作中抛出异常
- 重写下exceptionCaught方法(这个方法在OutboundHandler中被标记为废弃)
重写组装下channelPipeline,第二个OutboundHandler中抛出异常
结果得到的输出如下
咦?异常被吃掉了!!
不仅没有走进exceptionCaught方法,也没有其他异常抛出。
只是对后续handler的write方法不再执行,而flush方法还是都执行了一遍。
我们从源码找找原因吧。跟一下断点,马上就找到了原因:
在
AbstractChannelHandlerContext中,对OutboundHandler的write方法做了异常捕获,然后对ChannelPromise进行了通知。
后续源码就不展开了,有兴趣的同学自己打断点跟一下,比较清楚。
那么问题来了,怎么在OutboundHandler中捕获异常呢?很明显就是直接添加ChannelPromise的回调。
上代码:
在前面提到的ExceptionHandler中,复写write方法,然后注册一个ChannelPromise的Listener就行了。
当然,这个ExceptionHandler同样要注册到ChannelPipeline。
千万注意!!这里ExceptionHandler同样是添加到ChannelPipeline的tail方向的最后,而不是添加在head方向。
无论是inboundHandler或者是outboundHandler的异常,都是按序向tail方向传递的。
异常就这样抓到了。
4. ChannelHandler的最佳实践
其实前面已经对ChannelHandler的常用机制做了介绍,这里简单再介绍下两个最佳实践。
4.1 不在ChannelHandler中做耗时处理
这一点其实在前一篇《 深入Netty逻辑架构,从Reactor线程模型开始》已经提到过,这里作为自定义ChannelHandler的最佳实践再强调一下,不在ChannelHandler中做耗时处理。
这里包括两点。
一是不在I/O线程中直接处理耗时操作。
二是也不把耗时操作放进EventLoop的任务队列中。
由于Netty4的无锁串行化设计,一旦任何耗时操作阻塞了某个EventLoop,那么这个EventLoop上的各个channel都会被阻塞。更详细内容可以参考上一篇《 深入Netty逻辑架构,从Reactor线程模型开始》。
所以,我们对于耗时操作,我们要放在自己的业务线程池中进行处理,如果需要发送response,需要提交任务到EventLoop的任务队列中执行。
给个简单的demo。
4.2 统一的异常处理
在本文的第三节中,讲解了ChannelHandler的异常传播机制。
对于InboundHandler来说,如果你有跟handler特定相关的异常,可以直接在handler里进行exceptionCaught。如果是一些通用的异常,可以自定义ExceptionHandler注册到ChannelPipeline的末尾进行统一拦截。
对于OutboudHandler来说,就是通过自定义ExceptionHandler,重写对应方法,并注册ChannelPromise的Listener。同样的,ExceptionHandler注册到ChannelPipeline的末尾进行统一拦截。
所以,总结下如何添加一个“统一”的异常拦截器呢?
- 自定义ExceptionHandler继承ChannelDuplexHandler,并注册到 tail节点前(ChannelPipeline的最后一个节点)
- 对于Inbound事件,我们需要在exceptionCaught()进行处理
- 对于Outbound事件,我们需要不同的ChannelFutureListener
异常拦截器的注册位置应该在tail方向的最后一个Handler。
注意,统一异常处理除了更优雅处理通用异常外,也是排查故障的好帮手。比如有时候对于编解码异常,可以在统一处理异常处捕获,快速定位问题。
5.小结
来简单回顾下吧。
本文介绍了什么是ChannelHandler和ChannelPipeline。能厘清InboundChannelHandler、OutboundChannelHandler、ChannelHandlerContext是什么吗?
然后对ChannelHandler的事件传播机制、异常处理机制做了详细介绍。
最后说明了日常开发中ChannelHandler的最佳实践。
希望对大家有所帮助。
参考书目:
《Netty in Action》
都看到最后了,原创不易,点个关注,点个赞吧~
文章持续更新,可以微信搜索「阿丸笔记 」第一时间阅读,回复【笔记】获取Canal、MySQL、HBase、JAVA实战笔记,回复【资料】获取一线大厂面试资料。
知识碎片重新梳理,构建Java知识图谱:github.com/saigu/JavaK…(历史文章查阅非常方便)
Netty基础招式——ChannelHandler的最佳实践的更多相关文章
- Netty常用招式——ChannelHandler与编解码
本文是Netty系列第8篇 上一篇文章我们深入学习了Netty逻辑架构中的核心组件ChannelHandler和ChannelPipeline,并介绍了它在日常开发使用中的最佳实践.文中也提到了,Ch ...
- Android学习之基础知识七—碎片的最佳实践
一.Android碎片(Fragment)的最佳实践——简易版新闻应用 第一步:新建FragmentBestPractice项目,在app/build.gradle当中添加:RecyclerView ...
- 结合异步模型,再次总结Netty多线程编码最佳实践
更多技术分享可关注我 前言 本文重点总结Netty多线程的一些编码最佳实践和注意事项,并且顺便对Netty的线程调度模型,和异步模型做了一个汇总.原文:结合异步模型,再次总结Netty多线程编码最 ...
- Scala 深入浅出实战经典 第65讲:Scala中隐式转换内幕揭秘、最佳实践及其在Spark中的应用源码解析
王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-87讲)完整视频.PPT.代码下载:百度云盘:http://pan.baidu.com/s/1c0noOt6 ...
- Vue.js最佳实践(五招让你成为Vue.js大师)
对大部分人来说,掌握Vue.js基本的几个API后就已经能够正常地开发前端网站.但如果你想更加高效地使用Vue来开发,成为Vue.js大师,那下面我要传授的这五招你一定得认真学习一下了. 第一招:化繁 ...
- Javascript模块化编程(一)模块的写法最佳实践六、输入全局变量 独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。 为了在模块内部调用全局变量,必须显式地将其他变量输入模块。
Javascript模块化编程,已经成为一个迫切的需求.理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块但是,Javascript不是一种模块化编程语言,它不支持类clas ...
- java-mybaits-015-mybatis逆向工程最佳实践【基础mybatis-generator、tk.mybatis、mubatis-plus】
一.概述 三款框架的功能对比 Mybatis-generator 通用Mapper Mybatis-Plus 代码生成器 支持自动生成Model,Mapper,Mapper XML文件 生成方式不够灵 ...
- Vue.js最佳实践(五招助你成为vuejs大师)
转自https://www.jb51.net/article/139448.htm 本文面向对象是有一定Vue.js编程经验的开发者.如果有人需要Vue.js入门系列的文章可以在评论区告诉我,有空就给 ...
- Python自动化运维 技术与最佳实践PDF高清完整版免费下载|百度云盘|Python基础教程免费电子书
点击获取提取码:7bl4 一.内容简介 <python自动化运维:技术与最佳实践>一书在中国运维领域将有"划时代"的重要意义:一方面,这是国内第一本从纵.深和实践角度探 ...
随机推荐
- VRRP简介以及配置案例
一.背景 二.VRRP 概念介绍 三.实验操作 一.背景 局域网中的用户终端通常采用配置一个默认网关的形式访问外部网络,如果此时默认网关设备发生故障,将中断所有用户终端的网络访问,这很可能会给用户带来 ...
- python读取json文件制作股票价格走势
- 『动善时』JMeter基础 — 51、使用JMeter测试WebService接口
目录 1.什么是WebService 2.WebService和SOAP的关系 3.什么是WSDL 4.测试WebService接口前的准备 (1)如何判断是WebService接口 (2)如何获取W ...
- 温故知新,CSharp遇见字符串比较(String Comparison),更佳科学的比较字符串
背景 在C#中,我们经常会遇到需要比较字符串的场景,有时候甚至因为外部输入的不确定性,我们需要忽略大小写来进行比较,以达到判断业务的述求. 对字符串用法的建议 使用.NET进行开发时,请遵循以下简要建 ...
- 【Azure API 管理】解决调用REST API操作APIM(API Management)需要认证问题(Authentication failed, The 'Authorization' header is missing)
问题描述 在通过REST API的方式来管理APIM资源,需要调用Azure提供的management接口.而这所有的接口,都是需要有Token并且还需要正确的Token.如若不然,就会获取到如下的错 ...
- 『动善时』JMeter基础 — 54、JMeter聚合报告详解
目录 1.聚合报告介绍 2.聚合报告界面详解 3.聚合报告中信息点说明 (1)百分位数的说明 (2)吞吐量说明 提示:聚合报告组件的使用和察看结果树组件的使用方式相同.本篇文章主要是详细的介绍一下聚合 ...
- Redis:Java链接redis单节点千万级别数据 写入,读取 性能测试
本文是对Redis 单节点,针对不同的数据类型,做插入行测试. 数据总条数为:10058624 环境说明: Redis 未做任何优化, 单节点 (服务器上, 内存64G) ...
- MySql:Navicat 连接不上虚拟机上的mysql容器
1.问题显示 通过windows主机navicat连接虚拟的mysql时报如下错误. 2.问题原因 由于navicat版本的问题,出现连接失败的原因:mysql8 之前的版本中加密规则是mysql_n ...
- 查看python的安装版本,位数及安装路径
一.想要查看ubuntu中安装的Python路径 方法一:whereis python (用来快速查找任何文件,是一个文件搜索命令,与locate的功能一样.执行whereis python 会将所有 ...
- SQL关联子查询
SQL关联子查询执行顺序: 1.先取到主查询中的相关数据,一次取一行主查询的数据 2.然后传入子查询,进行子查询 3.最后做主查询where筛选,注意子查询的where条件同样需要加在主查询后 参考: ...