From 8c18a0d35589f21678f614361a9ec1ba82794e13 Mon Sep 17 00:00:00 2001 From: Jakob Buchgraber Date: Fri, 9 Sep 2016 23:15:18 +0200 Subject: [PATCH] netty: use custom http2 headers for decoding. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The DefaultHttp2Headers class is a general-purpose Http2Headers implementation and provides much more functionality than we need in gRPC. In gRPC, when reading headers off the wire, we only inspect a handful of them, before converting to Metadata. This commit introduces a Http2Headers implementation that aims for insertion efficiency, a low memory footprint and fast conversion to Metadata. - Header names and values are stored in plain byte[]. - Insertion is O(1), while lookup is now O(n). - Binary header values are base64 decoded as they are inserted. - The byte[][] returned by namesAndValues() can directly be used to construct a new Metadata object. - For HTTP/2 request headers, the pseudo headers are no longer carried over to Metadata. A microbenchmark aiming to replicate the usage of Http2Headers in NettyClientHandler and NettyServerHandler shows decent throughput gains when compared to DefaultHttp2Headers. Benchmark Mode Cnt Score Error Units InboundHeadersBenchmark.defaultHeaders_clientHandler avgt 10 283.830 ± 4.063 ns/op InboundHeadersBenchmark.defaultHeaders_serverHandler avgt 10 1179.975 ± 21.810 ns/op InboundHeadersBenchmark.grpcHeaders_clientHandler avgt 10 190.108 ± 3.510 ns/op InboundHeadersBenchmark.grpcHeaders_serverHandler avgt 10 561.426 ± 9.079 ns/op Additionally, the memory footprint is reduced by more than 50%! gRPC Request Headers: 864 bytes Netty Request Headers: 1728 bytes gRPC Response Headers: 216 bytes Netty Response Headers: 528 bytes Furthermore, this change does most of the gRPC groundwork necessary to be able to cache higher ordered objects in HPACK's dynamic table, as discussed in [1]. [1] https://github.com/grpc/grpc-java/issues/2217 --- .../grpc/netty/InboundHeadersBenchmark.java | 202 +++++++ ...ark.java => OutboundHeadersBenchmark.java} | 2 +- core/src/main/java/io/grpc/Metadata.java | 4 + .../grpc/netty/GrpcHttp2HeadersDecoder.java | 511 ++++++++++++++++++ ...ers.java => GrpcHttp2OutboundHeaders.java} | 19 +- .../io/grpc/netty/NettyClientHandler.java | 6 +- .../io/grpc/netty/NettyServerHandler.java | 6 +- netty/src/main/java/io/grpc/netty/Utils.java | 20 +- .../netty/GrpcHttp2HeadersDecoderTest.java | 107 ++++ .../netty/GrpcHttp2InboundHeadersTest.java | 99 ++++ .../io/grpc/netty/NettyClientHandlerTest.java | 4 +- .../io/grpc/netty/NettyHandlerTestBase.java | 4 +- .../io/grpc/netty/NettyServerHandlerTest.java | 5 +- 13 files changed, 961 insertions(+), 28 deletions(-) create mode 100644 benchmarks/src/jmh/java/io/grpc/netty/InboundHeadersBenchmark.java rename benchmarks/src/jmh/java/io/grpc/netty/{HeadersBenchmark.java => OutboundHeadersBenchmark.java} (99%) create mode 100644 netty/src/main/java/io/grpc/netty/GrpcHttp2HeadersDecoder.java rename netty/src/main/java/io/grpc/netty/{GrpcHttp2Headers.java => GrpcHttp2OutboundHeaders.java} (86%) create mode 100644 netty/src/test/java/io/grpc/netty/GrpcHttp2HeadersDecoderTest.java create mode 100644 netty/src/test/java/io/grpc/netty/GrpcHttp2InboundHeadersTest.java diff --git a/benchmarks/src/jmh/java/io/grpc/netty/InboundHeadersBenchmark.java b/benchmarks/src/jmh/java/io/grpc/netty/InboundHeadersBenchmark.java new file mode 100644 index 0000000000..366e79887f --- /dev/null +++ b/benchmarks/src/jmh/java/io/grpc/netty/InboundHeadersBenchmark.java @@ -0,0 +1,202 @@ +/* + * Copyright 2016, Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.grpc.netty; + +import static io.grpc.netty.Utils.CONTENT_TYPE_HEADER; +import static io.grpc.netty.Utils.TE_TRAILERS; +import static io.netty.util.AsciiString.of; + +import io.grpc.netty.GrpcHttp2HeadersDecoder.GrpcHttp2RequestHeaders; +import io.grpc.netty.GrpcHttp2HeadersDecoder.GrpcHttp2ResponseHeaders; +import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.util.AsciiString; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.CompilerControl; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; + +import java.util.concurrent.TimeUnit; + +/** + * Benchmarks for {@link GrpcHttp2RequestHeaders} and {@link GrpcHttp2ResponseHeaders}. + */ +@State(Scope.Thread) +public class InboundHeadersBenchmark { + + private static AsciiString[] requestHeaders; + private static AsciiString[] responseHeaders; + + static { + setupRequestHeaders(); + setupResponseHeaders(); + } + + // Headers taken from the gRPC spec. + private static void setupRequestHeaders() { + requestHeaders = new AsciiString[18]; + int i = 0; + requestHeaders[i++] = of(":method"); + requestHeaders[i++] = of("POST"); + requestHeaders[i++] = of(":scheme"); + requestHeaders[i++] = of("http"); + requestHeaders[i++] = of(":path"); + requestHeaders[i++] = of("/google.pubsub.v2.PublisherService/CreateTopic"); + requestHeaders[i++] = of(":authority"); + requestHeaders[i++] = of("pubsub.googleapis.com"); + requestHeaders[i++] = of("te"); + requestHeaders[i++] = of("trailers"); + requestHeaders[i++] = of("grpc-timeout"); + requestHeaders[i++] = of("1S"); + requestHeaders[i++] = of("content-type"); + requestHeaders[i++] = of("application/grpc+proto"); + requestHeaders[i++] = of("grpc-encoding"); + requestHeaders[i++] = of("gzip"); + requestHeaders[i++] = of("authorization"); + requestHeaders[i] = of("Bearer y235.wef315yfh138vh31hv93hv8h3v"); + } + + private static void setupResponseHeaders() { + responseHeaders = new AsciiString[4]; + int i = 0; + responseHeaders[i++] = of(":status"); + responseHeaders[i++] = of("200"); + responseHeaders[i++] = of("grpc-encoding"); + responseHeaders[i] = of("gzip"); + } + + /** + * Checkstyle. + */ + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void grpcHeaders_serverHandler(Blackhole bh) { + serverHandler(bh, new GrpcHttp2RequestHeaders(4)); + } + + /** + * Checkstyle. + */ + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void defaultHeaders_serverHandler(Blackhole bh) { + serverHandler(bh, new DefaultHttp2Headers(true, 9)); + } + + /** + * Checkstyle. + */ + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void grpcHeaders_clientHandler(Blackhole bh) { + clientHandler(bh, new GrpcHttp2ResponseHeaders(2)); + } + + /** + * Checkstyle. + */ + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void defaultHeaders_clientHandler(Blackhole bh) { + clientHandler(bh, new DefaultHttp2Headers(true, 2)); + } + + @CompilerControl(CompilerControl.Mode.INLINE) + private static void serverHandler(Blackhole bh, Http2Headers headers) { + for (int i = 0; i < requestHeaders.length; i += 2) { + bh.consume(headers.add(requestHeaders[i], requestHeaders[i + 1])); + } + + // Sequence of headers accessed in NettyServerHandler + bh.consume(headers.get(TE_TRAILERS)); + bh.consume(headers.get(CONTENT_TYPE_HEADER)); + bh.consume(headers.method()); + bh.consume(headers.get(CONTENT_TYPE_HEADER)); + bh.consume(headers.path()); + + bh.consume(Utils.convertHeaders(headers)); + } + + @CompilerControl(CompilerControl.Mode.INLINE) + private static void clientHandler(Blackhole bh, Http2Headers headers) { + // NettyClientHandler does not directly access headers, but convert to Metadata immediately. + + bh.consume(headers.add(responseHeaders[0], responseHeaders[1])); + bh.consume(headers.add(responseHeaders[2], responseHeaders[3])); + + bh.consume(Utils.convertHeaders(headers)); + } + +// /** +// * Prints the size of the header objects in bytes. Needs JOL (Java Object Layout) as a +// * dependency. +// */ +// public static void main(String... args) { +// Http2Headers grpcRequestHeaders = new GrpcHttp2RequestHeaders(4); +// Http2Headers defaultRequestHeaders = new DefaultHttp2Headers(true, 9); +// for (int i = 0; i < requestHeaders.length; i += 2) { +// grpcRequestHeaders.add(requestHeaders[i], requestHeaders[i + 1]); +// defaultRequestHeaders.add(requestHeaders[i], requestHeaders[i + 1]); +// } +// long c = 10L; +// int m = ((int) c) / 20; +// +// long grpcRequestHeadersBytes = GraphLayout.parseInstance(grpcRequestHeaders).totalSize(); +// long defaultRequestHeadersBytes = +// GraphLayout.parseInstance(defaultRequestHeaders).totalSize(); +// +// System.out.printf("gRPC Request Headers: %d bytes%nNetty Request Headers: %d bytes%n", +// grpcRequestHeadersBytes, defaultRequestHeadersBytes); +// +// Http2Headers grpcResponseHeaders = new GrpcHttp2RequestHeaders(4); +// Http2Headers defaultResponseHeaders = new DefaultHttp2Headers(true, 9); +// for (int i = 0; i < responseHeaders.length; i += 2) { +// grpcResponseHeaders.add(responseHeaders[i], responseHeaders[i + 1]); +// defaultResponseHeaders.add(responseHeaders[i], responseHeaders[i + 1]); +// } +// +// long grpcResponseHeadersBytes = GraphLayout.parseInstance(grpcResponseHeaders).totalSize(); +// long defaultResponseHeadersBytes = +// GraphLayout.parseInstance(defaultResponseHeaders).totalSize(); +// +// System.out.printf("gRPC Response Headers: %d bytes%nNetty Response Headers: %d bytes%n", +// grpcResponseHeadersBytes, defaultResponseHeadersBytes); +// } +} diff --git a/benchmarks/src/jmh/java/io/grpc/netty/HeadersBenchmark.java b/benchmarks/src/jmh/java/io/grpc/netty/OutboundHeadersBenchmark.java similarity index 99% rename from benchmarks/src/jmh/java/io/grpc/netty/HeadersBenchmark.java rename to benchmarks/src/jmh/java/io/grpc/netty/OutboundHeadersBenchmark.java index 06ea63c36b..46737ede04 100644 --- a/benchmarks/src/jmh/java/io/grpc/netty/HeadersBenchmark.java +++ b/benchmarks/src/jmh/java/io/grpc/netty/OutboundHeadersBenchmark.java @@ -56,7 +56,7 @@ import java.util.concurrent.TimeUnit; * Header encoding benchmark. */ @State(Scope.Benchmark) -public class HeadersBenchmark { +public class OutboundHeadersBenchmark { @Param({"1", "5", "10", "20"}) public int headerCount; diff --git a/core/src/main/java/io/grpc/Metadata.java b/core/src/main/java/io/grpc/Metadata.java index 43ad9c4d2f..bfa9603b53 100644 --- a/core/src/main/java/io/grpc/Metadata.java +++ b/core/src/main/java/io/grpc/Metadata.java @@ -147,6 +147,10 @@ public final class Metadata { checkArgument(binaryValues.length % 2 == 0, "Odd number of key-value pairs: %s", binaryValues.length); for (int i = 0; i < binaryValues.length; i += 2) { + // The transport might provide an array with null values at the end. + if (binaryValues[i] == null) { + break; + } String name = new String(binaryValues[i], US_ASCII); storeAdd(name, new MetadataEntry(name.endsWith(BINARY_HEADER_SUFFIX), binaryValues[i + 1])); } diff --git a/netty/src/main/java/io/grpc/netty/GrpcHttp2HeadersDecoder.java b/netty/src/main/java/io/grpc/netty/GrpcHttp2HeadersDecoder.java new file mode 100644 index 0000000000..aed397f164 --- /dev/null +++ b/netty/src/main/java/io/grpc/netty/GrpcHttp2HeadersDecoder.java @@ -0,0 +1,511 @@ +/* + * Copyright 2016, Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * 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: + * + * 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.grpc.netty; + +import static com.google.common.base.Charsets.US_ASCII; +import static com.google.common.base.Preconditions.checkArgument; +import static io.grpc.netty.Utils.TE_HEADER; +import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_HEADER_TABLE_SIZE; +import static io.netty.handler.codec.http2.Http2Error.COMPRESSION_ERROR; +import static io.netty.handler.codec.http2.Http2Error.ENHANCE_YOUR_CALM; +import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR; +import static io.netty.handler.codec.http2.Http2Exception.connectionError; +import static io.netty.util.AsciiString.isUpperCase; + +import com.google.common.io.BaseEncoding; + +import io.grpc.Metadata; +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http2.DefaultHttp2HeadersDecoder; +import io.netty.handler.codec.http2.Http2Exception; +import io.netty.handler.codec.http2.Http2HeaderTable; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.Http2HeadersDecoder; +import io.netty.handler.codec.http2.internal.hpack.Decoder; +import io.netty.util.AsciiString; +import io.netty.util.internal.PlatformDependent; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A {@link Http2HeadersDecoder} that allows to use custom {@link Http2Headers} implementations. + * + *

Some of the code is copied from Netty's {@link DefaultHttp2HeadersDecoder}. + */ +abstract class GrpcHttp2HeadersDecoder implements Http2HeadersDecoder, + Http2HeadersDecoder.Configuration { + + private static final float HEADERS_COUNT_WEIGHT_NEW = 1 / 5f; + private static final float HEADERS_COUNT_WEIGHT_HISTORICAL = 1 - HEADERS_COUNT_WEIGHT_NEW; + + private final int maxHeaderSize; + + private final Decoder decoder; + private final Http2HeaderTable headerTable; + private float numHeadersGuess = 8; + + GrpcHttp2HeadersDecoder(int maxHeaderSize) { + this.maxHeaderSize = maxHeaderSize; + decoder = new Decoder(maxHeaderSize, DEFAULT_HEADER_TABLE_SIZE, 32); + headerTable = new GrpcHttp2HeaderTable(); + } + + @Override + public Http2HeaderTable headerTable() { + return headerTable; + } + + @Override + public int maxHeaderSize() { + return maxHeaderSize; + } + + @Override + public Http2Headers decodeHeaders(ByteBuf headerBlock) throws Http2Exception { + try { + GrpcHttp2InboundHeaders headers = newHeaders(1 + (int) numHeadersGuess); + decoder.decode(headerBlock, headers); + if (decoder.endHeaderBlock()) { + maxHeaderSizeExceeded(); + } + + numHeadersGuess = HEADERS_COUNT_WEIGHT_NEW * headers.numHeaders() + + HEADERS_COUNT_WEIGHT_HISTORICAL * numHeadersGuess; + + return headers; + } catch (IOException e) { + throw connectionError(COMPRESSION_ERROR, e, e.getMessage()); + } + } + + abstract GrpcHttp2InboundHeaders newHeaders(int numHeadersGuess); + + /** + * Respond to headers block resulting in the maximum header size being exceeded. + * @throws Http2Exception If we can not recover from the truncation. + */ + private void maxHeaderSizeExceeded() throws Http2Exception { + throw connectionError(ENHANCE_YOUR_CALM, "Header size exceeded max allowed bytes (%d)", + maxHeaderSize); + } + + @Override + public Configuration configuration() { + return this; + } + + private final class GrpcHttp2HeaderTable implements Http2HeaderTable { + + private int maxHeaderListSize = Integer.MAX_VALUE; + + @Override + public void maxHeaderTableSize(int max) throws Http2Exception { + if (max < 0) { + throw connectionError(PROTOCOL_ERROR, "Header Table Size must be non-negative but was %d", + max); + } + try { + decoder.setMaxHeaderTableSize(max); + } catch (Throwable t) { + throw connectionError(PROTOCOL_ERROR, t.getMessage(), t); + } + } + + @Override + public int maxHeaderTableSize() { + return decoder.getMaxHeaderTableSize(); + } + + @Override + public void maxHeaderListSize(int max) throws Http2Exception { + if (max < 0) { + // Over 2^31 - 1 (minus in integer) size is set to the maximun value + maxHeaderListSize = Integer.MAX_VALUE; + } else { + maxHeaderListSize = max; + } + } + + @Override + public int maxHeaderListSize() { + return maxHeaderListSize; + } + } + + static final class GrpcHttp2ServerHeadersDecoder extends GrpcHttp2HeadersDecoder { + + GrpcHttp2ServerHeadersDecoder(int maxHeaderSize) { + super(maxHeaderSize); + } + + @Override GrpcHttp2InboundHeaders newHeaders(int numHeadersGuess) { + return new GrpcHttp2RequestHeaders(numHeadersGuess); + } + } + + static final class GrpcHttp2ClientHeadersDecoder extends GrpcHttp2HeadersDecoder { + + GrpcHttp2ClientHeadersDecoder(int maxHeaderSize) { + super(maxHeaderSize); + } + + @Override GrpcHttp2InboundHeaders newHeaders(int numHeadersGuess) { + return new GrpcHttp2ResponseHeaders(numHeadersGuess); + } + } + + /** + * A {@link Http2Headers} implementation optimized for inbound/received headers. + * + *

Header names and values are stored in simple arrays, which makes insert run in O(1) + * and retrievial a O(n). Header name equality is not determined by the equals implementation of + * {@link CharSequence} type, but by comparing two names byte to byte. + * + *

All {@link CharSequence} input parameters and return values are required to be of type + * {@link AsciiString}. + */ + abstract static class GrpcHttp2InboundHeaders extends AbstractHttp2Headers { + + private static final AsciiString binaryHeaderSuffix = + new AsciiString(Metadata.BINARY_HEADER_SUFFIX.getBytes(US_ASCII)); + + private byte[][] namesAndValues; + private AsciiString[] values; + private int namesAndValuesIdx; + + GrpcHttp2InboundHeaders(int numHeadersGuess) { + checkArgument(numHeadersGuess > 0, "numHeadersGuess needs to be gt zero."); + namesAndValues = new byte[numHeadersGuess * 2][]; + values = new AsciiString[numHeadersGuess]; + } + + protected Http2Headers add(AsciiString name, AsciiString value) { + if (namesAndValuesIdx == namesAndValues.length) { + expandHeadersAndValues(); + } + byte[] nameBytes = bytes(name); + byte[] valueBytes = toBinaryValue(name, value); + values[namesAndValuesIdx / 2] = value; + namesAndValues[namesAndValuesIdx] = nameBytes; + namesAndValuesIdx++; + namesAndValues[namesAndValuesIdx] = valueBytes; + namesAndValuesIdx++; + return this; + } + + protected CharSequence get(AsciiString name) { + for (int i = 0; i < namesAndValuesIdx; i += 2) { + if (equals(name, namesAndValues[i])) { + return values[i / 2]; + } + } + return null; + } + + @Override + public List getAll(CharSequence csName) { + AsciiString name = requireAsciiString(csName); + List returnValues = new ArrayList(4); + for (int i = 0; i < namesAndValuesIdx; i += 2) { + if (equals(name, namesAndValues[i])) { + returnValues.add(values[i / 2]); + } + } + return returnValues; + } + + /** + * Returns the header names and values as bytes. An even numbered index contains the + * {@code byte[]} representation of a header name (in insertion order), and the subsequent + * odd index number contains the corresponding header value. + * + *

The values of binary headers (with a -bin suffix), are already base64 decoded. + * + *

The array may contain several {@code null} values at the end. A {@code null} value an + * index means that all higher numbered indices also contain {@code null} values. + */ + byte[][] namesAndValues() { + return namesAndValues; + } + + /** + * Returns the number of none-null headers in {@link #namesAndValues()}. + */ + protected int numHeaders() { + return namesAndValuesIdx / 2; + } + + protected static boolean equals(AsciiString str0, byte[] str1) { + return equals(str0.array(), str0.arrayOffset(), str0.length(), str1, 0, str1.length); + } + + protected static boolean equals(AsciiString str0, AsciiString str1) { + return equals(str0.array(), str0.arrayOffset(), str0.length(), str1.array(), + str1.arrayOffset(), str1.length()); + } + + protected static boolean equals(byte[] bytes0, int offset0, int length0, byte[] bytes1, + int offset1, int length1) { + if (length0 != length1) { + return false; + } + return PlatformDependent.equals(bytes0, offset0, bytes1, offset1, length0); + } + + private static byte[] toBinaryValue(AsciiString name, AsciiString value) { + return name.endsWith(binaryHeaderSuffix) + ? BaseEncoding.base64().decode(value) + : bytes(value); + } + + protected static byte[] bytes(AsciiString str) { + return str.isEntireArrayUsed() ? str.array() : str.toByteArray(); + } + + protected static AsciiString requireAsciiString(CharSequence cs) { + if (!(cs instanceof AsciiString)) { + throw new IllegalArgumentException("AsciiString expected. Was: " + cs.getClass().getName()); + } + return (AsciiString) cs; + } + + protected static boolean isPseudoHeader(AsciiString str) { + return !str.isEmpty() && str.charAt(0) == ':'; + } + + protected AsciiString validateName(AsciiString str) { + int offset = str.arrayOffset(); + int length = str.length(); + final byte[] data = str.array(); + for (int i = offset; i < offset + length; i++) { + if (isUpperCase(data[i])) { + PlatformDependent.throwException(connectionError(PROTOCOL_ERROR, + "invalid header name '%s'", str)); + } + } + return str; + } + + private void expandHeadersAndValues() { + int newValuesLen = Math.max(2, values.length + values.length / 2); + int newNamesAndValuesLen = newValuesLen * 2; + + byte[][] newNamesAndValues = new byte[newNamesAndValuesLen][]; + AsciiString[] newValues = new AsciiString[newValuesLen]; + System.arraycopy(namesAndValues, 0, newNamesAndValues, 0, namesAndValues.length); + System.arraycopy(values, 0, newValues, 0, values.length); + namesAndValues = newNamesAndValues; + values = newValues; + } + + @Override + public int size() { + return numHeaders(); + } + } + + /** + * A {@link GrpcHttp2InboundHeaders} implementation, optimized for HTTP/2 request headers. That + * is, HTTP/2 request pseudo headers are stored in dedicated fields and are NOT part of the + * array returned by {@link #namesAndValues()}. + * + *

This class only implements the methods used by {@link NettyServerHandler} and tests. All + * other methods throw an {@link UnsupportedOperationException}. + */ + static final class GrpcHttp2RequestHeaders extends GrpcHttp2InboundHeaders { + + private static final AsciiString PATH_HEADER = AsciiString.of(":path"); + private static final AsciiString AUTHORITY_HEADER = AsciiString.of(":authority"); + private static final AsciiString METHOD_HEADER = AsciiString.of(":method"); + private static final AsciiString SCHEME_HEADER = AsciiString.of(":scheme"); + + private AsciiString path; + private AsciiString authority; + private AsciiString method; + private AsciiString scheme; + private AsciiString te; + + GrpcHttp2RequestHeaders(int numHeadersGuess) { + super(numHeadersGuess); + } + + @Override + public Http2Headers add(CharSequence csName, CharSequence csValue) { + AsciiString name = validateName(requireAsciiString(csName)); + AsciiString value = requireAsciiString(csValue); + if (isPseudoHeader(name)) { + addPseudoHeader(name, value); + return this; + } + if (equals(TE_HEADER, name)) { + te = value; + return this; + } + return add(name, value); + } + + @Override + public CharSequence get(CharSequence csName) { + AsciiString name = requireAsciiString(csName); + checkArgument(!isPseudoHeader(name), "Use direct accessor methods for pseudo headers."); + if (equals(TE_HEADER, name)) { + return te; + } + return get(name); + } + + private void addPseudoHeader(CharSequence csName, CharSequence csValue) { + AsciiString name = requireAsciiString(csName); + AsciiString value = requireAsciiString(csValue); + + if (equals(PATH_HEADER, name)) { + path = value; + } else if (equals(AUTHORITY_HEADER, name)) { + authority = value; + } else if (equals(METHOD_HEADER, name)) { + method = value; + } else if (equals(SCHEME_HEADER, name)) { + scheme = value; + } else { + PlatformDependent.throwException( + connectionError(PROTOCOL_ERROR, "Illegal pseudo-header '%s' in request.", name)); + } + } + + @Override + public CharSequence path() { + return path; + } + + @Override + public CharSequence authority() { + return authority; + } + + @Override + public CharSequence method() { + return method; + } + + @Override + public CharSequence scheme() { + return scheme; + } + + + /** + * This method is called in tests only. + */ + @Override + public List getAll(CharSequence csName) { + AsciiString name = requireAsciiString(csName); + if (isPseudoHeader(name)) { + // This code should never be reached. + throw new IllegalArgumentException("Use direct accessor methods for pseudo headers."); + } + if (equals(TE_HEADER, name)) { + return Collections.singletonList((CharSequence) te); + } + return super.getAll(csName); + } + + /** + * This method is called in tests only. + */ + @Override + public int size() { + int size = 0; + if (path != null) { + size++; + } + if (authority != null) { + size++; + } + if (method != null) { + size++; + } + if (scheme != null) { + size++; + } + if (te != null) { + size++; + } + size += super.size(); + return size; + } + } + + /** + * This class only implements the methods used by {@link NettyClientHandler} and tests. All + * other methods throw an {@link UnsupportedOperationException}. + * + *

Unlike in {@link GrpcHttp2ResponseHeaders} the {@code :status} pseudo-header is not treated + * special and is part of {@link #namesAndValues}. + */ + static final class GrpcHttp2ResponseHeaders extends GrpcHttp2InboundHeaders { + + GrpcHttp2ResponseHeaders(int numHeadersGuess) { + super(numHeadersGuess); + } + + @Override + public Http2Headers add(CharSequence csName, CharSequence csValue) { + AsciiString name = validateName(requireAsciiString(csName)); + AsciiString value = requireAsciiString(csValue); + return add(name, value); + } + + @Override + public CharSequence get(CharSequence csName) { + AsciiString name = requireAsciiString(csName); + return get(name); + } + } +} diff --git a/netty/src/main/java/io/grpc/netty/GrpcHttp2Headers.java b/netty/src/main/java/io/grpc/netty/GrpcHttp2OutboundHeaders.java similarity index 86% rename from netty/src/main/java/io/grpc/netty/GrpcHttp2Headers.java rename to netty/src/main/java/io/grpc/netty/GrpcHttp2OutboundHeaders.java index ebc9a3c225..d4fbfd6f95 100644 --- a/netty/src/main/java/io/grpc/netty/GrpcHttp2Headers.java +++ b/netty/src/main/java/io/grpc/netty/GrpcHttp2OutboundHeaders.java @@ -41,14 +41,15 @@ import java.util.NoSuchElementException; /** * A custom implementation of Http2Headers that only includes methods used by gRPC. */ -final class GrpcHttp2Headers extends AbstractHttp2Headers { +final class GrpcHttp2OutboundHeaders extends AbstractHttp2Headers { private final AsciiString[] normalHeaders; private final AsciiString[] preHeaders; private static final AsciiString[] EMPTY = new AsciiString[]{}; - static GrpcHttp2Headers clientRequestHeaders(byte[][] serializedMetadata, AsciiString authority, - AsciiString path, AsciiString method, AsciiString scheme, AsciiString userAgent) { + static GrpcHttp2OutboundHeaders clientRequestHeaders(byte[][] serializedMetadata, + AsciiString authority, AsciiString path, AsciiString method, AsciiString scheme, + AsciiString userAgent) { AsciiString[] preHeaders = new AsciiString[] { Http2Headers.PseudoHeaderName.AUTHORITY.value(), authority, Http2Headers.PseudoHeaderName.PATH.value(), path, @@ -58,22 +59,22 @@ final class GrpcHttp2Headers extends AbstractHttp2Headers { Utils.TE_HEADER, Utils.TE_TRAILERS, Utils.USER_AGENT, userAgent, }; - return new GrpcHttp2Headers(preHeaders, serializedMetadata); + return new GrpcHttp2OutboundHeaders(preHeaders, serializedMetadata); } - static GrpcHttp2Headers serverResponseHeaders(byte[][] serializedMetadata) { + static GrpcHttp2OutboundHeaders serverResponseHeaders(byte[][] serializedMetadata) { AsciiString[] preHeaders = new AsciiString[] { Http2Headers.PseudoHeaderName.STATUS.value(), Utils.STATUS_OK, Utils.CONTENT_TYPE_HEADER, Utils.CONTENT_TYPE_GRPC, }; - return new GrpcHttp2Headers(preHeaders, serializedMetadata); + return new GrpcHttp2OutboundHeaders(preHeaders, serializedMetadata); } - static GrpcHttp2Headers serverResponseTrailers(byte[][] serializedMetadata) { - return new GrpcHttp2Headers(EMPTY, serializedMetadata); + static GrpcHttp2OutboundHeaders serverResponseTrailers(byte[][] serializedMetadata) { + return new GrpcHttp2OutboundHeaders(EMPTY, serializedMetadata); } - private GrpcHttp2Headers(AsciiString[] preHeaders, byte[][] serializedMetadata) { + private GrpcHttp2OutboundHeaders(AsciiString[] preHeaders, byte[][] serializedMetadata) { normalHeaders = new AsciiString[serializedMetadata.length]; for (int i = 0; i < normalHeaders.length; i++) { normalHeaders[i] = new AsciiString(serializedMetadata[i], false); diff --git a/netty/src/main/java/io/grpc/netty/NettyClientHandler.java b/netty/src/main/java/io/grpc/netty/NettyClientHandler.java index a184358304..054d79e59a 100644 --- a/netty/src/main/java/io/grpc/netty/NettyClientHandler.java +++ b/netty/src/main/java/io/grpc/netty/NettyClientHandler.java @@ -45,6 +45,7 @@ import io.grpc.StatusException; import io.grpc.internal.ClientTransport.PingCallback; import io.grpc.internal.GrpcUtil; import io.grpc.internal.Http2Ping; +import io.grpc.netty.GrpcHttp2HeadersDecoder.GrpcHttp2ClientHeadersDecoder; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; @@ -58,9 +59,7 @@ import io.netty.handler.codec.http2.DefaultHttp2ConnectionDecoder; import io.netty.handler.codec.http2.DefaultHttp2ConnectionEncoder; import io.netty.handler.codec.http2.DefaultHttp2FrameReader; import io.netty.handler.codec.http2.DefaultHttp2FrameWriter; -import io.netty.handler.codec.http2.DefaultHttp2HeadersDecoder; import io.netty.handler.codec.http2.DefaultHttp2LocalFlowController; -import io.netty.handler.codec.http2.Http2CodecUtil; import io.netty.handler.codec.http2.Http2Connection; import io.netty.handler.codec.http2.Http2ConnectionAdapter; import io.netty.handler.codec.http2.Http2ConnectionDecoder; @@ -116,8 +115,7 @@ class NettyClientHandler extends AbstractNettyHandler { int flowControlWindow, int maxHeaderListSize, Ticker ticker) { Preconditions.checkArgument(maxHeaderListSize > 0, "maxHeaderListSize must be positive"); - Http2HeadersDecoder headersDecoder = new DefaultHttp2HeadersDecoder( - maxHeaderListSize, Http2CodecUtil.DEFAULT_HEADER_TABLE_SIZE, true, 32); + Http2HeadersDecoder headersDecoder = new GrpcHttp2ClientHeadersDecoder(maxHeaderListSize); Http2FrameReader frameReader = new DefaultHttp2FrameReader(headersDecoder); Http2FrameWriter frameWriter = new DefaultHttp2FrameWriter(); Http2Connection connection = new DefaultHttp2Connection(false); diff --git a/netty/src/main/java/io/grpc/netty/NettyServerHandler.java b/netty/src/main/java/io/grpc/netty/NettyServerHandler.java index 166053ac22..c2230792c2 100644 --- a/netty/src/main/java/io/grpc/netty/NettyServerHandler.java +++ b/netty/src/main/java/io/grpc/netty/NettyServerHandler.java @@ -50,6 +50,7 @@ import io.grpc.Status; import io.grpc.internal.GrpcUtil; import io.grpc.internal.ServerStreamListener; import io.grpc.internal.ServerTransportListener; +import io.grpc.netty.GrpcHttp2HeadersDecoder.GrpcHttp2ServerHeadersDecoder; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; @@ -61,9 +62,7 @@ import io.netty.handler.codec.http2.DefaultHttp2ConnectionDecoder; import io.netty.handler.codec.http2.DefaultHttp2ConnectionEncoder; import io.netty.handler.codec.http2.DefaultHttp2FrameReader; import io.netty.handler.codec.http2.DefaultHttp2FrameWriter; -import io.netty.handler.codec.http2.DefaultHttp2HeadersDecoder; import io.netty.handler.codec.http2.DefaultHttp2LocalFlowController; -import io.netty.handler.codec.http2.Http2CodecUtil; import io.netty.handler.codec.http2.Http2Connection; import io.netty.handler.codec.http2.Http2ConnectionDecoder; import io.netty.handler.codec.http2.Http2ConnectionEncoder; @@ -112,8 +111,7 @@ class NettyServerHandler extends AbstractNettyHandler { int maxMessageSize) { Preconditions.checkArgument(maxHeaderListSize > 0, "maxHeaderListSize must be positive"); Http2FrameLogger frameLogger = new Http2FrameLogger(LogLevel.DEBUG, NettyServerHandler.class); - Http2HeadersDecoder headersDecoder = new DefaultHttp2HeadersDecoder( - maxHeaderListSize, Http2CodecUtil.DEFAULT_HEADER_TABLE_SIZE, true, 32); + Http2HeadersDecoder headersDecoder = new GrpcHttp2ServerHeadersDecoder(maxHeaderListSize); Http2FrameReader frameReader = new Http2InboundFrameLogger( new DefaultHttp2FrameReader(headersDecoder), frameLogger); Http2FrameWriter frameWriter = diff --git a/netty/src/main/java/io/grpc/netty/Utils.java b/netty/src/main/java/io/grpc/netty/Utils.java index 354a0aca22..e68f693ba4 100644 --- a/netty/src/main/java/io/grpc/netty/Utils.java +++ b/netty/src/main/java/io/grpc/netty/Utils.java @@ -33,6 +33,8 @@ package io.grpc.netty; import static io.grpc.internal.GrpcUtil.CONTENT_TYPE_KEY; import static io.grpc.internal.GrpcUtil.USER_AGENT_KEY; +import static io.grpc.internal.TransportFrameUtil.toHttp2Headers; +import static io.grpc.internal.TransportFrameUtil.toRawSerializedHeaders; import static io.netty.util.CharsetUtil.UTF_8; import com.google.common.annotations.VisibleForTesting; @@ -42,7 +44,7 @@ import io.grpc.Metadata; import io.grpc.Status; import io.grpc.internal.GrpcUtil; import io.grpc.internal.SharedResourceHolder.Resource; -import io.grpc.internal.TransportFrameUtil; +import io.grpc.netty.GrpcHttp2HeadersDecoder.GrpcHttp2InboundHeaders; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.handler.codec.http2.Http2Exception; @@ -88,6 +90,9 @@ class Utils { static boolean validateHeaders = false; public static Metadata convertHeaders(Http2Headers http2Headers) { + if (http2Headers instanceof GrpcHttp2InboundHeaders) { + return new Metadata(((GrpcHttp2InboundHeaders) http2Headers).namesAndValues()); + } return new Metadata(convertHeadersToArray(http2Headers)); } @@ -100,7 +105,7 @@ class Utils { headerValues[i++] = bytes(entry.getKey()); headerValues[i++] = bytes(entry.getValue()); } - return TransportFrameUtil.toRawSerializedHeaders(headerValues); + return toRawSerializedHeaders(headerValues); } private static byte[] bytes(CharSequence seq) { @@ -121,8 +126,8 @@ class Utils { Preconditions.checkNotNull(defaultPath, "defaultPath"); Preconditions.checkNotNull(authority, "authority"); - return GrpcHttp2Headers.clientRequestHeaders( - TransportFrameUtil.toHttp2Headers(headers), + return GrpcHttp2OutboundHeaders.clientRequestHeaders( + toHttp2Headers(headers), authority, defaultPath, HTTP_METHOD, @@ -131,10 +136,13 @@ class Utils { } public static Http2Headers convertServerHeaders(Metadata headers) { - return GrpcHttp2Headers.serverResponseHeaders(TransportFrameUtil.toHttp2Headers(headers)); + return GrpcHttp2OutboundHeaders.serverResponseHeaders(toHttp2Headers(headers)); } public static Metadata convertTrailers(Http2Headers http2Headers) { + if (http2Headers instanceof GrpcHttp2InboundHeaders) { + return new Metadata(((GrpcHttp2InboundHeaders) http2Headers).namesAndValues()); + } return new Metadata(convertHeadersToArray(http2Headers)); } @@ -142,7 +150,7 @@ class Utils { if (!headersSent) { return convertServerHeaders(trailers); } - return GrpcHttp2Headers.serverResponseTrailers(TransportFrameUtil.toHttp2Headers(trailers)); + return GrpcHttp2OutboundHeaders.serverResponseTrailers(toHttp2Headers(trailers)); } public static Status statusFromThrowable(Throwable t) { diff --git a/netty/src/test/java/io/grpc/netty/GrpcHttp2HeadersDecoderTest.java b/netty/src/test/java/io/grpc/netty/GrpcHttp2HeadersDecoderTest.java new file mode 100644 index 0000000000..6652579514 --- /dev/null +++ b/netty/src/test/java/io/grpc/netty/GrpcHttp2HeadersDecoderTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2016, Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.grpc.netty; + +import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_HEADER_TABLE_SIZE; +import static io.netty.util.AsciiString.of; +import static org.junit.Assert.assertEquals; + +import io.grpc.netty.GrpcHttp2HeadersDecoder.GrpcHttp2ClientHeadersDecoder; +import io.grpc.netty.GrpcHttp2HeadersDecoder.GrpcHttp2ServerHeadersDecoder; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.DefaultHttp2HeadersEncoder; +import io.netty.handler.codec.http2.Http2Exception; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.Http2HeadersDecoder; +import io.netty.handler.codec.http2.Http2HeadersEncoder; +import io.netty.handler.codec.http2.Http2HeadersEncoder.SensitivityDetector; +import io.netty.util.ReferenceCountUtil; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Tests for {@link GrpcHttp2HeadersDecoder}. + */ +@RunWith(JUnit4.class) +public class GrpcHttp2HeadersDecoderTest { + + private static final SensitivityDetector NEVER_SENSITIVE = new SensitivityDetector() { + @Override + public boolean isSensitive(CharSequence name, CharSequence value) { + return false; + } + }; + + @Test + public void decode_requestHeaders() throws Http2Exception { + Http2HeadersDecoder decoder = new GrpcHttp2ServerHeadersDecoder(8192); + Http2HeadersEncoder encoder = + new DefaultHttp2HeadersEncoder(DEFAULT_HEADER_TABLE_SIZE, NEVER_SENSITIVE); + + Http2Headers headers = new DefaultHttp2Headers(false); + headers.add(of(":scheme"), of("https")).add(of(":method"), of("GET")) + .add(of(":path"), of("index.html")).add(of(":authority"), of("foo.grpc.io")) + .add(of("custom"), of("header")); + ByteBuf encodedHeaders = ReferenceCountUtil.releaseLater(Unpooled.buffer()); + encoder.encodeHeaders(headers, encodedHeaders); + + Http2Headers decodedHeaders = decoder.decodeHeaders(encodedHeaders); + assertEquals(headers.get(of(":scheme")), decodedHeaders.scheme()); + assertEquals(headers.get(of(":method")), decodedHeaders.method()); + assertEquals(headers.get(of(":path")), decodedHeaders.path()); + assertEquals(headers.get(of(":authority")), decodedHeaders.authority()); + assertEquals(headers.get(of("custom")), decodedHeaders.get(of("custom"))); + assertEquals(headers.size(), decodedHeaders.size()); + } + + @Test + public void decode_responseHeaders() throws Http2Exception { + Http2HeadersDecoder decoder = new GrpcHttp2ClientHeadersDecoder(8192); + Http2HeadersEncoder encoder = + new DefaultHttp2HeadersEncoder(DEFAULT_HEADER_TABLE_SIZE, NEVER_SENSITIVE); + + Http2Headers headers = new DefaultHttp2Headers(false); + headers.add(of(":status"), of("200")).add(of("custom"), of("header")); + ByteBuf encodedHeaders = ReferenceCountUtil.releaseLater(Unpooled.buffer()); + encoder.encodeHeaders(headers, encodedHeaders); + + Http2Headers decodedHeaders = decoder.decodeHeaders(encodedHeaders); + assertEquals(headers.get(of(":status")), decodedHeaders.get(of(":status"))); + assertEquals(headers.get(of("custom")), decodedHeaders.get(of("custom"))); + assertEquals(headers.size(), decodedHeaders.size()); + } +} diff --git a/netty/src/test/java/io/grpc/netty/GrpcHttp2InboundHeadersTest.java b/netty/src/test/java/io/grpc/netty/GrpcHttp2InboundHeadersTest.java new file mode 100644 index 0000000000..9d3e8fe5f6 --- /dev/null +++ b/netty/src/test/java/io/grpc/netty/GrpcHttp2InboundHeadersTest.java @@ -0,0 +1,99 @@ +/* + * Copyright 2016, Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.grpc.netty; + +import static io.netty.util.AsciiString.of; +import static junit.framework.TestCase.assertNotSame; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import com.google.common.io.BaseEncoding; + +import io.grpc.netty.GrpcHttp2HeadersDecoder.GrpcHttp2InboundHeaders; +import io.grpc.netty.GrpcHttp2HeadersDecoder.GrpcHttp2RequestHeaders; +import io.grpc.netty.GrpcHttp2HeadersDecoder.GrpcHttp2ResponseHeaders; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.util.AsciiString; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.Random; + +/** + * Tests for {@link GrpcHttp2RequestHeaders} and {@link GrpcHttp2ResponseHeaders}. + */ +@RunWith(JUnit4.class) +public class GrpcHttp2InboundHeadersTest { + + @Test + public void basicCorrectness() { + Http2Headers headers = new GrpcHttp2RequestHeaders(1); + headers.add(of(":method"), of("POST")); + headers.add(of("content-type"), of("application/grpc+proto")); + headers.add(of(":path"), of("/google.pubsub.v2.PublisherService/CreateTopic")); + headers.add(of(":scheme"), of("https")); + headers.add(of("te"), of("trailers")); + headers.add(of(":authority"), of("pubsub.googleapis.com")); + headers.add(of("foo"), of("bar")); + + assertEquals(7, headers.size()); + // Number of headers without the pseudo headers and 'te' header. + assertEquals(2, ((GrpcHttp2InboundHeaders)headers).numHeaders()); + + assertEquals(of("application/grpc+proto"), headers.get(of("content-type"))); + assertEquals(of("/google.pubsub.v2.PublisherService/CreateTopic"), headers.path()); + assertEquals(of("https"), headers.scheme()); + assertEquals(of("POST"), headers.method()); + assertEquals(of("pubsub.googleapis.com"), headers.authority()); + assertEquals(of("trailers"), headers.get(of("te"))); + assertEquals(of("bar"), headers.get(of("foo"))); + } + + @Test + public void binaryHeadersShouldBeBase64Decoded() { + Http2Headers headers = new GrpcHttp2RequestHeaders(1); + + byte[] data = new byte[100]; + new Random().nextBytes(data); + headers.add(of("foo-bin"), of(BaseEncoding.base64().encode(data))); + + assertEquals(1, headers.size()); + + byte[][] namesAndValues = ((GrpcHttp2InboundHeaders)headers).namesAndValues(); + + assertEquals(of("foo-bin"), new AsciiString(namesAndValues[0])); + assertNotSame(data, namesAndValues[1]); + assertArrayEquals(data, namesAndValues[1]); + } + +} diff --git a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java index c9e547ed45..f6f8b65cc3 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java @@ -62,6 +62,8 @@ import io.grpc.Status; import io.grpc.StatusException; import io.grpc.internal.ClientTransport; import io.grpc.internal.ClientTransport.PingCallback; +import io.grpc.internal.GrpcUtil; +import io.grpc.netty.GrpcHttp2HeadersDecoder.GrpcHttp2ClientHeadersDecoder; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; @@ -115,7 +117,7 @@ public class NettyClientHandlerTest extends NettyHandlerTestBase { /** * Must be called by subclasses to initialize the handler and channel. */ - protected final void initChannel() throws Exception { + protected final void initChannel(GrpcHttp2HeadersDecoder headersDecoder) throws Exception { content = Unpooled.copiedBuffer("hello world", UTF_8); frameWriter = spy(new DefaultHttp2FrameWriter()); - frameReader = new DefaultHttp2FrameReader(); + frameReader = new DefaultHttp2FrameReader(headersDecoder); handler = newHandler(); diff --git a/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java b/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java index 702104364b..9a79cd068e 100644 --- a/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java @@ -58,11 +58,13 @@ import io.grpc.Attributes; import io.grpc.Metadata; import io.grpc.Status; import io.grpc.Status.Code; +import io.grpc.internal.GrpcUtil; import io.grpc.internal.MessageFramer; import io.grpc.internal.ServerStream; import io.grpc.internal.ServerStreamListener; import io.grpc.internal.ServerTransportListener; import io.grpc.internal.WritableBuffer; +import io.grpc.netty.GrpcHttp2HeadersDecoder.GrpcHttp2ServerHeadersDecoder; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufUtil; @@ -120,7 +122,8 @@ public class NettyServerHandlerTest extends NettyHandlerTestBase