AI图鉴

AI图鉴首页 › AI 资讯 › Netty 4.2.x 源码深度解析 (二十一):Pipeline 事件传播 —

Netty 4.2.x 源码深度解析 (二十一):Pipeline 事件传播 —— Inbound 与 Outbound 的双向流转

来源:掘金 · 发布时间:10-28 · 由 AI图鉴每小时自动聚合

ChannelPipeline 是一条由 ChannelHandlerContext 节点首尾相接构成的双向链表,两端分别是 HeadContext、TailContext 两个哨兵节点。链条搭好之后

ChannelPipeline 是一条由 ChannelHandlerContext 节点首尾相接构成的双向链表,两端分别是 HeadContext、TailContext 两个哨兵节点。链条搭好之后,事件如何在链上流动,才是责任链模式的灵魂所在。Netty 把 Channel 上的 IO 操作分成两类:一类是由底层网络触发、从 IO 线程向业务代码层层流入的 Inbound 事件,比如"注册完成""连接激活""读到了数据";另一类是由业务代码发起、向底层 Unsafe 逐层下沉的 Outbound 事件,比如"绑定端口""建立连接""写出数据""关闭连接"。这两类事件在同一条 Pipeline 上沿相反方向传播:Inbound 从 HeadContext 向 TailContext 正向推进,Outbound 从 TailContext 向 HeadContext 反向回溯,最终以 HeadContext 为分水岭,完成了"网络数据输入"与"业务数据输出"的对称闭环。理解这套双向传播机制,是读懂 handler 之间如何做事件分发、为什么 ctx.write() 与 channel.write() 行为会不一样的关键。

围绕 Inbound 正向传播、Outbound 反向传播、写刷新的语义分离与异常处理四条主线,以下几个问题将贯穿本文的源码分析主线:

- DefaultChannelPipeline 的 fire 方法为何要先判断 head.executor().inEventLoop(),非 EventLoop 线程调用时又是如何把事件切回目标线程的?

- findContextInbound() 与 findContextOutbound() 如何分别在 next 链和 prev 链上查找下一个匹配的 ctx,skipContext() 又凭什么用 executionMask 跳过未实现该事件的 handler?fireChannelRead() 内那段三分支内联派发,背后隐藏着什么性能考虑?

- Outbound 事件为何统一从 TailContext 起步,ctx.write() 与 ctx.channel().write() 的传播起点差异会带来怎样不同的 handler 拦截结果?

- write() 与 flush() 在语义上如何分离,writeAndFlush() 又是如何通过 flush 标志合并 MASK_WRITE | MASK_FLUSH 一次完成"写缓冲 + 刷出"?

- Inbound 异常与 Outbound 异常为何走两条不同的处理路径,notifyOutboundHandlerException() 使 Promise 失败与 invokeExceptionCaught() 沿 Inbound 链传播各自适用于什么场景?

本文将沿着 Inbound 正向传播与 Outbound 反向传播两条互逆的路径,先建立双向传播的对称模型,再分别深入两类事件的查找、跳过与派发机制,随后拆解 write/flush 的分层语义与起点差异,最后分析异常传播的两条处理链路。

一、双向传播模型:Inbound 与 Outbound 的对称设计

1.1 两类事件与两条传播方向

所谓责任链传播,是指 Pipeline 本身只负责"存链",事件的流动则依赖 ChannelHandlerContext 上的一组 fire 与 invoke 方法,由每个 ctx 逐步把事件传递给下一个 ctx。这个"存链"与"传链"的分离,正是 Netty 责任链设计的精妙之处:链表结构是静态的,事件传播是动态的,二者通过 ChannelHandlerContext 上的方法调用在运行期粘合起来。

Netty 根据事件的来源方向,把它们划成泾渭分明的两类。Inbound 事件由底层网络触发,从 IO 线程流入业务代码,方向是"从底层到应用";Outbound 事件由业务代码发起,流向底层 Unsafe,方向是"从应用到底层"。这两类事件不仅来源相反,在 Pipeline 上的传播方向也恰好相反,如下图所示:

可以看到,HeadContext 与 TailContext 这两个哨兵恰好承担了相反方向的职责:HeadContext 既是 Inbound 的起点,又是 Outbound 的终点;TailContext 既是 Outbound 的起点,又是 Inbound 的终点。Inbound 沿 next 链正向推进,Outbound 沿 prev 链反向回溯。

两类事件的另一组差异体现在触发源与落点上。Inbound 由 IO 线程或底层网络触发,发起点是 HeadContext,终点是 TailContext,消息走到链尾仍无人处理时,由它负责释放资源或告警;Outbound 由业务代码发起,发起点是 TailContext,终点是 HeadContext,最终落到 unsafe.* 上执行真正的底层操作。查找目标时,Inbound 用 findContextInbound(mask),Outbound 用 findContextOutbound(mask),而二者共用同一个 skipContext(executionMask) 判定,用 executionMask 跳过不关心该事件的 handler。

1.2 ChannelInboundInvoker 与 ChannelOutboundInvoker:两类事件的契约

Inbound 事件的"传播契约"定义在 ChannelInboundInvoker 接口里,它只声明了九个 fire 方法,每个方法对应一个 Inbound 事件:

io.netty.channel.ChannelInboundInvoker

public interface ChannelInboundInvoker {

ChannelInboundInvoker fireChannelRegistered();

ChannelInboundInvoker fireChannelUnregistered();

ChannelInboundInvoker fireChannelActive();

ChannelInboundInvoker fireChannelInactive();

ChannelInboundInvoker fireExceptionCaught(Throwable cause);

ChannelInboundInvoker fireUserEventTriggered(Object event);

ChannelInboundInvoker fireChannelRead(Object msg);

ChannelInboundInvoker fireChannelReadComplete();

ChannelInboundInvoker fireChannelWritabilityChanged();

}

这九个方法可以粗略分成两组:生命周期的四个(fireChannelRegistered、fireChannelActive、fireChannelInactive、fireChannelUnregistered),以及数据、异常与用户事件的五个(fireChannelRead、fireChannelReadComplete、fireExceptionCaught、fireUserEventTriggered、fireChannelWritabilityChanged)。约定俗成的命名里,fire 前缀表示"向下一个 handler 抛出事件",是 Inbound 传播的动词。

与 Inbound 对应,Outbound 事件的操作契约定义在 ChannelOutboundInvoker 接口里,它声明的是"动作"而非"通知":

io.netty.channel.ChannelOutboundInvoker

// 仅列核心动作方法,无参 default 重载与 newPromise 等辅助方法省略

public interface ChannelOutboundInvoker {

ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise);

ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise);

ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise);

ChannelFuture disconnect(ChannelPromise promise);

ChannelFuture close(ChannelPromise promise);

ChannelFuture deregister(ChannelPromise promise);

ChannelOutboundInvoker read();

ChannelFuture write(Object msg, ChannelPromise promise);

ChannelOutboundInvoker flush();

ChannelFuture writeAndFlush(Object msg, ChannelPromise promise);

}

Outbound 的方法命名没有 fire 前缀,因为它们是"我要执行某个操作"的主动请求,而不是"发生了什么"的被动通知。值得留意的是,Outbound 方法大多返回 ChannelFuture,意味着这些操作是异步的,其结果通过 promise 通知;只有 read()、flush() 两个方法返回的是自身类型,因为它们的语义更倾向"触发一次动作"而非"等待一个结果"。这两份接口契约一起,构成了 Pipeline 双向传播的完整事件清单。

1.3 HeadContext 与 TailContext:双向的分水岭

两个哨兵节点之所以能同时扮演起点与终点两种角色,根源在于它们各自实现的方式不同。HeadContext 同时实现了 ChannelInboundHandler 与 ChannelOutboundHandler 两个接口,而 TailContext 只实现了 ChannelInboundHandler:

io.netty.channel.DefaultChannelPipeline.HeadContext / TailContext

final class HeadContext extends AbstractChannelHandlerContext

implements ChannelOutboundHandler, ChannelInboundHandler {

private final Unsafe unsafe;

HeadContext(DefaultChannelPipeline pipeline) {

super(pipeline, null, HEAD_NAME, HeadContext.class);

// 直接拿到底层 Unsafe,作为 Outbound 事件的最终执行者

unsafe = pipeline.channel().unsafe();

setAddComplete();

}

// ...

}

// 只处理 Inbound 的兜底节点

final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler {

TailContext(DefaultChannelPipeline pipeline) {

super(pipeline, null, TAIL_NAME, TailContext.class);

setAddComplete();

}

// ...

}

HeadContext 的构造器里保存了对底层 Unsafe 的引用,这决定了它是 Outbound 事件的最终落点:所有 Outbound 请求回溯到头之后,都由它把请求交给 unsafe 去真正执行。而 TailContext 只实现 Inbound 接口,意味着当 Inbound 事件一路传到最后却无人处理时,由它来做"兜底",释放消息、告警异常。两个 setAddComplete() 调用则把自己的状态直接置为 ADD_COMPLETE:哨兵节点没有真正的业务 handler,构造时即视为"已就绪",不再等待 handlerAdded 回调,这也为后文 invokeHandler() 的门控逻辑埋下了伏笔。

二、Inbound 事件传播:从 Head 到 Tail 的正向流转

2.1 fire 方法入口:EventLoop 检查与线程切换

Inbound 传播的入口是 DefaultChannelPipeline 上的 fire 方法。以 fireChannelRead 为例,它的实现极其统一:

io.netty.channel.DefaultChannelPipeline#fireChannelRead

@Override

public final ChannelPipeline fireChannelRead(Object msg) {

if (head.executor().inEventLoop()) {

if (head.invokeHandler()) {

head.channelRead(head, msg);

} else {

head.fireChannelRead(msg);

}

} else {

head.executor().execute(() -> fireChannelRead(msg));

}

return this;

}

这段代码看似简单,却藏着两层关键设计。第一层是线程切换:head.executor().inEventLoop() 判断当前线程是否就是 HeadContext 绑定的 EventLoop,如果不是,就把整个 fireChannelRead 通过 execute() 重新提交回 EventLoop 执行。这个判断贯穿了所有 fire 方法与所有 handler 的传播点,是 Netty 保证"handler 永远在它绑定的 EventLoop 线程上串行执行"的基石,也正因为有了它,handler 内部才能放心地不加锁地读写自身状态。

第二层是入口双分支:head.invokeHandler() 为 true 时,直接调用 head.channelRead(head, msg),注意这里调用的是 HeadContext 自己覆写的 channelRead 方法;为 false 时则调用 head.fireChannelRead(msg) 跳过 Head 继续向前传播。对于 HeadContext 而言,由于构造器里已经 setAddComplete(),invokeHandler() 恒为 true,所以入口总是走前者,HeadContext 被当作"第一个 Inbound handler"来调用。它的 channelRead 实现本身不消费消息,只是把事件继续抛给下一个 handler:

io.netty.channel.DefaultChannelPipeline.HeadContext#channelRead

@Override

public void channelRead(ChannelHandlerContext ctx, Object msg) {

ctx.fireChannelRead(msg);

}

HeadContext 扮演的是"起点转发器"的角色:它接到事件后,立刻调用 ctx.fireChannelRead(msg) 把事件交回传播链。这里的 ctx 就是 HeadContext 自身,ctx.fireChannelRead 会进入 AbstractChannelHandlerContext.fireChannelRead,开始真正的逐个 handler 派发。

2.2 fireChannelRead 的完整传播链路

从 HeadContext 转发出来的 fireChannelRead,属于 AbstractChannelHandlerContext 的实例方法,它是 Inbound 传播真正的核心:

io.netty.channel.AbstractChannelHandlerContext#fireChannelRead

@Override

public ChannelHandlerContext fireChannelRead(final Object msg) {

// 沿 next 链查找下一个实现 channelRead 的 handler

AbstractChannelHandlerContext next = findContextInbound(MASK_CHANNEL_READ);

if (next.executor().inEventLoop()) {

// 泄漏追踪:消息进入 handler 前打点

final Object m = pipeline.touch(msg, next);

if (next.invokeHandler()) {

try {

final ChannelHandler handler = next.handler();

final DefaultChannelPipeline.HeadContext headContext = pipeline.head;

if (handler == headContext) {

headContext.channelRead(next, m);

} else if (handler instanceof ChannelDuplexHandler) {

((ChannelDuplexHandler) handler).channelRead(next, m);

} else {

((ChannelInboundHandler) handler).channelRead(next, m);

}

} catch (Throwable t) {

next.invokeExceptionCaught(t);

}

} else {

next.fireChannelRead(m);

}

} else {

next.executor().execute(() -> fireChannelRead(msg));

}

return this;

}

整个方法可以拆成三步来读。第一步是 findContextInbound(MASK_CHANNEL_READ):沿 next 链找到下一个"实现了 channelRead"的 ctx,如果某个 handler 没有覆写 channelRead,就被直接跳过。第二步是 next.executor().inEventLoop() 的线程检查:目标 handler 若绑定在另一个 EventExecutor 上,就把本次传播整体提交到那个执行器,从而把"线程边界"也纳入传播语义。第三步是真正的派发:next.invokeHandler() 为 true 时调用 handler 的方法,否则调用 next.fireChannelRead(m) 继续向前转发。

先看那个 pipeline.touch(msg, next)。它是 Netty 泄漏检测的一部分:在泄漏检测开启时,读到的消息在进入每个 handler 之前都会打一次点,记录下"这条消息现在到了哪个 handler";若未开启,touch 方法会直接原样返回消息,不做任何额外动作。一旦消息后来发生泄漏(没有被释放),ResourceLeakDetector 就能根据这些打点日志,准确地定位出是哪一个 handler 手里"扣着没还"。

再看包裹派发的那段 try/catch:handler 的 channelRead 一旦抛出 Throwable,就调用 next.invokeExceptionCaught(t) 把异常转入异常传播链(本文第五章展开),而不是让异常直接崩掉 IO 线程。这个"异常兜底"几乎出现在每一个 fire 方法与 handler 派发点,是 Netty 保证事件循环健壮性的通用手段。

2.3 findContextInbound 与 skipContext:查找与跳过

上一步反复出现的 findContextInbound 是 Inbound 传播的方向感来源,它与 Outbound 的 findContextOutbound 成对出现,连同 skipContext 一起构成"查找目标 ctx"的完整逻辑:

io.netty.channel.AbstractChannelHandlerContext#findContextInbound / #findContextOutbound

private AbstractChannelHandlerContext findContextInbound(int mask) {

AbstractChannelHandlerContext ctx = this;

EventExecutor currentExecutor = executor();

do {

ctx = ctx.next;

} while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_INBOUND));

return ctx;

}

private AbstractChannelHandlerContext findContextOutbound(int mask) {

AbstractChannelHandlerContext ctx = this;

EventExecutor currentExecutor = executor();

do {

ctx = ctx.prev;

} while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_OUTBOUND));

return ctx;

}

private static boolean skipContext(

AbstractChannelHandlerContext ctx, EventExecutor currentExecutor, int mask, int onlyMask) {

return (ctx.executionMask & (onlyMask | mask)) == 0 ||

(ctx.executor() == currentExecutor && (ctx.executionMask & mask) == 0);

}

两个查找方法的结构完全对称:Inbound 沿 ctx.next 前进,Outbound 沿 ctx.prev 回溯,都用 do-while 循环配合 skipContext 决定是否继续走。skipContext 的返回值为 true 表示"应该跳过这个 ctx",它有两个通过 || 连接的跳过条件。

第一个条件 (ctx.executionMask & (onlyMask | mask)) == 0 判断的是"方向不沾边":onlyMask 是 MASK_ONLY_INBOUND 或 MASK_ONLY_OUTBOUND,是该方向全部事件的位集合。若 ctx 的 executionMask 与它无交集,即 ctx 完全不实现这个方向的事件,就整体跳过;mask 单独并入,是因为 MASK_EXCEPTION_CAUGHT 不在 MASK_ONLY_INBOUND 内,需要单独纳入判断。

第二个条件 (ctx.executor() == currentExecutor && (ctx.executionMask & mask) == 0) 判断的是"同执行器下没实现":当目标 ctx 与当前 handler 用的是同一个 EventExecutor 时,只要它没实现这个具体事件(mask 位不命中),就可以跳过;反之,如果目标 ctx 用的是不同的执行器,即便它没实现这个事件也不能跳过,因为必须把线程切过去才能保证跨执行器场景下的事件顺序(跨执行器不跳过的语义正源于此)。这两个条件的精妙之处在于,它把"类型过滤"与"顺序保证"两件事,用一个 executionMask 位图就统一表达了。

2.4 invokeHandler 门控与三分支内联派发

findContextInbound 找到目标 ctx 后,真正决定"要不要调用这个 handler"的是 invokeHandler():

io.netty.channel.AbstractChannelHandlerContext#invokeHandler

boolean invokeHandler() {

int handlerState = this.handlerState;

return handlerState == ADD_COMPLETE || (!ordered && handlerState == ADD_PENDING);

}

它读的是 handlerState 这个状态位,逻辑很直白:只有当 handler 已经处于 ADD_COMPLETE(handlerAdded 已回调)时才调用它;在 !ordered(非顺序执行器)的场景下,ADD_PENDING 状态也放行。这个门控的价值在于,DefaultChannelPipeline 有时会先把 handler 挂进链表、却还没调用它的 handlerAdded(延迟添加机制),此时事件若直接触达 handler,handler 可能处于"半初始化"状态。于是 Netty 用 invokeHandler() 把这种 handler 挡在门外,只把事件继续转发。

派发本身采用的是那段三分支内联写法。它之所以要先判 handler == headContext,再判 handler instanceof ChannelDuplexHandler,最后才强转 ChannelInboundHandler,是为了规避一个 JVM 方法调用层面的性能退化:当一个 handler 同时实现了 Inbound 与 Outbound 两个接口时(即 duplex handler),对它做接口类型校验(instanceof)时,会竞争每个类仅有一个槽位的 secondary_super_cache,多线程下频繁刷新该槽位会引发 CPU cache line 在处理器间 ping-pong,导致扩展性下降。Netty 用这段显式多分支先按具体类型分流,规避这一退化。

2.5 TailContext 终点:消息的兜底释放

如果事件一路传下来没有任何 handler 消费,最终会抵达 TailContext。拿 channelRead 来说,TailContext 的实现是:

io.netty.channel.DefaultChannelPipeline.TailContext#channelRead

(onUnhandledInboundMessage 为外部类 DefaultChannelPipeline 的 protected 方法)

@Override

public void channelRead(ChannelHandlerContext ctx, Object msg) {

onUnhandledInboundMessage(ctx, msg);

}

protected void onUnhandledInboundMessage(ChannelHandlerContext ctx, Object msg) {

onUnhandledInboundMessage(msg);

if (logger.isDebugEnabled()) {

logger.debug("Discarded message pipeline : {}. Channel : {}.",

ctx.pipeline().names(), ctx.channel());

}

}

protected void onUnhandledInboundMessage(Object msg) {

try {

logger

本文由 AI图鉴自动抓取并翻译,版权归原作者所有;如需查看原文请点下方链接。

相关 AI 工具

相关免费羊毛

同源资讯

阅读原文(juejin.cn)

标题、摘要与正文均来自该信源的公开页面,版权归原作者所有;本站自动聚合与翻译,仅作出处跳转。

← 返回 AI 资讯列表AI工具图鉴首页

返回 AI图鉴首页 · 提交工具 · 商务合作 · 关于本站 · 596 款 AI 工具分类导航 · 每日 AI 热门项目与动态