第 6 / 20 篇

第 5 章 Pipeline:消息的处理路线

5.1 本书使用的四个处理器

每条连接建立时,我们安装:

ch.pipeline().addLast(
    new LineBasedFrameDecoder(1024),
    new StringDecoder(CharsetUtil.UTF_8),
    new StringEncoder(CharsetUtil.UTF_8),
    new ReplyHandler());

LineBasedFrameDecoder 从字节流中找出以换行结束的完整帧,默认移除行分隔符。StringDecoder 把完整帧的 UTF-8 字节转换为 Java 字符串。ReplyHandler 根据字符串生成回答。StringEncoder 在发送时把字符串转换为字节。

入站事件大体沿加入顺序前进,出站事件大体沿反方向前进;只实现了相应方向接口的处理器才处理该方向的事件。[2]

加入顺序: 行解码器 → 字符串解码器 → 字符串编码器 → 业务处理器
入站方向: ──────────────────────────────────→
出站方向: ←──────────────────────────────────

把编码器放在业务处理器之前,是为了让业务处理器通过 ctx.writeAndFlush 发出的字符串,在出站传播时能遇到编码器。

5.2 ctx 与 channel 的起点不同

ctx 是当前处理器的上下文。ctx.write 从当前上下文向前寻找出站处理器;channel.write 从 Pipeline 尾部发起出站传播。处理器位置改变时,两种写法可能走过不同的处理器集合。

同样,入站处理器要把事件交给后面的处理器,可以调用 ctx.fireChannelRead(msg)。如果既不转发也不处理消息,消息就在这里停止。涉及 ByteBuf 时,还必须考虑谁释放它。

5.3 SimpleChannelInboundHandler 的便利与边界

本书业务处理器继承 SimpleChannelInboundHandler<String>,实现 channelRead0。它默认会在回调结束后自动释放匹配类型的引用计数消息。这里接收的是 String,没有 ByteBuf 的释放负担。

如果改成 SimpleChannelInboundHandler<ByteBuf>,回调结束后默认自动释放收到的 ByteBuf。把这个对象存起来稍后使用,或原样写出去时,必须先明确引用计数的所有权,不能照搬字符串写法。

5.4 一个 Handler 对象能给所有连接共用吗

带连接状态的处理器通常应每连接创建一个实例。ChannelInitializer 中 new ReplyHandler() 正是这样做的。只有确认处理器可安全共享,并符合 Netty 的共享约定时,才使用 @Sharable。一个处理器可能在多条连接的不同线程上被调用,普通成员变量不自动变成线程安全。

小练习: 若把 StringEncoder 放到业务处理器之后,业务里仍使用 ctx.writeAndFlush,会有什么风险?