Carefully manage Keep-Alive/Close connection headers in all examples (#8966)

Motivation:

"Connection: close" header should be specified each time we're going
to close an underlying TCP connection when sending HTTP/1.1 reply.

Modifications:

Introduces changes made in #8914 for the following examples:

* WebSocket index page and WebSocket server handler
* HelloWorld server
* SPDY server handler
* HTTP/1.1 server handler from HTTP/2 HelloWorld example
* HTTP/1.1 server handler from tiles example

Result:

Keep-Alive connections management conforms with RFCs.
This commit is contained in:
Oleksii Kachaiev 2019-04-02 12:10:11 -07:00 committed by Norman Maurer
parent 6491f359c7
commit d0beb8dea1
7 changed files with 121 additions and 59 deletions

View File

@ -110,15 +110,18 @@ public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<Ful
public static final String HTTP_DATE_GMT_TIMEZONE = "GMT";
public static final int HTTP_CACHE_SECONDS = 60;
private FullHttpRequest request;
@Override
public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
this.request = request;
if (!request.decoderResult().isSuccess()) {
sendError(ctx, BAD_REQUEST);
return;
}
if (!GET.equals(request.method())) {
sendError(ctx, METHOD_NOT_ALLOWED);
this.sendError(ctx, METHOD_NOT_ALLOWED);
return;
}
@ -126,21 +129,21 @@ public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<Ful
final String uri = request.uri();
final String path = sanitizeUri(uri);
if (path == null) {
sendError(ctx, FORBIDDEN);
this.sendError(ctx, FORBIDDEN);
return;
}
File file = new File(path);
if (file.isHidden() || !file.exists()) {
sendError(ctx, NOT_FOUND);
this.sendError(ctx, NOT_FOUND);
return;
}
if (file.isDirectory()) {
if (uri.endsWith("/")) {
sendListing(ctx, file, uri, keepAlive);
this.sendListing(ctx, file, uri);
} else {
sendRedirect(ctx, uri + '/', keepAlive);
this.sendRedirect(ctx, uri + '/');
}
return;
}
@ -161,7 +164,7 @@ public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<Ful
long ifModifiedSinceDateSeconds = ifModifiedSinceDate.getTime() / 1000;
long fileLastModifiedSeconds = file.lastModified() / 1000;
if (ifModifiedSinceDateSeconds == fileLastModifiedSeconds) {
sendNotModified(ctx, keepAlive);
this.sendNotModified(ctx);
return;
}
}
@ -182,6 +185,8 @@ public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<Ful
if (!keepAlive) {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
} else if (request.protocolVersion().equals(HTTP_1_0)) {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
// Write the initial line and the header.
@ -266,7 +271,7 @@ public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<Ful
private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[^-\\._]?[^<>&\\\"]*");
private static void sendListing(ChannelHandlerContext ctx, File dir, String dirPath, boolean keepAlive) {
private void sendListing(ChannelHandlerContext ctx, File dir, String dirPath) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
@ -306,22 +311,22 @@ public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<Ful
response.content().writeBytes(buffer);
buffer.release();
sendAndCleanupConnection(ctx, response, keepAlive);
this.sendAndCleanupConnection(ctx, response);
}
private static void sendRedirect(ChannelHandlerContext ctx, String newUri, boolean keepAlive) {
private void sendRedirect(ChannelHandlerContext ctx, String newUri) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND);
response.headers().set(HttpHeaderNames.LOCATION, newUri);
sendAndCleanupConnection(ctx, response, keepAlive);
this.sendAndCleanupConnection(ctx, response);
}
private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
FullHttpResponse response = new DefaultFullHttpResponse(
HTTP_1_1, status, Unpooled.copiedBuffer("Failure: " + status + "\r\n", CharsetUtil.UTF_8));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
sendAndCleanupConnection(ctx, response, false);
this.sendAndCleanupConnection(ctx, response);
}
/**
@ -330,24 +335,27 @@ public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<Ful
* @param ctx
* Context
*/
private static void sendNotModified(ChannelHandlerContext ctx, boolean keepAlive) {
private void sendNotModified(ChannelHandlerContext ctx) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, NOT_MODIFIED);
setDateHeader(response);
sendAndCleanupConnection(ctx, response, keepAlive);
this.sendAndCleanupConnection(ctx, response);
}
/**
* If Keep-Alive is disabled, attaches "Connection: close" header to the response
* and closes the connection after the response being sent.
*/
private static void sendAndCleanupConnection(ChannelHandlerContext ctx, FullHttpResponse response,
boolean keepAlive) {
private void sendAndCleanupConnection(ChannelHandlerContext ctx, FullHttpResponse response) {
final FullHttpRequest request = this.request;
final boolean keepAlive = HttpUtil.isKeepAlive(request);
HttpUtil.setContentLength(response, response.content().readableBytes());
if (!keepAlive) {
// We're going to close the connection as soon as the response is sent,
// so we should also make it clear for the client.
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
} else if (request.protocolVersion().equals(HTTP_1_0)) {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
ChannelFuture flushPromise = ctx.writeAndFlush(response);

View File

@ -16,27 +16,28 @@
package io.netty.example.http.helloworld;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.util.AsciiString;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import static io.netty.handler.codec.http.HttpResponseStatus.*;
import static io.netty.handler.codec.http.HttpVersion.*;
import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE;
import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
public class HttpHelloWorldServerHandler extends SimpleChannelInboundHandler<HttpObject> {
private static final byte[] CONTENT = { 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd' };
private static final AsciiString CONTENT_TYPE = AsciiString.cached("Content-Type");
private static final AsciiString CONTENT_LENGTH = AsciiString.cached("Content-Length");
private static final AsciiString CONNECTION = AsciiString.cached("Connection");
private static final AsciiString KEEP_ALIVE = AsciiString.cached("keep-alive");
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
@ -52,11 +53,17 @@ public class HttpHelloWorldServerHandler extends SimpleChannelInboundHandler<Htt
response.headers().set(CONTENT_TYPE, "text/plain");
response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes());
if (!keepAlive) {
ctx.write(response).addListener(ChannelFutureListener.CLOSE);
} else {
if (keepAlive && req.protocolVersion().equals(HttpVersion.HTTP_1_0)) {
response.headers().set(CONNECTION, KEEP_ALIVE);
ctx.write(response);
} else {
// Tell the client we're going to close the connection.
response.headers().set(CONNECTION, CLOSE);
}
ChannelFuture f = ctx.write(response);
if (!keepAlive) {
f.addListener(ChannelFutureListener.CLOSE);
}
}
}

View File

@ -155,8 +155,7 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObj
} catch (ErrorDataDecoderException e1) {
e1.printStackTrace();
responseContent.append(e1.getMessage());
writeResponse(ctx.channel());
ctx.channel().close();
writeResponse(ctx.channel(), true);
return;
}
@ -180,8 +179,7 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObj
} catch (ErrorDataDecoderException e1) {
e1.printStackTrace();
responseContent.append(e1.getMessage());
writeResponse(ctx.channel());
ctx.channel().close();
writeResponse(ctx.channel(), true);
return;
}
responseContent.append('o');
@ -307,24 +305,27 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObj
}
private void writeResponse(Channel channel) {
writeResponse(channel, false);
}
private void writeResponse(Channel channel, boolean forceClose) {
// Convert the response content to a ChannelBuffer.
ByteBuf buf = copiedBuffer(responseContent.toString(), CharsetUtil.UTF_8);
responseContent.setLength(0);
// Decide whether to close the connection or not.
boolean close = request.headers().contains(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE, true)
|| request.protocolVersion().equals(HttpVersion.HTTP_1_0)
&& !request.headers().contains(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE, true);
boolean keepAlive = HttpUtil.isKeepAlive(request) && !forceClose;
// Build the response object.
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buf);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes());
if (!close) {
// There's no need to add 'Content-Length' header
// if this is the last response.
response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes());
if (!keepAlive) {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
} else if (request.protocolVersion().equals(HttpVersion.HTTP_1_0)) {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
Set<Cookie> cookies;
@ -343,7 +344,7 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObj
// Write the response.
ChannelFuture future = channel.writeAndFlush(response);
// Close the connection after the write operation is done if necessary.
if (close) {
if (!keepAlive) {
future.addListener(ChannelFutureListener.CLOSE);
}
}
@ -428,8 +429,20 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObj
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes());
// Decide whether to close the connection or not.
boolean keepAlive = HttpUtil.isKeepAlive(request);
if (!keepAlive) {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
} else if (request.protocolVersion().equals(HttpVersion.HTTP_1_0)) {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
// Write the response.
ctx.channel().writeAndFlush(response);
ChannelFuture future = ctx.channel().writeAndFlush(response);
// Close the connection after the write operation is done if necessary.
if (!keepAlive) {
future.addListener(ChannelFutureListener.CLOSE);
}
}
@Override

View File

@ -17,7 +17,6 @@ package io.netty.example.http.websocketx.benchmarkserver;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
@ -36,9 +35,16 @@ import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
import io.netty.util.CharsetUtil;
import static io.netty.handler.codec.http.HttpMethod.*;
import static io.netty.handler.codec.http.HttpResponseStatus.*;
import static io.netty.handler.codec.http.HttpVersion.*;
import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE;
import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_0;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
/**
* Handles handshakes and messages
@ -137,9 +143,15 @@ public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object>
}
// Send the response and close the connection if necessary.
ChannelFuture f = ctx.channel().writeAndFlush(res);
if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
// Tell the client we're going to close the connection.
res.headers().set(CONNECTION, CLOSE);
ctx.writeAndFlush(res).addListener(ChannelFutureListener.CLOSE);
} else {
if (req.protocolVersion().equals(HTTP_1_0)) {
res.headers().set(CONNECTION, KEEP_ALIVE);
}
ctx.writeAndFlush(res);
}
}

View File

@ -17,7 +17,6 @@ package io.netty.example.http.websocketx.server;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
@ -31,11 +30,16 @@ import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.ssl.SslHandler;
import io.netty.util.CharsetUtil;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE;
import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_0;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
/**
@ -69,7 +73,7 @@ public class WebSocketIndexPageHandler extends SimpleChannelInboundHandler<FullH
ByteBuf content = WebSocketServerIndexPage.getContent(webSocketLocation);
FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, OK, content);
res.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
res.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8");
HttpUtil.setContentLength(res, content.readableBytes());
sendHttpResponse(ctx, req, res);
@ -94,9 +98,15 @@ public class WebSocketIndexPageHandler extends SimpleChannelInboundHandler<FullH
}
// Send the response and close the connection if necessary.
ChannelFuture f = ctx.channel().writeAndFlush(res);
if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
// Tell the client we're going to close the connection.
res.headers().set(CONNECTION, CLOSE);
ctx.writeAndFlush(res).addListener(ChannelFutureListener.CLOSE);
} else {
if (req.protocolVersion().equals(HTTP_1_0)) {
res.headers().set(CONNECTION, KEEP_ALIVE);
}
ctx.writeAndFlush(res);
}
}

View File

@ -18,8 +18,11 @@ package io.netty.example.http2.helloworld.server;
import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE;
import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE;
import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_0;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import static java.util.Objects.requireNonNull;
@ -32,7 +35,6 @@ import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpHeaderValues;
/**
* HTTP handler that responds with a "Hello World"
@ -59,11 +61,15 @@ public class HelloWorldHttp1Handler extends SimpleChannelInboundHandler<FullHttp
response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes());
if (!keepAlive) {
ctx.write(response).addListener(ChannelFutureListener.CLOSE);
} else {
response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE);
if (keepAlive) {
if (req.protocolVersion().equals(HTTP_1_0)) {
response.headers().set(CONNECTION, KEEP_ALIVE);
}
ctx.write(response);
} else {
// Tell the client we're going to close the connection.
response.headers().set(CONNECTION, CLOSE);
ctx.write(response).addListener(ChannelFutureListener.CLOSE);
}
}

View File

@ -17,8 +17,11 @@
package io.netty.example.http2.tiles;
import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE;
import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE;
import static io.netty.handler.codec.http.HttpUtil.isKeepAlive;
import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_0;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
@ -26,7 +29,6 @@ import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpHeaderValues;
import java.util.concurrent.TimeUnit;
@ -49,9 +51,13 @@ public final class Http1RequestHandler extends Http2RequestHandler {
HttpUtil.setContentLength(response, response.content().readableBytes());
ctx.executor().schedule(() -> {
if (isKeepAlive(request)) {
response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE);
if (request.protocolVersion().equals(HTTP_1_0)) {
response.headers().set(CONNECTION, KEEP_ALIVE);
}
ctx.writeAndFlush(response);
} else {
// Tell the client we're going to close the connection.
response.headers().set(CONNECTION, CLOSE);
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
}, latency, TimeUnit.MILLISECONDS);