资讯详情

资讯详情

Netty inBound与outBound处理器传播机制深度解析

1. 项目概述为什么读懂 Channel 的 inBound/outBound 处理器才是 Netty 真正入门的分水岭Netty 不是“会用 EventLoopGroup Bootstrap 就算会了”的框架它是一套精密运转的反应式网络引擎。我带过不少刚从 Spring Boot 转过来的后端同学他们能快速写出一个 EchoServer也能配好 SSL 和心跳检测但一旦遇到连接突然断开时 Handler 执行顺序混乱、自定义编解码器里ctx.fireChannelRead()调用后数据消失、或者ChannelPipeline.addLast()插入位置不对导致日志打不出来——就全卡在“不知道谁先谁后、谁该发给谁”这个点上。而这个问题的根子就在Channel 的 inBound 与 outBound 处理器的双向传播机制里。这不是一个“看看文档就能懂”的概念它是 Netty 整个事件驱动模型的骨架。inBound 事件如channelActive、channelRead、exceptionCaught从底层 socket 向上穿透 pipeline像水流从地底涌向地面outBound 操作如bind、connect、write、flush则从用户代码向下驱动到底层 socket像指令从指挥台逐级下达至前线士兵。二者在 pipeline 中严格遵循“单向链表双向指针”的设计既不交叉也不混流却通过ctx.fireXXX()和ctx.xxx()形成闭环协同。很多人写 Handler 时只关注自己channelRead()里做了什么却没意识到你调用ctx.fireChannelRead(msg)是把消息交给下一个 inBound Handler而调用ctx.write(msg)则是把消息塞进 outBound 链路——这两条路在 pipeline 里根本不在同一层走的是完全不同的指针路径。这个标题里的“Netty十一”不是随意编号而是踩准了学习曲线的关键坡度前十个章节可能讲完线程模型、ByteBuf、EventLoop但只要没吃透 inBound/outBound 的触发时机、传播方向、上下文切换逻辑和异常兜底规则后续所有高级功能——比如零拷贝传输、流量整形、协议栈分层、甚至自定义 EventLoop 分组——都会变成空中楼阁。我自己在某公司做网关中间件重构时就因为没厘清ChannelOutboundHandlerAdapter中write()方法内ctx.write()和ctx.writeAndFlush()的传播差异导致一批请求在重试逻辑中被重复 write 两次最终引发下游服务幂等校验失败。这种问题不会报错只会静默出错排查起来要翻三天源码。所以这篇内容不是“又一篇 Netty 源码解析”而是聚焦一个具体切口以ChannelPipeline为舞台以AbstractChannelHandlerContext为枢纽以HeadContext和TailContext为边界完整还原一次 read 事件如何从 epollWait 返回经由 NIO ByteBuf 解析穿越多个 inBound Handler最终抵达业务 handler再看一次 write 操作如何从业务层发起经由编码器、缓冲区管理、flush 触发最终落到底层 socket 写队列。过程中你会看到为什么addFirst()和addLast()插入的 Handler 行为截然不同为什么ctx.pipeline().fireChannelRead()和ctx.fireChannelRead()效果一样但ctx.channel().pipeline().fireChannelRead()却可能绕过当前上下文为什么exceptionCaught()必须显式调用ctx.fireExceptionCaught()才能继续传播而其他 inBound 事件默认自动传播……这些不是约定俗成的“规范”而是由invokeChannelRead()、invokeWrite()等十几个核心方法的调用栈和next/prev指针走向决定的硬逻辑。适合谁读如果你已经能手写一个基于 Netty 的 RPC 框架雏形但对 pipeline 调试仍靠加日志猜顺序如果你正在排查粘包半包问题却搞不清LengthFieldBasedFrameDecoder是在哪个环节截断数据如果你想实现一个带熔断降级的ChannelOutboundHandler但不确定write()异常该在exceptionCaught()还是close()里处理——那么这篇就是为你写的。它不讲泛泛而谈的设计思想只拆真实调用链补全每一行关键代码背后的“为什么”并附上可复现的断点调试路径和状态快照。2. 核心设计原理inBound/outBound 不是两种 Handler而是两种传播契约2.1 本质区别事件流向 vs 操作流向由接口契约强制约束Netty 并没有定义一个叫 “inBoundHandler” 或 “outBoundHandler” 的具体类它靠的是两个空接口来划清界限public interface ChannelInboundHandler extends ChannelHandler { void channelRegistered(ChannelHandlerContext ctx) throws Exception; void channelActive(ChannelHandlerContext ctx) throws Exception; void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception; void channelReadComplete(ChannelHandlerContext ctx) throws Exception; void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception; // ... 其他 inBound 方法 } public interface ChannelOutboundHandler extends ChannelHandler { void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception; void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) throws Exception; void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception; void flush(ChannelHandlerContext ctx) throws Exception; // ... 其他 outBound 方法 }注意这两个接口没有任何继承关系它们是平行的。一个 Handler 类可以同时implements ChannelInboundHandler, ChannelOutboundHandler比如LoggingHandler也可以只实现其中一个比如StringEncoder只实现 outBoundSimpleChannelInboundHandler只实现 inBound。Netty 不靠类名、不靠注解只靠你是否实现了对应接口的方法来决定这个 Handler 在 pipeline 中参与哪条链路。这带来一个关键认知转变不是“这个 Handler 是 inBound 的”而是“当某个 inBound 事件发生时Netty 会遍历 pipeline只调用那些实现了对应 inBound 方法的 Handler”。同理ctx.write()触发的是 outBound 链路Netty 会从当前ctx开始沿着next指针向 tail 方向查找第一个实现了write()方法的 outBound Handler然后调用它。如果中间某个 Handler 没有实现write()Netty 就跳过它继续找下一个——这就是为什么你可以放心插入一个只处理channelRead()的 Handler它对write()完全无感不会干扰 outBound 流程。这个设计的精妙在于它把“行为归属”和“执行时机”彻底解耦。同一个ChannelHandlerContext实例既可以作为 inBound 事件的接收者调用ctx.fireChannelRead()也可以作为 outBound 操作的发起者调用ctx.write()但它内部维护着两套独立的指针next和prev用于 inBound 传播nextContext和prevContext实际是同一组指针但使用方式不同用于 outBound 传播。我们后面会看到HeadContext和TailContext这两个特殊节点正是靠精准控制这两套指针的起止才让双向链路互不干扰。2.2 Pipeline 的物理结构双向链表 四个锚点构成传播高速公路ChannelPipeline的底层是一个双向链表每个节点是AbstractChannelHandlerContext简称ctx。它的结构不是简单的A - B - C而是[HeadContext] ↔ [UserHandler1] ↔ [UserHandler2] ↔ [TailContext] ↑ ↑ ↑ ↑ inBound inBound inBound inBound outBound outBound outBound outBound但关键在于inBound 和 outBound 的遍历起点和终点完全不同。inBound 事件传播路径从HeadContext开始沿next指针向TailContext方向单向推进。HeadContext是 inBound 的入口守门员它负责将底层 NIO 事件如epollWait返回的就绪 fd转换成 Netty 事件如channelRead然后fire给下一个ctx。TailContext是 inBound 的终点哨兵它不处理业务逻辑只做兜底如果事件一路传到它这里还没被消费比如没人重写channelRead()它就打印 warn 日志或抛异常。outBound 操作传播路径从TailContext开始沿prev指针向HeadContext方向单向推进。TailContext是 outBound 的入口守门员它接收来自用户代码的ctx.write()调用然后invoke前一个ctx的write()方法。HeadContext是 outBound 的终点哨兵它不处理业务编码只干最底层的事把ByteBuf写入 socket 的java.nio.channels.SocketChannel.write()并触发flush。这个“inBound 从 head 入、tail 出outBound 从 tail 入、head 出”的设计是 Netty 高效性的基石。它避免了每次事件都要从头扫描整个 pipeline 判断“谁该处理”而是用指针直接定位。你可以把它想象成一条双车道高速公路上行车道inBound车流从东Head往西Tail开每个收费站Handler检查车牌事件类型符合条件就放行并盖章处理否则直接放行。下行车道outBound车流从西Tail往东Head开每个收费站检查货物清单write 参数符合要求就装货编码否则直接放行。而HeadContext和TailContext就是这条高速的东、西两个收费站总控室它们不参与具体业务只负责统一收发、统一分发、统一兜底。2.3 传播动作的本质fireXXX()是 inBound 的接力棒invokeXXX()是 outBound 的点火开关很多初学者混淆ctx.fireChannelRead()和ctx.invokeChannelRead()以为只是命名不同。其实它们是两条完全不同的执行路径ctx.fireChannelRead(msg)这是 inBound 传播的标准动作。它不做任何业务处理只做一件事调用next.invokeChannelRead(m)。这里的next是当前ctx的下一个节点invokeChannelRead()是AbstractChannelHandlerContext的一个 final 方法它内部会检查next是否实现了ChannelInboundHandler如果实现了就反射调用其channelRead()否则直接调用next.invokeChannelRead()把接力棒传下去。这是一个纯粹的、无条件的、向下的事件传递函数。ctx.invokeChannelRead()这是fireChannelRead()的底层实现但你不应该手动调用它。它被设计为 pipeline 内部专用外部 Handler 应该只用fireXXX()。手动调用invokeChannelRead()会绕过next指针的校验逻辑可能导致空指针或跳过必要检查。而 outBound 的核心是invokeXXX()ctx.write(msg, promise)这是 outBound 传播的标准动作。它会调用prev.invokeWrite()注意是prev不是next即向上游向 Head查找第一个能处理 write 的 Handler。invokeWrite()同样是 final 方法它会检查prev是否实现了ChannelOutboundHandler再决定是否调用其write()。ctx.flush()同理调用prev.invokeFlush()。所以fireXXX()是 inBound 的“推”动作pushinvokeXXX()是 outBound 的“拉”动作pull。这个“推/拉”之分决定了你在写 Handler 时的思维模式写 inBound Handler你收到事件后如果需要继续传播就ctx.fireChannelRead(msg)如果不想让下游知道就不调用它比如你在这里做了完整业务处理并msg.release()了就不该再 fire。写 outBound Handler你收到write()调用后如果需要上游更靠近 Head的 Handler 处理就ctx.write(msg, promise)如果这是最后一环比如你就是编码器那就自己把msg编码成ByteBuf然后ctx.write()给HeadContext。提示ctx.write()和ctx.writeAndFlush()的区别就在于前者只把数据写入 pipeline 的 outbound 缓冲区后者还会紧接着调用ctx.flush()。这就像寄快递write()是把包裹放进快递站分拣区writeAndFlush()是不仅放进去还立刻通知快递员发车。如果只write不flush数据可能一直卡在ChannelOutboundBuffer里直到下一次flush或channel.close()时才真正发出。2.4 HeadContext 与 TailContext两个沉默的架构师撑起整个传播骨架HeadContext和TailContext是 Netty 最容易被忽略、却最关键的两个类。它们不写业务逻辑却定义了整个 pipeline 的行为边界。HeadContext它同时实现了ChannelInboundHandler和ChannelOutboundHandler但它的 inBound 方法全是空实现除了channelRead()outBound 方法则全是底层操作。channelRead()这是 inBound 链路的唯一入口。它接收从NioEventLoop来的原始ByteBuffer调用pipeline.fireChannelRead()把它转成ByteBuf并 fire 给下一个 Handler。它自己不处理业务只做格式转换和分发。write()这是 outBound 链路的唯一出口。它接收上游传来的ByteBuf调用unsafe.write()AbstractUnsafe的实例把数据真正写入 socket 的java.nio.channels.SocketChannel。它不关心编码、不关心压缩只干最脏最累的 IO 活。TailContext它也同时实现了两个接口但角色相反。channelRead()这是 inBound 链路的最后守门员。它什么都不做只打印一句Discarded message日志。如果你的业务 Handler 忘记msg.release()或者没消费掉channelRead的消息它就会在这里被发现。write()这是 outBound 链路的第一入口。它不处理任何业务只做一件事调用next.invokeWrite()把write请求向上向 Head传递。它自己不编码、不加密只负责接单并转交。这两个类的存在让 pipeline 的行为变得可预测、可调试、可兜底。没有HeadContext你就无法把 NIO 的ByteBuffer统一转成 Netty 的ByteBuf没有TailContext一个没被消费的channelRead事件就会静默丢失让你排查问题时一头雾水。它们就像建筑的地基和封顶你看不见它们的装饰但整栋楼都靠它们撑着。3. 源码级实操解析一次完整的 read/write 生命周期跟踪3.1 场景设定构建可断点调试的最小闭环为了看清 inBound/outBound 的每一步我们需要一个极简但完整的可运行环境。以下是我在线上排查问题时常用的“四行代码”调试模板// 1. 创建 ServerBootstrap ServerBootstrap b new ServerBootstrap(); b.group(new NioEventLoopGroup(1), new NioEventLoopGroup(1)) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 1024) .childOption(ChannelOption.TCP_NODELAY, true) .childHandler(new ChannelInitializerSocketChannel() { Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline p ch.pipeline(); // 插入三个典型 HandlerinBound-only, both, outBound-only p.addLast(decoder, new LineBasedFrameDecoder(1024)); p.addLast(handler, new SimpleChannelInboundHandlerString() { Override protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { System.out.println(【业务Handler】收到: msg); // 关键这里主动触发一次 outBound write ctx.writeAndFlush(Echo: msg \n); } }); p.addLast(encoder, new StringEncoder()); } }); // 2. 绑定端口 ChannelFuture f b.bind(8080).sync(); f.channel().closeFuture().sync();这个例子包含LineBasedFrameDecoder纯 inBound Handler负责按换行符切分帧。SimpleChannelInboundHandler业务 Handler处理String类型消息并主动writeAndFlush()。StringEncoder纯 outBound Handler负责把String编码成ByteBuf。启动后用telnet localhost 8080发送hello就能触发一次完整的 read → decode → business → encode → write → flush 流程。我们在关键位置打上断点就能看到指针如何跳转。3.2 inBound 全流程从 epollWait 到业务 handler 的七步追踪我们从NioEventLoop的processSelectedKey()开始这是 inBound 事件的源头Step 1NioEventLoop.processSelectedKey()—— 事件就绪准备读取当epollWait返回某个fd可读时processSelectedKey()被调用。它判断readyOps READ为真于是执行unsafe.read()。这里的unsafe是AbstractNioChannel的内部类它代表了底层 IO 的不安全操作。Step 2AbstractNioByteChannel.NioByteUnsafe.read()—— 分配缓冲区执行 readread()方法会调用config.getRecvByteBufAllocator().allocate(allocHandle)分配一个ByteBuf通常是PooledDirectByteBuf调用javaChannel().read(buf.nioBuffer())把数据从 socket 读入buf记录读取字节数到allocHandle用于后续内存分配策略调整。Step 3AbstractNioByteChannel.NioByteUnsafe.read()—— 将 ByteBuf fire 给 pipeline读完后它调用pipeline.fireChannelRead(byteBuf)。注意这是ChannelPipeline接口的方法实际调用的是DefaultChannelPipeline.fireChannelRead()。Step 4DefaultChannelPipeline.fireChannelRead()—— 从 HeadContext 启动 inBound 传播这个方法很简单public final ChannelPipeline fireChannelRead(Object msg) { AbstractChannelHandlerContext.invokeChannelRead(head, msg); return this; }它直接调用HeadContext的invokeChannelRead()把msg即刚读到的ByteBuf传进去。Step 5AbstractChannelHandlerContext.invokeChannelRead()—— 检查并调用下一个 HandlerinvokeChannelRead()是 final 方法它会检查next是否为 null不会因为HeadContext.next指向decoder检查next的executor是否是当前线程是都在NioEventLoop线程如果next实现了ChannelInboundHandlerdecoder实现了就调用next.invokeChannelRead(msg)否则调用next.invokeChannelRead(msg)递归但实际不会发生因为decoder是第一个用户 Handler。Step 6LineBasedFrameDecoder.channelRead()—— 执行解码fire 新消息decoder收到原始ByteBuf后按\n切分得到一个或多个ByteBuf。对每个切分后的ByteBuf它调用ctx.fireChannelRead(frame)。注意这里的ctx是decoder自己的上下文fireChannelRead()会调用decoder.next.invokeChannelRead(frame)也就是handler的invokeChannelRead()。Step 7SimpleChannelInboundHandler.channelRead0()—— 业务逻辑终点handler收到String类型消息LineBasedFrameDecoder已帮我们转好了执行System.out.println()然后调用ctx.writeAndFlush()。此时inBound 流程结束控制权移交 outBound。注意SimpleChannelInboundHandler的channelRead()方法内部会自动调用msg.release()所以你不用手动释放。但如果你用的是ChannelInboundHandlerAdapter就必须自己msg.release()否则内存泄漏。这是 inBound Handler 的一个关键差异点。3.3 outBound 全流程从业务 write 到 socket write 的六步追踪接续上一步handler中的ctx.writeAndFlush(Echo: hello\n)是 outBound 的起点Step 1AbstractChannelHandlerContext.writeAndFlush()—— 启动 outBound 传播writeAndFlush()方法会先调用write(msg, newPromise())再调用flush()。我们先看write()。Step 2AbstractChannelHandlerContext.write()—— 从当前 ctx 向 prev 查找write()方法的核心是private void write(Object msg, boolean flush, ChannelPromise promise) { AbstractChannelHandlerContext next findContextOutbound(); next.invokeWrite(msg, promise); }findContextOutbound()会从当前ctx即handler的 ctx开始沿prev指针向上找直到找到第一个实现了ChannelOutboundHandler的节点。在这个例子中handler.prev是decoder但decoder没有实现ChannelOutboundHandler所以继续找decoder.prev即encoder。encoder实现了ChannelOutboundHandler于是next就是encoder的 ctx。Step 3AbstractChannelHandlerContext.invokeWrite()—— 调用 encoder 的 writeinvokeWrite()是 final 方法它会检查nextencoder是否实现了ChannelOutboundHandler是调用next.handler().write(next, msg, promise)即StringEncoder.write()。Step 4StringEncoder.write()—— 执行编码fire 新消息StringEncoder收到String调用CharsetUtil.encode()得到ByteBuf然后调用ctx.write(byteBuf, promise)。注意这里的ctx是encoder自己的上下文write()会再次调用findContextOutbound()这次encoder.prev是handlerhandler没有实现ChannelOutboundHandler再往上是decoder也没有再往上是HeadContext它实现了所以next变成HeadContext。Step 5HeadContext.write()—— 将 ByteBuf 交给 unsafeHeadContext.write()方法会public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { unsafe.write(msg, promise); }unsafe.write()是AbstractUnsafe的方法它把ByteBuf放入ChannelOutboundBuffer一个环形缓冲区并标记为待刷新。Step 6AbstractChannelHandlerContext.flush()—— 触发真正的 socket write回到writeAndFlush()的第二步flush()被调用。它同样调用findContextOutbound()找到HeadContext然后调用HeadContext.flush()public void flush(ChannelHandlerContext ctx) throws Exception { unsafe.flush(); }unsafe.flush()会遍历ChannelOutboundBuffer调用javaChannel().write(buffer)把数据真正写入 socket。至此outBound 流程完成Echo: hello\n被发送回客户端。实测心得如果你在handler里只调用ctx.write()不调用ctx.flush()数据会卡在ChannelOutboundBuffer里。你可以用 JMX 或ChannelOutboundBuffer.totalPendingSize()查看缓冲区大小这是排查“数据发不出去”问题的第一线索。3.4 关键参数与状态快照理解指针跳转的黄金三要素要真正掌握传播逻辑必须盯住三个核心变量它们在每次fire或invoke时都会变化变量类型作用调试技巧ctxAbstractChannelHandlerContext当前 Handler 的上下文包含handler()、pipeline()、executor()等引用在 IDE 断点处展开ctx重点看handler.getClass().getSimpleName()和next/prev字段值nextAbstractChannelHandlerContextinBound 传播的下一个节点指向tail方向在invokeChannelRead()断点处观察next是否为null或指向预期 HandlerprevAbstractChannelHandlerContextoutBound 传播的上一个节点指向head方向在invokeWrite()断点处观察prev的handler是否是你期望的 Encoder 或 Head我习惯在AbstractChannelHandlerContext.invokeChannelRead()和invokeWrite()的第一行打条件断点invokeChannelRead()断点条件ctx.handler().getClass().getSimpleName().contains(Decoder) || ctx.handler().getClass().getSimpleName().contains(Handler)invokeWrite()断点条件ctx.handler().getClass().getSimpleName().contains(Encoder) || ctx.handler().getClass().getSimpleName().equals(HeadContext)这样每次 inBound/outBound 传播经过关键节点时IDE 都会停住你可以清晰看到ctx、next、prev的实时值比看几百行源码直观十倍。另一个重要状态是ChannelPipeline的names列表。在DefaultChannelPipeline构造时它会维护一个ArrayListString names记录所有 Handler 的注册顺序。你可以通过pipeline.names()获取它在调试时打印出来System.out.println(Pipeline order: pipeline.names()); // 输出Pipeline order: [head, decoder, handler, encoder, tail]这个顺序和next/prev指针完全一致是验证你对 pipeline 结构理解是否正确的黄金标尺。4. 常见问题与避坑指南那些让老手也挠头的传播陷阱4.1 问题速查表高频故障现象与根因定位现象可能根因快速验证方法解决方案channelRead()被调用但msg是ByteBuf而非StringLineBasedFrameDecoder没生效或decoder插入位置错误应在handler之前在handler.channelRead()断点处msg.getClass().getName()是否为io.netty.buffer.PooledDirectByteBuf检查p.addLast(decoder, ...)是否在p.addLast(handler, ...)之前确认decoder的maxFrameLength足够大writeAndFlush()后客户端收不到数据StringEncoder没生效或encoder插入位置错误应在handler之后在HeadContext.write()断点处msg的类型是否为String如果是说明encoder没触发检查p.addLast(encoder, ...)是否在p.addLast(handler, ...)之后确认encoder的charset与客户端一致exceptionCaught()没被下游 Handler 捕获当前 Handler 的exceptionCaught()方法里没有调用ctx.fireExceptionCaught(cause)在handler.exceptionCaught()断点处检查是否有ctx.fireExceptionCaught(cause)这行代码在自定义 Handler 的exceptionCaught()末尾必须加上ctx.fireExceptionCaught(cause)除非你明确要终止传播channelInactive()后还能收到channelRead()ChannelHandler没加Sharable注解被多个 Channel 共享状态混乱查看handler类上是否有Sharable在handler的channelActive()里打日志看是否被多次调用无状态 Handler 加Sharable有状态 Handler如计数器不要加改用ChannelHandlerAdapter每次 new 一个实例write()调用后ChannelOutboundBuffer积压大量数据flush()没被及时调用或autoFlush为 false调用channel.pipeline().channel().unsafe().outboundBuffer().totalPendingSize()看返回值是否持续增长在write()后紧跟flush()或设置channel.config().setAutoRead(true)让 Netty 自动 flush4.2 独家避坑经验从血泪教训中提炼的 5 条铁律铁律 1永远不要在 inBound Handler 的channelRead()里直接ctx.write()这是新手最大误区。channelRead()是 inBound 事件ctx.write()是 outBound 操作二者语义冲突。正确做法是在channelRead0()SimpleChannelInboundHandler里处理业务然后调用ctx.writeAndFlush()。如果你用ChannelInboundHandlerAdapter必须手动msg.release()否则ctx.write()会把未释放的ByteBuf塞进 outBound 缓冲区造成内存泄漏。铁律 2addFirst()和addLast()的语义取决于你是 inBound 还是 outBound 视角p.addFirst(h1, h1)对 inBoundh1成为HeadContext的下一个最先收到channelRead()对 outBoundh1成为TailContext的上一个最后收到write()。p.addLast(h2, h2)对 inBoundh2成为TailContext的上一个最后收到channelRead()对 outBoundh2成为HeadContext的上一个最先收到write()。所以编码器outBound通常addLast()解码器inBound通常addFirst()这是由它们在各自链路中的“职责位置”决定的不是随意约定。铁律 3ctx.pipeline().fireChannelRead()和ctx.fireChannelRead()效果相同但ctx.channel().pipeline().fireChannelRead()可能绕过当前 ctxctx.fireChannelRead()是ctx的快捷方式它等价于ctx.pipeline().fireChannelRead()。但ctx.channel().pipeline().fireChannelRead()是从Channel对象重新获取 pipeline再 fire它不经过当前ctx的任何拦截逻辑。在绝大多数场景下你应该用ctx.fireXXX()因为它保证了上下文一致性。铁律 4ChannelPromise不是可选配件而是 outBound 的生命线write(msg, promise)中的promise是 outBound 操作的异步结果容器。如果你传nullNetty 会创建一个VoidChannelPromise你将无法监听write是否成功。正确做法是在业务 Handler 中用ctx.newPromise()创建新 promise或复用传入的 promise。例如Override public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { // 创建新 promise以便监听 write 结果 ChannelPromise p ctx.newPromise(); ctx.writeAndFlush(Echo: msg, p); p.addListener((ChannelFutureListener) future - { if (future.isSuccess()) { System.out.println(Write success); } else { System.err.println(Write failed: future.cause()); } }); }铁律 5exceptionCaught()的传播是唯一需要你手动fire的 inBound 事件所有其他 inBound 事件channelActive、channelRead、channelReadComplete在AbstractChannelHandlerContext的invokeXXX()方法中都是自动调用next.invokeXXX()的。唯独exceptionCaught()它的invokeExceptionCaught()方法是static void invokeExceptionCaught(final AbstractChannelHandlerContext next, Throwable cause) { // ... 省略异常检查 try { next.invokeExceptionCaught(cause); } catch (Throwable t) { if (logger.isWarnEnabled()) { logger.warn(An exception was thrown by a user handlers exceptionCaught() method, t); } // 注意这里没有 fallback异常被吞了 } }它不会像invokeChannelRead()那样自动调用next.invokeExceptionCaught()。所以如果你重写了exceptionCaught()必须在方法末尾显式调用ctx.fireExceptionCaught(cause)否则传播链就断了。这是 Netty 源码里一个反直觉的设计也是线上事故的高发区。4.3 性能优化实战如何让 inBound/outBound 传播快 30%传播本身是轻量的但 Handler 的实现不当会拖慢整个 pipeline。以下是我在某金融网关项目中实测有效的三条优化优化 1用SimpleChannelInboundHandler替代ChannelInboundHandlerAdapterSimple
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →