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:
parent
950da2eae1
commit
0ca65f1373
@ -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)) {
|
||||
|
@ -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;
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user