From a803e107614bc8aa0886b9b7ae742463c6a156c1 Mon Sep 17 00:00:00 2001 From: franz1981 Date: Mon, 13 Feb 2023 14:12:39 +0100 Subject: [PATCH] Revert "Revert "Speed-up HTTP 1.1 header and line parsing (#12321)"" This reverts commit 9993e07356ea39edf14008789d3377d2bb62ea92. --- .../netty/handler/codec/http/HttpMethod.java | 8 + .../handler/codec/http/HttpObjectDecoder.java | 434 +++++++++++------- .../codec/http/HttpRequestDecoder.java | 167 +++++++ .../netty/handler/codec/http/HttpVersion.java | 11 +- .../main/java/io/netty/util/AsciiString.java | 4 +- .../io/netty/util/internal/StringUtil.java | 14 +- ...HttpFragmentedRequestDecoderBenchmark.java | 126 +++++ .../HttpPipelinedRequestDecoderBenchmark.java | 112 +++++ ...mark.java => HttpRequestDecoderUtils.java} | 57 +-- 9 files changed, 713 insertions(+), 220 deletions(-) create mode 100644 microbench/src/main/java/io/netty/microbench/http/HttpFragmentedRequestDecoderBenchmark.java create mode 100644 microbench/src/main/java/io/netty/microbench/http/HttpPipelinedRequestDecoderBenchmark.java rename microbench/src/main/java/io/netty/microbench/http/{HttpRequestDecoderBenchmark.java => HttpRequestDecoderUtils.java} (56%) diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpMethod.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpMethod.java index 3b7c1239a25..59db66e7425 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpMethod.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpMethod.java @@ -106,6 +106,14 @@ public class HttpMethod implements Comparable { * will be returned. Otherwise, a new instance will be returned. */ public static HttpMethod valueOf(String name) { + // fast-path + if (name == HttpMethod.GET.name()) { + return HttpMethod.GET; + } + if (name == HttpMethod.POST.name()) { + return HttpMethod.POST; + } + // "slow"-path HttpMethod result = methodMap.get(name); return result != null ? result : new HttpMethod(name); } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java index 4f36c46da89..26bff6f7705 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java @@ -25,8 +25,9 @@ import io.netty.handler.codec.DecoderResult; import io.netty.handler.codec.PrematureChannelClosureException; import io.netty.handler.codec.TooLongFrameException; +import io.netty.util.AsciiString; import io.netty.util.ByteProcessor; -import io.netty.util.internal.AppendableCharSequence; +import io.netty.util.internal.StringUtil; import java.util.List; @@ -137,14 +138,12 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { public static final boolean DEFAULT_VALIDATE_HEADERS = true; public static final int DEFAULT_INITIAL_BUFFER_SIZE = 128; public static final boolean DEFAULT_ALLOW_DUPLICATE_CONTENT_LENGTHS = false; - - private static final String EMPTY_VALUE = ""; - private final int maxChunkSize; private final boolean chunkedSupported; private final boolean allowPartialChunks; protected final boolean validateHeaders; private final boolean allowDuplicateContentLengths; + private final ByteBuf parserScratchBuffer; private final HeaderParser headerParser; private final LineParser lineParser; @@ -154,11 +153,19 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { private volatile boolean resetRequested; // These will be updated by splitHeader(...) - private CharSequence name; - private CharSequence value; - + private AsciiString name; + private String value; private LastHttpContent trailer; + @Override + protected void handlerRemoved0(ChannelHandlerContext ctx) throws Exception { + try { + parserScratchBuffer.release(); + } finally { + super.handlerRemoved0(ctx); + } + } + /** * The internal state of {@link HttpObjectDecoder}. * Internal use only. @@ -239,9 +246,9 @@ protected HttpObjectDecoder( checkPositive(maxHeaderSize, "maxHeaderSize"); checkPositive(maxChunkSize, "maxChunkSize"); - AppendableCharSequence seq = new AppendableCharSequence(initialBufferSize); - lineParser = new LineParser(seq, maxInitialLineLength); - headerParser = new HeaderParser(seq, maxHeaderSize); + parserScratchBuffer = Unpooled.buffer(initialBufferSize); + lineParser = new LineParser(parserScratchBuffer, maxInitialLineLength); + headerParser = new HeaderParser(parserScratchBuffer, maxHeaderSize); this.maxChunkSize = maxChunkSize; this.chunkedSupported = chunkedSupported; this.validateHeaders = validateHeaders; @@ -259,16 +266,12 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List ou case SKIP_CONTROL_CHARS: // Fall-through case READ_INITIAL: try { - AppendableCharSequence line = lineParser.parse(buffer); + ByteBuf line = lineParser.parse(buffer); if (line == null) { return; } - String[] initialLine = splitInitialLine(line); - if (initialLine.length < 3) { - // Invalid initial line - ignore. - currentState = State.SKIP_CONTROL_CHARS; - return; - } + final String[] initialLine = splitInitialLine(line); + assert initialLine.length == 3 : "initialLine::length must be 3"; message = createMessage(initialLine); currentState = State.READ_HEADER; @@ -373,11 +376,11 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List ou * read chunk, read and ignore the CRLF and repeat until 0 */ case READ_CHUNK_SIZE: try { - AppendableCharSequence line = lineParser.parse(buffer); + ByteBuf line = lineParser.parse(buffer); if (line == null) { return; } - int chunkSize = getChunkSize(line.toString()); + int chunkSize = getChunkSize(line.array(), line.arrayOffset() + line.readerIndex(), line.readableBytes()); this.chunkSize = chunkSize; if (chunkSize == 0) { currentState = State.READ_CHUNK_FOOTER; @@ -625,31 +628,35 @@ private State readHeaders(ByteBuf buffer) { final HttpMessage message = this.message; final HttpHeaders headers = message.headers(); - AppendableCharSequence line = headerParser.parse(buffer); + final HeaderParser headerParser = this.headerParser; + + ByteBuf line = headerParser.parse(buffer); if (line == null) { return null; } - if (line.length() > 0) { - do { - char firstChar = line.charAtUnsafe(0); - if (name != null && (firstChar == ' ' || firstChar == '\t')) { - //please do not make one line from below code - //as it breaks +XX:OptimizeStringConcat optimization - String trimmedLine = line.toString().trim(); - String valueStr = String.valueOf(value); - value = valueStr + ' ' + trimmedLine; - } else { - if (name != null) { - headers.add(name, value); - } - splitHeader(line); + int lineLength = line.readableBytes(); + while (lineLength > 0) { + final byte[] lineContent = line.array(); + final int startLine = line.arrayOffset() + line.readerIndex(); + final byte firstChar = lineContent[startLine]; + if (name != null && (firstChar == ' ' || firstChar == '\t')) { + //please do not make one line from below code + //as it breaks +XX:OptimizeStringConcat optimization + String trimmedLine = langAsciiString(lineContent, startLine, lineLength).trim(); + String valueStr = value; + value = valueStr + ' ' + trimmedLine; + } else { + if (name != null) { + headers.add(name, value); } + splitHeader(lineContent, startLine, lineLength); + } - line = headerParser.parse(buffer); - if (line == null) { - return null; - } - } while (line.length() > 0); + line = headerParser.parse(buffer); + if (line == null) { + return null; + } + lineLength = line.readableBytes(); } // Add the last header. @@ -732,12 +739,14 @@ private long contentLength() { } private LastHttpContent readTrailingHeaders(ByteBuf buffer) { - AppendableCharSequence line = headerParser.parse(buffer); + final HeaderParser headerParser = this.headerParser; + ByteBuf line = headerParser.parse(buffer); if (line == null) { return null; } LastHttpContent trailer = this.trailer; - if (line.length() == 0 && trailer == null) { + int lineLength = line.readableBytes(); + if (lineLength == 0 && trailer == null) { // We have received the empty line which signals the trailer is complete and did not parse any trailers // before. Just return an empty last content to reduce allocations. return LastHttpContent.EMPTY_LAST_CONTENT; @@ -747,21 +756,23 @@ private LastHttpContent readTrailingHeaders(ByteBuf buffer) { if (trailer == null) { trailer = this.trailer = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER, validateHeaders); } - while (line.length() > 0) { - char firstChar = line.charAtUnsafe(0); + while (lineLength > 0) { + final byte[] lineContent = line.array(); + final int startLine = line.arrayOffset() + line.readerIndex(); + final byte firstChar = lineContent[startLine]; if (lastHeader != null && (firstChar == ' ' || firstChar == '\t')) { List current = trailer.trailingHeaders().getAll(lastHeader); if (!current.isEmpty()) { int lastPos = current.size() - 1; //please do not make one line from below code //as it breaks +XX:OptimizeStringConcat optimization - String lineTrimmed = line.toString().trim(); + String lineTrimmed = langAsciiString(lineContent, startLine, line.readableBytes()).trim(); String currentLastPos = current.get(lastPos); current.set(lastPos, currentLastPos + lineTrimmed); } } else { - splitHeader(line); - CharSequence headerName = name; + splitHeader(lineContent, startLine, lineLength); + AsciiString headerName = name; if (!HttpHeaderNames.CONTENT_LENGTH.contentEqualsIgnoreCase(headerName) && !HttpHeaderNames.TRANSFER_ENCODING.contentEqualsIgnoreCase(headerName) && !HttpHeaderNames.TRAILER.contentEqualsIgnoreCase(headerName)) { @@ -776,6 +787,7 @@ private LastHttpContent readTrailingHeaders(ByteBuf buffer) { if (line == null) { return null; } + lineLength = line.readableBytes(); } this.trailer = null; @@ -786,53 +798,85 @@ private LastHttpContent readTrailingHeaders(ByteBuf buffer) { protected abstract HttpMessage createMessage(String[] initialLine) throws Exception; protected abstract HttpMessage createInvalidMessage(); - private static int getChunkSize(String hex) { - hex = hex.trim(); - for (int i = 0; i < hex.length(); i ++) { - char c = hex.charAt(i); - if (c == ';' || Character.isWhitespace(c) || Character.isISOControl(c)) { - hex = hex.substring(0, i); - break; + private static int getChunkSize(byte[] hex, int start, int length) { + // byte[] is produced by LineParse::parseLine that already skip ISO CTRL and Whitespace chars + int result = 0; + for (int i = 0; i < length; i++) { + final int digit = StringUtil.decodeHexNibble(hex[start + i]); + if (digit == -1) { + // uncommon path + if (hex[start + i] == ';') { + return result; + } + throw new NumberFormatException(); } + result *= 16; + result += digit; } - - return Integer.parseInt(hex, 16); + return result; } - private static String[] splitInitialLine(AppendableCharSequence sb) { - int aStart; - int aEnd; - int bStart; - int bEnd; - int cStart; - int cEnd; + private String[] splitInitialLine(ByteBuf asciiBuffer) { + final byte[] asciiBytes = asciiBuffer.array(); - aStart = findNonSPLenient(sb, 0); - aEnd = findSPLenient(sb, aStart); + final int arrayOffset = asciiBuffer.arrayOffset(); - bStart = findNonSPLenient(sb, aEnd); - bEnd = findSPLenient(sb, bStart); + final int startContent = arrayOffset + asciiBuffer.readerIndex(); - cStart = findNonSPLenient(sb, bEnd); - cEnd = findEndOfString(sb); + final int end = startContent + asciiBuffer.readableBytes(); - return new String[] { - sb.subStringUnsafe(aStart, aEnd), - sb.subStringUnsafe(bStart, bEnd), - cStart < cEnd? sb.subStringUnsafe(cStart, cEnd) : "" }; + final int aStart = findNonSPLenient(asciiBytes, startContent, end); + final int aEnd = findSPLenient(asciiBytes, aStart, end); + + final int bStart = findNonSPLenient(asciiBytes, aEnd, end); + final int bEnd = findSPLenient(asciiBytes, bStart, end); + + final int cStart = findNonSPLenient(asciiBytes, bEnd, end); + final int cEnd = findEndOfString(asciiBytes, Math.max(cStart - 1, startContent), end); + + return new String[]{ + splitFirstWordInitialLine(asciiBytes, aStart, aEnd - aStart), + splitSecondWordInitialLine(asciiBytes, bStart, bEnd - bStart), + cStart < cEnd ? splitThirdWordInitialLine(asciiBytes, cStart, cEnd - cStart) : StringUtil.EMPTY_STRING}; } - private void splitHeader(AppendableCharSequence sb) { - final int length = sb.length(); - int nameStart; - int nameEnd; - int colonEnd; - int valueStart; - int valueEnd; + protected String splitFirstWordInitialLine(final byte[] asciiContent, int start, int length) { + return langAsciiString(asciiContent, start, length); + } + + protected String splitSecondWordInitialLine(final byte[] asciiContent, int start, int length) { + return langAsciiString(asciiContent, start, length); + } + + protected String splitThirdWordInitialLine(final byte[] asciiContent, int start, int length) { + return langAsciiString(asciiContent, start, length); + } + + /** + * This method shouldn't exist: look at https://bugs.openjdk.org/browse/JDK-8295496 for more context + */ + private static String langAsciiString(final byte[] asciiContent, int start, int length) { + if (length == 0) { + return StringUtil.EMPTY_STRING; + } + // DON'T REMOVE: it helps JIT to use a simpler intrinsic stub for System::arrayCopy based on the call-site + if (start == 0) { + if (length == asciiContent.length) { + return new String(asciiContent, 0, 0, asciiContent.length); + } + return new String(asciiContent, 0, 0, length); + } + return new String(asciiContent, 0, start, length); + } - nameStart = findNonWhitespace(sb, 0); - for (nameEnd = nameStart; nameEnd < length; nameEnd ++) { - char ch = sb.charAtUnsafe(nameEnd); + private void splitHeader(byte[] line, int start, int length) { + final int end = start + length; + int nameEnd; + final int nameStart = findNonWhitespace(line, start, end); + // hoist this load out of the loop, because it won't change! + final boolean isDecodingRequest = isDecodingRequest(); + for (nameEnd = nameStart; nameEnd < end; nameEnd ++) { + byte ch = line[nameEnd]; // https://tools.ietf.org/html/rfc7230#section-3.2.4 // // No whitespace is allowed between the header field-name and colon. In @@ -847,67 +891,93 @@ private void splitHeader(AppendableCharSequence sb) { // is done in the DefaultHttpHeaders implementation. // // In the case of decoding a response we will "skip" the whitespace. - (!isDecodingRequest() && isOWS(ch))) { + (!isDecodingRequest && isOWS(ch))) { break; } } - if (nameEnd == length) { + if (nameEnd == end) { // There was no colon present at all. throw new IllegalArgumentException("No colon found"); } - - for (colonEnd = nameEnd; colonEnd < length; colonEnd ++) { - if (sb.charAtUnsafe(colonEnd) == ':') { + int colonEnd; + for (colonEnd = nameEnd; colonEnd < end; colonEnd ++) { + if (line[colonEnd] == ':') { colonEnd ++; break; } } - - name = sb.subStringUnsafe(nameStart, nameEnd); - valueStart = findNonWhitespace(sb, colonEnd); - if (valueStart == length) { - value = EMPTY_VALUE; + name = splitHeaderName(line, nameStart, nameEnd - nameStart); + final int valueStart = findNonWhitespace(line, colonEnd, end); + if (valueStart == end) { + value = StringUtil.EMPTY_STRING; } else { - valueEnd = findEndOfString(sb); - value = sb.subStringUnsafe(valueStart, valueEnd); + final int valueEnd = findEndOfString(line, start, end); + // no need to make uses of the ByteBuf's toString ASCII method here, and risk to get JIT confused + value = langAsciiString(line, valueStart, valueEnd - valueStart); } } - private static int findNonSPLenient(AppendableCharSequence sb, int offset) { - for (int result = offset; result < sb.length(); ++result) { - char c = sb.charAtUnsafe(result); + protected AsciiString splitHeaderName(byte[] sb, int start, int length) { + return new AsciiString(sb, start, length, true); + } + + private static int findNonSPLenient(byte[] sb, int offset, int end) { + for (int result = offset; result < end; ++result) { + byte c = sb[result]; // See https://tools.ietf.org/html/rfc7230#section-3.5 if (isSPLenient(c)) { continue; } - if (Character.isWhitespace(c)) { + if (isWhitespace(c)) { // Any other whitespace delimiter is invalid throw new IllegalArgumentException("Invalid separator"); } return result; } - return sb.length(); + return end; } - private static int findSPLenient(AppendableCharSequence sb, int offset) { - for (int result = offset; result < sb.length(); ++result) { - if (isSPLenient(sb.charAtUnsafe(result))) { + private static int findSPLenient(byte[] sb, int offset, int end) { + for (int result = offset; result < end; ++result) { + if (isSPLenient(sb[result])) { return result; } } - return sb.length(); + return end; + } + + private static final boolean[] SP_LENIENT_BYTES; + private static final boolean[] LATIN_WHITESPACE; + + static { + // See https://tools.ietf.org/html/rfc7230#section-3.5 + SP_LENIENT_BYTES = new boolean[256]; + SP_LENIENT_BYTES[128 + ' '] = true; + SP_LENIENT_BYTES[128 + 0x09] = true; + SP_LENIENT_BYTES[128 + 0x0B] = true; + SP_LENIENT_BYTES[128 + 0x0C] = true; + SP_LENIENT_BYTES[128 + 0x0D] = true; + // TO SAVE PERFORMING Character::isWhitespace ceremony + LATIN_WHITESPACE = new boolean[256]; + for (byte b = Byte.MIN_VALUE; b < Byte.MAX_VALUE; b++) { + LATIN_WHITESPACE[128 + b] = Character.isWhitespace(b); + } } - private static boolean isSPLenient(char c) { + private static boolean isSPLenient(byte c) { // See https://tools.ietf.org/html/rfc7230#section-3.5 - return c == ' ' || c == (char) 0x09 || c == (char) 0x0B || c == (char) 0x0C || c == (char) 0x0D; + return SP_LENIENT_BYTES[c + 128]; } - private static int findNonWhitespace(AppendableCharSequence sb, int offset) { - for (int result = offset; result < sb.length(); ++result) { - char c = sb.charAtUnsafe(result); - if (!Character.isWhitespace(c)) { + private static boolean isWhitespace(byte b) { + return LATIN_WHITESPACE[b + 128]; + } + + private static int findNonWhitespace(byte[] sb, int offset, int end) { + for (int result = offset; result < end; ++result) { + byte c = sb[result]; + if (!isWhitespace(c)) { return result; } else if (!isOWS(c)) { // Only OWS is supported for whitespace @@ -915,41 +985,72 @@ private static int findNonWhitespace(AppendableCharSequence sb, int offset) { " but received a '" + c + "' (0x" + Integer.toHexString(c) + ")"); } } - return sb.length(); + return end; } - private static int findEndOfString(AppendableCharSequence sb) { - for (int result = sb.length() - 1; result > 0; --result) { - if (!Character.isWhitespace(sb.charAtUnsafe(result))) { + private static int findEndOfString(byte[] sb, int start, int end) { + for (int result = end - 1; result > start; --result) { + if (!isWhitespace(sb[result])) { return result + 1; } } return 0; } - private static boolean isOWS(char ch) { - return ch == ' ' || ch == (char) 0x09; + private static boolean isOWS(byte ch) { + return ch == ' ' || ch == 0x09; } - private static class HeaderParser implements ByteProcessor { - private final AppendableCharSequence seq; - private final int maxLength; + private static class HeaderParser { + protected final ByteBuf seq; + protected final int maxLength; int size; - HeaderParser(AppendableCharSequence seq, int maxLength) { + HeaderParser(ByteBuf seq, int maxLength) { this.seq = seq; this.maxLength = maxLength; } - public AppendableCharSequence parse(ByteBuf buffer) { - final int oldSize = size; - seq.reset(); - int i = buffer.forEachByte(this); - if (i == -1) { - size = oldSize; + public ByteBuf parse(ByteBuf buffer) { + final int readableBytes = buffer.readableBytes(); + final int readerIndex = buffer.readerIndex(); + final int maxBodySize = maxLength - size; + // adding 2 to account for both CR (if present) and LF + final int maxBodySizeWithCRLF = maxBodySize + 2; + final int toProcess = Math.min(maxBodySizeWithCRLF, readableBytes); + final int toIndexExclusive = readerIndex + toProcess; + final int indexOfLf = buffer.indexOf(readerIndex, toIndexExclusive, HttpConstants.LF); + if (indexOfLf == -1) { + if (readableBytes > maxBodySize) { + // TODO: Respond with Bad Request and discard the traffic + // or close the connection. + // No need to notify the upstream handlers - just log. + // If decoding a response, just throw an exception. + throw newException(maxLength); + } return null; } - buffer.readerIndex(i + 1); + final int endOfSeqIncluded; + if (indexOfLf > readerIndex && buffer.getByte(indexOfLf - 1) == HttpConstants.CR) { + // Drop CR if we had a CRLF pair + endOfSeqIncluded = indexOfLf - 1; + } else { + endOfSeqIncluded = indexOfLf; + } + final int newSize = endOfSeqIncluded - readerIndex; + if (newSize == 0) { + seq.clear(); + buffer.readerIndex(indexOfLf + 1); + return seq; + } + int size = this.size + newSize; + if (size > maxLength) { + throw newException(maxLength); + } + this.size = size; + seq.clear(); + seq.writeBytes(buffer, readerIndex, newSize); + buffer.readerIndex(indexOfLf + 1); return seq; } @@ -957,35 +1058,6 @@ public void reset() { size = 0; } - @Override - public boolean process(byte value) throws Exception { - char nextByte = (char) (value & 0xFF); - if (nextByte == HttpConstants.LF) { - int len = seq.length(); - // Drop CR if we had a CRLF pair - if (len >= 1 && seq.charAtUnsafe(len - 1) == HttpConstants.CR) { - -- size; - seq.setLength(len - 1); - } - return false; - } - - increaseCount(); - - seq.append(nextByte); - return true; - } - - protected final void increaseCount() { - if (++ size > maxLength) { - // TODO: Respond with Bad Request and discard the traffic - // or close the connection. - // No need to notify the upstream handlers - just log. - // If decoding a response, just throw an exception. - throw newException(maxLength); - } - } - protected TooLongFrameException newException(int maxLength) { return new TooLongHttpHeaderException("HTTP header is larger than " + maxLength + " bytes."); } @@ -993,27 +1065,40 @@ protected TooLongFrameException newException(int maxLength) { private final class LineParser extends HeaderParser { - LineParser(AppendableCharSequence seq, int maxLength) { + LineParser(ByteBuf seq, int maxLength) { super(seq, maxLength); } @Override - public AppendableCharSequence parse(ByteBuf buffer) { + public ByteBuf parse(ByteBuf buffer) { + // Suppress a warning because HeaderParser.reset() is supposed to be called reset(); + final int readableBytes = buffer.readableBytes(); + if (readableBytes == 0) { + return null; + } + final int readerIndex = buffer.readerIndex(); + if (currentState == State.SKIP_CONTROL_CHARS && skipControlChars(buffer, readableBytes, readerIndex)) { + return null; + } return super.parse(buffer); } - @Override - public boolean process(byte value) throws Exception { - if (currentState == State.SKIP_CONTROL_CHARS) { - char c = (char) (value & 0xFF); - if (Character.isISOControl(c) || Character.isWhitespace(c)) { - increaseCount(); - return true; + private boolean skipControlChars(ByteBuf buffer, int readableBytes, int readerIndex) { + assert currentState == State.SKIP_CONTROL_CHARS; + final int maxToSkip = Math.min(maxLength, readableBytes); + final int firstNonControlIndex = buffer.forEachByte(readerIndex, maxToSkip, SKIP_CONTROL_CHARS_BYTES); + if (firstNonControlIndex == -1) { + buffer.skipBytes(maxToSkip); + if (readableBytes > maxLength) { + throw newException(maxLength); } - currentState = State.READ_INITIAL; + return true; } - return super.process(value); + // from now on we don't care about control chars + buffer.readerIndex(firstNonControlIndex); + currentState = State.READ_INITIAL; + return false; } @Override @@ -1021,4 +1106,21 @@ protected TooLongFrameException newException(int maxLength) { return new TooLongHttpLineException("An HTTP line is larger than " + maxLength + " bytes."); } } + + private static final boolean[] ISO_CONTROL_OR_WHITESPACE; + + static { + ISO_CONTROL_OR_WHITESPACE = new boolean[256]; + for (byte b = Byte.MIN_VALUE; b < Byte.MAX_VALUE; b++) { + ISO_CONTROL_OR_WHITESPACE[128 + b] = Character.isISOControl(b) || isWhitespace(b); + } + } + + private static final ByteProcessor SKIP_CONTROL_CHARS_BYTES = new ByteProcessor() { + + @Override + public boolean process(byte value) { + return ISO_CONTROL_OR_WHITESPACE[128 + value]; + } + }; } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpRequestDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpRequestDecoder.java index 469ecb3d439..974a8b4db91 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpRequestDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpRequestDecoder.java @@ -17,6 +17,7 @@ import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelPipeline; +import io.netty.util.AsciiString; /** * Decodes {@link ByteBuf}s into {@link HttpRequest}s and {@link HttpContent}s. @@ -76,6 +77,34 @@ */ public class HttpRequestDecoder extends HttpObjectDecoder { + private static final AsciiString Host = AsciiString.cached("Host"); + private static final AsciiString Connection = AsciiString.cached("Connection"); + private static final AsciiString ContentType = AsciiString.cached("Content-Type"); + private static final AsciiString ContentLength = AsciiString.cached("Content-Length"); + + private static final int GET_AS_INT = 'G' | 'E' << 8 | 'T' << 16; + private static final int POST_AS_INT = 'P' | 'O' << 8 | 'S' << 16 | 'T' << 24; + private static final long HTTP_1_1_AS_LONG = 'H' | 'T' << 8 | 'T' << 16 | 'P' << 24 | (long) '/' << 32 | + (long) '1' << 40 | (long) '.' << 48 | (long) '1' << 56; + + private static final long HTTP_1_0_AS_LONG = 'H' | 'T' << 8 | 'T' << 16 | 'P' << 24 | (long) '/' << 32 | + (long) '1' << 40 | (long) '.' << 48 | (long) '0' << 56; + + private static final int HOST_AS_INT = 'H' | 'o' << 8 | 's' << 16 | 't' << 24; + + private static final long CONNECTION_AS_LONG_0 = 'C' | 'o' << 8 | 'n' << 16 | 'n' << 24 | + (long) 'e' << 32 | (long) 'c' << 40 | (long) 't' << 48 | (long) 'i' << 56; + + private static final short CONNECTION_AS_SHORT_1 = 'o' | 'n' << 8; + + private static final long CONTENT_AS_LONG = 'C' | 'o' << 8 | 'n' << 16 | 't' << 24 | + (long) 'e' << 32 | (long) 'n' << 40 | (long) 't' << 48 | (long) '-' << 56; + + private static final int TYPE_AS_INT = 'T' | 'y' << 8 | 'p' << 16 | 'e' << 24; + + private static final long LENGTH_AS_LONG = 'L' | 'e' << 8 | 'n' << 16 | 'g' << 24 | + (long) 't' << 32 | (long) 'h' << 40; + /** * Creates a new instance with the default * {@code maxInitialLineLength (4096)}, {@code maxHeaderSize (8192)}, and @@ -125,6 +154,144 @@ protected HttpMessage createMessage(String[] initialLine) throws Exception { HttpMethod.valueOf(initialLine[0]), initialLine[1], validateHeaders); } + @Override + protected AsciiString splitHeaderName(final byte[] sb, final int start, final int length) { + final byte firstChar = sb[start]; + if (firstChar == 'H' && length == 4) { + if (isHost(sb, start)) { + return Host; + } + } else if (firstChar == 'C') { + if (length == 10) { + if (isConnection(sb, start)) { + return Connection; + } + } else if (length == 12) { + if (isContentType(sb, start)) { + return ContentType; + } + } else if (length == 14) { + if (isContentLength(sb, start)) { + return ContentLength; + } + } + } + return super.splitHeaderName(sb, start, length); + } + + private static boolean isHost(byte[] sb, int start) { + final int maybeHost = sb[start] | + sb[start + 1] << 8 | + sb[start + 2] << 16 | + sb[start + 3] << 24; + return maybeHost == HOST_AS_INT; + } + + private static boolean isConnection(byte[] sb, int start) { + final long maybeConnecti = sb[start] | + sb[start + 1] << 8 | + sb[start + 2] << 16 | + sb[start + 3] << 24 | + (long) sb[start + 4] << 32 | + (long) sb[start + 5] << 40 | + (long) sb[start + 6] << 48 | + (long) sb[start + 7] << 56; + if (maybeConnecti != CONNECTION_AS_LONG_0) { + return false; + } + final short maybeOn = (short) (sb[start + 8] | sb[start + 9] << 8); + return maybeOn == CONNECTION_AS_SHORT_1; + } + + private static boolean isContentType(byte[] sb, int start) { + final long maybeContent = sb[start] | + sb[start + 1] << 8 | + sb[start + 2] << 16 | + sb[start + 3] << 24 | + (long) sb[start + 4] << 32 | + (long) sb[start + 5] << 40 | + (long) sb[start + 6] << 48 | + (long) sb[start + 7] << 56; + if (maybeContent != CONTENT_AS_LONG) { + return false; + } + final int maybeType = sb[start + 8] | + sb[start + 9] << 8 | + sb[start + 10] << 16 | + sb[start + 11] << 24; + return maybeType == TYPE_AS_INT; + } + + private static boolean isContentLength(byte[] sb, int start) { + final long maybeContent = sb[start] | + sb[start + 1] << 8 | + sb[start + 2] << 16 | + sb[start + 3] << 24 | + (long) sb[start + 4] << 32 | + (long) sb[start + 5] << 40 | + (long) sb[start + 6] << 48 | + (long) sb[start + 7] << 56; + if (maybeContent != CONTENT_AS_LONG) { + return false; + } + final long maybeLength = sb[start + 8] | + sb[start + 9] << 8 | + sb[start + 10] << 16 | + sb[start + 11] << 24 | + (long) sb[start + 12] << 32 | + (long) sb[start + 13] << 40; + return maybeLength == LENGTH_AS_LONG; + } + + private static boolean isGetMethod(final byte[] sb, int start) { + final int maybeGet = sb[start] | + sb[start + 1] << 8 | + sb[start + 2] << 16; + return maybeGet == GET_AS_INT; + } + + private static boolean isPostMethod(final byte[] sb, int start) { + final int maybePost = sb[start] | + sb[start + 1] << 8 | + sb[start + 2] << 16 | + sb[start + 3] << 24; + return maybePost == POST_AS_INT; + } + + @Override + protected String splitFirstWordInitialLine(final byte[] sb, final int start, final int length) { + if (length == 3) { + if (isGetMethod(sb, start)) { + return HttpMethod.GET.name(); + } + } else if (length == 4) { + if (isPostMethod(sb, start)) { + return HttpMethod.POST.name(); + } + } + return super.splitFirstWordInitialLine(sb, start, length); + } + + @Override + protected String splitThirdWordInitialLine(final byte[] sb, final int start, final int length) { + if (length == 8) { + final long maybeHttp1_x = sb[start] | + sb[start + 1] << 8 | + sb[start + 2] << 16 | + sb[start + 3] << 24 | + (long) sb[start + 4] << 32 | + (long) sb[start + 5] << 40 | + (long) sb[start + 6] << 48 | + (long) sb[start + 7] << 56; + if (maybeHttp1_x == HTTP_1_1_AS_LONG) { + return HttpVersion.HTTP_1_1_STRING; + } else if (maybeHttp1_x == HTTP_1_0_AS_LONG) { + return HttpVersion.HTTP_1_0_STRING; + } + } + return super.splitThirdWordInitialLine(sb, start, length); + } + @Override protected HttpMessage createInvalidMessage() { return new DefaultFullHttpRequest(HttpVersion.HTTP_1_0, HttpMethod.GET, "/bad-request", validateHeaders); diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpVersion.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpVersion.java index 4ae39646206..ec287e6dbc7 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpVersion.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpVersion.java @@ -35,8 +35,8 @@ public class HttpVersion implements Comparable { private static final Pattern VERSION_PATTERN = Pattern.compile("(\\S+)/(\\d+)\\.(\\d+)"); - private static final String HTTP_1_0_STRING = "HTTP/1.0"; - private static final String HTTP_1_1_STRING = "HTTP/1.1"; + static final String HTTP_1_0_STRING = "HTTP/1.0"; + static final String HTTP_1_1_STRING = "HTTP/1.1"; /** * HTTP/1.0 @@ -59,6 +59,13 @@ public class HttpVersion implements Comparable { public static HttpVersion valueOf(String text) { ObjectUtil.checkNotNull(text, "text"); + // super fast-path + if (text == HTTP_1_1_STRING) { + return HTTP_1_1; + } else if (text == HTTP_1_0_STRING) { + return HTTP_1_0; + } + text = text.trim(); if (text.isEmpty()) { diff --git a/common/src/main/java/io/netty/util/AsciiString.java b/common/src/main/java/io/netty/util/AsciiString.java index d34da4a0ddf..bbd4222e3d0 100644 --- a/common/src/main/java/io/netty/util/AsciiString.java +++ b/common/src/main/java/io/netty/util/AsciiString.java @@ -94,7 +94,9 @@ public AsciiString(byte[] value, boolean copy) { */ public AsciiString(byte[] value, int start, int length, boolean copy) { if (copy) { - this.value = Arrays.copyOfRange(value, start, start + length); + final byte[] rangedCopy = new byte[length]; + System.arraycopy(value, start, rangedCopy, 0, rangedCopy.length); + this.value = rangedCopy; this.offset = 0; } else { if (isOutOfBounds(start, length, value.length)) { diff --git a/common/src/main/java/io/netty/util/internal/StringUtil.java b/common/src/main/java/io/netty/util/internal/StringUtil.java index aa772904760..dd4e9d2bb27 100644 --- a/common/src/main/java/io/netty/util/internal/StringUtil.java +++ b/common/src/main/java/io/netty/util/internal/StringUtil.java @@ -253,12 +253,24 @@ public static T toHexString(T dst, byte[] src, int offset * given, or {@code -1} if the character is invalid. */ public static int decodeHexNibble(final char c) { - assert HEX2B.length == (Character.MAX_VALUE + 1); // Character.digit() is not used here, as it addresses a larger // set of characters (both ASCII and full-width latin letters). return HEX2B[c]; } + /** + * Helper to decode half of a hexadecimal number from a string. + * @param b The ASCII character of the hexadecimal number to decode. + * Must be in the range {@code [0-9a-fA-F]}. + * @return The hexadecimal value represented in the ASCII character + * given, or {@code -1} if the character is invalid. + */ + public static int decodeHexNibble(final byte b) { + // Character.digit() is not used here, as it addresses a larger + // set of characters (both ASCII and full-width latin letters). + return HEX2B[b]; + } + /** * Decode a 2-digit hex byte from within a string. */ diff --git a/microbench/src/main/java/io/netty/microbench/http/HttpFragmentedRequestDecoderBenchmark.java b/microbench/src/main/java/io/netty/microbench/http/HttpFragmentedRequestDecoderBenchmark.java new file mode 100644 index 00000000000..17b1c9e88e5 --- /dev/null +++ b/microbench/src/main/java/io/netty/microbench/http/HttpFragmentedRequestDecoderBenchmark.java @@ -0,0 +1,126 @@ +/* + * Copyright 2023 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: + * + * https://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.microbench.http; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.PooledByteBufAllocator; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.HttpRequestDecoder; +import io.netty.microbench.util.AbstractMicrobenchmark; +import io.netty.util.ReferenceCountUtil; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.CompilerControl; +import org.openjdk.jmh.annotations.CompilerControl.Mode; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; + +import java.util.ArrayList; +import java.util.Queue; +import java.util.concurrent.TimeUnit; + +import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_INITIAL_BUFFER_SIZE; +import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_CHUNK_SIZE; +import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_HEADER_SIZE; +import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_INITIAL_LINE_LENGTH; +import static io.netty.microbench.http.HttpRequestDecoderUtils.CONTENT_LENGTH; +import static io.netty.microbench.http.HttpRequestDecoderUtils.CONTENT_MIXED_DELIMITERS; + +/** + * This benchmark is based on HttpRequestDecoderTest class. + */ +@State(Scope.Benchmark) +@Warmup(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) +public class HttpFragmentedRequestDecoderBenchmark extends AbstractMicrobenchmark { + @Param({ "64", "128" }) + public int headerFragmentBytes; + + @Param({ "false", "true" }) + public boolean direct; + + @Param({ "false", "true" }) + public boolean pooled; + + @Param({ "true", "false"}) + public boolean validateHeaders; + + private EmbeddedChannel channel; + + private ByteBuf[] fragmentedRequest; + + private static ByteBuf[] stepsBuffers(ByteBufAllocator alloc, byte[] content, int fragmentSize, boolean direct) { + // allocate a single big buffer and just slice it + final int headerLength = content.length - CONTENT_LENGTH; + final ArrayList bufs = new ArrayList(); + for (int a = 0; a < headerLength;) { + int amount = fragmentSize; + if (a + amount > headerLength) { + amount = headerLength - a; + } + final ByteBuf buf = direct? alloc.directBuffer(amount, amount) : alloc.heapBuffer(amount, amount); + buf.writeBytes(content, a, amount); + bufs.add(buf); + a += amount; + } + // don't split the content + // Should produce HttpContent + final ByteBuf buf = direct? + alloc.directBuffer(CONTENT_LENGTH, CONTENT_LENGTH) : + alloc.heapBuffer(CONTENT_LENGTH, CONTENT_LENGTH); + buf.writeBytes(content, content.length - CONTENT_LENGTH, CONTENT_LENGTH); + bufs.add(buf); + return bufs.toArray(new ByteBuf[0]); + } + + @Setup + public void initPipeline() { + final ByteBufAllocator allocator = pooled? PooledByteBufAllocator.DEFAULT : UnpooledByteBufAllocator.DEFAULT; + fragmentedRequest = stepsBuffers(allocator, CONTENT_MIXED_DELIMITERS, headerFragmentBytes, direct); + channel = new EmbeddedChannel( + new HttpRequestDecoder(DEFAULT_MAX_INITIAL_LINE_LENGTH, DEFAULT_MAX_HEADER_SIZE, DEFAULT_MAX_CHUNK_SIZE, + validateHeaders, DEFAULT_INITIAL_BUFFER_SIZE)); + } + + @TearDown + public void releaseStepBuffers() { + for (ByteBuf buf : fragmentedRequest) { + buf.release(); + } + } + + @Benchmark + @CompilerControl(Mode.DONT_INLINE) + public void testDecodeWholeRequestInMultipleStepsMixedDelimiters() { + final EmbeddedChannel channel = this.channel; + for (ByteBuf buf : this.fragmentedRequest) { + buf.resetReaderIndex(); + buf.retain(); + channel.writeInbound(buf); + final Queue decoded = channel.inboundMessages(); + Object o; + while ((o = decoded.poll()) != null) { + ReferenceCountUtil.release(o); + } + } + } +} diff --git a/microbench/src/main/java/io/netty/microbench/http/HttpPipelinedRequestDecoderBenchmark.java b/microbench/src/main/java/io/netty/microbench/http/HttpPipelinedRequestDecoderBenchmark.java new file mode 100644 index 00000000000..ef6fffb064c --- /dev/null +++ b/microbench/src/main/java/io/netty/microbench/http/HttpPipelinedRequestDecoderBenchmark.java @@ -0,0 +1,112 @@ +/* + * Copyright 2014 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: + * + * https://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.microbench.http; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.PooledByteBufAllocator; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.HttpRequestDecoder; +import io.netty.microbench.util.AbstractMicrobenchmark; +import io.netty.util.ReferenceCountUtil; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.CompilerControl; +import org.openjdk.jmh.annotations.CompilerControl.Mode; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; + +import java.util.Queue; +import java.util.concurrent.TimeUnit; + +import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_INITIAL_BUFFER_SIZE; +import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_CHUNK_SIZE; +import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_HEADER_SIZE; +import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_INITIAL_LINE_LENGTH; +import static io.netty.microbench.http.HttpRequestDecoderUtils.CONTENT_MIXED_DELIMITERS; + +/** + * This benchmark is based on HttpRequestDecoderTest class. + */ +@State(Scope.Benchmark) +@Warmup(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) +public class HttpPipelinedRequestDecoderBenchmark extends AbstractMicrobenchmark { + + @Param({ "false", "true" }) + public boolean direct; + + @Param({ "1", "16" }) + public int pipeline; + + @Param({ "false", "true" }) + public boolean pooled; + + @Param({ "true", "false" }) + public boolean validateHeaders; + + private EmbeddedChannel channel; + + private ByteBuf pipelinedRequest; + + @Setup + public void initPipeline() { + final ByteBufAllocator allocator = pooled? PooledByteBufAllocator.DEFAULT : UnpooledByteBufAllocator.DEFAULT; + pipelinedRequest = pipelined(allocator, CONTENT_MIXED_DELIMITERS, pipeline, direct); + channel = new EmbeddedChannel( + new HttpRequestDecoder(DEFAULT_MAX_INITIAL_LINE_LENGTH, DEFAULT_MAX_HEADER_SIZE, DEFAULT_MAX_CHUNK_SIZE, + validateHeaders, DEFAULT_INITIAL_BUFFER_SIZE)); + // this is a trick to save doing it each time + pipelinedRequest.retain((Integer.MAX_VALUE / 2 - 1) - pipeline); + } + + private static ByteBuf pipelined(ByteBufAllocator alloc, byte[] content, int pipeline, boolean direct) { + final int totalSize = pipeline * content.length; + final ByteBuf buf = direct? alloc.directBuffer(totalSize, totalSize) : alloc.heapBuffer(totalSize, totalSize); + for (int i = 0; i < pipeline; i++) { + buf.writeBytes(content); + } + return buf; + } + + @Benchmark + @CompilerControl(Mode.DONT_INLINE) + public void testDecodeWholePipelinedRequestMixedDelimiters() { + final EmbeddedChannel channel = this.channel; + final ByteBuf batch = this.pipelinedRequest; + final int refCnt = batch.refCnt(); + if (refCnt == 1) { + batch.retain((Integer.MAX_VALUE / 2 - 1) - pipeline); + } + batch.resetReaderIndex(); + channel.writeInbound(batch); + final Queue decoded = channel.inboundMessages(); + Object o; + while ((o = decoded.poll()) != null) { + ReferenceCountUtil.release(o); + } + } + + @TearDown + public void release() { + this.pipelinedRequest.release(pipelinedRequest.refCnt()); + } +} diff --git a/microbench/src/main/java/io/netty/microbench/http/HttpRequestDecoderBenchmark.java b/microbench/src/main/java/io/netty/microbench/http/HttpRequestDecoderUtils.java similarity index 56% rename from microbench/src/main/java/io/netty/microbench/http/HttpRequestDecoderBenchmark.java rename to microbench/src/main/java/io/netty/microbench/http/HttpRequestDecoderUtils.java index 1b1a0a54b2c..4cb2da780ee 100644 --- a/microbench/src/main/java/io/netty/microbench/http/HttpRequestDecoderBenchmark.java +++ b/microbench/src/main/java/io/netty/microbench/http/HttpRequestDecoderUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 The Netty Project + * Copyright 2023 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 @@ -15,31 +15,15 @@ */ package io.netty.microbench.http; -import io.netty.buffer.Unpooled; -import io.netty.channel.embedded.EmbeddedChannel; -import io.netty.handler.codec.http.HttpRequestDecoder; -import io.netty.microbench.util.AbstractMicrobenchmark; import io.netty.util.CharsetUtil; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.Measurement; -import org.openjdk.jmh.annotations.Param; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Warmup; -/** - * This benchmark is based on HttpRequestDecoderTest class. - */ -@State(Scope.Benchmark) -@Warmup(iterations = 10) -@Measurement(iterations = 20) -public class HttpRequestDecoderBenchmark extends AbstractMicrobenchmark { +final class HttpRequestDecoderUtils { - private static final byte[] CONTENT_MIXED_DELIMITERS = createContent("\r\n", "\n"); - private static final int CONTENT_LENGTH = 120; + private HttpRequestDecoderUtils() { + } - @Param({ "2", "4", "8", "16", "32" }) - public int step; + static final byte[] CONTENT_MIXED_DELIMITERS = createContent("\r\n", "\n"); + static final int CONTENT_LENGTH = 120; private static byte[] createContent(String... lineDelimiters) { String lineDelimiter; @@ -67,7 +51,7 @@ private static byte[] createContent(String... lineDelimiters) { "Sec-WebSocket-Key2: 8 Xt754O3Q3QW 0 _60" + lineDelimiter + "Content-Type: application/x-www-form-urlencoded" + lineDelimiter2 + "Content-Length: " + CONTENT_LENGTH + lineDelimiter + - "\r\n" + + "\r\n" + "1234567890\r\n" + "1234567890\r\n" + "1234567890\r\n" + @@ -81,31 +65,4 @@ private static byte[] createContent(String... lineDelimiters) { ).getBytes(CharsetUtil.US_ASCII); } - @Benchmark - public void testDecodeWholeRequestInMultipleStepsMixedDelimiters() { - testDecodeWholeRequestInMultipleSteps(CONTENT_MIXED_DELIMITERS, step); - } - - private static void testDecodeWholeRequestInMultipleSteps(byte[] content, int fragmentSize) { - final EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder()); - - final int headerLength = content.length - CONTENT_LENGTH; - - // split up the header - for (int a = 0; a < headerLength;) { - int amount = fragmentSize; - if (a + amount > headerLength) { - amount = headerLength - a; - } - - // if header is done it should produce an HttpRequest - channel.writeInbound(Unpooled.wrappedBuffer(content, a, amount).asReadOnly()); - a += amount; - } - - for (int i = CONTENT_LENGTH; i > 0; i --) { - // Should produce HttpContent - channel.writeInbound(Unpooled.wrappedBuffer(content, content.length - i, 1).asReadOnly()); - } - } }