Netty-Pipeline深度解析
首先我们知道,在NIO网络编程模型中,IO操作直接和channel相关,比如客户端的请求连接,或者向服务端发送数据, 服务端都要从客户端的channel获取这个数据
那么channelPipeline是什么?
其实,这个channelPepiline是Netty增加给原生的channel的组件,在ChannelPipeline
接口的上的注解阐述了channelPipeline
的作用,这个channelPipeline是高级过滤器的实现,netty将chanenl中数据导向channelPipeline,进而给了用户对channel中数据的百分百的控制权, 此外,channelPipeline数据结构是双向链表,每一个节点都是channelContext
,channelContext
里面维护了对应的handler和pipeline的引用, 大概总结一下: 通过chanelPipeline,用户客户轻松的往channel写数据,从channel读数据
创建pipeline
通过前面几篇博客的追踪,我们知道无论我们是通过反射创建出服务端的channel也好,还是直接new创建客户端的channel也好,随着父类构造函数的逐层调用,最终我们都会在Channel体系的顶级抽象类AbstractChannel
中,创建出Channel的一大组件 channelPipeline
于是我们程序的入口,AbstractChannel
的 pipeline = newChannelPipeline();
,跟进去,看到他的源码如下:
protected DefaultChannelPipeline newChannelPipeline() {
// todo 跟进去
return new DefaultChannelPipeline(this);
}
可以看到,它创建了一个DefaultChannelPipeline(thisChannel)
DefaultChannelPipeline
是channelPipeline的默认实现,他有着举足轻重的作用,我们看一下下面的 Channel
ChannelContext
ChannelPipeline
的继承体系图,我们可以看到图中两个类,其实都很重要,
他们之间有什么关系呢?
当我们看完了DefaultChannelPipeline()
构造中做了什么自然就知道了
// todo 来到这里
protected DefaultChannelPipeline(Channel channel) {
this.channel = ObjectUtil.checkNotNull(channel, "channel");
// todo 把当前的Channel 保存起来
succeededFuture = new SucceededChannelFuture(channel, null);
voidPromise = new VoidChannelPromise(channel, true);
// todo 这两方法很重要
// todo 设置尾
tail = new TailContext(this);
// todo 设置头
head = new HeadContext(this);
// todo 双向链表关联
head.next = tail;
tail.prev = head;
}
主要做了如下几件事:
- 初始化succeededFuture
- 初始化voidPromise
- 创建尾节点
- 创建头节点
- 关联头尾节点
其实,到现在为止,pipiline的初始化已经完成了,我们接着往下看
此外,我们看一下DefaultChannelPipeline
的内部类和方法,如下图()
我们关注我圈出来的几部分
- 两个重要的内部类
- 头结点 HeaderContext
- 尾节点 TailContext
- PendingHandlerAddedTask 添加完handler之后处理的任务
- PendingHandlerCallBack 添加完handler的回调
- PengdingHandlerRemovedTask 移除Handler之后的任务
- 大量的addXXX方法,
final AbstractChannelHandlerContext head;
final AbstractChannelHandlerContext tail;
跟进它的封装方法:
TailContext(DefaultChannelPipeline pipeline) {
super(pipeline, null, TAIL_NAME, true, false);
setAddComplete();
}
// todo 来到这里
AbstractChannelHandlerContext(DefaultChannelPipeline pipeline, EventExecutor executor, String name,
boolean inbound, boolean outbound) {
this.name = ObjectUtil.checkNotNull(name, "name");
// todo 为ChannelContext的pipeline附上值了
this.pipeline = pipeline;
this.executor = executor;
this.inbound = inbound;
this.outbound = outbound;
// Its ordered if its driven by the EventLoop or the given Executor is an instanceof OrderedEventExecutor.
ordered = executor == null || executor instanceof OrderedEventExecutor;
}
如下图是HeaderContext和TailContext的声明截图:
我们可以看到,这个tail节点是inbound类型的处理器,一开始确实很纳闷,难道header不应该是Inbound类型的吗?我也不买关子了,直接说为啥
是的,header确实是Inbound类型的处理器, 同时也是出站处理器 (评论区有个老哥说的也很清楚,可以瞅瞅)
因为,对netty来说用发送过来的数据,要就从header节点开始往后传播,怎么传播呢? 因为是双向链表,直接找后一个节点,什么类型的节点呢? inbound类型的,于是数据msg就从header之后的第一个结点往后传播,如果说,一直到最后,都只是传播数据而没有任何处理就会传播到tail节点,因为tail也是inbound类型的, tail节点会替我们释放掉这个msg,防止内存泄露,当然如果我们自己使用了msg,而没往后传播,也没有释放,内存泄露是早晚的时,这就是为啥tail是Inbound类型的, header节点和它相反,在下面说
ok,现在知道了ChannelPipeline的创建了吧
Channelpipeline与ChannelHandler和ChannelHandlerContext之间的关系
它三者的关系也直接说了, 在上面pipeline
的创建的过程中, DefaultChannelPipeline
中的头尾节点都是ChannelHandlerContext
, 这就意味着, 在pipeline双向链表的结构中,每一个节点都是一个ChannelHandlerContext
, 而且每一个 ChannelHandlerContext
维护一个handler
,这一点不信可以看上图,ChannelHandlerContext
的实现类DefaultChannelHandlerContext
的实现类, 源码如下:
final class DefaultChannelHandlerContext extends AbstractChannelHandlerContext {
// todo Context里面有了 handler的引用
private final ChannelHandler handler;
// todo 创建默认的 ChannelHandlerContext,
DefaultChannelHandlerContext(
DefaultChannelPipeline pipeline, EventExecutor executor, String name, ChannelHandler handler) {
super(pipeline, executor, name, isInbound(handler), isOutbound(handler));
if (handler == null) {
throw new NullPointerException("handler");
}
this.handler = handler;
ChannelHandlerContext
接口同时继承ChannelOutBoundInvoker
和和ChannelInBoundInvoker
使得他同时拥有了传播入站事件和出站事件的能力, ChannelHandlerContext
把事件传播之后,是谁处理的呢? 当然是handler
下面给出ChannelHandler
的继承体系图,可以看到针对入站出来和出站处理ChannelHandler
有不同的继承分支应对
添加一个新的节点:
一般我们都是通过ChanelInitialezer
动态的一次性添加多个handler, 下面就去看看,在服务端启动过程中,ServerBootStrap
的init()
,如下源码:解析我写在代码下面
// todo 这是ServerBootStrapt对 他父类初始化 channel的实现, 用于初始化 NioServerSocketChannel
@Override
void init(Channel channel) throws Exception {
// todo ChannelOption 是在配置 Channel 的 ChannelConfig 的信息
final Map<ChannelOption<?>, Object> options = options0();
synchronized (options) {
// todo 把 NioserverSocketChannel 和 options Map传递进去, 给Channel里面的属性赋值
// todo 这些常量值全是关于和诸如TCP协议相关的信息
setChannelOptions(channel, options, logger);
}
// todo 再次一波 给Channel里面的属性赋值 attrs0()是获取到用户自定义的业务逻辑属性 -- AttributeKey
final Map<AttributeKey<?>, Object> attrs = attrs0();
// todo 这个map中维护的是 程序运行时的 动态的 业务数据 , 可以实现让业务数据随着netty的运行原来存进去的数据还能取出来
synchronized (attrs) {
for (Entry<AttributeKey<?>, Object> e : attrs.entrySet()) {
@SuppressWarnings("unchecked")
AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
channel.attr(key).set(e.getValue());
}
}
// todo------- options attrs : 都可以在创建BootStrap时动态的传递进去
// todo ChannelPipeline 本身 就是一个重要的组件, 他里面是一个一个的处理器, 说他是高级过滤器,交互的数据 会一层一层经过它
// todo 下面直接就调用了 p , 说明,在channel调用pipeline方法之前, pipeline已经被创建出来了!,
// todo 到底是什么时候创建出来的 ? 其实是在创建NioServerSocketChannel这个通道对象时,在他的顶级抽象父类(AbstractChannel)中创建了一个默认的pipeline对象
/// todo 补充: ChannelHandlerContext 是 ChannelHandler和Pipeline 交互的桥梁
ChannelPipeline p = channel.pipeline();
// todo workerGroup 处理IO线程
final EventLoopGroup currentChildGroup = childGroup;
// todo 我们自己添加的 Initializer
final ChannelHandler currentChildHandler = childHandler;
final Entry<ChannelOption<?>, Object>[] currentChildOptions;
final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
// todo 这里是我们在Server类中添加的一些针对新连接channel的属性设置, 这两者属性被acceptor使用到!!!
synchronized (childOptions) {
currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size()));
}
synchronized (childAttrs) {
currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size()));
}
// todo 默认 往NioServerSocketChannel的管道里面添加了一个 ChannelInitializer ,
// todo ( 后来我们自己添加的ChildHandler 就继承了的这个ChannelInitializer , 而这个就继承了的这个ChannelInitializer 实现了ChannelHandler)
p.addLast(new ChannelInitializer<Channel>() { // todo 进入addlast
// todo 这个ChannelInitializer 方便我们一次性往pipeline中添加多个处理器
@Override
public void initChannel(final Channel ch) throws Exception {
final ChannelPipeline pipeline = ch.pipeline();
// todo 获取bootStrap的handler 对象, 没有返回空
// todo 这个handler 针对bossgroup的Channel , 给他添加上我们在server类中添加的handler()里面添加处理器
ChannelHandler handler = config.handler();
if (handler != null) {
pipeline.addLast(handler);
}
// todo ServerBootstrapAcceptor 接收器, 是一个特殊的chanelHandler
ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
// todo !!! -- 这个很重要,在ServerBootStrap里面,netty已经为我们生成了接收器 --!!!
// todo 专门处理新连接的接入, 把新连接的channel绑定在 workerGroup中的某一条线程上
// todo 用于处理用户的请求, 但是还有没搞明白它是怎么触发执行的
pipeline.addLast(new ServerBootstrapAcceptor(
// todo 这些参数是用户自定义的参数
// todo NioServerSocketChannel, worker线程组 处理器 关系的事件
ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
}
});
这个函数真的是好长,但是我们的重点放在ChannelInitializer
身上, 现在的阶段, 当前的channel还没有注册上EventLoop上的Selector中
还有不是分析怎么添加handler? 怎么来这里了? 其实下面的 ServerBootstrapAcceptor就是一个handler
我们看一下上面的代码做了啥
ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
// todo !!! -- 这个很重要,在ServerBootStrap里面,netty已经为我们生成了接收器 --!!!
// todo 专门处理新连接的接入, 把新连接的channel绑定在 workerGroup中的某一条线程上
// todo 用于处理用户的请求, 但是还有没搞明白它是怎么触发执行的
pipeline.addLast(new ServerBootstrapAcceptor(
// todo 这些参数是用户自定义的参数
// todo NioServerSocketChannel, worker线程组 处理器 关系的事件
ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
懵逼不? 当时真的是给我整蒙圈了, 还没有关联上 EventLoop呢!!! 哪来的ch.eventLoop()....
后来整明白了,这其实是一个回调,netty提供给用户在任意时刻都可以往pipeline中添加handler的实现手段
那么在哪里回调呢? 其实是在 jdk原生的channel注册进EventLoop中的Selector后紧接着回调的,源码如下
private void register0(ChannelPromise promise) {
try {
// check if the channel is still open as it could be closed in the mean time when the register
// call was outside of the eventLoop
if (!promise.setUncancellable() || !ensureOpen(promise)) {
return;
}
boolean firstRegistration = neverRegistered;
// todo 进入这个方法doRegister()
// todo 它把系统创建的ServerSocketChannel 注册进了选择器
doRegister();
neverRegistered = false;
registered = true;
// Ensure we call handlerAdded(...) before we actually notify the promise. This is needed as the
// user may already fire events through the pipeline in the ChannelFutureListener.
// todo 确保在 notify the promise前调用 handlerAdded(...)
// todo 这是必需的,因为用户可能已经通过ChannelFutureListener中的管道触发了事件。
// todo 如果需要的话,执行HandlerAdded()方法
// todo 正是这个方法, 回调了前面我们添加 Initializer 中添加 Accpter的重要方法
pipeline.invokeHandlerAddedIfNeeded();
回调函数在 pipeline.invokeHandlerAddedIfNeeded();
, 看它的命名, 如果需要的话,执行handler已经添加完成了操作 哈哈,我们现在当然需要,刚添加了个ServerBootstrapAcceptor
在跟进入看源码之间,注意,方法是pipeline调用的, 哪个pipeline呢? 就是上面我们说的DefaultChannelPipeline
, ok,跟进源码,进入 DefaultChannelPipeline
// todo 执行handler的添加,如果 需要的话
final void invokeHandlerAddedIfNeeded() {
assert channel.eventLoop().inEventLoop();
if (firstRegistration) {
firstRegistration = false;
// todo 现在我们的channel已经注册在bossGroup中的eventLoop上了, 是时候回调执行那些在注册前添加的 handler了
callHandlerAddedForAllHandlers();
}
}
调用本类方法 callHandlerAddedForAllHandlers();
继续跟进下
// todo 回调原来在没有注册完成之前添加的handler
private void callHandlerAddedForAllHandlers() {
final PendingHandlerCallback pendingHandlerCallbackHead;
synchronized (this) {
assert !registered;
// This Channel itself was registered.
registered = true;
pendingHandlerCallbackHead = this.pendingHandlerCallbackHead;
// Null out so it can be GC'ed.
this.pendingHandlerCallbackHead = null;
}
PendingHandlerCallback task = pendingHandlerCallbackHead;
while (task != null) {
task.execute();
task = task.next;
}
}
我们它的动作task.execute();
其中的task是谁? pendingHandlerCallbackHead
这是DefaultChannelPipeline
的内部类, 它的作用就是辅助完成 添加handler之后的回调, 源码如下:
private abstract static class PendingHandlerCallback implements Runnable {
final AbstractChannelHandlerContext ctx;
PendingHandlerCallback next;
PendingHandlerCallback(AbstractChannelHandlerContext ctx) {
this.ctx = ctx;
}
abstract void execute();
}
我们跟进上一步的task.execute()
就会看到它的抽象方法,那么是谁实现的呢? 实现类是PendingHandlerAddedTask
同样是DefaultChannelPipeline
的内部类, 既然不是抽象类了, 就得同时实现他父类PendingHandlerCallback
的抽象方法,其实有两个一是个excute()
另一个是run()
--Runable
我们进入看它是如何实现excute
,源码如下:
@Override
void execute() {
EventExecutor executor = ctx.executor();
if (executor.inEventLoop()) {
callHandlerAdded0(ctx);
} else {
try {
executor.execute(this);
} catch (RejectedExecutionException e) {
if (logger.isWarnEnabled()) {
logger.warn(
"Can't invoke handlerAdded() as the EventExecutor {} rejected it, removing handler {}.",
executor, ctx.name(), e);
}
remove0(ctx);
ctx.setRemoved();
}
}
HandlerAdded()
的回调时机
我们往下追踪, 调用类本类方法callHandlerAdded0(ctx);
源码如下:
// todo 重点看看这个方法 , 入参是刚才添加的 Context
private void callHandlerAdded0(final AbstractChannelHandlerContext ctx) {
try {
// todo 在channel关联上handler之后并且把Context添加到了 Pipeline之后进行调用!!!
ctx.handler().handlerAdded(ctx); // todo 他是诸多的回调方法中第一个被调用的
ctx.setAddComplete(); // todo 修改状态
}
...
继续往下追踪
- ctx.handler() -- 获取到了当前的channel
- 调用channel的
.handlerAdded(ctx);
这个handlerAdded()
是定义在ChannelHandler中的回调方法, 什么时候回调呢? 当handler添加后回调, 因为我们知道,当服务端的channel在启动时,会通过 channelInitializer 添加那个ServerBootstrapAcceptor
,所以ServerBootstrapAcceptor
的handlerAdded()
的回调时机就在上面代码中的ctx.handler().handlerAdded(ctx);
如果直接点击去这个函数,肯定就是ChannelHandler
接口中去; 那么 新的问题来了,谁是实现类? 答案是抽象类 ChannelInitializer`` 就在上面我们添加ServerBootstrapAcceptor
就创建了一个ChannelInitializer
的匿名对象
它的继承体系图如下:
介绍这个ChannelInitializer
他是Netty提供的辅助类,用于提供针对channel的初始化工作,什么工作呢? 批量初始化channel
这个中有三个重要方法,如下
- 重写的channel的
handlerAdded()
, 这其实也是handlerAdded()
的回调的体现 - 自己的
initChannel()
- 自己的
remove()
继续跟进我们上面的handlerAdded(ChannelHandlerContext ctx)
源码如下:
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
initChannel(ctx); // todo 这个方法在上面, 进入 可以在 finally中 找到移除Initializer的逻辑
}
}
调用本类的 initChannel(ctx);
源码如下:
private boolean initChannel(ChannelHandlerContext ctx) throws Exception {
if (initMap.putIfAbsent(ctx, Boolean.TRUE) == null) { // Guard against re-entrance.
try {
initChannel((C) ctx.channel());
} catch (Throwable cause) {
// Explicitly call exceptionCaught(...) as we removed the handler before calling initChannel(...).
// We do so to prevent multiple calls to initChannel(...).
exceptionCaught(ctx, cause);
} finally {
// todo remove(ctx); 删除 ChannelInitializer
remove(ctx);
}
return true;
}
return false;
}
两个点
- 第一: 继续调用本类的抽象方法
initChannel((C) ctx.channel());
- 第二: 移除了
remove(ctx);
分开进行第一步
initChannel((C) ctx.channel());
初始化channel,这个函数被设计成了抽象的, 问题来了, 实现类是谁? 实现类其实刚才说了,就是netty在添加ServerBootStrapAcceptor
时创建的那个匿名内部类,我们跟进去看他的实现: 源码如下:
@Override
public void initChannel(final Channel ch) throws Exception {
final ChannelPipeline pipeline = ch.pipeline();
// todo 获取bootStrap的handler 对象, 没有返回空
// todo 这个handler 针对bossgroup的Channel , 给他添加上我们在server类中添加的handler()里面添加处理器
ChannelHandler handler = config.handler();
if (handler != null) {
pipeline.addLast(handler);
}
// todo ServerBootstrapAcceptor 接收器, 是一个特殊的chanelHandler
ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
// todo !!! -- 这个很重要,在ServerBootStrap里面,netty已经为我们生成了接收器 --!!!
// todo 专门处理新连接的接入, 把新连接的channel绑定在 workerGroup中的某一条线程上
// todo 用于处理用户的请求, 但是还有没搞明白它是怎么触发执行的
pipeline.addLast(new ServerBootstrapAcceptor(
// todo 这些参数是用户自定义的参数
// todo NioServerSocketChannel, worker线程组 处理器 关系的事件
ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
}
实际上就是完成了一次方法的回调,成功添加了ServerBootstrapAcceptor
处理器
删除一个节点
回来看第二步
remove(ctx);
删除一个节点, 把Initializer
删除了? 是的, 把这个初始化器删除了, 为啥要把它删除呢, 说了好多次, 其实他是一个辅助类, 目的就是通过他往channel中一次性添加多个handler, 现在handler也添加完成了, 留着他也没啥用,直接移除了
我们接着看它的源码
// todo 删除当前ctx 节点
private void remove(ChannelHandlerContext ctx) {
try {
ChannelPipeline pipeline = ctx.pipeline();
if (pipeline.context(this) != null) {
pipeline.remove(this);
}
} finally {
initMap.remove(ctx);
}
}
从pipeline中移除, 一路看过去,就会发现底层删除链表节点的操作
private static void remove0(AbstractChannelHandlerContext ctx) {
AbstractChannelHandlerContext prev = ctx.prev;
AbstractChannelHandlerContext next = ctx.next;
prev.next = next;
next.prev = prev;
}
inbound事件的传播
什么是inbound事件
inbound事件其实就是客户端主动发起事件,比如说客户端请求连接,连接后客户端有主动的给服务端发送需要处理的有效数据等,只要是客户端主动发起的事件,都算是Inbound事件,特征就是事件触发类型,当channel处于某个节点,触发服务端传播哪些动作
netty如何对待inbound
netty为了更好的处理channel中的数据,给jdk原生的channel添加了pipeline组件,netty会把原生jdk的channel中的数据导向这个pipeline,从pipeline中的header开始 往下传播, 用户对这个过程拥有百分百的控制权,可以把数据拿出来处理, 也可以往下传播,一直传播到tail节点,tail节点会进行回收,如果在传播的过程中,最终没到尾节点,自己也没回收,就会面临内存泄露的问题
一句话总结,面对Inbound的数据, 被动传播
netty知道客户端发送过来的数据是啥类型吗?
比如一个聊天程序,客户端可能发送的是心跳包,也可能发送的是聊天的内容,netty又不是人,他是不知道数据是啥的,他只知道来了数据,需要进一步处理,怎么处理呢? 把数据导向用户指定的handler链条
开始读源码
这里书接上一篇博客的尾部,事件的传播
重点步骤如下
第一步: 等待服务端启动完成
第二步: 使用telnet模拟发送请求 --- > 新连接的接入逻辑
第三步: register0(ChannelPromise promise)
方法中会传播channel激活事件 --> 目的是二次注册端口,
第三个也是我们程序的入手点: fireChannelActive()
源码如下:
@Override
public final ChannelPipeline fireChannelActive() {
// todo ChannelActive从head开始传播
AbstractChannelHandlerContext.invokeChannelActive(head);
return this;
}
调用了AbstractChannelHandlerContext
的invokeChannelActive
方法
在这里,我觉得特别有必须有必要告诉自己
AbstractChannelHandlerContext
的重要性,现在的DefaultChannelPipeline
中的每一个节点,包括header,tail,以及我们自己的添加的,都是AbstractChannelHandlerContext
类型,事件的传播围绕着AbstractChannelHandlerContext
的方法开始,温习它的继承体系如下图
接着回到AbstractChannelHandlerContext.invokeChannelActive(head);
, 很显然,这是个静态方法, 跟进去,源码如下:
// todo 来这
static void invokeChannelActive(final AbstractChannelHandlerContext next) {
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
next.invokeChannelActive();
...
}
- 第一点: inbound类型的事件是从header开始传播的 , next --> HeaderContext
- 第二点: HeaderContext其实就是
AbstractChannelHandlerContext
类型的,所以invokeChannelActive()
其实是当前类的方法
ok,跟进入看看他干啥了,源码:
// todo 使Channel活跃
private void invokeChannelActive() {
// todo 继续进去
((ChannelInboundHandler) handler()).channelActive(this);
}
我们看, 上面的代码做了如下几件事
- handler() -- 返回当前的 handler, 就是从HandlerContext中拿出handler
- 强转成
ChannelInboundHandler
类型的,因为他是InBound类型的处理器
如果我们用鼠标点击channelActive(this)
, 毫无疑问会进入ChannelInboundHandler
,看到的是抽象方法
那么问题来了, 谁实现的它?
其实是headerContext
头结点做的, 之前说过,Inbound事件,是从header开始传播的,继续跟进去, 看源码:
// todo 来到这里, 分两步, 1. 把ChannelActive事件继续往下传播, 传播结束之后,做第二件事
// todo 2. readIfIsAutoRead();
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// todo fireChannelActive是在做了实际的端口绑定之后才触发回调
ctx.fireChannelActive();
// todo 默认方式注册一个read事件
// todo 跟进去, readIfIsAutoRead 作用是 把已经注册进selector上的事件, 重新注册绑定上在初始化NioServerSocketChannel时添加的Accept事件
// todo 目的是 新连接到来时, selector轮询到accept事件, 使得netty可以进一步的处理这个事件
readIfIsAutoRead();
}
其实这里有两种重要的事情 , 上面我们也看到了:
- 向下传播
channelActive()
目的是让header后面的用户添加的handler中的channelActive()
被回调 readIfIsAutoRead();
就是去注册Netty能看懂的感兴趣的事件
下面我们看它的事件往下传播, 于是重新回到了AbstractChannelHandlerContext
, 源码如下:
public ChannelHandlerContext fireChannelActive() {
invokeChannelActive(findContextInbound());
return this;
}
findContextInbound()
找出下一个Inbound类型的处理器, 我们去看他的实现,源码如下:
private AbstractChannelHandlerContext findContextInbound() {
AbstractChannelHandlerContext ctx = this;
do {
ctx = ctx.next;
} while (!ctx.inbound);
return ctx;
}
是不是明明白白的? 从当前节点开始,往后变量整个链表, 下一个节点是谁呢? 在新链接接入的逻辑中,调用的ChannelInitializer
我手动 批量添加了三个InboundHandler,按照我添加的顺序,他们会依次被找到
继续跟进本类方法 invokeChannelActive(findContextInbound())
,源码如下
// todo 来这
static void invokeChannelActive(final AbstractChannelHandlerContext next) {
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
next.invokeChannelActive();
...
一开始的next--> HeaderContext
现在的 next就是header之后,我手动添加的Inbound的handler
同样是调用本类方法invokeChannelActive()
,源码如下:
// todo 使Channel活跃
private void invokeChannelActive() {
// todo 继续进去
((ChannelInboundHandler) handler()).channelActive(this);
再次看到,回调, 我添加的handler.channelActive(this);
,进入查看
public class MyServerHandlerA extends ChannelInboundHandlerAdapter {
// todo 当服务端的channel绑定上端口之后,就是 传播 channelActive 事件
// todo 事件传播到下面后,我们手动传播一个 channelRead事件
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.channel().pipeline().fireChannelRead("hello MyServerHandlerA");
}
在我处理器中,继续往下传播手动添加的数据"hello MyServerHandlerA"
同样她会按找上面的顺序依次传播下去
最终她会来到tail , 在tail做了如下的工作, 源码如下
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// todo channelRead
onUnhandledInboundMessage(msg);
}
protected void onUnhandledInboundException(Throwable cause) {
try {
logger.warn(
"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.",
cause);
} finally {
ReferenceCountUtil.release(cause);
}
}
为什么Tail节点是 Inbound 类型的处理器?
上一步就说明了为什么Tail为什么设计成Inbound, channel中的数据,无论服务端有没有使用,最终都要被释放掉,tail可以做到收尾的工作, 清理内存
outbound事件的传播
什么是outBound事件
创建的outbound事件如: connect,disconnect,bind,write,flush,read,close,register,deregister, outbound类型事件更多的是服务端主动发起的事件,如给主动channel绑定上端口,主动往channel写数据,主动关闭用户的的连接
开始读源码
最典型的outbound事件,就是服务端往客户端写数据,准备测试用例如下:
public class OutBoundHandlerB extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
System.out.println( "hello OutBoundHandlerB");
ctx.write(ctx, promise);
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
ctx.executor().schedule(()->{
// todo 模拟给 客户端一个响应
ctx.channel().write("Hello World");
// 写法二 : ctx.write("Hello World");
},3, TimeUnit.SECONDS);
}
}
public class OutBoundHandlerA extends ChannelOutboundHandlerAdapter {
// todo 当服务端的channel绑定上端口之后,就是 传播 channelActive 事件
// todo 事件传播到下面后,我们手动传播一个 channelRead事件
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
System.out.println( "hello OutBoundHandlerA");
ctx.write(ctx, promise);
}
}
public class OutBoundHandlerC extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
System.out.println( "hello OutBoundHandlerC");
ctx.write(ctx, promise);
}
}
下面我们把断点调试,把断点打在OutBoundHandlerB
的handlerAdded
上, 模拟向客户端发送数据, 启动程序,大概的流程如下
- 等待服务端的启动
- 服务端Selector轮询服务端channel可能发生的感兴趣的事件
- 使用telnet向服务端发送请求
- 服务端创建客户端的channel,在给客户端的原生的chanenl注册到 Selector上
- 通过
invokeChannelAddedIfNeeded()
将我们添加在Initializer中的handler添加到pipeline中- 挨个回调这些handler中的
channelAdded()
方法- 和我们添加进去的顺序相反
- C --> B --->A
- 这些childHandler,会添加在每一条客户端的channel的pipeline
- 挨个回调这些handler中的
- 传播channel注册完成事件
- 传播channelActive事件
- readIfAutoRead() 完成二次注册netty可以处理的感兴趣的事件
此外,我们上面的write以定时任务的形式提交,当用ctx中的唯一的线程执行器三秒后去执行任务,所以程序会继续下去绑定端口, 过了三秒后把定时任务聚合到普通任务队列中,那时才会执行我们OutBoundHandlerB
中的 ctx.channel().write("Hello World");
outBound类型的handler添加顺序和执行顺序有什么关系
因为Outbound类型的事件是从链表的tail开始传播的,所以执行的顺序和我们的添加进去的顺序相反
篇幅太长了,重写补一张图
从ctx.channel().write("Hello World");
开始跟源码, 鼠标直接跟进去,进入的是ChannelOutboundInvoker
, 往channel中写,我们进入DefaultChannelPipeline
的实现,源码如下
@Override
public final ChannelFuture write(Object msg) {
return tail.write(msg);
}
再一次的验证了,出站的事件是从尾部往前传递的, 我们知道,tail节点是DefaultChannelHandlerContext
类型的,所以我们看它的write()
方法是如何实现的
@Override
public ChannelFuture write(Object msg) {
return write(msg, newPromise());
}
其中msg-->我们要写会客户端的内容, newPromise()默认的promise()
,继续跟进本类方法write(msg, newPromise())
,源码如下:
@Override
public ChannelFuture write(final Object msg, final ChannelPromise promise) {
if (msg == null) {
throw new NullPointerException("msg");
}
try {
if (isNotValidPromise(promise, true)) {
ReferenceCountUtil.release(msg);
// cancelled
return promise;
}
} catch (RuntimeException e) {
ReferenceCountUtil.release(msg);
throw e;
}
write(msg, false, promise);
return promise;
}
上面做了很多判断,其中我们只关心write(msg, false, promise);
源码如下:
private void write(Object msg, boolean flush, ChannelPromise promise) {
AbstractChannelHandlerContext next = findContextOutbound();
final Object m = pipeline.touch(msg, next);
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
if (flush) {
next.invokeWriteAndFlush(m, promise);
} else {
next.invokeWrite(m, promise);
}
我们可以看到,重要的逻辑findContextOutbound();
它的源码如下, 从尾节点开始遍历链表,找到前一个outbound类型的handler
private AbstractChannelHandlerContext findContextOutbound() {
AbstractChannelHandlerContext ctx = this;
do {
ctx = ctx.prev;
} while (!ctx.outbound);
return ctx;
}
找到后,因为我们使用函数是write
而不是writeAndFlush
所以进入上面的else代码块invokeWrite
private void invokeWrite(Object msg, ChannelPromise promise) {
if (invokeHandler()) {
invokeWrite0(msg, promise);
} else {
write(msg, promise);
}
}
继续跟进invokeWrite0(msg, promise);
终于看到了handler的write逻辑
private void invokeWrite0(Object msg, ChannelPromise promise) {
try {
((ChannelOutboundHandler) handler()).write(this, msg, promise);
} catch (Throwable t) {
notifyOutboundHandlerException(t, promise);
}
}
其中:
- (ChannelOutboundHandler) handler() -- 是tail前面的节点
- 调用当前节点的write函数
实际上就是回调我们自己的添加的handler的write函数,我们跟进去,源码如下:
public class OutBoundHandlerC extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
System.out.println( "hello OutBoundHandlerC");
ctx.write(msg, promise);
}
}
我们继续调用write, 按照相同的逻辑,msg会继续往前传递
一直传递到HeadContext节点, 因为这个节点也是Outbound类型的, 这就是Outbound事件的传播,我们直接看HeaderContext是如何收尾的, 源码如下:
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
unsafe.write(msg, promise);
}
Header使用了unsafe类,这没毛病,和数据读写有关的操作,最终都离不开unsafe
为什么Header节点是outBound类型的处理器?
拿上面的write事件来说,msg经过这么多handler的加工,最终的目的是传递到客户端,所以netty把header设计为outBound类型的节点,由他完成往客户端的写
context.write()与context.channel().write()的区别
context.write()
,会从当前的节点开始往前传播context.channel().write()
从尾节点开始依次往前传播
异常的传播
netty中如果发生了异常的话,异常事件的传播和当前的节点是 入站和出站处理器是没关系的,一直往下一个节点传播,如果一直没有handler处理异常,最终由tail节点处理
最佳的异常处理解决方法
既然异常的传播和入站和出站类型的处理器没关系,那么我们就在pipeline的最后,也就是tail之前,添加我们的统一异常处理器就好了, 就像下面:
public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// todo 异常处理的最佳实践, 最pipeline的最后添加异常处理handler
channelPipeline.addLast(new myExceptionCaughtHandler());
}
}
public class myExceptionCaughtHandler extends ChannelInboundHandlerAdapter {
// 最终全部的异常都会来到这里
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if (cause instanceof 自定义异常1){
}else if(cause instanceof 自定义异常2){
}
// todo 下面不要往下传播了
// super.exceptionCaught(ctx, cause);
}
}
SimpleChannelInboundHandler 的特点
通过前面的分析,我们知道如果客户端的msg一味的往后传播,最终会传播到tail节点,由tail节点处理释放,从而避免了内存的泄露
如果我们的handler使用了msg之后没有往后传递就要倒霉了,时间久了就会出现内存泄露的问题
netty人性化的为我们提供的指定泛型的 SimpleChannelInboundHandler<T>
,可以为我们自动的释放内存,我们看他是如何做到的
/ todo 直接继承于ChanelInboundHandlerAdapter的实现 抽象类
// todo 我们自己的处理器, 同样可以继承SimpleChannelInboundHandler适配器,达到相同的效果
public abstract class SimpleChannelInboundHandler<I> extends ChannelInboundHandlerAdapter {
private final TypeParameterMatcher matcher;
private final boolean autoRelease;
protected SimpleChannelInboundHandler() {
this(true);
}
protected SimpleChannelInboundHandler(boolean autoRelease) {
matcher = TypeParameterMatcher.find(this, SimpleChannelInboundHandler.class, "I");
this.autoRelease = autoRelease;
}
protected SimpleChannelInboundHandler(Class<? extends I> inboundMessageType) {
this(inboundMessageType, true);
}
protected SimpleChannelInboundHandler(Class<? extends I> inboundMessageType, boolean autoRelease) {
matcher = TypeParameterMatcher.get(inboundMessageType);
this.autoRelease = autoRelease;
}
public boolean acceptInboundMessage(Object msg) throws Exception {
return matcher.match(msg);
}
// todo channelRead 完全被改写了
// todo 这其实又是一种设计模式 , 模板方法设计模式
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
boolean release = true;
try {
if (acceptInboundMessage(msg)) {
@SuppressWarnings("unchecked")
// todo 把消息进行了强转
I imsg = (I) msg;
// todo channelRead0()在他的父类中是抽象的,因此我们自己写handler时,需要重写它的这个抽象的 方法 , 在下面
// todo 这其实又是一种设计模式 , 模板方法设计模式
channelRead0(ctx, imsg);
} else {
release = false;
ctx.fireChannelRead(msg);
}
} finally {// todo 对msg的计数减一, 表示对消息的引用减一. 也就意味着我们不要在任何
if (autoRelease && release) {
ReferenceCountUtil.release(msg);
}
}
}
protected abstract void channelRead0(ChannelHandlerContext ctx, I msg) throws Exception;
}
- 它本身是抽象类,抽象方法是
channelRead0
,意味着我们需要重写这个方法 - 他继承了
ChannelInboundHandlerAdapter
这是个适配器类,使他可以仅实现部分自己需要的方法就ok
我们看它实现的channelRead
, 模板方法设计模式 主要做了如下三件事
- 将msg 强转成特定的泛型类型的数据
- 将ctx和msg传递给自己的chanenlRead0使用msg和ctx(ctx,msg)
- chanenlRead0使用msg和ctx
- 在finally代码块中,将msg释放
Netty-Pipeline深度解析的更多相关文章
- Kafka深度解析
本文转发自Jason’s Blog,原文链接 http://www.jasongj.com/2015/01/02/Kafka深度解析 背景介绍 Kafka简介 Kafka是一种分布式的,基于发布/订阅 ...
- java8Stream原理深度解析
Java8 Stream原理深度解析 Author:Dorae Date:2017年11月2日19:10:39 转载请注明出处 上一篇文章中简要介绍了Java8的函数式编程,而在Java8中另外一个比 ...
- mybatis 3.x源码深度解析与最佳实践(最完整原创)
mybatis 3.x源码深度解析与最佳实践 1 环境准备 1.1 mybatis介绍以及框架源码的学习目标 1.2 本系列源码解析的方式 1.3 环境搭建 1.4 从Hello World开始 2 ...
- 蓝鲸DevOps深度解析系列(2):蓝盾流水线初体验
关注嘉为科技,获取运维新知 前面一篇文章<蓝鲸DevOps深度解析系列(1):蓝盾平台总览>,我们总览了蓝鲸DevOps平台的背景.应用场景.特点和能力: 接下来我们继续解析蓝盾平台的 ...
- Kafka深度解析(如何在producer中指定partition)(转)
原文链接:Kafka深度解析 背景介绍 Kafka简介 Kafka是一种分布式的,基于发布/订阅的消息系统.主要设计目标如下: 以时间复杂度为O(1)的方式提供消息持久化能力,即使对TB级以上数据也能 ...
- Netty源码解析—客户端启动
Netty源码解析-客户端启动 Bootstrap示例 public final class EchoClient { static final boolean SSL = System.getPro ...
- Netty源码解析---服务端启动
Netty源码解析---服务端启动 一个简单的服务端代码: public class SimpleServer { public static void main(String[] args) { N ...
- Go netpoll I/O 多路复用构建原生网络模型之源码深度解析
导言 Go 基于 I/O multiplexing 和 goroutine 构建了一个简洁而高性能的原生网络模型(基于 Go 的I/O 多路复用 netpoll),提供了 goroutine-per- ...
- Netty Pipeline与ChannelHandler那些事
Pipeline和ChannelHandler是Netty处理流程的重要组成部分,ChannelHandler对应一个个业务处理器,Pipeline则是负责将各个ChannelHandler串起来的& ...
- Feign Ribbon Hystrix 三者关系 | 史上最全, 深度解析
史上最全: Feign Ribbon Hystrix 三者关系 | 深度解析 疯狂创客圈 Java 分布式聊天室[ 亿级流量]实战系列之 -25[ 博客园 总入口 ] 前言 疯狂创客圈(笔者尼恩创建的 ...
随机推荐
- 零元学Expression Blend 4 - Chapter 17 用实例了解互动控制项「CheckBox」I
原文:零元学Expression Blend 4 - Chapter 17 用实例了解互动控制项「CheckBox」I 本章将教大家如何运用CheckBox做实作上的变化:教你如何把CheckBox变 ...
- PHP 文件操作的各种姿势
使用 SPL 库 SPL 是 PHP 标准库,用于解决典型问题的一组接口与类的集合. 迭代器 FilesystemIterator 官方文档:http://php.net/manual/zh/clas ...
- delphi读取ini文件
ini文件在系统配置及应用程序参数保存与设置方面,具有很重要的作用,所以可视化的编程一族,如vb.vc.vfp.delphi等都提供了读写ini文件的方法,其中delphi中操作ini文件,最为简洁, ...
- Elevate Web Builder for Web Developers(类似于unigui的东西)
推荐一款pascal 语言的web 开发工具 这几天仔细研究了一款使用Pascal 语言开发web 的工具 具体介绍可以参照这里. 先上几张他开发的页面照.
- Window文件目录遍历 和 WIN32_FIND_DATA 结构(非常详细的中文注释)
第一部分 *百度百科提供的内容总结:WIN32_FIND_DAT 第二部分 *程序实例 第三部分 *一篇使用FindFirstFile和FindNextFile函数的博文 第一部分 ...
- QTcpSocket 对连接服务器中断的不同情况进行判定(六种情况,其中一种使用IsNetworkAlive API方法)
简述 对于一个C/S结构的程序,客户端有些时候需要实时得知与服务器的连接状态.而对于客户端与服务器断开连接的因素很多,现在就目前遇到的情况进行一下总结. 分为下面六种不同情况 客户端网线断开 客户端网 ...
- 95+强悍的jQuery图形效果插件
现在的网站越来越离不开图形,好的图像效果能让你的网站增色不少.通过JQuery图形效果插件可以很容易的给你的网站添加一些很酷的效果. 使用JQuery插件其实比想象的要容易很多,效果也超乎想象.在本文 ...
- 条款09:绝不在构造和析构过程中调用virtual函数
不该在构造函数和析构函数期间调用virtual函数,这一点是C++与jave/C#不同的地方之一. 假设有一个class继承体系,用来模拟股市交易如买进.卖出的订单等等.这样的交易一定要经过审计,所以 ...
- pip与conda的区别
pip和conda到底有什么不一样? 今天看到我的foreman开始报错去询问才发现.我们的python包管理工具已经从pip整体迁移到了conda..最近的迁移真的非常多..前端也在迁移打包 跟着发 ...
- pc微信浏览器打开页面显示空白,其他浏览器正常
pc微信浏览器不兼容es6的语法糖.