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图鉴自动抓取并翻译,版权归原作者所有;如需查看原文请点下方链接。
ChannelPipeline is a two-way chain consisting of the end of the ChannelhandlerContext node, which is the HeadContext and TailContext two sentry nodes. Once the chain is in place, it is the soul of the chain of responsibility that is how events flow through it. Netty divides IO operations on Channel into two categories: Inborn events triggered by a bottom network, which flow from the IO thread to the business code layer, such as "Registration Complete" "Connect Activation" reading data; and Outborn events initiated by business code, which sink down to the bottom Unsafe layer, such as "Build Port" creating a connection, "Close data." These two types of events spread in the opposite direction on the same page: Inbound from HeadContext to TailContext is moving forward, Outlook from TailContext to HeadContext in reverse, and finally, HeadContext as a watershed, completing the symmetrical closed loop between "web data entry" and "business data output". Understanding this set of two-way communication mechanisms is key to understanding how events are distributed between handlers and why ctx.write() is different from chanell.write() behavior.
The four main lines around which the Inborn is spreading, Outlook is spreading backwards, writing new semantic separations and anomalies, and the following questions will run through the main lines of the source code analysis in this paper:
- Delfault ChannelPipeline's fire method to determine why head.executor().inEventLoop(), how did the event get back to the target when the nonEventLop route was called?
-FindContextIncast() and FindContextOutbound() how to find the next matching ctx,skipContext() on the next chain and prev, respectively, and how to skip handler over the non-achievable event with an external Mask? What's behind that three-brand joint distribution?
- Why does the Outbaund event begin from the TailContext, ctx.write() and ctx.channel().write() spread starting point lead to different handler intercept results?
- write() how to segregate with flush(), writeAndFlush() how to merge MASK WRITE | MASK FLUSH at a time "write buffer + brush out"?
- Why does the Inbound anomaly and the Outbound anomaly take two different processing paths, notify Outlook HandlerException() make Promise fail and invokeExceptionCaught() spread their respective scenarios along the Inborn chain?
This paper will follow the two opposing paths that are being transmitted from Inbound to Outlook, starting with a two-way symmetric model of transmission, going into the search, skipping and distribution mechanisms for each of the two types of events, then breaking down the divide between the phrases and starting points of the write/flush, and finally analysing the two treatment links of unusual transmission.
I. Two-way dissemination model: symmetric design for Inbound and Outlook
1.1 Two types of events and two transmission directions
So-called chain-of-responsibility transmission means Pipeline itself is responsible only for the chain of storage, while the flow of events depends on a set of fire and invoke methods on Channel HandlerContext, with each ctx gradually passing the event to the next ctx. The separation of the chain from the chain is precisely what Netty designed the chain of responsibility: The chain table structure is static, the event transmission is dynamic, and the two are used to bind during the running period by the Channel HandlerContext method.
Netty classified the events into two distinct categories depending on their origin. The Inborn event is triggered by the bottom network, from the IO thread to the business code, in the direction of "from the bottom to the application"; the Outbound event is initiated by the business code and flows to the bottom Unsafe, in the direction of "from application to the bottom". The two types of events are not only from the opposite source, but also from the opposite direction on Pipeline, as shown in the figure below:
As can be seen, HeadContext and TailContext have taken on the opposite responsibility: HeadContext is both the starting point and the end point of Inbound; TaylorContext is both the starting point and the end point of Outlook. Inborn is advancing along the next chain, Outlook is backtraced along the prev chain.
Another set of differences between the two types of events is reflected in the trigger source and location. The launch point is HeadContext, which is responsible for releasing resources or alerting when the message is left unprocessed at the end of the chain; Outlook, which is initiated by business code, is TaylorContext, which ends with HeadContext and ends with real bottom operations on unsafe.*. In searching for the target, the Inbound decision is based on the FindContextIncast (mask), the Outlook is based on the FindContextOutboard (mask), and the two are based on the same sskipContext (executionMask) decision to skip Handler without concern.
1.2 ChannelInbondInvoker and ChannelOutbondInvoker: contracts for two types of events
The Inborn event, defined as the "discretion contract" in the ChannelInboundInvoker interface, only states nine fire-based methods, each of which corresponds to an Inborn event:
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();
}
The nine approaches can be grouped in two broad groups: four (firechannelregistered, FirechannelActive, FirechannelInactive, FirechannelUnregistered) and five (firechannelRead, FirechannelReadcomplete, FireExceptionCaught, FireUserEventTriggered, FirechannelWritability Changed). In the customary name, the prefix to the "throw the event to the next handler" is the verb that's spread from Inborn.
Corresponding to Inbound, the Operational Contract for the Outlook event is defined in the Channel Outlook Invoker interface, which says "action" rather than "notification":
io.netty.channel.ChannelOutboundInvoker
/ / List only the core action method, unscrew reload and newPromise etc.
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);
}
The Outlook method name doesn't have a fire prefix because they're active requests for "I'm going to do something," not passive notifications of "what happened." It is interesting to note that most of the Outboard methods return to ChannelFuture, which means that the operations are an anecdotal, and the results are notified by promise; only read(), flush() are returned by their own type because their semantic preference is to "trigger an action" rather than "wait a result". Together, the interfaces constitute a complete list of events disseminated in two directions by Pipeline.
1.3 HeadContext and TailContext: Two-way watershed
The two sentinel nodes were able to play both the starting and the end roles because of the different ways in which they each achieved. HeadContext also achieved two interfaces between ChannelInbondHandler and ChannelOutbornHandler, while TaylorContext achieved only ChannelInbondHandler:
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);
/ / Go straight to the bottom Unsafe as the final implementer of the Outlook event
unsafe = pipeline.channel().unsafe();
setAddComplete();
}
// ...
}
// Handle only the bottom node of Inborn
final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler {
TailContext(DefaultChannelPipeline pipeline) {
super(pipeline, null, TAIL_NAME, TailContext.class);
setAddComplete();
}
// ...
}
HeadContext's constructor saves a reference to the bottom Unsafe, which determines the final destination of the Outboard event: all Outboard requests go back to the beginning, and it gives them to unsafe for real execution. And TailContext only made the Inborn interface, which meant that when the Inborn incident reached the end of the road and was left unattended, it was used as a "dippin" to release news and alarms. Two setAddComplete() calls place their state directly at ADD COMPLETE: Sentry node has no real business handler, is constructed as "ready" and no longer waiting for handlerAdded to return, which also creates an ambush for the late invokehandler() door logic.
ii. Inborn event transmission: positive flow from Head to Tail
2.1 Fire Method Entry: EventLoop Check and Thread Switch
The Inborn transmission portal is the Fire Method on Default Channel Pipeline. To take the example of FireChannelRead, it has achieved great uniformity:
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;
}
The code looks simple, but it contains two layers of critical design. The first level is a linear transition: head.executor().inEventLoop() determines whether the current thread is the EventLoop bound by HeadContext and, if not, re-submits the whole FireChannelRead() back to EventLoop for execution. This judgement runs through the transmission point of all the fire method and all the handler, and it's Netty's cornerstone of the guarantee that "handler will always be running on the Event Loop line that it bound" and because of it, Handler is able to read and write his own state without locks.
The second level is the double branch of the entrance: head.invokeHandler() calls head.channelRead (head, msg) directly when it is true, noting that HeadContext calls himself by overwhelming the channelRead method, and calls head.fireChannelRead (msg) for false to skip the Head forward. For HeadContext, as the builder is setAddComplete(), invokeHandler() is always true, the entrance is always taken, HeadContext is called as "the first Inborn Handler". Its chanelread realizes that it doesn't consume news, but simply throws the event to the next handler:
io.netty.channel.DefaultChannelPipeline.HeadContext#channelRead
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ctx.fireChannelRead(msg);
}
HeadContext plays the role of a "start transponder": It immediately called ctx.fire ChannelRead (msg) to return the incident to the transmission chain. The ctx here is the HeadContext itself, ctx.firechannelRead will enter the Abstract ChannelHandlerContext.fire ChannelRead, and start a real hand-by-handler distribution.
2.2 Full transmission link for FireChannelRead
From HeadContext, fire ChannelRead, which belongs to the case of Abstract ChannelhandlerContext, is the real core of Inborn dissemination:
io.netty.channel.AbstractChannelHandlerContext#fireChannelRead
@Override
public ChannelHandlerContext fireChannelRead(final Object msg) {
/ / Find next handler along the next chain to achieve chanelread
AbstractChannelHandlerContext next = findContextInbound(MASK_CHANNEL_READ);
if (next.executor().inEventLoop()) {
/ Leak Track: A little bit before the message goes 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;
}
The whole approach can be read in three steps. The first step is to find CONTEXTInbound (MASK CHANNEL READ): to find the next ctx "achievement of ChannelRead" along the next chain, and if a Handler does not overwrite ChannelRead, he will skip it. The second step is a thread check for next.executor().inEventLoop(): if the target handler is tied to another EventExecutor, then submit the entire transmission to that implementer, so that the line boundary is included in the communication syntax. The third step is the real distribution: next.invokehandler() calls handler for true times, or next.fire Channel Read(m) continues forward.
First look at that Pipeline. Touch. It's part of Netty's leak detection: At the time of the leak detection, the readings were done once before entering every handler, recording "which handler the message is now; if not, the Touch method returns the message as it is, without any additional action. Once the news is leaked (not released), the research Leak Detector can use these logs to pinpoint which handler's hand is holding back.
And when you look at the part of the package sent by try/catch:handler's Channelread, once it's thrown out of Throwable, you call next.invokeExceptCaught(t) to turn the anomaly into an abnormal transmission chain (expanded in Chapter V of this paper), rather than to let the anomaly simply collapse the IO thread. This "unusual lid" appears almost every fire method and handler distribution point, and it's Netty's common way of guaranteeing the cycle of event strength.
2.3 FindContextIncast and SkipContext: Find and Skip
The last re-emergence of findContextIncast is the source of the orientation of the Inbound transmission, which appears in pairs with the FindContextOutboard, which, along with sskipContext, constitutes the complete logic of finding target 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);
}
The structure of both search methods is fully symmetrical: Inbound follows ctx.next, Outlook follows ctx.prev backtraces, all with do-wire loops to fit SkipContext to decide whether or not to continue. The return value for sskipContext is true "should skip this ctx" and it has two jump conditions to connect via twig.
The first condition (ctx.executionMask & (onlyMask | mask)) = 0 = = 0 judge "in the direction without sides": orlyMask is the position of MASK ONLY INBOUND or MASK ONLY OUTBOUND for all events in that direction. If ctx 's exectionMask does not interact with it, i.e., the event that ctx does not achieve this direction at all, the whole thing skips; mask is merged separately because MASK EXCEPTION CAUGHT is not in MASK ONLY INBOUND and needs to be included in a separate judgement.
The second condition (ctx.executor() == currentExecutor & (ctx.executionMask & mask) = 0) judges "unrealized under an implementer": When the target tx is the same as the current handler, it skips as long as it does not achieve the specific event (mask does not hit); on the contrary, if the target tx uses a different implementer, even if it does not, it cannot skip the event, because it is necessary to cut the thread to ensure the sequence of events across the executioner scene (this is where the synonym that crosses the implementer does not skip). The essence of these two conditions is that it uses "type filters" and "order guarantees" to be expressed in a single map of the situation.
2.4 InvokeHandler Gate Control and Internal Distribution
When you find the target ctx, you really decide if you want to call this handler is invokehandler():
io.netty.channel.AbstractChannelHandlerContext#invokeHandler
boolean invokeHandler() {
int handlerState = this.handlerState;
return handlerState == ADD_COMPLETE || (!ordered && handlerState == ADD_PENDING);
}
It reads HandlerState, which is a state-level, logically clear: it is only called when Handler is already in the ADD COMPLETE (handlerAdded) scene; in the context of the!ordered (unordered implementer), the ADD PENDING state is released. The value of this door control is that Default Channel Pipeline sometimes puts Handler on the chain, but has not called it HandlerAdded (Delay Added Mechanism), when the event reaches handler, Handler may be in a semi-initial state. So Netty used invokehandler() to keep this handler out of the door and just keep the incident going.
The distribution itself is based on the three-branch combination. The reason why it had to start with handler = headContext, and then handler instanceof ChannelDuplexhandler, was to turn to ChannelInboundHandler to avoid the degradation of the performance of an JVM approach at the level: when a handler simultaneously achieved the two interfaces between Inbook and Outlook (i.e., duplex handler), it would compete for an intance-type verification (instanceof) of only one slot in each class, which would trigger the expansional decline of the CPU cache line. Netty uses this explicit multi-branch to divert the degradation by specific types.
2.5 TailContext End: End of the message
If the incident goes all the way down without any handler consumption, it will eventually reach TailContext. Take Channel Head, and TailContext achieves:
io.netty.channel.DefaultChannelPipeline.TailContext#channelRead
(onUnhandInboundMessage for External ChannelPipeline)
@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
Auto-fetched and translated by AI Atlas. All rights belong to the original author.