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.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.AsciiString;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.ReferenceCounted;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
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.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
@ -54,12 +49,6 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator {
* A codec that the source can be upgraded to.
*/
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.
* 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);
}
/**
* 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
* 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 UpgradeCodecFactory upgradeCodecFactory;
private boolean handlingUpgrade;
/**
* 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 upgradeCodecs the codecs (in order of preference) that this server supports
* upgrading to from the source codec.
* @param maxContentLength the maximum length of the aggregated content.
* @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
*/
public HttpServerUpgradeHandler(SourceCodec sourceCodec,
Collection<UpgradeCodec> upgradeCodecs, int maxContentLength) {
public HttpServerUpgradeHandler(SourceCodec sourceCodec, UpgradeCodecFactory upgradeCodecFactory) {
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);
if (sourceCodec == null) {
throw new NullPointerException("sourceCodec");
}
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);
}
this.sourceCodec = checkNotNull(sourceCodec, "sourceCodec");
this.upgradeCodecFactory = checkNotNull(upgradeCodecFactory, "upgradeCodecFactory");
}
@Override
@ -248,8 +262,20 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator {
*/
private boolean upgrade(final ChannelHandlerContext ctx, final FullHttpRequest request) {
// Select the best protocol based on those requested in the UPGRADE header.
CharSequence upgradeHeader = request.headers().get(HttpHeaderNames.UPGRADE);
final UpgradeCodec upgradeCodec = selectUpgradeCodec(upgradeHeader);
final ArrayList<String> requestedProtocols = splitHeader(request.headers().get(HttpHeaderNames.UPGRADE));
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) {
// None of the requested protocols are supported, don't upgrade.
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.
Collection<String> requiredHeaders = upgradeCodec.requiredUpgradeHeaders();
Set<CharSequence> values = splitHeader(connectionHeader);
if (!values.contains(HttpHeaderNames.UPGRADE) || !values.containsAll(requiredHeaders)) {
List<String> values = splitHeader(connectionHeader);
if (!values.contains(UPGRADE_STRING) || !values.containsAll(requiredHeaders)) {
return false;
}
@ -276,12 +302,14 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator {
}
// 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,
// 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);
final UpgradeCodec finalUpgradeCodec = upgradeCodec;
ctx.writeAndFlush(upgradeResponse).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
@ -289,7 +317,7 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator {
if (future.isSuccess()) {
// Perform the upgrade to the new protocol.
sourceCodec.upgradeFrom(ctx);
upgradeCodec.upgradeTo(ctx, request, upgradeResponse);
finalUpgradeCodec.upgradeTo(ctx, request, upgradeResponse);
// Notify that the upgrade has occurred. Retain the event to offset
// the release() in the finally block.
@ -309,33 +337,13 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator {
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.
*/
private static FullHttpResponse createUpgradeResponse(UpgradeCodec upgradeCodec) {
private static FullHttpResponse createUpgradeResponse(String upgradeProtocol) {
DefaultFullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, SWITCHING_PROTOCOLS);
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");
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
* part with whitespace removed.
*/
private static Set<CharSequence> splitHeader(CharSequence header) {
StringBuilder builder = new StringBuilder(header.length());
Set<CharSequence> protocols = new TreeSet<CharSequence>(AsciiString.CHARSEQUENCE_CASE_INSENSITIVE_ORDER);
private static ArrayList<String> splitHeader(CharSequence header) {
final StringBuilder builder = new StringBuilder(header.length());
final ArrayList<String> protocols = new ArrayList<String>(4);
for (int i = 0; i < header.length(); ++i) {
char c = header.charAt(i);
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.http.HttpResponseStatus.BAD_REQUEST;
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.writeFrameHeader;
import static io.netty.handler.codec.http2.Http2FrameTypes.SETTINGS;
@ -72,11 +71,6 @@ public class Http2ServerUpgradeCodec implements HttpServerUpgradeHandler.Upgrade
frameReader = new DefaultHttp2FrameReader();
}
@Override
public String protocol() {
return HTTP_UPGRADE_PROTOCOL_NAME;
}
@Override
public Collection<String> requiredUpgradeHeaders() {
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.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpMessage;
import io.netty.handler.codec.http.HttpServerCodec;
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.ssl.SslContext;
import java.util.Collections;
/**
* 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.
*/
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;
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.
*/
private static void configureClearText(SocketChannel ch) {
HttpServerCodec sourceCodec = new HttpServerCodec();
HttpServerUpgradeHandler.UpgradeCodec upgradeCodec =
new Http2ServerUpgradeCodec(new HelloWorldHttp2Handler());
HttpServerUpgradeHandler upgradeHandler =
new HttpServerUpgradeHandler(sourceCodec, Collections.singletonList(upgradeCodec), 65536);
final ChannelPipeline p = ch.pipeline();
final HttpServerCodec sourceCodec = new HttpServerCodec();
ch.pipeline().addLast(sourceCodec);
ch.pipeline().addLast(upgradeHandler);
ch.pipeline().addLast(new SimpleChannelInboundHandler<HttpMessage>() {
p.addLast(sourceCodec);
p.addLast(new HttpServerUpgradeHandler(sourceCodec, upgradeCodecFactory));
p.addLast(new SimpleChannelInboundHandler<HttpMessage>() {
@Override
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.
System.err.println("Directly talking: " + msg.protocolVersion() + " (no upgrade was attempted)");
ctx.pipeline().replace(this, "http-hello-world",
new HelloWorldHttp1Handler("Direct. No Upgrade Attempted."));
new HelloWorldHttp1Handler("Direct. No Upgrade Attempted."));
ctx.fireChannelRead(msg);
}
});
ch.pipeline().addLast(new UserEventLogger());
p.addLast(new UserEventLogger());
}
/**