Lazily instantiate HttpServerUpgradeHandler.UpgradeCodec

Related: #3814

Motivation:

To implement the support for an upgrade from cleartext HTTP/1.1
connection to cleartext HTTP/2 (h2c) connection, a user usually uses
HttpServerUpgradeHandler.

It does its job, but it requires a user to instantiate the UpgradeCodecs
for all supported protocols upfront. It means redundancy for the
connections that are not upgraded.

Modifications:

- Change the constructor of HttpServerUpgradeHandler
  - Accept UpgraceCodecFactory instead of UpgradeCodecs
- The default constructor of HttpServerUpgradeHandler sets the
  maxContentLength to 0 now, which shouldn't be a problem because a
  usual upgrade request is a GET.
- Update the examples accordingly

Result:

A user can instantiate Http2ServerUpgradeCodec and its related objects
(Http2Connection, Http2FrameReader/Writer, Http2FrameListener, etc) only
when necessary.
This commit is contained in:
Trustin Lee 2015-06-09 19:24:35 +09:00
parent 950da2eae1
commit 0ca65f1373
3 changed files with 96 additions and 82 deletions

View File

@ -17,21 +17,16 @@ package io.netty.handler.codec.http;
import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.util.AsciiString;
import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCountUtil;
import io.netty.util.ReferenceCounted; import io.netty.util.ReferenceCounted;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import static io.netty.handler.codec.http.HttpResponseStatus.SWITCHING_PROTOCOLS; import static io.netty.handler.codec.http.HttpResponseStatus.SWITCHING_PROTOCOLS;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
/** /**
* A server-side handler that receives HTTP requests and optionally performs a protocol switch if * A server-side handler that receives HTTP requests and optionally performs a protocol switch if
@ -54,12 +49,6 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator {
* A codec that the source can be upgraded to. * A codec that the source can be upgraded to.
*/ */
public interface UpgradeCodec { public interface UpgradeCodec {
/**
* Returns the name of the protocol supported by this codec, as indicated by the
* {@link HttpHeaderNames#UPGRADE} header.
*/
String protocol();
/** /**
* Gets all protocol-specific headers required by this protocol for a successful upgrade. * Gets all protocol-specific headers required by this protocol for a successful upgrade.
* Any supplied header will be required to appear in the {@link HttpHeaderNames#CONNECTION} header as well. * Any supplied header will be required to appear in the {@link HttpHeaderNames#CONNECTION} header as well.
@ -87,6 +76,20 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator {
void upgradeTo(ChannelHandlerContext ctx, FullHttpRequest upgradeRequest, FullHttpResponse upgradeResponse); void upgradeTo(ChannelHandlerContext ctx, FullHttpRequest upgradeRequest, FullHttpResponse upgradeResponse);
} }
/**
* Creates a new {@link UpgradeCodec} for the requested protocol name.
*/
public interface UpgradeCodecFactory {
/**
* Invoked by {@link HttpServerUpgradeHandler} for all the requested protocol names in the order of
* the client preference. The first non-{@code null} {@link UpgradeCodec} returned by this method
* will be selected.
*
* @return a new {@link UpgradeCodec}, or {@code null} if the specified protocol name is not supported
*/
UpgradeCodec newUpgradeCodec(String protocol);
}
/** /**
* User event that is fired to notify about the completion of an HTTP upgrade * User event that is fired to notify about the completion of an HTTP upgrade
* to another protocol. Contains the original upgrade request so that the response * to another protocol. Contains the original upgrade request so that the response
@ -160,33 +163,44 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator {
} }
} }
private final Map<String, UpgradeCodec> upgradeCodecMap; private static final String UPGRADE_STRING = HttpHeaderNames.UPGRADE.toString();
private final SourceCodec sourceCodec; private final SourceCodec sourceCodec;
private final UpgradeCodecFactory upgradeCodecFactory;
private boolean handlingUpgrade; private boolean handlingUpgrade;
/** /**
* Constructs the upgrader with the supported codecs. * Constructs the upgrader with the supported codecs.
* <p>
* The handler instantiated by this constructor will reject an upgrade request with non-empty content.
* It should not be a concern because an upgrade request is most likely a GET request.
* If you have a client that sends a non-GET upgrade request, please consider using
* {@link #HttpServerUpgradeHandler(SourceCodec, UpgradeCodecFactory, int)} to specify the maximum
* length of the content of an upgrade request.
* </p>
* *
* @param sourceCodec the codec that is being used initially. * @param sourceCodec the codec that is being used initially
* @param upgradeCodecs the codecs (in order of preference) that this server supports * @param upgradeCodecFactory the factory that creates a new upgrade codec
* upgrading to from the source codec. * for one of the requested upgrade protocols
* @param maxContentLength the maximum length of the aggregated content.
*/ */
public HttpServerUpgradeHandler(SourceCodec sourceCodec, public HttpServerUpgradeHandler(SourceCodec sourceCodec, UpgradeCodecFactory upgradeCodecFactory) {
Collection<UpgradeCodec> upgradeCodecs, int maxContentLength) { this(sourceCodec, upgradeCodecFactory, 0);
}
/**
* Constructs the upgrader with the supported codecs.
*
* @param sourceCodec the codec that is being used initially
* @param upgradeCodecFactory the factory that creates a new upgrade codec
* for one of the requested upgrade protocols
* @param maxContentLength the maximum length of the content of an upgrade request
*/
public HttpServerUpgradeHandler(
SourceCodec sourceCodec, UpgradeCodecFactory upgradeCodecFactory, int maxContentLength) {
super(maxContentLength); super(maxContentLength);
if (sourceCodec == null) {
throw new NullPointerException("sourceCodec"); this.sourceCodec = checkNotNull(sourceCodec, "sourceCodec");
} this.upgradeCodecFactory = checkNotNull(upgradeCodecFactory, "upgradeCodecFactory");
if (upgradeCodecs == null) {
throw new NullPointerException("upgradeCodecs");
}
this.sourceCodec = sourceCodec;
upgradeCodecMap = new LinkedHashMap<String, UpgradeCodec>(upgradeCodecs.size());
for (UpgradeCodec upgradeCodec : upgradeCodecs) {
String name = upgradeCodec.protocol().toUpperCase(Locale.US);
upgradeCodecMap.put(name, upgradeCodec);
}
} }
@Override @Override
@ -248,8 +262,20 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator {
*/ */
private boolean upgrade(final ChannelHandlerContext ctx, final FullHttpRequest request) { private boolean upgrade(final ChannelHandlerContext ctx, final FullHttpRequest request) {
// Select the best protocol based on those requested in the UPGRADE header. // Select the best protocol based on those requested in the UPGRADE header.
CharSequence upgradeHeader = request.headers().get(HttpHeaderNames.UPGRADE); final ArrayList<String> requestedProtocols = splitHeader(request.headers().get(HttpHeaderNames.UPGRADE));
final UpgradeCodec upgradeCodec = selectUpgradeCodec(upgradeHeader); final int numRequestedProtocols = requestedProtocols.size();
UpgradeCodec upgradeCodec = null;
String upgradeProtocol = null;
for (int i = 0; i < numRequestedProtocols; i ++) {
final String p = requestedProtocols.get(i);
final UpgradeCodec c = upgradeCodecFactory.newUpgradeCodec(p);
if (c != null) {
upgradeProtocol = p;
upgradeCodec = c;
break;
}
}
if (upgradeCodec == null) { if (upgradeCodec == null) {
// None of the requested protocols are supported, don't upgrade. // None of the requested protocols are supported, don't upgrade.
return false; return false;
@ -263,8 +289,8 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator {
// Make sure the CONNECTION header contains UPGRADE as well as all protocol-specific headers. // Make sure the CONNECTION header contains UPGRADE as well as all protocol-specific headers.
Collection<String> requiredHeaders = upgradeCodec.requiredUpgradeHeaders(); Collection<String> requiredHeaders = upgradeCodec.requiredUpgradeHeaders();
Set<CharSequence> values = splitHeader(connectionHeader); List<String> values = splitHeader(connectionHeader);
if (!values.contains(HttpHeaderNames.UPGRADE) || !values.containsAll(requiredHeaders)) { if (!values.contains(UPGRADE_STRING) || !values.containsAll(requiredHeaders)) {
return false; return false;
} }
@ -276,12 +302,14 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator {
} }
// Create the user event to be fired once the upgrade completes. // Create the user event to be fired once the upgrade completes.
final UpgradeEvent event = new UpgradeEvent(upgradeCodec.protocol(), request); final UpgradeEvent event = new UpgradeEvent(upgradeProtocol, request);
// Prepare and send the upgrade response. Wait for this write to complete before upgrading, // Prepare and send the upgrade response. Wait for this write to complete before upgrading,
// since we need the old codec in-place to properly encode the response. // since we need the old codec in-place to properly encode the response.
final FullHttpResponse upgradeResponse = createUpgradeResponse(upgradeCodec); final FullHttpResponse upgradeResponse = createUpgradeResponse(upgradeProtocol);
upgradeCodec.prepareUpgradeResponse(ctx, request, upgradeResponse); upgradeCodec.prepareUpgradeResponse(ctx, request, upgradeResponse);
final UpgradeCodec finalUpgradeCodec = upgradeCodec;
ctx.writeAndFlush(upgradeResponse).addListener(new ChannelFutureListener() { ctx.writeAndFlush(upgradeResponse).addListener(new ChannelFutureListener() {
@Override @Override
public void operationComplete(ChannelFuture future) throws Exception { public void operationComplete(ChannelFuture future) throws Exception {
@ -289,7 +317,7 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator {
if (future.isSuccess()) { if (future.isSuccess()) {
// Perform the upgrade to the new protocol. // Perform the upgrade to the new protocol.
sourceCodec.upgradeFrom(ctx); sourceCodec.upgradeFrom(ctx);
upgradeCodec.upgradeTo(ctx, request, upgradeResponse); finalUpgradeCodec.upgradeTo(ctx, request, upgradeResponse);
// Notify that the upgrade has occurred. Retain the event to offset // Notify that the upgrade has occurred. Retain the event to offset
// the release() in the finally block. // the release() in the finally block.
@ -309,33 +337,13 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator {
return true; return true;
} }
/**
* Looks up the most desirable supported upgrade codec from the list of choices in the UPGRADE
* header. If no suitable codec was found, returns {@code null}.
*/
private UpgradeCodec selectUpgradeCodec(CharSequence upgradeHeader) {
Set<CharSequence> requestedProtocols = splitHeader(upgradeHeader);
// Retain only the protocols that are in the protocol map. Maintain the original insertion
// order into the protocolMap, so that the first one in the remaining set is the most
// desirable protocol for the server.
Set<String> supportedProtocols = new LinkedHashSet<String>(upgradeCodecMap.keySet());
supportedProtocols.retainAll(requestedProtocols);
if (!supportedProtocols.isEmpty()) {
String protocol = supportedProtocols.iterator().next().toUpperCase(Locale.US);
return upgradeCodecMap.get(protocol);
}
return null;
}
/** /**
* Creates the 101 Switching Protocols response message. * Creates the 101 Switching Protocols response message.
*/ */
private static FullHttpResponse createUpgradeResponse(UpgradeCodec upgradeCodec) { private static FullHttpResponse createUpgradeResponse(String upgradeProtocol) {
DefaultFullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, SWITCHING_PROTOCOLS); DefaultFullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, SWITCHING_PROTOCOLS);
res.headers().add(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE); res.headers().add(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE);
res.headers().add(HttpHeaderNames.UPGRADE, upgradeCodec.protocol()); res.headers().add(HttpHeaderNames.UPGRADE, upgradeProtocol);
res.headers().add(HttpHeaderNames.CONTENT_LENGTH, "0"); res.headers().add(HttpHeaderNames.CONTENT_LENGTH, "0");
return res; return res;
} }
@ -344,9 +352,9 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator {
* Splits a comma-separated header value. The returned set is case-insensitive and contains each * Splits a comma-separated header value. The returned set is case-insensitive and contains each
* part with whitespace removed. * part with whitespace removed.
*/ */
private static Set<CharSequence> splitHeader(CharSequence header) { private static ArrayList<String> splitHeader(CharSequence header) {
StringBuilder builder = new StringBuilder(header.length()); final StringBuilder builder = new StringBuilder(header.length());
Set<CharSequence> protocols = new TreeSet<CharSequence>(AsciiString.CHARSEQUENCE_CASE_INSENSITIVE_ORDER); final ArrayList<String> protocols = new ArrayList<String>(4);
for (int i = 0; i < header.length(); ++i) { for (int i = 0; i < header.length(); ++i) {
char c = header.charAt(i); char c = header.charAt(i);
if (Character.isWhitespace(c)) { if (Character.isWhitespace(c)) {

View File

@ -31,7 +31,6 @@ import java.util.List;
import static io.netty.handler.codec.base64.Base64Dialect.URL_SAFE; import static io.netty.handler.codec.base64.Base64Dialect.URL_SAFE;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http2.Http2CodecUtil.FRAME_HEADER_LENGTH; import static io.netty.handler.codec.http2.Http2CodecUtil.FRAME_HEADER_LENGTH;
import static io.netty.handler.codec.http2.Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME;
import static io.netty.handler.codec.http2.Http2CodecUtil.HTTP_UPGRADE_SETTINGS_HEADER; import static io.netty.handler.codec.http2.Http2CodecUtil.HTTP_UPGRADE_SETTINGS_HEADER;
import static io.netty.handler.codec.http2.Http2CodecUtil.writeFrameHeader; import static io.netty.handler.codec.http2.Http2CodecUtil.writeFrameHeader;
import static io.netty.handler.codec.http2.Http2FrameTypes.SETTINGS; import static io.netty.handler.codec.http2.Http2FrameTypes.SETTINGS;
@ -72,11 +71,6 @@ public class Http2ServerUpgradeCodec implements HttpServerUpgradeHandler.Upgrade
frameReader = new DefaultHttp2FrameReader(); frameReader = new DefaultHttp2FrameReader();
} }
@Override
public String protocol() {
return HTTP_UPGRADE_PROTOCOL_NAME;
}
@Override @Override
public Collection<String> requiredUpgradeHeaders() { public Collection<String> requiredUpgradeHeaders() {
return REQUIRED_UPGRADE_HEADERS; return REQUIRED_UPGRADE_HEADERS;

View File

@ -19,21 +19,35 @@ package io.netty.example.http2.helloworld.server;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpMessage; import io.netty.handler.codec.http.HttpMessage;
import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpServerUpgradeHandler; import io.netty.handler.codec.http.HttpServerUpgradeHandler;
import io.netty.handler.codec.http.HttpServerUpgradeHandler.UpgradeCodec;
import io.netty.handler.codec.http.HttpServerUpgradeHandler.UpgradeCodecFactory;
import io.netty.handler.codec.http2.Http2CodecUtil;
import io.netty.handler.codec.http2.Http2ServerUpgradeCodec; import io.netty.handler.codec.http2.Http2ServerUpgradeCodec;
import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContext;
import java.util.Collections;
/** /**
* Sets up the Netty pipeline for the example server. Depending on the endpoint config, sets up the * Sets up the Netty pipeline for the example server. Depending on the endpoint config, sets up the
* pipeline for NPN or cleartext HTTP upgrade to HTTP/2. * pipeline for NPN or cleartext HTTP upgrade to HTTP/2.
*/ */
public class Http2ServerInitializer extends ChannelInitializer<SocketChannel> { public class Http2ServerInitializer extends ChannelInitializer<SocketChannel> {
private static final UpgradeCodecFactory upgradeCodecFactory = new UpgradeCodecFactory() {
@Override
public UpgradeCodec newUpgradeCodec(String protocol) {
if (Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME.equals(protocol)) {
return new Http2ServerUpgradeCodec(new HelloWorldHttp2Handler());
} else {
return null;
}
}
};
private final SslContext sslCtx; private final SslContext sslCtx;
public Http2ServerInitializer(SslContext sslCtx) { public Http2ServerInitializer(SslContext sslCtx) {
@ -60,25 +74,23 @@ public class Http2ServerInitializer extends ChannelInitializer<SocketChannel> {
* Configure the pipeline for a cleartext upgrade from HTTP to HTTP/2. * Configure the pipeline for a cleartext upgrade from HTTP to HTTP/2.
*/ */
private static void configureClearText(SocketChannel ch) { private static void configureClearText(SocketChannel ch) {
HttpServerCodec sourceCodec = new HttpServerCodec(); final ChannelPipeline p = ch.pipeline();
HttpServerUpgradeHandler.UpgradeCodec upgradeCodec = final HttpServerCodec sourceCodec = new HttpServerCodec();
new Http2ServerUpgradeCodec(new HelloWorldHttp2Handler());
HttpServerUpgradeHandler upgradeHandler =
new HttpServerUpgradeHandler(sourceCodec, Collections.singletonList(upgradeCodec), 65536);
ch.pipeline().addLast(sourceCodec); p.addLast(sourceCodec);
ch.pipeline().addLast(upgradeHandler); p.addLast(new HttpServerUpgradeHandler(sourceCodec, upgradeCodecFactory));
ch.pipeline().addLast(new SimpleChannelInboundHandler<HttpMessage>() { p.addLast(new SimpleChannelInboundHandler<HttpMessage>() {
@Override @Override
protected void channelRead0(ChannelHandlerContext ctx, HttpMessage msg) throws Exception { protected void channelRead0(ChannelHandlerContext ctx, HttpMessage msg) throws Exception {
// If this handler is hit then no upgrade has been attempted and the client is just talking HTTP. // If this handler is hit then no upgrade has been attempted and the client is just talking HTTP.
System.err.println("Directly talking: " + msg.protocolVersion() + " (no upgrade was attempted)"); System.err.println("Directly talking: " + msg.protocolVersion() + " (no upgrade was attempted)");
ctx.pipeline().replace(this, "http-hello-world", ctx.pipeline().replace(this, "http-hello-world",
new HelloWorldHttp1Handler("Direct. No Upgrade Attempted.")); new HelloWorldHttp1Handler("Direct. No Upgrade Attempted."));
ctx.fireChannelRead(msg); ctx.fireChannelRead(msg);
} }
}); });
ch.pipeline().addLast(new UserEventLogger());
p.addLast(new UserEventLogger());
} }
/** /**