【问题标题】:Netty: Add and Remove FrameDecoder dynamically from a Pipeline, Protocol encapsulationNetty:从管道中动态添加和删除 FrameDecoder,协议封装
【发布时间】:2015-08-04 20:54:02
【问题描述】:

我已经使用 Netty 3.3.1-Final 3 周了。 我的协议有 3 个步骤,每个步骤需要不同的 FrameDecoder

  • 读取参数
  • 传输一些数据
  • 数据管道相互关闭

我遇到了很多我无法理解的“阻塞”问题。终于在我看来,阅读 org.jboss.netty.example.portunification 示例时,我在尝试 动态更改我的 FrameDecoder 时遇到了一些缓冲区问题:一个缓冲区FrameDecoder 在更改下一个时(可能)不为空...

有没有办法在 Netty 中轻松做到这一点?我必须更改我的协议吗?我需要编写一个大的 FrameDecoder 并管理一个状态吗? 如果是这样,如何避免具有公共子部分的不同协议之间的代码重复(例如“读取参数”)?

今天我想到了一个FrameDecoderUnifier(代码如下)的想法,目的是热添加和删除一些FrameDecoder,你怎么看?

感谢您的帮助!

雷诺

----------- FrameDecoderUnifier 类 --------------

    /**
     * This FrameDecoder is able to forward the unused bytes from one decoder to the next one. It provides
     * a safe way to replace a FrameDecoder inside a Pipeline.
     * It is not safe to just add and remove FrameDecoder dynamically from a Pipeline because there is a risk
     * of unread bytes inside the buffer of the FrameDecoder you wan't to remove.
     */
    public class FrameDecoderUnifier extends FrameDecoder {

        private final Method frameDecoderDecodeMethod;
        volatile boolean skip = false;
        LastFrameEventHandler eventHandler;
        LinkedList<Entry> entries;
        Entry entry = null;

        public FrameDecoderUnifier(LastFrameEventHandler eventHandler) {
            this.eventHandler = eventHandler;
            this.entries = new LinkedList<Entry>();
            try {
                this.frameDecoderDecodeMethod = FrameDecoder.class.getMethod("decode", ChannelHandlerContext.class, Channel.class, ChannelBuffer.class);
            } catch (NoSuchMethodException ex) {
                throw new RuntimeException(ex);
            } catch (SecurityException ex) {
                throw new RuntimeException(ex);
            }
        }

        public void addLast(FrameDecoder decoder, LastFrameIdentifier identifier) {
            entries.addLast(new Entry(decoder, identifier));
        }

        private Object callDecode(FrameDecoder decoder, ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) throws Exception {
            return frameDecoderDecodeMethod.invoke(decoder, ctx, channel, buffer);
        }

        @Override
        protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) throws Exception {
            if (entry == null && !entries.isEmpty()) {
                entry = entries.getFirst();
            }

            if (entry == null) {
                return buffer; //No framing, no decoding
            }

            //Perform the decode operation
            Object obj = callDecode(entry.getDecoder(), ctx, channel, buffer);

            if (obj != null && entry.getIdentifier().isLastFrame(obj)) {
                //Fire event
                eventHandler.lastObjectDecoded(entry.getDecoder(), obj);
                entry = null;
            }
            return obj;
        }

        /**
         * You can use this interface to take some action when the current decoder is changed for the next one.
         * This can be useful to change some upper Handler in the pipeline.
         */
        public interface LastFrameEventHandler {

            public void lastObjectDecoded(FrameDecoder decoder, Object obj);
        }

        public interface LastFrameIdentifier {

            /**
             * True if after this frame, we should disable this decoder.
             * @param obj
             * @return 
             */
            public abstract boolean isLastFrame(Object decodedObj);
        }

        private class Entry {

            FrameDecoder decoder;
            LastFrameIdentifier identifier;

            public Entry(FrameDecoder decoder, LastFrameIdentifier identifier) {
                this.decoder = decoder;
                this.identifier = identifier;
            }

            public FrameDecoder getDecoder() {
                return decoder;
            }

            public LastFrameIdentifier getIdentifier() {
                return identifier;
            }
        }
}

【问题讨论】:

    标签: java netty


    【解决方案1】:

    我也遇到过类似的问题,从管道中移除帧解码器似乎并不能阻止它被调用,并且没有明显的方法可以使解码器表现得好像它不在链:Netty 坚持 decode() 至少读取一个字节,因此您不能简单地返回传入的 ChannelBuffer,而返回 null 会停止处理传入数据,直到下一个数据包到达,从而停止协议解码过程。

    首先:FrameDecoder 的 Netty 3.7 文档实际上有一节“用管道中的另一个解码器替换解码器”。它说:

    仅仅通过调用是不可能实现这一点的 ChannelPipeline#replace()

    相反,它建议通过返回一个包含解码的第一个数据包和接收到的其余数据的数组来传递数据。

    return new Object[] { firstMessage, buf.readBytes(buf.readableBytes()) };
    

    重要的是,在此之前必须启用“展开”,但这部分很容易错过并且没有解释。我能找到的最好的线索是Netty issue 132,这显然导致了 FrameDecoders 上的“展开”标志。如果为真,解码器将以对下游处理程序透明的方式将此类数组解包为对象。看看源代码似乎可以确认这就是“展开”的意思。

    其次:似乎有一种更简单的方法,因为该示例还显示了如何将数据沿管道向下传递而不改变。例如,在完成其工作后,我的同步数据包 FrameDecoder 设置了一个内部标志并将其自身从管道中移除,并正常返回解码后的对象。设置标志后的任何后续调用都只需像这样传递数据:

    protected Object decode(ChannelHandlerContext ctx,
                            Channel channel, ChannelBuffer cbuf) throws Exception {
    
        // Close the door on more than one sync packet being decoded
        if (m_received) {
            // Pass on the data to the next handler in the pipeline.
            // Note we can't just return cbuf as-is, we must drain it
            // and return a new one.  Otherwise Netty will detect that
            // no bytes were read and throw an IllegalStateException.
            return cbuf.readBytes(cbuf.readableBytes());
        }
    
        // Handle the framing
        ChannelBuffer decoded = (ChannelBuffer) super.decode(ctx, channel, cbuf);
        if (decoded == null) {
            return null;
        }
    
        // Remove ourselves from the pipeline now
        ctx.getPipeline().remove(this);
        m_received = true;
    
        // Can we assume an array backed ChannelBuffer?
        // I have only hints that we can't, so let's copy the bytes out.
        byte[] sequence = new byte[magicSequence.length];
        decoded.readBytes(sequence);
    
        // We got the magic sequence?  Return the appropriate SyncMsg
        return new SyncMsg(Arrays.equals(sequence, magicSequence));
    }
    

    从 LengthFieldBasedFrameDecoder 派生的解码器保持在下游并处理所有后续数据帧。到目前为止对我有用。

    【讨论】:

      【解决方案2】:

      我认为,应该避免使用帧解码器根据某些状态切换内部解码器动态添加/删除上层处理程序,因为

      • 难以理解/调试代码
      • 处理程序没有明确定义的职责(这就是您删除/添加处理程序的原因吗?一个处理程序应该处理一种或多种(相关)类型的协议消息,而不是许多处理程序相同类型的消息)
      • 理想情况下,帧解码器只提取协议帧,而不是根据状态解码帧(这里帧解码器可以有内部解码器链来解码帧并触发带有解码消息的 MessageEvent,上面的处理程序可以对解码的消息做出反应) .

      更新:这里我考虑了一个协议,其中每条消息都可以有一个唯一的标签/标识符,并且清楚地标记了消息的结尾(例如Tag Length Value 帧格式)

      【讨论】:

      • 你好@Jestan,让我们想象一个shell应用程序想要打开一个子shell应用程序(就像你使用ssh或从shell切换到bash时发生的事情......)。你不会热插拔一个不同的 FrameDecoder 来做到这一点吗?我使用基于状态的协议来保存一些字节:RECEIVE A, if A==A0 RECEIVE B else RECEIVE C. 其中 A、B 和 C 需要 3 个不同的解码器。我想另一种更简单的方法是使用一个单一的统一协议,其中 A 作为可能包含 B 或 C 的令牌的标头...这导致了我的第二个问题:如何避免代码重复??
      • 当您需要支持多种协议时,拥有多个帧解码器是可以的,但在您的情况下,您只有一个具有多个帧解码器的协议(并且根据收到的消息添加/删除了一些处理程序类型)。如果只支持一种协议,我认为,帧解码器的职责是“无论消息类型或状态如何都提取帧”(在这种情况下,消息必须使用标识标签或 tlv 格式进行编码,并且帧解码器可以将解码委托给解码器链以解码提取的帧,然后触发带有解码消息的 MessagEevent)
      • 感谢您的帮助@Jestan!好吧,我们需要在这里明确定义什么是协议!如果我理解你的话,那么设计一个帧解码算法依赖于先前解码帧的上层解释的协议不是一个好主意。我知道我在上层处理程序中处理的状态/步骤应该成为 Frame 本身的标志??意思是Tokens A=[DATA] or B=[DATA] with FrameDecoderA and FrameDecoderB 应该变成Token C=[AorB+DATA] with one single FrameDecoderC,是这样吗?这有点笼统,但实施起来可能更简单?
      • 关于解码你明白了我的意思:),顺便说一句,我不反对在解码器/处理程序中设置状态,我是说,如果协议的帧格式可以有标识符/标签在标头中,您可以使用简单的帧解码器来提取帧 A 或 B 或 C,并有一个消息处理程序/处理程序来接收它们并做出反应(当然,接收 A、B 和 C 的处理程序必须有一个状态)。 P.S 这里的框架意味着类似 gist.github.com/1582634 。这将有助于避免删除/添加处理程序(这一行是我回答的重点)
      • 好的@Jestan,我想我们可以结束这个问题,我改写答案。 重新设计您的协议以拥有一种独特的帧格式(意味着一种从字节流中重新组装帧的独特方法)。您总是可以这样做,因为处理不同的链式 FrameDecoder 相当于在您的框架中插入一个“状态”标头并使用一个单独的 FrameDecoder!我知道这是 Netty 设计的工作方式,对于部分代码的可重用性来说似乎有点痛苦。感谢您的帮助@Jestan!问候,雷诺
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-04-17
      • 1970-01-01
      • 2012-10-04
      • 1970-01-01
      • 2013-12-03
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多