Netty之ChannelHandler
一、概述
handler是控制socket io的各个生命周期的业务实现,netty实现了很多种协议所以有很多handler类,这儿主要关注Handler的设计、作用以及使用方法。
二、Channel
Channel与JDK中的Channel作用相当,是对I/O操作的封装,比如read(),write(),connect(),close()和bind()等,是Netty中核心对象。我们经常使用的NioServerSocketChannel,NioSocketChannel就是其具体实现。Channel的状态改变都会触发相应事件传递到Pipeline中,被ChannelHandler处理,下面为Channel经常触发的事件:
事件 | 说明 |
---|---|
ChannelRegistered | 这里的注册强调的是Channel与EventLoop是否关联,满足下列条件才能称为registered:**1. 将相关socket注册到selector上 ;2. Channel与EventLoop关联起来 ** |
ChannelActive | 满足下列两个条件,就是active了:1. channel 完成了注册;2. channel 绑定到了本地端口(对NioServerSocketChannel而言)或连接到了远程主机(对NioSocketChannel而言) |
三、ChannelHandler
ChannelHandler 是处理数据的核心对象,其对源端Channel触发的一系列事件进行处理。Netty提供了两个非常重要的子接口:
1、ChannelInboundHandler
ChannelInboundHandler方法如下图所示,从方法名可看出,其是对Channel触发事件的处理。
channelRegistered
在channel注册到线程池的时候会被触发。
channelUnregistered
在channel关闭的时候触发。
channelActive
registered完成之后且channel处理active状态,首次注册状态。主要见AbstractChannel的register0(promise)方法。
channelnactive
unregistered之前执行,主要见AbstractChannel的deregister方法。
channelRead
这个主要见pipeline的fireChannelRead方法,其被channel在获取到数据的阶段进行调用,进而触发到handler的channelRead方法。
channelReadComplete
这个和上面read方法一样,fireChannelReadComplete方法,被channel运行过程中read过程完成会进行调用。
userEventTriggered
这个也是由pipeline提供的方法作为入口fireUserEventTriggered,这个就是触发一个事件了,以IdleStateHandler为例,其一般作为心跳检测事件,放入线程池执行,判断空闲就会触发该方法,传导到各个handler。
channelWritabilityChanged
这个入口也在pipeline,但是好像没有怎么用到,channel并没有调用这个方法,一般也没怎么用该方法。
exceptionCaught
这个入口同样在pipeline,被channel的读取过程抛出异常时触发,当然不只这一个地方。
上述方法追根溯源都是通过pipeline来触发整个执行链的,后面讲到context时会详细说明一下这个过程。只需要知道入口在这个地方即可。
2、ChannelOutboundHandler
ChannelOutboundHandler的方法如下图所示,都是与底层网络交互的;其实这些方法最后都会转调到Channel中定义的I/O方法上去,因为只有Channel才是与网络进行交互的。
bind :绑定端口的操作
connect:连接远程地址>
disconnect:断开连接
close:端口绑定的端口
deregister:取消注册到线程池中
read:设置监听读取事件
write:写入数据的操作
flush:刷新缓冲区,立刻发送。
上述接口都是和连接相关的处理,而且这些都是通过pipeline的相关方法触发的,最终调用的是tail对象。实际上这个handler比较鸡肋,原因channel那节其实已经说明过,大部分实现类都做不了什么,最终都交给headContext对象调用unsafe相关方法由其处理了。大部分handler只对write方法进行了处理,其他都直接交给其他的handler处理了。我印象中之前Netty5好像要取消in和out的区别,outhandler太鸡肋了(Netty5已经放弃开发,官方的说法是forkjoin框架开发很复杂,还没准备好开发)。
2.1 理解Inbound和Outbound:
Inbound: 数据或事件由外向内流,比如Channel注册成功触发的注册事件;Channel读好数据后的ChannelRead事件等;或者说是与网络交互完成,接下来要由Netty处理;
Outbound: 数据或事件由内向外流,比如Netty处理好数据后要write到网络;去网络方向read数据(read这一事件从内向外,read好数据后就是由外向内);或者说需要与网络进行交互的操作都称为Outbound;
3、ChannelHandler与其他类之间的关系
这个图可以看到ChannelHandler的基本结构,其有两个子类接口,ChannelInboundHandler和ChannelOutboundHandler。这也就是我之前常常提到的handler分成in和out类型,针对不同操作的方法。
3.1 handlerAdded
handler被添加到channel的时候执行,这个动作就是由pipeline的添加handler方法完成的。对于服务端,在客户端连接进来的时候,就通过ServerBootstrapAcceptor的read方法,为每一个channel添加了handler。该方法对于handler而言是第一个触发的方法。
3.2 handlerRemoved
handler被移除channel的时候执行,这个动作在pipeline中也存在,基本上是在channel断开的时候会进行这一步。
3.3 exceptionCaught
捕获channel生命周期中异常处理的类,该方法被移动到ChannelInboundHandler中了。已经废弃。
注解Sharable表明该handler可以被多个pipeline重复使用。
四、ChannelHandlerContext
ChannelHandlerContext是ChannelHandler相互之间以及ChannelHandler与ChannelPipeline之间交互的桥梁。当创建一个Channel时,也会创建相应的ChannelHandlerContext。ChannelHandlerContext另一个重要的作用是在Pipeline中传播事件,一般和pipeline联合使用,管理handler链。handler本身没有持有顺序关系,都是通过handlerContext完成的。handlerContext自身会通过配合handler造成顺着构成的链式顺序调用下去,这里会仔细说明一下这个过程。先看中间的方法:
接口方法就不一一介绍了,看到的都是一些熟悉的方法,针对业务的起始调用fireXXX(),其它的方法就不进行介绍了。handlerContext和pipeline一样实现类很少,基本使用的就是DefaultChannelHandlerContext,其继承自AbstractChannelHandlerContext,大部分方法也都是在抽象父类中。下面具体介绍一下是如何工作的:
还是回到pipeline的构造handler链的过程:
protected DefaultChannelPipeline(Channel channel) {
this.channel = ObjectUtil.checkNotNull(channel, "channel");
succeededFuture = new SucceededChannelFuture(channel, null);
voidPromise = new VoidChannelPromise(channel, true);
tail = new TailContext(this);
head = new HeadContext(this);
head.next = tail;
tail.prev = head;
}
pipeline初始化的时候构建了tail和head两个handler,这两个是特殊的,位置不能替换的。
public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
final AbstractChannelHandlerContext newCtx;
synchronized (this) {
checkMultiplicity(handler);
newCtx = newContext(group, filterName(name, handler), handler);
addLast0(newCtx);
// If the registered is false it means that the channel was not registered on an eventloop yet.
// In this case we add the context to the pipeline and add a task that will call
// ChannelHandler.handlerAdded(...) once the channel is registered.
if (!registered) {
newCtx.setAddPending();
callHandlerCallbackLater(newCtx, true);
return this;
}
EventExecutor executor = newCtx.executor();
if (!executor.inEventLoop()) {
newCtx.setAddPending();
executor.execute(new Runnable() {
@Override
public void run() {
callHandlerAdded0(newCtx);
}
});
return this;
}
}
callHandlerAdded0(newCtx);
return this;
}
private void addLast0(AbstractChannelHandlerContext newCtx) {
AbstractChannelHandlerContext prev = tail.prev;
newCtx.prev = prev;
newCtx.next = tail;
prev.next = newCtx;
tail.prev = newCtx;
}
先判断这个handler有没有被其他pipeline加载过,是否是sharable类型,进行相关设置。通过handler创建context。然后插入到tail前面,后面就是添加handler之后触发的方法,触发handlerAdd方法。通过addLast0方法,可以看见context中的参数prev和next被初始化了。也就是说通过当前的context能够找到前一个和后一个context了。下面抽出handler接口的其中一个方法,我们来研究一下handler链是怎么执行的。
public ChannelHandlerContext fireChannelActive() {
invokeChannelActive(findContextInbound());
return this;
}
static void invokeChannelActive(final AbstractChannelHandlerContext next) {
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
next.invokeChannelActive();
} else {
executor.execute(new Runnable() {
@Override
public void run() {
next.invokeChannelActive();
}
});
}
}
private void invokeChannelActive() {
if (invokeHandler()) {
try {
((ChannelInboundHandler) handler()).channelActive(this);
} catch (Throwable t) {
notifyHandlerException(t);
}
} else {
fireChannelActive();
}
}
private AbstractChannelHandlerContext findContextInbound() {
AbstractChannelHandlerContext ctx = this;
do {
ctx = ctx.next;
} while (!ctx.inbound);
return ctx;
}
handler的任何一个方法,在context中都是由三个这样的方法来构成链式执行的。static方法都是给pipeline调用的,其入参就是head,handler链的头结点。调用了invokeChannelActive()方法,这里就是我们的handler方法相关内容被触发了。乍一看好像执行完了就没了啊,这个地方要接下去就需要handler配合了。随便找一个handler,比如ChannelInboundHandlerAdapter,其相关方法都调用了ctx.fireChannelActive(); 这里就接上了,ctx的fireChannelActive不就是回到了invokeChannelActive()方法了。其入参就不再是当前的context,而是通过findContextInbound来找到下一个in类型的handler,这也说明这个接口是针对in类型的接口。所以一个基本的循环就是ctx.invokeChannelActive(ctx) ->ctx.invokeChannelActive() -> hander.channelActive()->ctx.fireChannelActive(ctx)->findContextInbound()->ctx.invokeChannelActive(ctx)。这么个循环链,起点就是pipeline的invokeChannelActive(head),终点就是handler.channelActive()没有调用ctx.channel的时候,最后的tailContext就是没有执行任何操作,所以执行到这链路就断了。
五、ChannelPipeline
ChannelPipeline是一系列ChannelHandler的组合,与Channel相关的所有数据和事件都会流经相应的ChannelPipeline进行处理。ChannelPipeline也可以触发事件传播,与ChannelHandlerContext触发不同的是,ChannelPipeline触发的事件都是从第一个ChannelHandler开始处理,而ChannelHandlerContext触发的事件总是从下一个ChannelHandlerContext开始处理
Pipeline的事件流
当事件流在ChannelPipeline中流动时,可以调用ChannelHandlerContext或ChannelPipeline的Outbound方法改变事件的流向
在程序中,要手动触发Inbound到Outbound方向的转变;举个例子,比如我们定义了若干个InboundHandler,处理了用户数据后,返回给用户一个Success,那么必须在某个InboundHandler里调用ChannelHandlerContext.write()方法让数据流改变方向,否则,永远都无法给用户返回;若不手动改变方向,输入数据最终会走到TailContext(默认最后一个InboundHandler)中,而这个Handler几乎什么都没做,那么OutBound方法永远都等不到被调用
六、事件流
无论是NioSocketChannel还是NioServerSocketChannel,Channel的事件都是按照下图所示流转。
七、总结
本节对handler的一个基本内容进行了说明,主要讲解了handlerContext的执行过程,明确了调用链的入口在于pipeline,通过pipeline和context的控制,保证链的执行。channel的生命周期控制pipeline就能控制整个handler的执行。下图给个具体说明:
八、站在巨人的肩膀上
Netty之ChannelHandler的更多相关文章
- Netty的ChannelHandler,ChannelHandlerContext,ChannelPipeline
本小节一起学习一下ChannelHandler,ChannelHandlerContext,ChannelPipeline这三个Netty常用的组件,不探究它们的底层源码,我们就简单的分析一下用法 首 ...
- 【Netty】ChannelHandler和ChannelPipeline
一.前言 前面学习了Netty的ByteBuf,接着学习ChannelHandler和ChannelPipeline. 二.ChannelHandler和ChannelPipeline 2.1 Cha ...
- 【Netty】ChannelHandler和codec
一.前言 前面学习了Netty的codec框架,下面接着学习ChannelHandler与codec之间的关联. 二.ChannelHandler和codec Netty为不同的协议提供了处理器和编解 ...
- Netty之ChannelHandler(三)
ChannelHandler是netty中的核心处理部分,我们使用netty的绝大部分代码都写在这部分,所以了解它的一些机制和特性是很有必要的. 一.Channel Channel接口抽象了底层soc ...
- Netty中ChannelHandler的生命周期
在使用Netty进行网络编程的时候,通常需要在网络连接的不同阶段进行相应的操作,比如在连接建立时,客户端向服务端发起认证,在接收到数据时对数据内容进行解析等等.那么,连接的不同阶段在netty中如何表 ...
- netty源码解解析(4.0)-16 ChannelHandler概览
本章开始分析ChannelHandler实现代码.ChannelHandler是netty为开发者提供的实现定制业务的主要接口,开发者在使用netty时,最主要的工作就是实现自己的ChannelHan ...
- Netty Pipeline与ChannelHandler那些事
Pipeline和ChannelHandler是Netty处理流程的重要组成部分,ChannelHandler对应一个个业务处理器,Pipeline则是负责将各个ChannelHandler串起来的& ...
- Netty基础招式——ChannelHandler的最佳实践
本文是Netty系列第7篇 上一篇文章我们深入学习了Netty逻辑架构中的核心组件EventLoop和EventLoopGroup,掌握了Netty的线程模型,并且介绍了Netty4线程模型中的无锁串 ...
- 基于Netty的私有协议栈的开发
基于Netty的私有协议栈的开发 书是人类进步的阶梯,每读一本书都使自己得以提升,以前看书都是看了就看了,当时感觉受益匪浅,时间一长就又还回到书本了!所以说,好记性不如烂笔头,以后每次看完一本书都写一 ...
随机推荐
- 二、LINUX文本处理三剑客之grep
1. grep一般格式:grep [选项] 基本正则表达式 [文件],其中基本正则表达式需要用引号引起来 引号引起来的作用:a.防止被误解为shell命令,b.可以用来查找多个单词组成的字符串 gre ...
- java中邮件通知
// 客户信息 Tkhxx tkhxx = new Tkhxx(); try { String msg = tkhxx.toString(); MailUtil.simpleMailSend(Mail ...
- JavaDailyReports10_17
学习JavaWeb第一天 输出我的第一个HelloWorld! 1 <%@ page language="java" import="java.util.*&quo ...
- 超级电容(Supercapacitor) 和电池的比较
之前看到同事在电路设计里使用了超级电容来进行供电,好奇为什么没有用到普通的电池,于是就是找了找两个的区别.有篇文章讲得挺好,所以就直接翻译一下. 超级电容有点像普通电池和一般电容的结合体,能比一般的电 ...
- 这是一篇SQL注入文章
目录 注入原理: 1.寻找注入点的方式或注入的地方可能包括. 2.注入点判断方法. 3.注入分类. 数字型: 字符型: 搜索型: XX型(也叫其他型): 4.注入提交方式. 5.注入攻击类型与方式. ...
- 远程控制卡 使用ipmitools设置ipmi
远程控制卡 使用ipmitools设置ipmi 使用DELL的远程控制卡可以方便的管理服务器 在CentOS中可以使用ipmitools管理 IPMI( Intelligent Platform Ma ...
- 对于home主页的切换处理
经过测试,发现,在home首页的时候,滑动到某一个位置的时候,如果再点击tabbar中的"购物车"."分类"或者"我的"的时候,再点击到首页 ...
- git分支的创建与分支之间合并的底层原理
开发一个版本,采用的发布流程: (1).从master的最新代码拉取一个开发分支,在上面进行开发(这里假设开发分支为dev) (2).在开发分支上不断地进行提交版本,期间,master也会有因为其他版 ...
- 30天自制操作系统-day3
30天自制操作系统-day3 前2天我们分别使用了直接使用二进制编辑器和简单的汇编指令生成了img文件,今天我们尝试一下使用稍微复杂一点的汇编指令 os.asm文件内容如下: ; hello-os ; ...
- i5 11300H和i5 10300H 的区别
i5-11300H 为 4 核 8 线程,主频 3.1GHz,睿频 4.4GHz,三级缓存 8MB 选 i5-11300H还是i5 10300h 这些点很重要!看完你就知道了https://list. ...