mirror of https://github.com/grpc/grpc-java.git
netty: use custom http2 headers for decoding.
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
This commit is contained in:
parent
de9c320196
commit
8c18a0d355
|
|
@ -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);
|
||||
// }
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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]));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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<CharSequence> getAll(CharSequence csName) {
|
||||
AsciiString name = requireAsciiString(csName);
|
||||
List<CharSequence> returnValues = new ArrayList<CharSequence>(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.
|
||||
*
|
||||
* <p>The values of binary headers (with a -bin suffix), are already base64 decoded.
|
||||
*
|
||||
* <p>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()}.
|
||||
*
|
||||
* <p>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<CharSequence> 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}.
|
||||
*
|
||||
* <p>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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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]);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<NettyClientHand
|
|||
MockitoAnnotations.initMocks(this);
|
||||
lifecycleManager = new ClientTransportLifecycleManager(listener);
|
||||
|
||||
initChannel();
|
||||
initChannel(new GrpcHttp2ClientHeadersDecoder(GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE));
|
||||
|
||||
grpcHeaders = new DefaultHttp2Headers()
|
||||
.scheme(HTTPS)
|
||||
|
|
|
|||
|
|
@ -91,10 +91,10 @@ public abstract class NettyHandlerTestBase<T extends Http2ConnectionHandler> {
|
|||
/**
|
||||
* 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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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<NettyServerHand
|
|||
any(Metadata.class)))
|
||||
.thenReturn(streamListener);
|
||||
when(transportListener.transportReady(any(Attributes.class))).thenReturn(Attributes.EMPTY);
|
||||
initChannel();
|
||||
|
||||
initChannel(new GrpcHttp2ServerHeadersDecoder(GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE));
|
||||
|
||||
// Simulate receipt of the connection preface
|
||||
channelRead(Http2CodecUtil.connectionPrefaceBuf());
|
||||
|
|
|
|||
Loading…
Reference in New Issue