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

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

Modifications:

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

Result:

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

251 lines
11 KiB
Java

/*
* Copyright 2016 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.handler.codec.http2;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.EncoderException;
import io.netty.handler.codec.MessageToMessageCodec;
import io.netty.handler.codec.http.DefaultHttpContent;
import io.netty.handler.codec.http.DefaultLastHttpContent;
import io.netty.handler.codec.http.FullHttpMessage;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpMessage;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpScheme;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.ssl.SslHandler;
import io.netty.util.Attribute;
import io.netty.util.AttributeKey;
import io.netty.util.internal.UnstableApi;
import java.util.List;
/**
* This handler converts from {@link Http2StreamFrame} to {@link HttpObject},
* and back. It can be used as an adapter in conjunction with {@link
* Http2MultiplexCodec} to make http/2 connections backward-compatible with
* {@link ChannelHandler}s expecting {@link HttpObject}
*
* For simplicity, it converts to chunked encoding unless the entire stream
* is a single header.
*/
@UnstableApi
@Sharable
public class Http2StreamFrameToHttpObjectCodec extends MessageToMessageCodec<Http2StreamFrame, HttpObject> {
private static final AttributeKey<HttpScheme> SCHEME_ATTR_KEY =
AttributeKey.valueOf(HttpScheme.class, "STREAMFRAMECODEC_SCHEME");
private final boolean isServer;
private final boolean validateHeaders;
public Http2StreamFrameToHttpObjectCodec(final boolean isServer,
final boolean validateHeaders) {
this.isServer = isServer;
this.validateHeaders = validateHeaders;
}
public Http2StreamFrameToHttpObjectCodec(final boolean isServer) {
this(isServer, true);
}
@Override
public boolean acceptInboundMessage(Object msg) throws Exception {
return (msg instanceof Http2HeadersFrame) || (msg instanceof Http2DataFrame);
}
@Override
protected void decode(ChannelHandlerContext ctx, Http2StreamFrame frame) throws Exception {
if (frame instanceof Http2HeadersFrame) {
Http2HeadersFrame headersFrame = (Http2HeadersFrame) frame;
Http2Headers headers = headersFrame.headers();
Http2FrameStream stream = headersFrame.stream();
int id = stream == null ? 0 : stream.id();
final CharSequence status = headers.status();
// 100-continue response is a special case where Http2HeadersFrame#isEndStream=false
// but we need to decode it as a FullHttpResponse to play nice with HttpObjectAggregator.
if (null != status && HttpResponseStatus.CONTINUE.codeAsText().contentEquals(status)) {
final FullHttpMessage fullMsg = newFullMessage(id, headers, ctx.alloc());
ctx.fireChannelRead(fullMsg);
return;
}
if (headersFrame.isEndStream()) {
if (headers.method() == null && status == null) {
LastHttpContent last = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER, validateHeaders);
HttpConversionUtil.addHttp2ToHttpHeaders(id, headers, last.trailingHeaders(),
HttpVersion.HTTP_1_1, true, true);
ctx.fireChannelRead(last);
} else {
FullHttpMessage full = newFullMessage(id, headers, ctx.alloc());
ctx.fireChannelRead(full);
}
} else {
HttpMessage req = newMessage(id, headers);
if (!HttpUtil.isContentLengthSet(req)) {
req.headers().add(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
}
ctx.fireChannelRead(req);
}
} else if (frame instanceof Http2DataFrame) {
Http2DataFrame dataFrame = (Http2DataFrame) frame;
if (dataFrame.isEndStream()) {
ctx.fireChannelRead(new DefaultLastHttpContent(dataFrame.content().retain(), validateHeaders));
} else {
ctx.fireChannelRead(new DefaultHttpContent(dataFrame.content().retain()));
}
}
}
private void encodeLastContent(LastHttpContent last, List<Object> out) {
boolean needFiller = !(last instanceof FullHttpMessage) && last.trailingHeaders().isEmpty();
if (last.content().isReadable() || needFiller) {
out.add(new DefaultHttp2DataFrame(last.content().retain(), last.trailingHeaders().isEmpty()));
}
if (!last.trailingHeaders().isEmpty()) {
Http2Headers headers = HttpConversionUtil.toHttp2Headers(last.trailingHeaders(), validateHeaders);
out.add(new DefaultHttp2HeadersFrame(headers, true));
}
}
/**
* Encode from an {@link HttpObject} to an {@link Http2StreamFrame}. This method will
* be called for each written message that can be handled by this encoder.
*
* NOTE: 100-Continue responses that are NOT {@link FullHttpResponse} will be rejected.
*
* @param ctx the {@link ChannelHandlerContext} which this handler belongs to
* @param obj the {@link HttpObject} message to encode
* @param out the {@link List} into which the encoded msg should be added
* needs to do some kind of aggregation
* @throws Exception is thrown if an error occurs
*/
@Override
protected void encode(ChannelHandlerContext ctx, HttpObject obj, List<Object> out) throws Exception {
// 100-continue is typically a FullHttpResponse, but the decoded
// Http2HeadersFrame should not be marked as endStream=true
if (obj instanceof HttpResponse) {
final HttpResponse res = (HttpResponse) obj;
if (res.status().equals(HttpResponseStatus.CONTINUE)) {
if (res instanceof FullHttpResponse) {
final Http2Headers headers = toHttp2Headers(ctx, res);
out.add(new DefaultHttp2HeadersFrame(headers, false));
return;
} else {
throw new EncoderException(
HttpResponseStatus.CONTINUE.toString() + " must be a FullHttpResponse");
}
}
}
if (obj instanceof HttpMessage) {
Http2Headers headers = toHttp2Headers(ctx, (HttpMessage) obj);
boolean noMoreFrames = false;
if (obj instanceof FullHttpMessage) {
FullHttpMessage full = (FullHttpMessage) obj;
noMoreFrames = !full.content().isReadable() && full.trailingHeaders().isEmpty();
}
out.add(new DefaultHttp2HeadersFrame(headers, noMoreFrames));
}
if (obj instanceof LastHttpContent) {
LastHttpContent last = (LastHttpContent) obj;
encodeLastContent(last, out);
} else if (obj instanceof HttpContent) {
HttpContent cont = (HttpContent) obj;
out.add(new DefaultHttp2DataFrame(cont.content().retain(), false));
}
}
private Http2Headers toHttp2Headers(final ChannelHandlerContext ctx, final HttpMessage msg) {
if (msg instanceof HttpRequest) {
msg.headers().set(
HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(),
connectionScheme(ctx));
}
return HttpConversionUtil.toHttp2Headers(msg, validateHeaders);
}
private HttpMessage newMessage(final int id,
final Http2Headers headers) throws Http2Exception {
return isServer ?
HttpConversionUtil.toHttpRequest(id, headers, validateHeaders) :
HttpConversionUtil.toHttpResponse(id, headers, validateHeaders);
}
private FullHttpMessage newFullMessage(final int id,
final Http2Headers headers,
final ByteBufAllocator alloc) throws Http2Exception {
return isServer ?
HttpConversionUtil.toFullHttpRequest(id, headers, alloc, validateHeaders) :
HttpConversionUtil.toFullHttpResponse(id, headers, alloc, validateHeaders);
}
@Override
public void handlerAdded(final ChannelHandlerContext ctx) throws Exception {
super.handlerAdded(ctx);
// this handler is typically used on an Http2StreamChannel. At this
// stage, ssl handshake should've been established. checking for the
// presence of SslHandler in the parent's channel pipeline to
// determine the HTTP scheme should suffice, even for the case where
// SniHandler is used.
final Attribute<HttpScheme> schemeAttribute = connectionSchemeAttribute(ctx);
if (schemeAttribute.get() == null) {
final HttpScheme scheme = isSsl(ctx) ? HttpScheme.HTTPS : HttpScheme.HTTP;
schemeAttribute.set(scheme);
}
}
protected boolean isSsl(final ChannelHandlerContext ctx) {
final Channel connChannel = connectionChannel(ctx);
return null != connChannel.pipeline().get(SslHandler.class);
}
private static HttpScheme connectionScheme(ChannelHandlerContext ctx) {
final HttpScheme scheme = connectionSchemeAttribute(ctx).get();
return scheme == null ? HttpScheme.HTTP : scheme;
}
private static Attribute<HttpScheme> connectionSchemeAttribute(ChannelHandlerContext ctx) {
final Channel ch = connectionChannel(ctx);
return ch.attr(SCHEME_ATTR_KEY);
}
private static Channel connectionChannel(ChannelHandlerContext ctx) {
final Channel ch = ctx.channel();
return ch instanceof Http2StreamChannel ? ch.parent() : ch;
}
}