From 06e7627b5f39b96c05042d7bd85218560293fc19 Mon Sep 17 00:00:00 2001 From: Scott Mitchell Date: Sat, 12 Nov 2016 10:40:35 -0800 Subject: [PATCH] Read Only Http2Headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: A read only implementation of Http2Headers can allow for a more efficient usage of memory and more performant combined construction and iteration during serialization. Modifications: - Add a new ReadOnlyHttp2Headers class Result: ReadOnlyHttp2Headers exists and can be used for performance reasons when appropriate. ``` Benchmark (headerCount) Mode Cnt Score Error Units ReadOnlyHttp2HeadersBenchmark.defaultClientHeaders 1 avgt 20 96.156 ± 1.902 ns/op ReadOnlyHttp2HeadersBenchmark.defaultClientHeaders 5 avgt 20 157.925 ± 3.847 ns/op ReadOnlyHttp2HeadersBenchmark.defaultClientHeaders 10 avgt 20 236.257 ± 2.663 ns/op ReadOnlyHttp2HeadersBenchmark.defaultClientHeaders 20 avgt 20 392.861 ± 3.932 ns/op ReadOnlyHttp2HeadersBenchmark.defaultServerHeaders 1 avgt 20 48.759 ± 0.466 ns/op ReadOnlyHttp2HeadersBenchmark.defaultServerHeaders 5 avgt 20 113.122 ± 0.948 ns/op ReadOnlyHttp2HeadersBenchmark.defaultServerHeaders 10 avgt 20 192.698 ± 1.936 ns/op ReadOnlyHttp2HeadersBenchmark.defaultServerHeaders 20 avgt 20 348.974 ± 3.111 ns/op ReadOnlyHttp2HeadersBenchmark.defaultTrailers 1 avgt 20 35.694 ± 0.271 ns/op ReadOnlyHttp2HeadersBenchmark.defaultTrailers 5 avgt 20 98.993 ± 2.933 ns/op ReadOnlyHttp2HeadersBenchmark.defaultTrailers 10 avgt 20 171.035 ± 5.068 ns/op ReadOnlyHttp2HeadersBenchmark.defaultTrailers 20 avgt 20 330.621 ± 3.381 ns/op ReadOnlyHttp2HeadersBenchmark.readOnlyClientHeaders 1 avgt 20 40.573 ± 0.474 ns/op ReadOnlyHttp2HeadersBenchmark.readOnlyClientHeaders 5 avgt 20 56.516 ± 0.660 ns/op ReadOnlyHttp2HeadersBenchmark.readOnlyClientHeaders 10 avgt 20 76.890 ± 0.776 ns/op ReadOnlyHttp2HeadersBenchmark.readOnlyClientHeaders 20 avgt 20 117.531 ± 1.393 ns/op ReadOnlyHttp2HeadersBenchmark.readOnlyServerHeaders 1 avgt 20 29.206 ± 0.264 ns/op ReadOnlyHttp2HeadersBenchmark.readOnlyServerHeaders 5 avgt 20 44.587 ± 0.312 ns/op ReadOnlyHttp2HeadersBenchmark.readOnlyServerHeaders 10 avgt 20 64.458 ± 1.169 ns/op ReadOnlyHttp2HeadersBenchmark.readOnlyServerHeaders 20 avgt 20 107.179 ± 0.881 ns/op ReadOnlyHttp2HeadersBenchmark.readOnlyTrailers 1 avgt 20 21.563 ± 0.202 ns/op ReadOnlyHttp2HeadersBenchmark.readOnlyTrailers 5 avgt 20 41.019 ± 0.440 ns/op ReadOnlyHttp2HeadersBenchmark.readOnlyTrailers 10 avgt 20 64.053 ± 0.785 ns/op ReadOnlyHttp2HeadersBenchmark.readOnlyTrailers 20 avgt 20 113.737 ± 4.433 ns/op ``` --- .../codec/http2/DefaultHttp2Headers.java | 2 +- .../codec/http2/ReadOnlyHttp2Headers.java | 831 ++++++++++++++++++ .../codec/http2/DefaultHttp2HeadersTest.java | 2 +- .../codec/http2/ReadOnlyHttp2HeadersTest.java | 187 ++++ .../netty/handler/codec/DefaultHeaders.java | 2 +- .../io/netty/util/internal/EmptyArrays.java | 3 + .../microbench/headers/HeadersBenchmark.java | 2 +- .../ReadOnlyHttp2HeadersBenchmark.java | 139 +++ 8 files changed, 1164 insertions(+), 4 deletions(-) create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/ReadOnlyHttp2Headers.java create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/ReadOnlyHttp2HeadersTest.java create mode 100644 microbench/src/main/java/io/netty/microbench/headers/ReadOnlyHttp2HeadersBenchmark.java diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Headers.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Headers.java index 043d06e301..613b73b116 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Headers.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Headers.java @@ -34,7 +34,7 @@ public class DefaultHttp2Headers return !isUpperCase(value); } }; - private static final NameValidator HTTP2_NAME_VALIDATOR = new NameValidator() { + static final NameValidator HTTP2_NAME_VALIDATOR = new NameValidator() { @Override public void validateName(CharSequence name) { if (name == null || name.length() == 0) { diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/ReadOnlyHttp2Headers.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/ReadOnlyHttp2Headers.java new file mode 100644 index 0000000000..155fbf11fb --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/ReadOnlyHttp2Headers.java @@ -0,0 +1,831 @@ +/* + * 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.http2; + +import io.netty.handler.codec.Headers; +import io.netty.util.AsciiString; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +import static io.netty.handler.codec.CharSequenceValueConverter.INSTANCE; +import static io.netty.handler.codec.http2.DefaultHttp2Headers.HTTP2_NAME_VALIDATOR; +import static io.netty.util.internal.EmptyArrays.EMPTY_ASCII_STRINGS; + +/** + * A variant of {@link Http2Headers} which only supports read-only methods. + *

+ * Any array passed to this class may be used directly in the underlying data structures of this class. If these + * arrays may be modified it is the caller's responsibility to supply this class with a copy of the array. + *

+ * This may be a good alternative to {@link DefaultHttp2Headers} if your have a fixed set of headers which will not + * change. + */ +public final class ReadOnlyHttp2Headers implements Http2Headers { + private static final byte PSEUDO_HEADER_TOKEN = (byte) ':'; + private final AsciiString[] pseudoHeaders; + private final AsciiString[] otherHeaders; + + /** + * Used to create read only object designed to represent trailers. + *

+ * If this is used for a purpose other than trailers you may violate the header serialization ordering defined by + * RFC 7540, 8.1.2.1. + * @param validateHeaders {@code true} will run validation on each header name/value pair to ensure protocol + * compliance. + * @param otherHeaders A an array of key:value pairs. Must not contain any + * pseudo headers + * or {@code null} names/values. + * A copy will NOT be made of this array. If the contents of this array + * may be modified externally you are responsible for passing in a copy. + * @return A read only representation of the headers. + */ + public static ReadOnlyHttp2Headers trailers(boolean validateHeaders, AsciiString... otherHeaders) { + return new ReadOnlyHttp2Headers(validateHeaders, EMPTY_ASCII_STRINGS, otherHeaders); + } + + /** + * Create a new read only representation of headers used by clients. + * @param validateHeaders {@code true} will run validation on each header name/value pair to ensure protocol + * compliance. + * @param method The value for {@link PseudoHeaderName#METHOD}. + * @param path The value for {@link PseudoHeaderName#PATH}. + * @param scheme The value for {@link PseudoHeaderName#SCHEME}. + * @param authority The value for {@link PseudoHeaderName#AUTHORITY}. + * @param otherHeaders A an array of key:value pairs. Must not contain any + * pseudo headers + * or {@code null} names/values. + * A copy will NOT be made of this array. If the contents of this array + * may be modified externally you are responsible for passing in a copy. + * @return a new read only representation of headers used by clients. + */ + public static ReadOnlyHttp2Headers clientHeaders(boolean validateHeaders, + AsciiString method, AsciiString path, + AsciiString scheme, AsciiString authority, + AsciiString... otherHeaders) { + return new ReadOnlyHttp2Headers(validateHeaders, + new AsciiString[] { + PseudoHeaderName.METHOD.value(), method, PseudoHeaderName.PATH.value(), path, + PseudoHeaderName.SCHEME.value(), scheme, PseudoHeaderName.AUTHORITY.value(), authority + }, + otherHeaders); + } + + /** + * Create a new read only representation of headers used by servers. + * @param validateHeaders {@code true} will run validation on each header name/value pair to ensure protocol + * compliance. + * @param status The value for {@link PseudoHeaderName#STATUS}. + * @param otherHeaders A an array of key:value pairs. Must not contain any + * pseudo headers + * or {@code null} names/values. + * A copy will NOT be made of this array. If the contents of this array + * may be modified externally you are responsible for passing in a copy. + * @return a new read only representation of headers used by servers. + */ + public static ReadOnlyHttp2Headers serverHeaders(boolean validateHeaders, + AsciiString status, + AsciiString... otherHeaders) { + return new ReadOnlyHttp2Headers(validateHeaders, + new AsciiString[] { PseudoHeaderName.STATUS.value(), status }, + otherHeaders); + } + + private ReadOnlyHttp2Headers(boolean validateHeaders, AsciiString[] pseudoHeaders, AsciiString... otherHeaders) { + assert (pseudoHeaders.length & 1) == 0; // pseudoHeaders are only set internally so assert should be enough. + if ((otherHeaders.length & 1) != 0) { + throw newInvalidArraySizeException(); + } + if (validateHeaders) { + validateHeaders(pseudoHeaders, otherHeaders); + } + this.pseudoHeaders = pseudoHeaders; + this.otherHeaders = otherHeaders; + } + + private static IllegalArgumentException newInvalidArraySizeException() { + return new IllegalArgumentException("pseudoHeaders and otherHeaders must be arrays of [name, value] pairs"); + } + + private static void validateHeaders(AsciiString[] pseudoHeaders, AsciiString... otherHeaders) { + // We are only validating values... so start at 1 and go until end. + for (int i = 1; i < pseudoHeaders.length; i += 2) { + // pseudoHeaders names are only set internally so they are assumed to be valid. + if (pseudoHeaders[i] == null) { + throw new IllegalArgumentException("pseudoHeaders value at index " + i + " is null"); + } + } + + boolean seenNonPseudoHeader = false; + final int otherHeadersEnd = otherHeaders.length - 1; + for (int i = 0; i < otherHeadersEnd; i += 2) { + AsciiString name = otherHeaders[i]; + HTTP2_NAME_VALIDATOR.validateName(name); + if (!seenNonPseudoHeader && !name.isEmpty() && name.byteAt(0) != PSEUDO_HEADER_TOKEN) { + seenNonPseudoHeader = true; + } else if (seenNonPseudoHeader && !name.isEmpty() && name.byteAt(0) == PSEUDO_HEADER_TOKEN) { + throw new IllegalArgumentException( + "otherHeaders name at index " + i + " is a pseudo header that appears after non-pseudo headers."); + } + if (otherHeaders[i + 1] == null) { + throw new IllegalArgumentException("otherHeaders value at index " + (i + 1) + " is null"); + } + } + } + + private AsciiString get0(CharSequence name) { + final int nameHash = AsciiString.hashCode(name); + + final int pseudoHeadersEnd = pseudoHeaders.length - 1; + for (int i = 0; i < pseudoHeadersEnd; i += 2) { + AsciiString roName = pseudoHeaders[i]; + if (roName.hashCode() == nameHash && roName.contentEqualsIgnoreCase(name)) { + return pseudoHeaders[i + 1]; + } + } + + final int otherHeadersEnd = otherHeaders.length - 1; + for (int i = 0; i < otherHeadersEnd; i += 2) { + AsciiString roName = otherHeaders[i]; + if (roName.hashCode() == nameHash && roName.contentEqualsIgnoreCase(name)) { + return otherHeaders[i + 1]; + } + } + return null; + } + + @Override + public CharSequence get(CharSequence name) { + return get0(name); + } + + @Override + public CharSequence get(CharSequence name, CharSequence defaultValue) { + CharSequence value = get(name); + return value != null ? value : defaultValue; + } + + @Override + public CharSequence getAndRemove(CharSequence name) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public CharSequence getAndRemove(CharSequence name, CharSequence defaultValue) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public List getAll(CharSequence name) { + final int nameHash = AsciiString.hashCode(name); + List values = new ArrayList(); + + final int pseudoHeadersEnd = pseudoHeaders.length - 1; + for (int i = 0; i < pseudoHeadersEnd; i += 2) { + AsciiString roName = pseudoHeaders[i]; + if (roName.hashCode() == nameHash && roName.contentEqualsIgnoreCase(name)) { + values.add(pseudoHeaders[i + 1]); + } + } + + final int otherHeadersEnd = otherHeaders.length - 1; + for (int i = 0; i < otherHeadersEnd; i += 2) { + AsciiString roName = otherHeaders[i]; + if (roName.hashCode() == nameHash && roName.contentEqualsIgnoreCase(name)) { + values.add(otherHeaders[i + 1]); + } + } + + return values; + } + + @Override + public List getAllAndRemove(CharSequence name) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Boolean getBoolean(CharSequence name) { + AsciiString value = get0(name); + return value != null ? INSTANCE.convertToBoolean(value) : null; + } + + @Override + public boolean getBoolean(CharSequence name, boolean defaultValue) { + Boolean value = getBoolean(name); + return value != null ? value : defaultValue; + } + + @Override + public Byte getByte(CharSequence name) { + AsciiString value = get0(name); + return value != null ? INSTANCE.convertToByte(value) : null; + } + + @Override + public byte getByte(CharSequence name, byte defaultValue) { + Byte value = getByte(name); + return value != null ? value : defaultValue; + } + + @Override + public Character getChar(CharSequence name) { + AsciiString value = get0(name); + return value != null ? INSTANCE.convertToChar(value) : null; + } + + @Override + public char getChar(CharSequence name, char defaultValue) { + Character value = getChar(name); + return value != null ? value : defaultValue; + } + + @Override + public Short getShort(CharSequence name) { + AsciiString value = get0(name); + return value != null ? INSTANCE.convertToShort(value) : null; + } + + @Override + public short getShort(CharSequence name, short defaultValue) { + Short value = getShort(name); + return value != null ? value : defaultValue; + } + + @Override + public Integer getInt(CharSequence name) { + AsciiString value = get0(name); + return value != null ? INSTANCE.convertToInt(value) : null; + } + + @Override + public int getInt(CharSequence name, int defaultValue) { + Integer value = getInt(name); + return value != null ? value : defaultValue; + } + + @Override + public Long getLong(CharSequence name) { + AsciiString value = get0(name); + return value != null ? INSTANCE.convertToLong(value) : null; + } + + @Override + public long getLong(CharSequence name, long defaultValue) { + Long value = getLong(name); + return value != null ? value : defaultValue; + } + + @Override + public Float getFloat(CharSequence name) { + AsciiString value = get0(name); + return value != null ? INSTANCE.convertToFloat(value) : null; + } + + @Override + public float getFloat(CharSequence name, float defaultValue) { + Float value = getFloat(name); + return value != null ? value : defaultValue; + } + + @Override + public Double getDouble(CharSequence name) { + AsciiString value = get0(name); + return value != null ? INSTANCE.convertToDouble(value) : null; + } + + @Override + public double getDouble(CharSequence name, double defaultValue) { + Double value = getDouble(name); + return value != null ? value : defaultValue; + } + + @Override + public Long getTimeMillis(CharSequence name) { + AsciiString value = get0(name); + return value != null ? INSTANCE.convertToTimeMillis(value) : null; + } + + @Override + public long getTimeMillis(CharSequence name, long defaultValue) { + Long value = getTimeMillis(name); + return value != null ? value : defaultValue; + } + + @Override + public Boolean getBooleanAndRemove(CharSequence name) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public boolean getBooleanAndRemove(CharSequence name, boolean defaultValue) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Byte getByteAndRemove(CharSequence name) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public byte getByteAndRemove(CharSequence name, byte defaultValue) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Character getCharAndRemove(CharSequence name) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public char getCharAndRemove(CharSequence name, char defaultValue) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Short getShortAndRemove(CharSequence name) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public short getShortAndRemove(CharSequence name, short defaultValue) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Integer getIntAndRemove(CharSequence name) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public int getIntAndRemove(CharSequence name, int defaultValue) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Long getLongAndRemove(CharSequence name) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public long getLongAndRemove(CharSequence name, long defaultValue) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Float getFloatAndRemove(CharSequence name) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public float getFloatAndRemove(CharSequence name, float defaultValue) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Double getDoubleAndRemove(CharSequence name) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public double getDoubleAndRemove(CharSequence name, double defaultValue) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Long getTimeMillisAndRemove(CharSequence name) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public long getTimeMillisAndRemove(CharSequence name, long defaultValue) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public boolean contains(CharSequence name) { + return get(name) != null; + } + + @Override + public boolean contains(CharSequence name, CharSequence value) { + final int nameHash = AsciiString.hashCode(name); + final int valueHash = AsciiString.hashCode(value); + + final int pseudoHeadersEnd = pseudoHeaders.length - 1; + for (int i = 0; i < pseudoHeadersEnd; i += 2) { + AsciiString roName = pseudoHeaders[i]; + AsciiString roValue = pseudoHeaders[i + 1]; + if (roName.hashCode() == nameHash && roValue.hashCode() == valueHash && + roName.contentEqualsIgnoreCase(name) && roValue.contentEqualsIgnoreCase(value)) { + return true; + } + } + + final int otherHeadersEnd = otherHeaders.length - 1; + for (int i = 0; i < otherHeadersEnd; i += 2) { + AsciiString roName = otherHeaders[i]; + AsciiString roValue = otherHeaders[i + 1]; + if (roName.hashCode() == nameHash && roValue.hashCode() == valueHash && + roName.contentEqualsIgnoreCase(name) && roValue.contentEqualsIgnoreCase(value)) { + return true; + } + } + return false; + } + + @Override + public boolean containsObject(CharSequence name, Object value) { + if (value instanceof CharSequence) { + return contains(name, (CharSequence) value); + } + return contains(name, value.toString()); + } + + @Override + public boolean containsBoolean(CharSequence name, boolean value) { + return contains(name, String.valueOf(value)); + } + + @Override + public boolean containsByte(CharSequence name, byte value) { + return contains(name, String.valueOf(value)); + } + + @Override + public boolean containsChar(CharSequence name, char value) { + return contains(name, String.valueOf(value)); + } + + @Override + public boolean containsShort(CharSequence name, short value) { + return contains(name, String.valueOf(value)); + } + + @Override + public boolean containsInt(CharSequence name, int value) { + return contains(name, String.valueOf(value)); + } + + @Override + public boolean containsLong(CharSequence name, long value) { + return contains(name, String.valueOf(value)); + } + + @Override + public boolean containsFloat(CharSequence name, float value) { + return false; + } + + @Override + public boolean containsDouble(CharSequence name, double value) { + return contains(name, String.valueOf(value)); + } + + @Override + public boolean containsTimeMillis(CharSequence name, long value) { + return contains(name, String.valueOf(value)); + } + + @Override + public int size() { + return (pseudoHeaders.length + otherHeaders.length) >>> 1; + } + + @Override + public boolean isEmpty() { + return pseudoHeaders.length == 0 && otherHeaders.length == 0; + } + + @Override + public Set names() { + if (isEmpty()) { + return Collections.emptySet(); + } + Set names = new LinkedHashSet(size()); + final int pseudoHeadersEnd = pseudoHeaders.length - 1; + for (int i = 0; i < pseudoHeadersEnd; i += 2) { + names.add(pseudoHeaders[i]); + } + + final int otherHeadersEnd = otherHeaders.length - 1; + for (int i = 0; i < otherHeadersEnd; i += 2) { + names.add(otherHeaders[i]); + } + return names; + } + + @Override + public Http2Headers add(CharSequence name, CharSequence value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers add(CharSequence name, Iterable values) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers add(CharSequence name, CharSequence... values) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers addObject(CharSequence name, Object value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers addObject(CharSequence name, Iterable values) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers addObject(CharSequence name, Object... values) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers addBoolean(CharSequence name, boolean value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers addByte(CharSequence name, byte value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers addChar(CharSequence name, char value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers addShort(CharSequence name, short value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers addInt(CharSequence name, int value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers addLong(CharSequence name, long value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers addFloat(CharSequence name, float value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers addDouble(CharSequence name, double value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers addTimeMillis(CharSequence name, long value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers add(Headers headers) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers set(CharSequence name, CharSequence value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers set(CharSequence name, Iterable values) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers set(CharSequence name, CharSequence... values) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers setObject(CharSequence name, Object value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers setObject(CharSequence name, Iterable values) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers setObject(CharSequence name, Object... values) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers setBoolean(CharSequence name, boolean value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers setByte(CharSequence name, byte value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers setChar(CharSequence name, char value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers setShort(CharSequence name, short value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers setInt(CharSequence name, int value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers setLong(CharSequence name, long value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers setFloat(CharSequence name, float value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers setDouble(CharSequence name, double value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers setTimeMillis(CharSequence name, long value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers set(Headers headers) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers setAll(Headers headers) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public boolean remove(CharSequence name) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers clear() { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Iterator> iterator() { + return new ReadOnlyIterator(); + } + + @Override + public Http2Headers method(CharSequence value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers scheme(CharSequence value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers authority(CharSequence value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers path(CharSequence value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public Http2Headers status(CharSequence value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public CharSequence method() { + return get(PseudoHeaderName.METHOD.value()); + } + + @Override + public CharSequence scheme() { + return get(PseudoHeaderName.SCHEME.value()); + } + + @Override + public CharSequence authority() { + return get(PseudoHeaderName.AUTHORITY.value()); + } + + @Override + public CharSequence path() { + return get(PseudoHeaderName.PATH.value()); + } + + @Override + public CharSequence status() { + return get(PseudoHeaderName.STATUS.value()); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(getClass().getSimpleName()).append('['); + String separator = ""; + for (Map.Entry entry : this) { + builder.append(separator); + builder.append(entry.getKey()).append(": ").append(entry.getValue()); + separator = ", "; + } + return builder.append(']').toString(); + } + + private final class ReadOnlyIterator implements Map.Entry, + Iterator> { + private int i; + private AsciiString[] current = pseudoHeaders.length != 0 ? pseudoHeaders : otherHeaders; + private AsciiString key; + private AsciiString value; + + @Override + public boolean hasNext() { + return i != current.length; + } + + @Override + public Map.Entry next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + key = current[i]; + value = current[i + 1]; + i += 2; + if (i == current.length && current == pseudoHeaders) { + current = otherHeaders; + i = 0; + } + return this; + } + + @Override + public CharSequence getKey() { + return key; + } + + @Override + public CharSequence getValue() { + return value; + } + + @Override + public CharSequence setValue(CharSequence value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("read only"); + } + + @Override + public String toString() { + return key.toString() + '=' + value.toString(); + } + } +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersTest.java index 4f8b52fa50..3b0ab43a1a 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersTest.java @@ -153,7 +153,7 @@ public class DefaultHttp2HeadersTest { } } - private static void verifyPseudoHeadersFirst(Http2Headers headers) { + static void verifyPseudoHeadersFirst(Http2Headers headers) { CharSequence lastNonPseudoName = null; for (Entry entry: headers) { if (entry.getKey().length() == 0 || entry.getKey().charAt(0) != ':') { diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/ReadOnlyHttp2HeadersTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/ReadOnlyHttp2HeadersTest.java new file mode 100644 index 0000000000..65078852b0 --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/ReadOnlyHttp2HeadersTest.java @@ -0,0 +1,187 @@ +/* + * 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.http2; + +import io.netty.util.AsciiString; +import org.junit.Test; + +import java.util.Iterator; +import java.util.Map; + +import static io.netty.handler.codec.http2.DefaultHttp2HeadersTest.verifyPseudoHeadersFirst; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ReadOnlyHttp2HeadersTest { + @Test(expected = IllegalArgumentException.class) + public void notKeyValuePairThrows() { + ReadOnlyHttp2Headers.trailers(false, new AsciiString[]{ null }); + } + + @Test(expected = NullPointerException.class) + public void nullTrailersNotAllowed() { + ReadOnlyHttp2Headers.trailers(false, (AsciiString[]) null); + } + + @Test + public void nullHeaderNameNotChecked() { + ReadOnlyHttp2Headers.trailers(false, null, null); + } + + @Test(expected = Http2Exception.class) + public void nullHeaderNameValidated() { + ReadOnlyHttp2Headers.trailers(true, null, new AsciiString("foo")); + } + + @Test(expected = IllegalArgumentException.class) + public void pseudoHeaderNotAllowedAfterNonPseudoHeaders() { + ReadOnlyHttp2Headers.trailers(true, new AsciiString(":name"), new AsciiString("foo"), + new AsciiString("othername"), new AsciiString("goo"), + new AsciiString(":pseudo"), new AsciiString("val")); + } + + @Test(expected = IllegalArgumentException.class) + public void nullValuesAreNotAllowed() { + ReadOnlyHttp2Headers.trailers(true, new AsciiString("foo"), null); + } + + @Test + public void emtpyHeaderNameAllowed() { + ReadOnlyHttp2Headers.trailers(false, AsciiString.EMPTY_STRING, new AsciiString("foo")); + } + + @Test + public void testPseudoHeadersMustComeFirstWhenIteratingServer() { + Http2Headers headers = newServerHeaders(); + verifyPseudoHeadersFirst(headers); + } + + @Test + public void testPseudoHeadersMustComeFirstWhenIteratingClient() { + Http2Headers headers = newClientHeaders(); + verifyPseudoHeadersFirst(headers); + } + + @Test(expected = UnsupportedOperationException.class) + public void testIteratorReadOnlyClient() { + testIteratorReadOnly(newClientHeaders()); + } + + @Test(expected = UnsupportedOperationException.class) + public void testIteratorReadOnlyServer() { + testIteratorReadOnly(newServerHeaders()); + } + + @Test(expected = UnsupportedOperationException.class) + public void testIteratorReadOnlyTrailers() { + testIteratorReadOnly(newTrailers()); + } + + @Test(expected = UnsupportedOperationException.class) + public void testIteratorEntryReadOnlyClient() { + testIteratorEntryReadOnly(newClientHeaders()); + } + + @Test(expected = UnsupportedOperationException.class) + public void testIteratorEntryReadOnlyServer() { + testIteratorEntryReadOnly(newServerHeaders()); + } + + @Test(expected = UnsupportedOperationException.class) + public void testIteratorEntryReadOnlyTrailers() { + testIteratorEntryReadOnly(newTrailers()); + } + + @Test + public void testSize() { + Http2Headers headers = newTrailers(); + assertEquals(otherHeaders().length / 2, headers.size()); + } + + @Test + public void testIsNotEmpty() { + Http2Headers headers = newTrailers(); + assertFalse(headers.isEmpty()); + } + + @Test + public void testIsEmpty() { + Http2Headers headers = ReadOnlyHttp2Headers.trailers(false); + assertTrue(headers.isEmpty()); + } + + @Test + public void testContainsName() { + Http2Headers headers = newClientHeaders(); + assertTrue(headers.contains("Name1")); + assertTrue(headers.contains(Http2Headers.PseudoHeaderName.PATH.value())); + assertFalse(headers.contains(Http2Headers.PseudoHeaderName.STATUS.value())); + assertFalse(headers.contains("a missing header")); + } + + @Test + public void testContainsNameAndValue() { + Http2Headers headers = newClientHeaders(); + assertTrue(headers.contains("Name1", "Value1")); + assertTrue(headers.contains(Http2Headers.PseudoHeaderName.PATH.value(), "/foo")); + assertFalse(headers.contains(Http2Headers.PseudoHeaderName.STATUS.value(), "200")); + assertFalse(headers.contains("a missing header", "a missing value")); + } + + @Test + public void testGet() { + Http2Headers headers = newClientHeaders(); + assertTrue(AsciiString.contentEqualsIgnoreCase("value1", headers.get("Name1"))); + assertTrue(AsciiString.contentEqualsIgnoreCase("/foo", + headers.get(Http2Headers.PseudoHeaderName.PATH.value()))); + assertEquals(null, headers.get(Http2Headers.PseudoHeaderName.STATUS.value())); + assertEquals(null, headers.get("a missing header")); + } + + private void testIteratorReadOnly(Http2Headers headers) { + Iterator> itr = headers.iterator(); + assertTrue(itr.hasNext()); + itr.remove(); + } + + private void testIteratorEntryReadOnly(Http2Headers headers) { + Iterator> itr = headers.iterator(); + assertTrue(itr.hasNext()); + itr.next().setValue("foo"); + } + + private ReadOnlyHttp2Headers newServerHeaders() { + return ReadOnlyHttp2Headers.serverHeaders(false, new AsciiString("200"), otherHeaders()); + } + + private ReadOnlyHttp2Headers newClientHeaders() { + return ReadOnlyHttp2Headers.clientHeaders(false, new AsciiString("meth"), new AsciiString("/foo"), + new AsciiString("schemer"), new AsciiString("respect_my_authority"), otherHeaders()); + } + + private ReadOnlyHttp2Headers newTrailers() { + return ReadOnlyHttp2Headers.trailers(false, otherHeaders()); + } + + private AsciiString[] otherHeaders() { + return new AsciiString[] { + new AsciiString("name1"), new AsciiString("value1"), + new AsciiString("name2"), new AsciiString("value2"), + new AsciiString("name3"), new AsciiString("value3") + }; + } +} diff --git a/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java b/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java index 3de3a5a64a..d6f7971d7f 100644 --- a/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java +++ b/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java @@ -974,7 +974,7 @@ public class DefaultHeaders> implements Headers @Override public void remove() { - throw new UnsupportedOperationException("read-only iterator"); + throw new UnsupportedOperationException("read only"); } } diff --git a/common/src/main/java/io/netty/util/internal/EmptyArrays.java b/common/src/main/java/io/netty/util/internal/EmptyArrays.java index 27f0bc6a8e..e448ca5e26 100644 --- a/common/src/main/java/io/netty/util/internal/EmptyArrays.java +++ b/common/src/main/java/io/netty/util/internal/EmptyArrays.java @@ -16,6 +16,8 @@ package io.netty.util.internal; +import io.netty.util.AsciiString; + import java.nio.ByteBuffer; import java.security.cert.Certificate; import java.security.cert.X509Certificate; @@ -27,6 +29,7 @@ public final class EmptyArrays { public static final Object[] EMPTY_OBJECTS = {}; public static final Class[] EMPTY_CLASSES = {}; public static final String[] EMPTY_STRINGS = {}; + public static final AsciiString[] EMPTY_ASCII_STRINGS = {}; public static final StackTraceElement[] EMPTY_STACK_TRACE = {}; public static final ByteBuffer[] EMPTY_BYTE_BUFFERS = {}; public static final Certificate[] EMPTY_CERTIFICATES = {}; diff --git a/microbench/src/main/java/io/netty/microbench/headers/HeadersBenchmark.java b/microbench/src/main/java/io/netty/microbench/headers/HeadersBenchmark.java index 985f424198..83df7d160c 100644 --- a/microbench/src/main/java/io/netty/microbench/headers/HeadersBenchmark.java +++ b/microbench/src/main/java/io/netty/microbench/headers/HeadersBenchmark.java @@ -54,7 +54,7 @@ public class HeadersBenchmark extends AbstractMicrobenchmark { return (name.startsWith(":")) ? name.substring(1) : name; } - private static String toHttp2Name(String name) { + static String toHttp2Name(String name) { name = name.toLowerCase(); return (name.equals("host")) ? "xhost" : name; } diff --git a/microbench/src/main/java/io/netty/microbench/headers/ReadOnlyHttp2HeadersBenchmark.java b/microbench/src/main/java/io/netty/microbench/headers/ReadOnlyHttp2HeadersBenchmark.java new file mode 100644 index 0000000000..9efc2f3b20 --- /dev/null +++ b/microbench/src/main/java/io/netty/microbench/headers/ReadOnlyHttp2HeadersBenchmark.java @@ -0,0 +1,139 @@ +/* + * 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.microbench.headers; + +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpScheme; +import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.ReadOnlyHttp2Headers; +import io.netty.microbench.util.AbstractMicrobenchmark; +import io.netty.util.AsciiString; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +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.Threads; +import org.openjdk.jmh.annotations.Warmup; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +@Threads(1) +@State(Scope.Benchmark) +@Fork(2) +@Warmup(iterations = 10) +@Measurement(iterations = 10) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class ReadOnlyHttp2HeadersBenchmark extends AbstractMicrobenchmark { + private AsciiString[] headerNames; + private AsciiString[] headerValues; + + @Param({ "1", "5", "10", "20" }) + public int headerCount; + + private final AsciiString path = new AsciiString("/BigDynamicPayload"); + private final AsciiString authority = new AsciiString("io.netty"); + + @Setup + public void setUp() throws Exception { + headerNames = new AsciiString[headerCount]; + headerValues = new AsciiString[headerCount]; + for (int i = 0; i < headerCount; ++i) { + headerNames[i] = new AsciiString("key-" + i); + headerValues[i] = new AsciiString(UUID.randomUUID().toString()); + } + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public int defaultTrailers() { + Http2Headers headers = new DefaultHttp2Headers(false); + for (int i = 0; i < headerCount; ++i) { + headers.add(headerNames[i], headerValues[i]); + } + return iterate(headers); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public int readOnlyTrailers() { + return iterate(ReadOnlyHttp2Headers.trailers(false, buildPairs())); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public int defaultClientHeaders() { + Http2Headers headers = new DefaultHttp2Headers(false); + for (int i = 0; i < headerCount; ++i) { + headers.add(headerNames[i], headerValues[i]); + } + headers.method(HttpMethod.POST.asciiName()); + headers.scheme(HttpScheme.HTTPS.name()); + headers.path(path); + headers.authority(authority); + return iterate(headers); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public int readOnlyClientHeaders() { + return iterate(ReadOnlyHttp2Headers.clientHeaders(false, HttpMethod.POST.asciiName(), path, + HttpScheme.HTTPS.name(), authority, buildPairs())); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public int defaultServerHeaders() { + Http2Headers headers = new DefaultHttp2Headers(false); + for (int i = 0; i < headerCount; ++i) { + headers.add(headerNames[i], headerValues[i]); + } + headers.status(HttpResponseStatus.OK.codeAsText()); + return iterate(headers); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public int readOnlyServerHeaders() { + return iterate(ReadOnlyHttp2Headers.serverHeaders(false, HttpResponseStatus.OK.codeAsText(), buildPairs())); + } + + private static int iterate(Http2Headers headers) { + int length = 0; + for (Map.Entry entry : headers) { + length += entry.getKey().length() + entry.getValue().length(); + } + return length; + } + + private AsciiString[] buildPairs() { + AsciiString[] headerPairs = new AsciiString[headerCount * 2]; + for (int i = 0, j = 0; i < headerCount; ++i, ++j) { + headerPairs[j] = headerNames[i]; + headerPairs[++j] = headerValues[i]; + } + return headerPairs; + } +}