netty5/codec-redis/src/main/java/io/netty/handler/codec/redis/RedisDecoder.java
Norman Maurer 0e4c073bcf
Remove the intermediate List from ByteToMessageDecoder (and sub-class… (#8626)
Motivation:

ByteToMessageDecoder requires using an intermediate List to put results into. This intermediate list adds overhead (memory/CPU) which grows as the number of objects increases. This overhead can be avoided by directly propagating events through the ChannelPipeline via ctx.fireChannelRead(...). This also makes the semantics more clear and allows us to keep track if we need to call ctx.read() in all cases.

Modifications:

- Remove List from the method signature of ByteToMessageDecoder.decode(...) and decodeLast(...)
- Adjust all sub-classes
- Adjust unit tests
- Fix javadocs.

Result:

Adjust ByteToMessageDecoder as noted in https://github.com/netty/netty/issues/8525.
2019-12-16 21:00:32 +01:00

331 lines
13 KiB
Java

/*
* Copyright 2016 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.redis;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.util.ByteProcessor;
import io.netty.util.CharsetUtil;
import io.netty.util.internal.UnstableApi;
/**
* Decodes the Redis protocol into {@link RedisMessage} objects following
* <a href="http://redis.io/topics/protocol">RESP (REdis Serialization Protocol)</a>.
*
* {@link RedisMessage} parts can be aggregated to {@link RedisMessage} using
* {@link RedisArrayAggregator} or processed directly.
*/
@UnstableApi
public final class RedisDecoder extends ByteToMessageDecoder {
private final ToPositiveLongProcessor toPositiveLongProcessor = new ToPositiveLongProcessor();
private final boolean decodeInlineCommands;
private final int maxInlineMessageLength;
private final RedisMessagePool messagePool;
// current decoding states
private State state = State.DECODE_TYPE;
private RedisMessageType type;
private int remainingBulkLength;
private enum State {
DECODE_TYPE,
DECODE_INLINE, // SIMPLE_STRING, ERROR, INTEGER
DECODE_LENGTH, // BULK_STRING, ARRAY_HEADER
DECODE_BULK_STRING_EOL,
DECODE_BULK_STRING_CONTENT,
}
/**
* Creates a new instance with default {@code maxInlineMessageLength} and {@code messagePool}
* and inline command decoding disabled.
*/
public RedisDecoder() {
this(false);
}
/**
* Creates a new instance with default {@code maxInlineMessageLength} and {@code messagePool}.
* @param decodeInlineCommands if {@code true}, inline commands will be decoded.
*/
public RedisDecoder(boolean decodeInlineCommands) {
this(RedisConstants.REDIS_INLINE_MESSAGE_MAX_LENGTH, FixedRedisMessagePool.INSTANCE, decodeInlineCommands);
}
/**
* Creates a new instance with inline command decoding disabled.
* @param maxInlineMessageLength the maximum length of inline message.
* @param messagePool the predefined message pool.
*/
public RedisDecoder(int maxInlineMessageLength, RedisMessagePool messagePool) {
this(maxInlineMessageLength, messagePool, false);
}
/**
* Creates a new instance.
* @param maxInlineMessageLength the maximum length of inline message.
* @param messagePool the predefined message pool.
* @param decodeInlineCommands if {@code true}, inline commands will be decoded.
*/
public RedisDecoder(int maxInlineMessageLength, RedisMessagePool messagePool, boolean decodeInlineCommands) {
if (maxInlineMessageLength <= 0 || maxInlineMessageLength > RedisConstants.REDIS_MESSAGE_MAX_LENGTH) {
throw new RedisCodecException("maxInlineMessageLength: " + maxInlineMessageLength +
" (expected: <= " + RedisConstants.REDIS_MESSAGE_MAX_LENGTH + ")");
}
this.maxInlineMessageLength = maxInlineMessageLength;
this.messagePool = messagePool;
this.decodeInlineCommands = decodeInlineCommands;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
try {
for (;;) {
switch (state) {
case DECODE_TYPE:
if (!decodeType(in)) {
return;
}
break;
case DECODE_INLINE:
if (!decodeInline(ctx, in)) {
return;
}
break;
case DECODE_LENGTH:
if (!decodeLength(ctx, in)) {
return;
}
break;
case DECODE_BULK_STRING_EOL:
if (!decodeBulkStringEndOfLine(ctx, in)) {
return;
}
break;
case DECODE_BULK_STRING_CONTENT:
if (!decodeBulkStringContent(ctx, in)) {
return;
}
break;
default:
throw new RedisCodecException("Unknown state: " + state);
}
}
} catch (RedisCodecException e) {
resetDecoder();
throw e;
} catch (Exception e) {
resetDecoder();
throw new RedisCodecException(e);
}
}
private void resetDecoder() {
state = State.DECODE_TYPE;
remainingBulkLength = 0;
}
private boolean decodeType(ByteBuf in) throws Exception {
if (!in.isReadable()) {
return false;
}
type = RedisMessageType.readFrom(in, decodeInlineCommands);
state = type.isInline() ? State.DECODE_INLINE : State.DECODE_LENGTH;
return true;
}
private boolean decodeInline(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
ByteBuf lineBytes = readLine(in);
if (lineBytes == null) {
if (in.readableBytes() > maxInlineMessageLength) {
throw new RedisCodecException("length: " + in.readableBytes() +
" (expected: <= " + maxInlineMessageLength + ")");
}
return false;
}
ctx.fireChannelRead(newInlineRedisMessage(type, lineBytes));
resetDecoder();
return true;
}
private boolean decodeLength(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
ByteBuf lineByteBuf = readLine(in);
if (lineByteBuf == null) {
return false;
}
final long length = parseRedisNumber(lineByteBuf);
if (length < RedisConstants.NULL_VALUE) {
throw new RedisCodecException("length: " + length + " (expected: >= " + RedisConstants.NULL_VALUE + ")");
}
switch (type) {
case ARRAY_HEADER:
ctx.fireChannelRead(new ArrayHeaderRedisMessage(length));
resetDecoder();
return true;
case BULK_STRING:
if (length > RedisConstants.REDIS_MESSAGE_MAX_LENGTH) {
throw new RedisCodecException("length: " + length + " (expected: <= " +
RedisConstants.REDIS_MESSAGE_MAX_LENGTH + ")");
}
remainingBulkLength = (int) length; // range(int) is already checked.
return decodeBulkString(ctx, in);
default:
throw new RedisCodecException("bad type: " + type);
}
}
private boolean decodeBulkString(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
switch (remainingBulkLength) {
case RedisConstants.NULL_VALUE: // $-1\r\n
ctx.fireChannelRead(FullBulkStringRedisMessage.NULL_INSTANCE);
resetDecoder();
return true;
case 0:
state = State.DECODE_BULK_STRING_EOL;
return decodeBulkStringEndOfLine(ctx, in);
default: // expectedBulkLength is always positive.
ctx.fireChannelRead(new BulkStringHeaderRedisMessage(remainingBulkLength));
state = State.DECODE_BULK_STRING_CONTENT;
return decodeBulkStringContent(ctx, in);
}
}
// $0\r\n <here> \r\n
private boolean decodeBulkStringEndOfLine(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
if (in.readableBytes() < RedisConstants.EOL_LENGTH) {
return false;
}
readEndOfLine(in);
ctx.fireChannelRead(FullBulkStringRedisMessage.EMPTY_INSTANCE);
resetDecoder();
return true;
}
// ${expectedBulkLength}\r\n <here> {data...}\r\n
private boolean decodeBulkStringContent(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
final int readableBytes = in.readableBytes();
if (readableBytes == 0 || remainingBulkLength == 0 && readableBytes < RedisConstants.EOL_LENGTH) {
return false;
}
// if this is last frame.
if (readableBytes >= remainingBulkLength + RedisConstants.EOL_LENGTH) {
ByteBuf content = in.readSlice(remainingBulkLength);
readEndOfLine(in);
// Only call retain after readEndOfLine(...) as the method may throw an exception.
ctx.fireChannelRead(new DefaultLastBulkStringRedisContent(content.retain()));
resetDecoder();
return true;
}
// chunked write.
int toRead = Math.min(remainingBulkLength, readableBytes);
remainingBulkLength -= toRead;
ctx.fireChannelRead(new DefaultBulkStringRedisContent(in.readSlice(toRead).retain()));
return true;
}
private static void readEndOfLine(final ByteBuf in) {
final short delim = in.readShort();
if (RedisConstants.EOL_SHORT == delim) {
return;
}
final byte[] bytes = RedisCodecUtil.shortToBytes(delim);
throw new RedisCodecException("delimiter: [" + bytes[0] + "," + bytes[1] + "] (expected: \\r\\n)");
}
private RedisMessage newInlineRedisMessage(RedisMessageType messageType, ByteBuf content) {
switch (messageType) {
case INLINE_COMMAND:
return new InlineCommandRedisMessage(content.toString(CharsetUtil.UTF_8));
case SIMPLE_STRING: {
SimpleStringRedisMessage cached = messagePool.getSimpleString(content);
return cached != null ? cached : new SimpleStringRedisMessage(content.toString(CharsetUtil.UTF_8));
}
case ERROR: {
ErrorRedisMessage cached = messagePool.getError(content);
return cached != null ? cached : new ErrorRedisMessage(content.toString(CharsetUtil.UTF_8));
}
case INTEGER: {
IntegerRedisMessage cached = messagePool.getInteger(content);
return cached != null ? cached : new IntegerRedisMessage(parseRedisNumber(content));
}
default:
throw new RedisCodecException("bad type: " + messageType);
}
}
private static ByteBuf readLine(ByteBuf in) {
if (!in.isReadable(RedisConstants.EOL_LENGTH)) {
return null;
}
final int lfIndex = in.forEachByte(ByteProcessor.FIND_LF);
if (lfIndex < 0) {
return null;
}
ByteBuf data = in.readSlice(lfIndex - in.readerIndex() - 1); // `-1` is for CR
readEndOfLine(in); // validate CR LF
return data;
}
private long parseRedisNumber(ByteBuf byteBuf) {
final int readableBytes = byteBuf.readableBytes();
final boolean negative = readableBytes > 0 && byteBuf.getByte(byteBuf.readerIndex()) == '-';
final int extraOneByteForNegative = negative ? 1 : 0;
if (readableBytes <= extraOneByteForNegative) {
throw new RedisCodecException("no number to parse: " + byteBuf.toString(CharsetUtil.US_ASCII));
}
if (readableBytes > RedisConstants.POSITIVE_LONG_MAX_LENGTH + extraOneByteForNegative) {
throw new RedisCodecException("too many characters to be a valid RESP Integer: " +
byteBuf.toString(CharsetUtil.US_ASCII));
}
if (negative) {
return -parsePositiveNumber(byteBuf.skipBytes(extraOneByteForNegative));
}
return parsePositiveNumber(byteBuf);
}
private long parsePositiveNumber(ByteBuf byteBuf) {
toPositiveLongProcessor.reset();
byteBuf.forEachByte(toPositiveLongProcessor);
return toPositiveLongProcessor.content();
}
private static final class ToPositiveLongProcessor implements ByteProcessor {
private long result;
@Override
public boolean process(byte value) throws Exception {
if (value < '0' || value > '9') {
throw new RedisCodecException("bad byte in number: " + value);
}
result = result * 10 + (value - '0');
return true;
}
public long content() {
return result;
}
public void reset() {
result = 0;
}
}
}