BAEL-4061: Implementing a Simple HTTP Server with Netty (#9416)

* BAEL-4061: Implementing a Simple HTTP Server with Netty

* BAEL-4061: Added live test

* BAEL-4061: moved helper class from test to main folder

* BAEL-4061: updated tests to follow BDD
This commit is contained in:
Sampada
2020-06-04 08:18:57 +05:30
committed by GitHub
parent 6df3b2b6fb
commit ee146f5a13
5 changed files with 532 additions and 0 deletions
@@ -0,0 +1,116 @@
package com.baeldung.http.server;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
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_1;
import java.util.Set;
import io.netty.buffer.Unpooled;
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.HttpContent;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.ServerCookieDecoder;
import io.netty.handler.codec.http.cookie.ServerCookieEncoder;
import io.netty.util.CharsetUtil;
public class CustomHttpServerHandler extends SimpleChannelInboundHandler<Object> {
private HttpRequest request;
StringBuilder responseData = new StringBuilder();
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
if (msg instanceof HttpRequest) {
HttpRequest request = this.request = (HttpRequest) msg;
if (HttpUtil.is100ContinueExpected(request)) {
writeResponse(ctx);
}
responseData.setLength(0);
responseData.append(ResponseBuilder.addRequestAttributes(request));
responseData.append(ResponseBuilder.addHeaders(request));
responseData.append(ResponseBuilder.addParams(request));
}
responseData.append(ResponseBuilder.addDecoderResult(request));
if (msg instanceof HttpContent) {
HttpContent httpContent = (HttpContent) msg;
responseData.append(ResponseBuilder.addBody(httpContent));
responseData.append(ResponseBuilder.addDecoderResult(request));
if (msg instanceof LastHttpContent) {
LastHttpContent trailer = (LastHttpContent) msg;
responseData.append(ResponseBuilder.addLastResponse(request, trailer));
writeResponse(ctx, trailer, responseData);
}
}
}
private void writeResponse(ChannelHandlerContext ctx) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, CONTINUE, Unpooled.EMPTY_BUFFER);
ctx.write(response);
}
private void writeResponse(ChannelHandlerContext ctx, LastHttpContent trailer, StringBuilder responseData) {
boolean keepAlive = HttpUtil.isKeepAlive(request);
FullHttpResponse httpResponse = new DefaultFullHttpResponse(HTTP_1_1, ((HttpObject) trailer).decoderResult()
.isSuccess() ? OK : BAD_REQUEST, Unpooled.copiedBuffer(responseData.toString(), CharsetUtil.UTF_8));
httpResponse.headers()
.set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
if (keepAlive) {
httpResponse.headers()
.setInt(HttpHeaderNames.CONTENT_LENGTH, httpResponse.content()
.readableBytes());
httpResponse.headers()
.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
String cookieString = request.headers()
.get(HttpHeaderNames.COOKIE);
if (cookieString != null) {
Set<Cookie> cookies = ServerCookieDecoder.STRICT.decode(cookieString);
if (!cookies.isEmpty()) {
for (Cookie cookie : cookies) {
httpResponse.headers()
.add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie));
}
}
}
ctx.write(httpResponse);
if (!keepAlive) {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
.addListener(ChannelFutureListener.CLOSE);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
@@ -0,0 +1,64 @@
package com.baeldung.http.server;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
public class HttpServer {
private int port;
static Logger logger = LoggerFactory.getLogger(HttpServer.class);
public HttpServer(int port) {
this.port = port;
}
public static void main(String[] args) throws Exception {
int port = args.length > 0 ? Integer.parseInt(args[0]) : 8080;
new HttpServer(port).run();
}
public void run() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new HttpRequestDecoder());
p.addLast(new HttpResponseEncoder());
p.addLast(new CustomHttpServerHandler());
}
});
ChannelFuture f = b.bind(port)
.sync();
f.channel()
.closeFuture()
.sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
@@ -0,0 +1,120 @@
package com.baeldung.http.server;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.DecoderResult;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.util.CharsetUtil;
class ResponseBuilder {
static StringBuilder addRequestAttributes(HttpRequest request) {
StringBuilder responseData = new StringBuilder();
responseData.append("Version: ")
.append(request.protocolVersion())
.append("\r\n");
responseData.append("Host: ")
.append(request.headers()
.get(HttpHeaderNames.HOST, "unknown"))
.append("\r\n");
responseData.append("URI: ")
.append(request.uri())
.append("\r\n\r\n");
return responseData;
}
static StringBuilder addParams(HttpRequest request) {
StringBuilder responseData = new StringBuilder();
QueryStringDecoder queryStringDecoder = new QueryStringDecoder(request.uri());
Map<String, List<String>> params = queryStringDecoder.parameters();
if (!params.isEmpty()) {
for (Entry<String, List<String>> p : params.entrySet()) {
String key = p.getKey();
List<String> vals = p.getValue();
for (String val : vals) {
responseData.append("Parameter: ")
.append(key)
.append(" = ")
.append(val)
.append("\r\n");
}
}
responseData.append("\r\n");
}
return responseData;
}
static StringBuilder addHeaders(HttpRequest request) {
StringBuilder responseData = new StringBuilder();
HttpHeaders headers = request.headers();
if (!headers.isEmpty()) {
for (Map.Entry<String, String> header : headers) {
CharSequence key = header.getKey();
CharSequence value = header.getValue();
responseData.append(key)
.append(" = ")
.append(value)
.append("\r\n");
}
responseData.append("\r\n");
}
return responseData;
}
static StringBuilder addBody(HttpContent httpContent) {
StringBuilder responseData = new StringBuilder();
ByteBuf content = httpContent.content();
if (content.isReadable()) {
responseData.append(content.toString(CharsetUtil.UTF_8)
.toUpperCase());
responseData.append("\r\n");
}
return responseData;
}
static StringBuilder addDecoderResult(HttpObject o) {
StringBuilder responseData = new StringBuilder();
DecoderResult result = o.decoderResult();
if (!result.isSuccess()) {
responseData.append("..Decoder Failure: ");
responseData.append(result.cause());
responseData.append("\r\n");
}
return responseData;
}
static StringBuilder addLastResponse(HttpRequest request, LastHttpContent trailer) {
StringBuilder responseData = new StringBuilder();
responseData.append("Good Bye!\r\n");
if (!trailer.trailingHeaders()
.isEmpty()) {
responseData.append("\r\n");
for (CharSequence name : trailer.trailingHeaders()
.names()) {
for (CharSequence value : trailer.trailingHeaders()
.getAll(name)) {
responseData.append("P.S. Trailing Header: ");
responseData.append(name)
.append(" = ")
.append(value)
.append("\r\n");
}
}
responseData.append("\r\n");
}
return responseData;
}
}