netty基础05_管道和消息处理器
ChannelHandler是消息处理器;
ChannelHandler接口中定义了很多方法,每一个方法对应一个事件;
当Channel和ChannelHandler生命周期中状态改变时会触发各种事件,也就导致了事件对应的方法会被调用;
1.Channel的生命周期
netty用Channel表示一个连接;
Channel有4种状态:
一个Channel的生命周期中状态的改变:创建 -》注册 -》激活 -》连接断开;
每当Channel的状态发生改变时,将会生成对应的事件。
这些事件将会被转发给 ChannelPipeline 中的 ChannelHandler;
可以在ChannelHandler中重写方法来对这些事件进行处理;
2.ChannelHadler生命周期
ChannelHandler接口中定义了一些方法,在其生命周期中触发事件时,事件对应的方法会被调用;
每一个方法都接受一个 ChannelHandlerContext 参数;
ChannelHandler接口有两个子接口:ChannelInboundHandler、ChannelOutboundHandler ,分别用来处理进站消息和出站消息;
两个子接口都为各自生命周期中的事件定义了对应的方法;
1) ChannelInboundHandler的生命周期
ChannelInboundHandler是ChannelHadler的子接口,用来处理进站消息;
ChannelInboundHandler定义了一些方法;
这些方法将会在数据被接收时或者与其对应的 Channel 状态发生改变时被调用;
netty提供了适配器ChannelInboundHandlerAdapter来实现这些方法;
需要处理消息进站事件时,可以考虑通过继承适配器来实现;
释放资源:
当某个 ChannelInboundHandler 的实现重写 channelRead()方法时,它将负责显式地释放与池化的 ByteBuf 实例相关的内存;
Netty 为此提供了一个实用方法 ReferenceCountUtil.release();
public class DiscardHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ReferenceCountUtil.release(msg); } }
netty提供的SimpleChannelInboundHandler扩展了ChannelInboundHandlerAdapter,可以使用来做一些常用的简单消息处理;
SimpleChannelInboundHandler 会自动释放资源,不需要显示释放资源;
不应该存储指向任何消息的引用供将来使用,因为这些引用都将会失效;
public class SimpleDiscardHandler extends SimpleChannelInboundHandler<Object> { @Override public void channelRead0(ChannelHandlerContext ctx,Object msg) { // 不需要在这里释放资源 } }
3) ChannelOutboundHandler的生命周期
ChannelOutboundHandler也是ChannelHandler的一个子接口,用来处理出站消息;
几乎所有的方法都将 ChannelPromise 作为参数;
ChannelPromise继承自ChannelFuture,用来返回消息处理的结果;
适配器 ChannelOutboundHandlerAdapter提供了这些方法的基本实现,可以拓展适配器来实现自己的业务;
3.资源管理
调用 ChannelInboundHandler.channelRead()或者 ChannelOutboundHandler.write()方法来处理数据时,需要确保没有任何的资源泄漏;
Netty 使用引用计数来处理池化的 ByteBuf;
因此使用完某个ByteBuf 后,需要调整其引用计数来释放资源;
1)资源泄露
Netty提供了一个类ResourceLeakDetector来诊断潜在的资源泄漏问题;
有4种泄露检测级别:
ResourceLeakDetector默认采用SIMPLE级别的检测;
如果检测到了内存泄露,将会产生类似于下面的日志消息:
LEAK: ByteBuf.release() was not called before it's garbage-collected. Enable advanced leak reporting to find out where the leak occurred. To enable advanced leak reporting, specify the JVM option '-Dio.netty.leakDetectionLevel=ADVANCED' or call ResourceLeakDetector.setLevel().
此时可以根据提示提高检测级别来找到找到问题位置;
设置检测级别:
泄露检测级别可以通过将下面的 Java 系统属性设置为表中的一个值来定义:
java -Dio.netty.leakDetectionLevel=ADVANCED
也可以调用ResourceLeakDetector的setLevel()方法;
将检测级别设置为ADVANCED后,重启程序,可以找到资源泄露的位置;
例如:
Running io.netty.handler.codec.xml.XmlFrameDecoderTest 15:03:36.886 [main] ERROR io.netty.util.ResourceLeakDetector - LEAK: ByteBuf.release() was not called before it's garbage-collected. Recent access records: 1 #1: io.netty.buffer.AdvancedLeakAwareByteBuf.toString( AdvancedLeakAwareByteBuf.java:697) io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithXml( XmlFrameDecoderTest.java:157) io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithTwoMessages( XmlFrameDecoderTest.java:133)
2)资源释放
1】入站消息
当重写ChannelInboundHandler的channelRead方法时,可以通过ReferenceCountUtil.release(msg)来释放消息;
如果是消息处理器是继承SimpleChannelInboundHandler,重写channelRead0方法,则不需要显示释放;
这个实现会在消息被 channelRead0()方法消费之后自动释放消息。
2】出站消息
如果重写了ChannelOutboundHandler的write方法,并且需要丢弃消息时,需要释放资源;
public class DiscardOutboundHandler extends ChannelOutboundHandlerAdapter { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { ReferenceCountUtil.release(msg); //释放消息 promise.setSuccess(); //通知ChannelPromise数据已被处理 } }
调用ReferenceCountUtil.release(msg)来释放资源;
不仅要释放资源,还要通知 ChannelPromise,否则可能会出现 ChannelFutureListener 收不到某个消息已经被处理了的通知的情况;
总之:
如果一个消息被消费或者丢弃了, 并且没有传递给 ChannelPipeline 中的下一个ChannelOutboundHandler,
那么用户就有责任调用 ReferenceCountUtil.release()来释放资源
如果消息到达了实际的传输层, 那么当它被写入时或者 Channel 关闭时,都将被自动释放。
4.管道 (ChannelPipeline)
管道是ChannelHandler的容器;
每一次创建了新的Channel ,都会新建一个新的 ChannelPipeline并绑定到Channel上。
channel和管道的关联是永久性的,Channel既不能附上另一个ChannelPipeline 也不能分离当前这个。
1)关于管道
ChannelPipeline 主要由一系列的ChannelHandler所组成的;
Netty 总是将 ChannelPipeline 的入站口作为头部,而将出站口作为尾端;
ChannelPipeline提供了通过ChannelPipeline本身传播事件的方法:
如果一个入站事件被触发,它将被从 ChannelPipeline 的头部开始一直被传播到 Channel Pipeline 的尾端;
一个出站 I/O 事件将从 ChannelPipeline 的最右边开始,然后向左传播;
2)管道中处理器执行顺序
在 ChannelPipeline 传播事件时,它会测试 ChannelPipeline 中的下一个 ChannelHandler 的类型是否和事件的运动方向相匹配;
如果不匹配, ChannelPipeline 将跳过该ChannelHandler 并前进到下一个,直到它找到和该事件所期望的方向相匹配的为止;
也就是说:
对于channelInboundHandler,总是会从传递事件的开始,向链表末尾方向遍历执行可用的inboundHandler。
对于channelOutboundHandler,总是会从write事件执行的开始,向链表头部方向遍历执行可用的outboundHandler。
例如:
ch.pipeline().addLast(new OutboundHandler1()); ch.pipeline().addLast(new OutboundHandler2()); ch.pipeline().addLast(new InboundHandler1()); ch.pipeline().addLast(new InboundHandler2());
管道中处理器的顺序为head->out1->out2->in1->in2->tail
Inbound的执行顺序为read->in1->in2
在Inbound执行write后,outbound执行顺序为out1<-out2<-write
总结:
InboundHandler是按顺序传递,OutboundHandler是逆序传递。
3) ChannelHandler 的执行和阻塞
通常 ChannelPipeline 中的每一个 ChannelHandler 都是通过它的 EventLoop(I/O 线程)来处理传递给它的事件的;
为了防止对整体的 I/O 处理产生负面的影响,最好不要阻塞这个线程;
但有时可能需要与那些使用阻塞 API 的遗留代码进行交互,从而导致EventLoop中的线程被阻塞;
对于这种情况,netty提供了EventExecutorGroup;
EventExecutorGroup 就是专门来处理耗时业务的线程池;
如果一个事件被传递给一个自定义的 EventExecutorGroup,它将被包含在这个 EventExecutorGroup 中的某个 EventExecutor 所处理,从而被从该Channel 本身的 EventLoop 中移除。
Netty 提供了一个叫EventExecutorGroup的默认实现DefaultEventExecutorGroup;
ChannelPipeline 有一些接受一个 EventExecutorGroup 的 add()方法;
在管道中添加消息处理器时,可以带上EventExecutorGroup的子类实例;
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() { //使用处理器初始化器,可以在管道中添加多个处理器 static final EventExecutorGroup group = new DefaultEventExecutorGroup(16); //EventExecutorGroup的实例 @Override protected void initChannel(SocketChannel ch)throws Exception { ChannelPipeline p = ch.pipeline(); //在管道中添加消息处理器,如果处理耗时任务或可能阻塞线程时可带上EventExecutorGroup作为参数,将消息交给EventExecutorGroup处理,防止影响整体io速度 pipeline.addLast(group, "handler", new MyBusinessLogicHandler()); } }
4)管道api
1】编辑管道
例如:
ChannelPipeline pipeline = ...; // 得到管道的引用 FirstHandler firstHandler = new FirstHandler(); pipeline.addLast("handler1", firstHandler); //往管道尾部中添加消息处理器1:h1 pipeline.addFirst("handler2", new SecondHandler()); //处理器2添加到管道头部:h2->h1 pipeline.addLast("handler3", new ThirdHandler()); //处理器3添加到尾部:h2->h1->h3 pipeline.remove("handler3"); //(通过名字移除)从管道中移除h3:h2->h1 pipeline.remove(firstHandler); //(通过引用移除)从管道中移除h1:h2 pipeline.replace("handler2", "handler4", new ForthHandler()); //用h4替换h2:h4
2】查询管道
3】触发事件
入站事件:
出站事件:
3.上下文 ChannelHandlerContext
ChannelHandlerContext表示ChannelHandler和管道之间的关联;
当一个ChannelHandler加入到管道中时会创建一个ChannelHandlerContext实例;
ChannelHandler通过ChannelHandlerContext与同一个管道中的其它ChannelHandler交互;
@Sharable 注解
一个ChannelHandler实例可能会加入到多个管道中,因此一个ChannelHandler可能对应多个ChannelHandlerContext;
ChannelHandler添加到多个管道时需要在ChannelHandler的实现类添加@Sharable 注解,否则将引发一个异常。
需要在保证线程安全的情况下使用@Sharable 注解;(例如需要在事件处理方法中修改状态时需要同步)
api:
ChannelHandlerContext中也有write方法,用来写出一个出站消息,和Channel;
Channel和ChannelPipeline的write方法写出的消息会从管道尾部开始传递,依次被管道上每一个出站消息处理器处理;
而ChannelHandlerContext的write写出的消息会从当前ChannelHandler的前一个出站消息处理器开始传递;
4.异常处理
1)进站异常处理
如果在处理进站事件的过程中有异常被抛出,这个异常消息将从异常所产生的的 ChannelInboundHandler那里开始传递;
异常处理方式:
可以通过重写ChannelInboundHandler接口的exceptionCaught方法来处理异常;
因为进站异常消息和其它进站消息一样,都是从管道头流向管道尾部,因此为了保证管道中所有异常都被处理,一般在最后一个入站处理器那里处理异常;
如果异常消息每被处理,netty会 Netty将会记录该异常没有被处理的事实;
即 Netty 将会通过 Warning 级别的日志记录该异常到达了 ChannelPipeline 的尾端,但没有被处理,并尝试释放该异常;
public class InboundExceptionHandler extends ChannelInboundHandlerAdapter { @Override public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) { cause.printStackTrace(); //打印异常信息 ctx.close(); //关闭连接 } }
2)出站异常处理
1】使用ChannelFuture处理异常
每个出站操作都将返回一个 ChannelFuture;
注册到 ChannelFuture 的 ChannelFutureListener 将在操作完成时被通知该操作是成功了还是出错了;
代码:
ChannelFuture future = channel.write(someMessage); //通过write方法写出一个出站事件 future.addListener(new ChannelFutureListener() { //添加监视器 @Override public void operationComplete(ChannelFuture f) { //当写出事件完成时会获得异步通知 if (!f.isSuccess()) { //如果有异常该如何处理 f.cause().printStackTrace(); f.channel().close(); } } });
2】使用ChannelPromise处理异常
几乎所有的 ChannelOutboundHandler 上的方法都会传入一个 ChannelPromise的实例;
作为 ChannelFuture 的子类, ChannelPromise 也可以被分配用于异步通知的监听器;
除此之外ChannelPromise 还具有提供立即通知的可写方法:
ChannelPromise setSuccess() ->立即发送成功通知
ChannelPromise setFailure(Throwable cause) ->立即发送失败通知
代码:
public class OutboundExceptionHandler extends ChannelOutboundHandlerAdapter { @Override public void write(ChannelHandlerContext ctx, Object msg,ChannelPromise promise) { //重写出站处理器的write方法,里面有个参数ChannelPromise promise.addListener(new ChannelFutureListener() { //ChannelPromise是ChannelFuture 的子类,也可以添加监听器,用来获得write方法完成时的异步通知 @Override public void operationComplete(ChannelFuture f) { if (!f.isSuccess()) { f.cause().printStackTrace(); f.channel().close(); } } }); } }