Netty之Channel*
Netty之Channel*
本文内容主要参考**<<Netty In Action>> ** 和Netty
的文档和源码,偏笔记向.
先简略了解一下ChannelPipeline
和ChannelHandler
.
想象一个流水线车间.当组件从流水线头部进入,穿越流水线,流水线上的工人按顺序对组件进行加工,到达流水线尾部时商品组装完成.
可以将ChannelPipeline
当做流水线,ChannelHandler
当做流水线工人.源头的组件当做event,如read,write等等.
1.1 Channel
Channel
连接了网络套接字或能够进行I/O操作的组件,如 read, write, connect, bind.
我们可以通过Channel
获取一些信息.
Channel
的当前状态(如,是否连接,是否打开)Channel
的配置参数,如buffer的size- 支持的I/O操作
- 处理所有I/O事件的
ChannelPipeline
和与通道相关的请求
Channel
接口定义了一组和ChannelInboundHandler
API密切相关的状态模型.
当
Channel
的状态改变,会生成对应的event.这些event会转发给ChannelPipeline
中的ChannelHandler
,handler会对其进行响应.
1.2 ChannelHandler生命周期
下面列出了 interface ChannelHandler 定义的生命周期操作, 在 ChannelHandler被添加到 ChannelPipeline 中或者被从 ChannelPipeline 中移除时会调用这些操作。这些方法中的每一个都接受一个 ChannelHandlerContext 参数
1.3 ChannelInboundHandler 接口
ChannelInboundHandler
处理入站数据以及各种状态变化,当Channel
状态发生改变会调用ChannelInboundHandler
中的一些生命周期方法.这些方法与Channel
的生命密切相关.
入站数据,就是进入socket
的数据.下面展示一些该接口的生命周期API
当某个
ChannelInboundHandler
的实现重写channelRead()
方法时,它将负责显式地
释放与池化的 ByteBuf 实例相关的内存。 Netty 为此提供了一个实用方法ReferenceCountUtil.release()
.
@Sharable
public class DiscardHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ReferenceCountUtil.release(msg);
}
}
这种方式还挺繁琐的,Netty提供了一个SimpleChannelInboundHandler
,重写channelRead0()
方法,就可以在调用过程中会自动释放资源.
public class SimpleDiscardHandler
extends SimpleChannelInboundHandler<Object> {
@Override
public void channelRead0(ChannelHandlerContext ctx,
Object msg) {
// 不用调用ReferenceCountUtil.release(msg)也会释放资源
}
}
原理就是这样,channelRead
方法包装了channelRead0
方法.
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
boolean release = true;
try {
if (acceptInboundMessage(msg)) {
@SuppressWarnings("unchecked")
I imsg = (I) msg;
channelRead0(ctx, imsg);
} else {
release = false;
ctx.fireChannelRead(msg);
}
} finally {
if (autoRelease && release) {
ReferenceCountUtil.release(msg);
}
}
}
1.4 ChannelOutboundHandler
出站操作和数据将由 ChannelOutboundHandler 处理。它的方法将被 Channel、 ChannelPipeline 以及 ChannelHandlerContext 调用。
ChannelOutboundHandler 的一个强大的功能是可以按需推迟操作或者事件,这使得可以通过一些复杂的方法来处理请求。例如, 如果到远程节点的写入被暂停了, 那么你可以推迟冲刷操作并在稍后继续。
ChannelPromise与ChannelFuture: ChannelOutboundHandler中的大部分方法都需要一个ChannelPromise参数, 以便在操作完成时得到通知。 ChannelPromiseChannelFuture的一个子类,其定义了一些可写的方法,如setSuccess()和setFailure(), 从而使ChannelFuture不可变.
1.5 ChannelHandler适配器
ChannelHandlerAdapter顾名思义,就是handler的适配器.你需要知道什么是适配器模式,假设有一个A接口,我们需要A的subclass实现功能,但是B类中正好有我们需要的功能,不想复制粘贴B中的方法和属性了,那么可以写一个适配器类Adpter继承B实现A,这样一来Adpter是A的子类并且能直接使用B中的方法,这种模式就是适配器模式.
就比如Netty中的SslHandler
类,想使用ByteToMessageDecoder
中的方法进行解码,但是必须是ChannelHandler
子类对象才能加入到ChannelPipeline
中,通过如下签名和其实现细节(SslHandler
实现细节就不贴了)就能够作为一个Handler去处理消息了.
public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundHandler
下图是ChannelHandler和Adpter的UML图示.
ChannelHandlerAdapter提供了一些实用方法
isSharable()
如果其对应的实现被标注为 Sharable, 那么这个方法将返回 true, 表示它可以被添加到多个 ChannelPipeline中 .如果想在自己的ChannelHandler中使用这些适配器类,只需要扩展他们,重写那些想要自定义的方法即可.
1.6 资源管理
在使用ChannelInboundHandler.channelRead()
或ChannelOutboundHandler.write()
方法处理数据时要避免资源泄露,ByteBuf那篇文章提到过引用计数,当使用完某个ByteBuf之后记得调整引用计数.
Netty提供了一个class ResourceLeakDetector
来帮助诊断资源泄露,这能够帮助你判断应用的运行情况,但是如果希望提高吞吐量(比如搞一些竞赛),关闭内存诊断可以提高吞吐量.
泄露检测级别可以通过将下面的 Java 系统属性设置为表中的一个值来定义:
java -Dio.netty.leakDetectionLevel=ADVANCED
如果带着该 JVM 选项重新启动你的应用程序,你将看到自己的应用程序最近被泄漏的缓冲
区被访问的位置。下面是一个典型的由单元测试产生的泄漏报告:
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)
...
应用程序处理消息释放资源
消费入站消息释放资源
@Sharable
public class DiscardInboundHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ReferenceCountUtil.release(msg);// 用于释放资源的工具类
}
}
SimpleChannelInboundHandler
中的channelRead0()会消费消息之后自动释放资源.
出站释放资源
@Sharable
public class DiscardOutboundHandler
extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx,
Object msg, ChannelPromise promise) {
// 还是通过util工具类释放资源
ReferenceCountUtil.release(msg);
// 通知ChannelPromise,消息已经处理
promise.setSuccess();
}
}
重要的是, 不仅要释放资源,还要通知 ChannelPromise。否则可能会出现 ChannelFutureListener 收不到某个消息已经被处理了的通知的情况。总之,如果一个消息被消费或者丢弃了, 并且没有传递给 ChannelPipeline 中的下一个ChannelOutboundHandler, 那么用户就有责任调用ReferenceCountUtil.release()。如果消息到达了实际的传输层, 那么当它被写入时或者 Channel 关闭时,都将被自动释放。
2 ChannelPipelin接口
Channel和ChannelPipeline
每一个新创建的 Channel 都将会被分配一个新的 ChannelPipeline。这项关联是永久性的; Channel 既不能附加另外一个 ChannelPipeline,也不能分离其当前的。在 Netty 组件的生命周期中,这是一项固定的操作,不需要开发人员的任何干预。
ChannelHandler和ChannelHandlerContext
根据事件的起源,事件将会被 ChannelInboundHandler 或者 ChannelOutboundHandler 处理。随后, 通过调用 ChannelHandlerContext 实现,它将被转发给同一超类型的下一个ChannelHandler。
ChannelHandlerContext使得ChannelHandler能够和它的ChannelPipeline以及其他的ChannelHandler 交 互 。 ChannelHandler 可 以 通 知 其 所 属 的 ChannelPipeline 中 的 下 一 个ChannelHandler,甚至可以动态修改它所属的ChannelPipeline.
ChannelPipelin和ChannelHandler
这是一个同时具有入站和出站 ChannelHandler 的 ChannelPipeline 的布局,并且印证了我们之前的关于 ChannelPipeline 主要由一系列的 ChannelHandler 所组成的说法。 ChannelPipeline 还提供了通过 ChannelPipeline 本身传播事件的方法。如果一个入站事件被触发,它将被从 ChannelPipeline 的头部开始一直被传播到 Channel Pipeline 的尾端。
你可能会说, 从事件途经 ChannelPipeline 的角度来看, ChannelPipeline 的头部和尾端取决于该事件是入站的还是出站的。然而 Netty 总是将 ChannelPipeline 的入站口(图 的左侧)作为头部,而将出站口(该图的右侧)作为尾端。
当你完成了通过调用 ChannelPipeline.add*()方法将入站处理器( ChannelInboundHandler)和 出 站 处 理 器 ( ChannelOutboundHandler ) 混 合 添 加 到 ChannelPipeline 之 后 , 每 一 个ChannelHandler 从头部到尾端的顺序位置正如同我们方才所定义它们的一样。因此,如果你将图 6-3 中的处理器( ChannelHandler)从左到右进行编号,那么第一个被入站事件看到的 ChannelHandler 将是1,而第一个被出站事件看到的 ChannelHandler 将是 5。在 ChannelPipeline 传播事件时,它会测试 ChannelPipeline 中的下一个 ChannelHandler 的类型是否和事件的运动方向相匹配。如果不匹配, ChannelPipeline 将跳过该ChannelHandler 并前进到下一个,直到它找到和该事件所期望的方向相匹配的为止。 (当然, ChannelHandler 也可以同时实现ChannelInboundHandler 接口和 ChannelOutboundHandler 接口。)
2.1 修改ChannelPipeline
修改指的是添加或删除ChannelHandler
代码示例
ChannelPipeline pipeline = ..;
FirstHandler firstHandler = new FirstHandler();
// 先添加一个Handler到ChannelPipeline中
pipeline.addLast("handler1", firstHandler);
// 这个Handler放在了first,意味着放在了handler1之前
pipeline.addFirst("handler2", new SecondHandler());
// 这个Handler被放到了last,意味着在handler1之后
pipeline.addLast("handler3", new ThirdHandler());
...
// 通过名称删除
pipeline.remove("handler3");
// 通过对象删除
pipeline.remove(firstHandler);
// 名称"handler2"替换成名称"handler4",并切handler2的实例替换成了handler4的实例
pipeline.replace("handler2", "handler4", new ForthHandler());
这种方式非常灵活,按照需要更换或插入handler
达到我们想要的效果.
ChannelHandler的执行和阻塞
通常 ChannelPipeline 中的每一个 ChannelHandler 都是通过它的 EventLoop( I/O 线程)来处理传递给它的事件的。所以至关重要的是不要阻塞这个线程,因为这会对整体的 I/O 处理产生负面的影响。
但有时可能需要与那些使用阻塞 API 的遗留代码进行交互。对于这种情况, ChannelPipeline 有一些接受一个 EventExecutorGroup 的 add()方法。如果一个事件被传递给一个自定义的 EventExecutorGroup ,它将被包含在这个 EventExecutorGroup 中的某个 EventExecutor 所处理,从而被从该Channel 本身的 EventLoop 中移除。对于这种用例, Netty 提供了一个叫 DefaultEventExecutorGroup 的默认实现。
pipeline对handler的操作
2.2 ChannelPipeline的出入站api
入站
出站
- ChannelPipeline 保存了与 Channel 相关联的 ChannelHandler
- ChannelPipeline 可以根据需要,通过添加或者删除 ChannelHandler 来动态地修改
- ChannelPipeline 有着丰富的 API 用以被调用,以响应入站和出站事件
3 ChannelHandlerContext接口
每当有ChannelHandler
添加到ChannelPipeline
中,都会创建ChannelHandlerContext
.如果调用Channel
或ChannelPipeline
上的方法,会沿着整个ChannelPipeline
传播,如果调用ChannelHandlerContext
上的相同方法,则会从对应的当前ChannelHandler
进行传播.
API
ChannelHandlerContext
和ChannelHandler
之间的关联(绑定)是永远不会改变的,所以缓存对它的引用是安全的;- 如同我们在本节开头所解释的一样,相对于其他类的同名方法,
ChannelHandlerContext
的方法将产生更短的事件流, 应该尽可能地利用这个特性来获得最大的性能。
3.1 使用CHannelHandlerContext
从ChannelHandlerContext访问channel
ChannelHandlerContext ctx = ..;
// 获取channel引用
Channel channel = ctx.channel();
// 通过channel写入缓冲区
channel.write(Unpooled.copiedBuffer("Netty in Action",
CharsetUtil.UTF_8));
从ChannelHandlerContext访问ChannelPipeline
ChannelHandlerContext ctx = ..;
// 获取ChannelHandlerContext
ChannelPipeline pipeline = ctx.pipeline();
// 通过ChannelPipeline写入缓冲区
pipeline.write(Unpooled.copiedBuffer("Netty in Action",
CharsetUtil.UTF_8));
有时候我们不想从头传递数据,想跳过几个handler,从某个handler开始传递数据.我们必须获取目标handler之前的handler关联的ChannelHandlerContext.
ChannelHandlerContext ctx = ..;
// 直接通过ChannelHandlerContext写数据,发送到下一个handler
ctx.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));
好了,ChannelHandlerContext的基本使用应该掌握了,但是你真的理解ChannelHandlerContext,ChannelPipeline和Channelhandler之间的关系了吗.我们老看一下Netty的源码.
先看一下AbstractChannelHandlerContext
类,这个类像不像双向链表中的一个Node,
abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
implements ChannelHandlerContext, ResourceLeakHint {
...
volatile AbstractChannelHandlerContext next;
volatile AbstractChannelHandlerContext prev;
...
}
再来看一看DefaultChannelPipeline
,ChannelPipeline
中拥有ChannelHandlerContext
这个节点的head和tail,
而且DefaultChannelPipeline
类中并没有ChannelHandler
成员或handler数组.
public class DefaultChannelPipeline implements ChannelPipeline {
...
final AbstractChannelHandlerContext head;
final AbstractChannelHandlerContext tail;
...
所以addFirst
向pipeline中添加了handler到底添加到哪了呢.看一下pipeline中的addFirst方法
@Override
public final ChannelPipeline addFirst(String name, ChannelHandler handler) {
return addFirst(null, name, handler);
}
@Override
public final ChannelPipeline addFirst(EventExecutorGroup group, String name, ChannelHandler handler) {
final AbstractChannelHandlerContext newCtx;
synchronized (this) {
// 检查handler是否具有复用能力,不重要
checkMultiplicity(handler);
// 名称,不重要.
name = filterName(name, handler);
// 这个方法创建了DefaultChannelHandlerContext,handler是其一个成员属性
// 你现在应该明白了上面说的添加handler会创建handlerContext了吧
newCtx = newContext(group, name, handler);
// 这个方法
addFirst0(newCtx);
// 这个方法是调整pipeline中HandlerContext的指针,
// 就是更新HandlerContext链表节点之间的位置
private void addFirst0(AbstractChannelHandlerContext newCtx) {
AbstractChannelHandlerContext nextCtx = head.next;
newCtx.prev = head;
newCtx.next = nextCtx;
head.next = newCtx;
nextCtx.prev = newCtx;
}
简单总结一下,pipeline拥有context(本身像一个链表的节点)组成的节点的双向链表首尾,可以看做pipeline拥有一个context链表,context拥有成员handler,这便是三者之间的关系.实际上,handler作为消息处理的主要组件,实现了和pipeline的解耦,我们可以只有一个handler,但是被封装进不同的context能够被不同的pipeline使用.
3.2 handler和context高级用法
缓存ChannelHandlerContext引用
@Sharable
public class WriteHandler extends ChannelHandlerAdapter {
private ChannelHandlerContext ctx;
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
this.ctx = ctx;
}
public void send(String msg) {
ctx.writeAndFlush(msg);
}
}
因为一个 ChannelHandler 可以从属于多个 ChannelPipeline,所以它也可以绑定到多个 ChannelHandlerContext 实例。 对于这种用法指在多个ChannelPipeline 中共享同一个 ChannelHandler, 对应的 ChannelHandler 必须要使用@Sharable 注解标注; 否则,试图将它添加到多个 ChannelPipeline 时将会触发异常。
@Sharable错误用法
@Sharable
public class UnsharableHandler extends ChannelInboundHandlerAdapter {
private int count;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
count++;
System.out.println("channelRead(...) called the "
+ count + " time");
ctx.fireChannelRead(msg);
}
}
这段代码的问题在于它拥有状态 , 即用于跟踪方法调用次数的实例变量count。将这个类的一个实例添加到ChannelPipeline将极有可能在它被多个并发的Channel访问时导致问题。(当然,这个简单的问题可以通过使channelRead()方法变为同步方法来修正。)
总之,只应该在确定了你的 ChannelHandler 是线程安全的时才使用@Sharable 注解。
4.1 入站异常处理
处理入站事件的过程中有异常被抛出,那么它将从它在ChannelInboundHandler里被触发的那一点开始流经 ChannelPipeline。要想处理这种类型的入站异常,你需要在你的 ChannelInboundHandler 实现中重写下面的方法。
public void exceptionCaught(
ChannelHandlerContext ctx, Throwable cause) throws Exception
// 基本处理方式
public class InboundExceptionHandler extends ChannelInboundHandlerAdapter {
@Override
public void exceptionCaught(ChannelHandlerContext ctx,
Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
因为异常将会继续按照入站方向流动(就像所有的入站事件一样), 所以实现了前面所示逻辑的 ChannelInboundHandler 通常位于 ChannelPipeline 的最后。这确保了所有的入站异常都总是会被处理,无论它们可能会发生在ChannelPipeline 中的什么位置。
-
ChannelHandler.exceptionCaught()的默认实现是简单地将当前异常转发给ChannelPipeline 中的下一个 ChannelHandler;
-
如果异常到达了 ChannelPipeline 的尾端,它将会被记录为未被处理;
-
要想定义自定义的处理逻辑,你需要重写 exceptionCaught()方法。然后你需要决定是否需要将该异常传播出去。
4.2 出站异常处理
- 每个出站操作都将返回一个 ChannelFuture。 注册到 ChannelFuture 的 ChannelFutureListener 将在操作完成时被通知该操作是成功了还是出错了。
- 几乎所有的 ChannelOutboundHandler 上的方法都会传入一个 ChannelPromise
的实例。作为 ChannelFuture 的子类, ChannelPromise 也可以被分配用于异步通
知的监听器。但是, ChannelPromise 还具有提供立即通知的可写方法:
ChannelPromise setSuccess();
ChannelPromise setFailure(Throwable cause);
1.添加ChannelFutureListener到ChannelFuture
ChannelFuture future = channel.write(someMessage);
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture f) {
if (!f.isSuccess()) {
f.cause().printStackTrace();
f.channel().close();
}
}
});
2.添加ChannelFutureListener到ChannelPromise
public class OutboundExceptionHandler extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx, Object msg,
ChannelPromise promise) {
promise.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture f) {
if (!f.isSuccess()) {
f.cause().printStackTrace();
f.channel().close();
}
}
});
}
}