All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.netty.handler.codec.ReplayingDecoder Maven / Gradle / Ivy

There is a newer version: 5.0.0.Alpha2
Show newest version
/*
 * Copyright 2012 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package io.netty.handler.codec;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.MessageBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import io.netty.util.Signal;

/**
 * A specialized variation of {@link ByteToMessageDecoder} which enables implementation
 * of a non-blocking decoder in the blocking I/O paradigm.
 * 

* The biggest difference between {@link ReplayingDecoder} and * {@link ByteToMessageDecoder} is that {@link ReplayingDecoder} allows you to * implement the {@code decode()} and {@code decodeLast()} methods just like * all required bytes were received already, rather than checking the * availability of the required bytes. For example, the following * {@link ByteToByteDecoder} implementation: *

 * public class IntegerHeaderFrameDecoder extends {@link ByteToMessageDecoder}<{@link ByteBuf}> {
 *
 *   {@code @Override}
 *   protected ByteBuf decode({@link ChannelHandlerContext} ctx,
 *                           {@link ByteBuf} in) throws Exception {
 *
 *     if (in.readableBytes() < 4) {
 *        return null;
 *     }
 *
 *     in.markReaderIndex();
 *     int length = in.readInt();
 *
 *     if (in.readableBytes() < length) {
 *        in.resetReaderIndex();
 *        return null;
 *     }
 *
 *     return in.readBytes(length);
 *   }
 * }
 * 
* is simplified like the following with {@link ReplayingDecoder}: *
 * public class IntegerHeaderFrameDecoder
 *      extends {@link ReplayingDecoder}<{@link ByteBuf},{@link Void}> {
 *
 *   protected Object decode({@link ChannelHandlerContext} ctx,
 *                           {@link ByteBuf} buf) throws Exception {
 *
 *     return buf.readBytes(buf.readInt());
 *   }
 * }
 * 
* *

How does this work?

*

* {@link ReplayingDecoder} passes a specialized {@link ByteBuf} * implementation which throws an {@link Error} of certain type when there's not * enough data in the buffer. In the {@code IntegerHeaderFrameDecoder} above, * you just assumed that there will be 4 or more bytes in the buffer when * you call {@code buf.readInt()}. If there's really 4 bytes in the buffer, * it will return the integer header as you expected. Otherwise, the * {@link Error} will be raised and the control will be returned to * {@link ReplayingDecoder}. If {@link ReplayingDecoder} catches the * {@link Error}, then it will rewind the {@code readerIndex} of the buffer * back to the 'initial' position (i.e. the beginning of the buffer) and call * the {@code decode(..)} method again when more data is received into the * buffer. *

* Please note that {@link ReplayingDecoder} always throws the same cached * {@link Error} instance to avoid the overhead of creating a new {@link Error} * and filling its stack trace for every throw. * *

Limitations

*

* At the cost of the simplicity, {@link ReplayingDecoder} enforces you a few * limitations: *

    *
  • Some buffer operations are prohibited.
  • *
  • Performance can be worse if the network is slow and the message * format is complicated unlike the example above. In this case, your * decoder might have to decode the same part of the message over and over * again.
  • *
  • You must keep in mind that {@code decode(..)} method can be called many * times to decode a single message. For example, the following code will * not work: *
     public class MyDecoder extends {@link ReplayingDecoder}<{@link Integer}, {@link Void}> {
     *
     *   private final Queue<Integer> values = new LinkedList<Integer>();
     *
     *   {@code @Override}
     *   public {@link Integer} decode(.., {@link ByteBuf} in) throws Exception {
     *
     *     // A message contains 2 integers.
     *     values.offer(buffer.readInt());
     *     values.offer(buffer.readInt());
     *
     *     // This assertion will fail intermittently since values.offer()
     *     // can be called more than two times!
     *     assert values.size() == 2;
     *     return values.poll() + values.poll();
     *   }
     * }
    * The correct implementation looks like the following, and you can also * utilize the 'checkpoint' feature which is explained in detail in the * next section. *
     public class MyDecoder extends {@link ReplayingDecoder}<{@link Integer}, {@link Void}> {
     *
     *   private final Queue<Integer> values = new LinkedList<Integer>();
     *
     *   {@code @Override}
     *   public {@link Integer} decode(.., {@link ByteBuf} buffer) throws Exception {
     *
     *     // Revert the state of the variable that might have been changed
     *     // since the last partial decode.
     *     values.clear();
     *
     *     // A message contains 2 integers.
     *     values.offer(buffer.readInt());
     *     values.offer(buffer.readInt());
     *
     *     // Now we know this assertion will never fail.
     *     assert values.size() == 2;
     *     return values.poll() + values.poll();
     *   }
     * }
    *
  • *
* *

Improving the performance

*

* Fortunately, the performance of a complex decoder implementation can be * improved significantly with the {@code checkpoint()} method. The * {@code checkpoint()} method updates the 'initial' position of the buffer so * that {@link ReplayingDecoder} rewinds the {@code readerIndex} of the buffer * to the last position where you called the {@code checkpoint()} method. * *

Calling {@code checkpoint(T)} with an {@link Enum}

*

* Although you can just use {@code checkpoint()} method and manage the state * of the decoder by yourself, the easiest way to manage the state of the * decoder is to create an {@link Enum} type which represents the current state * of the decoder and to call {@code checkpoint(T)} method whenever the state * changes. You can have as many states as you want depending on the * complexity of the message you want to decode: * *

 * public enum MyDecoderState {
 *   READ_LENGTH,
 *   READ_CONTENT;
 * }
 *
 * public class IntegerHeaderFrameDecoder
 *      extends {@link ReplayingDecoder}<MyDecoderState> {
 *
 *   private int length;
 *
 *   public IntegerHeaderFrameDecoder() {
 *     // Set the initial state.
 *     super(MyDecoderState.READ_LENGTH);
 *   }
 *
 *   {@code @Override}
 *   protected {@link Object} decode({@link ChannelHandlerContext} ctx,
 *                           {@link ByteBuf} in) throws Exception {
 *     switch (state()) {
 *     case READ_LENGTH:
 *       length = buf.readInt();
 *       checkpoint(MyDecoderState.READ_CONTENT);
 *     case READ_CONTENT:
 *       ByteBuf frame = buf.readBytes(length);
 *       checkpoint(MyDecoderState.READ_LENGTH);
 *       return frame;
 *     default:
 *       throw new Error("Shouldn't reach here.");
 *     }
 *   }
 * }
 * 
* *

Calling {@code checkpoint()} with no parameter

*

* An alternative way to manage the decoder state is to manage it by yourself. *

 * public class IntegerHeaderFrameDecoder
 *      extends {@link ReplayingDecoder}<{@link Void}> {
 *
 *   private boolean readLength;
 *   private int length;
 *
 *   {@code @Override}
 *   protected {@link Object} decode({@link ChannelHandlerContext} ctx,
 *                           {@link ByteBuf} in) throws Exception {
 *     if (!readLength) {
 *       length = buf.readInt();
 *       readLength = true;
 *       checkpoint();
 *     }
 *
 *     if (readLength) {
 *       ByteBuf frame = buf.readBytes(length);
 *       readLength = false;
 *       checkpoint();
 *       return frame;
 *     }
 *   }
 * }
 * 
* *

Replacing a decoder with another decoder in a pipeline

*

* If you are going to write a protocol multiplexer, you will probably want to * replace a {@link ReplayingDecoder} (protocol detector) with another * {@link ReplayingDecoder}, {@link ByteToByteDecoder}, {@link ByteToMessageDecoder} or {@link MessageToMessageDecoder} * (actual protocol decoder). * It is not possible to achieve this simply by calling * {@link ChannelPipeline#replace(ChannelHandler, String, ChannelHandler)}, but * some additional steps are required: *

 * public class FirstDecoder extends {@link ReplayingDecoder}<{@link Void}> {
 *
 *     {@code @Override}
 *     protected Object decode({@link ChannelHandlerContext} ctx,
 *                             {@link ByteBuf} in) {
 *         ...
 *         // Decode the first message
 *         Object firstMessage = ...;
 *
 *         // Add the second decoder
 *         ctx.pipeline().addLast("second", new SecondDecoder());
 *
 *         // Remove the first decoder (me)
 *         ctx.pipeline().remove(this);
 *
 *         if (buf.isReadable()) {
 *             // Hand off the remaining data to the second decoder
 *             return new Object[] { firstMessage, buf.readBytes(super.actualReadableBytes()) };
 *         } else {
 *             // Nothing to hand off
 *             return firstMessage;
 *         }
 *     }
 * 
* @param * the state type which is usually an {@link Enum}; use {@link Void} if state management is * unused */ public abstract class ReplayingDecoder extends ByteToMessageDecoder { static final Signal REPLAY = new Signal(ReplayingDecoder.class.getName() + ".REPLAY"); private ByteBuf cumulation; private ReplayingDecoderBuffer replayable; private S state; private int checkpoint = -1; private boolean decodeWasNull; /** * Creates a new instance with no initial state (i.e: {@code null}). */ protected ReplayingDecoder() { this(null); } /** * Creates a new instance with the specified initial state. */ protected ReplayingDecoder(S initialState) { state = initialState; } /** * Stores the internal cumulative buffer's reader position. */ protected void checkpoint() { checkpoint = cumulation.readerIndex(); } /** * Stores the internal cumulative buffer's reader position and updates * the current decoder state. */ protected void checkpoint(S state) { checkpoint(); state(state); } /** * Returns the current state of this decoder. * @return the current state of this decoder */ protected S state() { return state; } /** * Sets the current state of this decoder. * @return the old state of this decoder */ protected S state(S newState) { S oldState = state; state = newState; return oldState; } /** * Returns the actual number of readable bytes in the internal cumulative * buffer of this decoder. You usually do not need to rely on this value * to write a decoder. Use it only when you muse use it at your own risk. * This method is a shortcut to {@link #internalBuffer() internalBuffer().readableBytes()}. */ protected int actualReadableBytes() { return internalBuffer().readableBytes(); } /** * Returns the internal cumulative buffer of this decoder. You usually * do not need to access the internal buffer directly to write a decoder. * Use it only when you must use it at your own risk. */ protected ByteBuf internalBuffer() { return cumulation; } @Override public final ByteBuf newInboundBuffer(ChannelHandlerContext ctx) throws Exception { cumulation = newInboundBuffer0(ctx); replayable = new ReplayingDecoderBuffer(cumulation); return cumulation; } protected ByteBuf newInboundBuffer0(ChannelHandlerContext ctx) throws Exception { return super.newInboundBuffer(ctx); } @Override public final void discardInboundReadBytes(ChannelHandlerContext ctx) throws Exception { ByteBuf in = ctx.inboundByteBuffer(); final int oldReaderIndex = in.readerIndex(); discardInboundReadBytes0(ctx); final int newReaderIndex = in.readerIndex(); checkpoint -= oldReaderIndex - newReaderIndex; } protected void discardInboundReadBytes0(ChannelHandlerContext ctx) throws Exception { super.discardInboundReadBytes(ctx); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { try { replayable.terminate(); ByteBuf in = cumulation; if (in.isReadable()) { callDecode(ctx, in); } if (ctx.nextInboundMessageBuffer().unfoldAndAdd(decodeLast(ctx, replayable))) { ctx.fireInboundBufferUpdated(); } } catch (Signal replay) { // Ignore replay.expect(REPLAY); } catch (Throwable t) { if (t instanceof CodecException) { ctx.fireExceptionCaught(t); } else { ctx.fireExceptionCaught(new DecoderException(t)); } } finally { ctx.fireChannelInactive(); } } @Override protected void callDecode(ChannelHandlerContext ctx, ByteBuf buf) { boolean wasNull = false; ByteBuf in = cumulation; MessageBuf out = ctx.nextInboundMessageBuffer(); boolean decoded = false; while (in.isReadable()) { try { int oldReaderIndex = checkpoint = in.readerIndex(); Object result = null; S oldState = state; try { result = decode(ctx, replayable); if (result == null) { if (oldReaderIndex == in.readerIndex() && oldState == state) { throw new IllegalStateException( "null cannot be returned if no data is consumed and state didn't change."); } else { // Previous data has been discarded or caused state transition. // Probably it is reading on. continue; } } } catch (Signal replay) { replay.expect(REPLAY); // Return to the checkpoint (or oldPosition) and retry. int checkpoint = this.checkpoint; if (checkpoint >= 0) { in.readerIndex(checkpoint); } else { // Called by cleanup() - no need to maintain the readerIndex // anymore because the buffer has been released already. } } if (result == null) { wasNull = true; // Seems like more data is required. // Let us wait for the next notification. break; } wasNull = false; if (oldReaderIndex == in.readerIndex() && oldState == state) { throw new IllegalStateException( "decode() method must consume at least one byte " + "if it returned a decoded message (caused by: " + getClass() + ')'); } // A successful decode if (out.unfoldAndAdd(result)) { decoded = true; if (isSingleDecode()) { break; } } } catch (Throwable t) { if (decoded) { decoded = false; ctx.fireInboundBufferUpdated(); } if (t instanceof CodecException) { ctx.fireExceptionCaught(t); } else { ctx.fireExceptionCaught(new DecoderException(t)); } break; } } if (decoded) { decodeWasNull = false; ctx.fireInboundBufferUpdated(); } else { if (wasNull) { decodeWasNull = true; } } } @Override public void channelReadSuspended(ChannelHandlerContext ctx) throws Exception { if (decodeWasNull) { decodeWasNull = false; if (!ctx.channel().config().isAutoRead()) { ctx.read(); } } super.channelReadSuspended(ctx); } }