/* * 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 divconq.net; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.DecoderException; import io.netty.util.internal.RecyclableArrayList; import io.netty.util.internal.StringUtil; import java.util.List; /** * {@link ChannelInboundHandlerAdapter} which decodes bytes in a stream-like fashion from one {@link ByteBuf} to an * other Message type. * * For example here is an implementation which reads all readable bytes from * the input {@link ByteBuf} and create a new {@link ByteBuf}. * * <pre> * public class SquareDecoder extends {@link ByteToMessageDecoder} { * {@code @Override} * public void decode({@link ChannelHandlerContext} ctx, {@link ByteBuf} in, List<Object> out) * throws {@link Exception} { * out.add(in.readBytes(in.readableBytes())); * } * } * </pre> * * Be aware that sub-classes of {@link ByteToMessageDecoder} <strong>MUST NOT</strong> * annotated with Sharable. */ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter { ByteBuf cumulation; private boolean singleDecode; private boolean decodeWasNull; private boolean first; protected ByteToMessageDecoder() { if (this.isSharable()) { throw new IllegalStateException("@Sharable annotation is not allowed"); } } /* * If set then only one message is decoded on each {@link #channelRead(ChannelHandlerContext, Object)} * call. This may be useful if you need to do some protocol upgrade and want to make sure nothing is mixed up. * * Default is {@code false} as this has performance impacts. * * @param v set to single decoder mode */ public void setSingleDecode(boolean v) { this.singleDecode = v; } /* * If {@code true} then only one message is decoded on each * {@link #channelRead(ChannelHandlerContext, Object)} call. * * Default is {@code false} as this has performance impacts. * * @return true if this is in single decoder mode */ public boolean isSingleDecode() { return this.singleDecode; } /* * 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 must use it at your own risk. * This method is a shortcut to {@link #internalBuffer() internalBuffer().readableBytes()}. * * @return readable bytes in the cumulative buffer */ protected int actualReadableBytes() { return this.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. * * @return the cumulative buffer */ protected ByteBuf internalBuffer() { if (this.cumulation != null) { return this.cumulation; } else { return Unpooled.EMPTY_BUFFER; } } @Override public final void handlerRemoved(ChannelHandlerContext ctx) throws Exception { ByteBuf buf = internalBuffer(); int readable = buf.readableBytes(); if (buf.isReadable()) { ByteBuf bytes = buf.readBytes(readable); buf.release(); ctx.fireChannelRead(bytes); } else { buf.release(); } this.cumulation = null; ctx.fireChannelReadComplete(); this.handlerRemoved0(ctx); } /* * Gets called after the {@link ByteToMessageDecoder} was removed from the actual context and it doesn't handle * events anymore. * * @param ctx the {@link ChannelHandlerContext} which this {@link ByteToMessageDecoder} belongs to * @throws Exception is thrown if an error accour */ protected void handlerRemoved0(ChannelHandlerContext ctx) throws Exception { } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof ByteBuf) { RecyclableArrayList out = RecyclableArrayList.newInstance(); try { ByteBuf data = (ByteBuf) msg; this.first = this.cumulation == null; if (this.first) { this.cumulation = data; } else { if (this.cumulation.writerIndex() > this.cumulation.maxCapacity() - data.readableBytes() || this.cumulation.refCnt() > 1) { // Expand cumulation (by replace it) when either there is not more room in the buffer // or if the refCnt is greater then 1 which may happen when the user use slice().retain() or // duplicate().retain(). // // See: // - https://github.com/netty/netty/issues/2327 // - https://github.com/netty/netty/issues/1764 this.expandCumulation(ctx, data.readableBytes()); } this.cumulation.writeBytes(data); data.release(); } this.callDecode(ctx, this.cumulation, out); } catch (DecoderException e) { throw e; } catch (Throwable t) { throw new DecoderException(t); } finally { if (this.cumulation != null && !this.cumulation.isReadable()) { this.cumulation.release(); this.cumulation = null; } int size = out.size(); this.decodeWasNull = size == 0; for (int i = 0; i < size; i ++) { ctx.fireChannelRead(out.get(i)); } out.recycle(); } } else { ctx.fireChannelRead(msg); } } private void expandCumulation(ChannelHandlerContext ctx, int readable) { ByteBuf oldCumulation = this.cumulation; this.cumulation = ctx.alloc().buffer(oldCumulation.readableBytes() + readable); this.cumulation.writeBytes(oldCumulation); oldCumulation.release(); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { if (this.cumulation != null && !this.first && this.cumulation.refCnt() == 1) { // discard some bytes if possible to make more room in the // buffer but only if the refCnt == 1 as otherwise the user may have // used slice().retain() or duplicate().retain(). // // See: // - https://github.com/netty/netty/issues/2327 // - https://github.com/netty/netty/issues/1764 this.cumulation.discardSomeReadBytes(); } if (this.decodeWasNull) { this.decodeWasNull = false; if (ctx.channel().config().isAutoRead()) { ctx.read(); } } ctx.fireChannelReadComplete(); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { RecyclableArrayList out = RecyclableArrayList.newInstance(); try { if (this.cumulation != null) { callDecode(ctx, this.cumulation, out); decodeLast(ctx, this.cumulation, out); } else { decodeLast(ctx, Unpooled.EMPTY_BUFFER, out); } } catch (DecoderException e) { throw e; } catch (Exception e) { throw new DecoderException(e); } finally { try { if (this.cumulation != null) { this.cumulation.release(); this.cumulation = null; } int size = out.size(); for (int i = 0; i < size; i++) { ctx.fireChannelRead(out.get(i)); } if (size > 0) { // Something was read, call fireChannelReadComplete() ctx.fireChannelReadComplete(); } ctx.fireChannelInactive(); } finally { // recycle in all cases out.recycle(); } } } /* * Called once data should be decoded from the given {@link ByteBuf}. This method will call * {@link #decode(ChannelHandlerContext, ByteBuf, List)} as long as decoding should take place. * * @param ctx the {@link ChannelHandlerContext} which this {@link ByteToMessageDecoder} belongs to * @param in the {@link ByteBuf} from which to read data * @param out the {@link List} to which decoded messages should be added */ protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { try { while (in.isReadable()) { int outSize = out.size(); int oldInputLength = in.readableBytes(); this.decode(ctx, in, out); // Check if this handler was removed before continuing the loop. // If it was removed, it is not safe to continue to operate on the buffer. // // See https://github.com/netty/netty/issues/1664 if (ctx.isRemoved()) { break; } if (outSize == out.size()) { if (oldInputLength == in.readableBytes()) { break; } else { continue; } } if (oldInputLength == in.readableBytes()) { throw new DecoderException( StringUtil.simpleClassName(getClass()) + ".decode() did not read anything but decoded a message."); } if (this.isSingleDecode()) { break; } } } catch (DecoderException e) { throw e; } catch (Throwable cause) { throw new DecoderException(cause); } } /* * Decode the from one {@link ByteBuf} to an other. This method will be called till either the input * {@link ByteBuf} has nothing to read when return from this method or till nothing was read from the input * {@link ByteBuf}. * * @param ctx the {@link ChannelHandlerContext} which this {@link ByteToMessageDecoder} belongs to * @param in the {@link ByteBuf} from which to read data * @param out the {@link List} to which decoded messages should be added * @throws Exception is thrown if an error occur */ protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception; /* * Is called one last time when the {@link ChannelHandlerContext} goes in-active. Which means the * {@link #channelInactive(ChannelHandlerContext)} was triggered. * * By default this will just call {@link #decode(ChannelHandlerContext, ByteBuf, List)} but sub-classes may * override this for some special cleanup operation. * * @param ctx the {@link ChannelHandlerContext} which this {@link ByteToMessageDecoder} belongs to * @param in the {@link ByteBuf} from which to read data * @param out the {@link List} to which decoded messages should be added * @throws Exception is thrown if an error occur */ protected void decodeLast(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { this.decode(ctx, in, out); } }