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

View File

@ -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();
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,180 @@
package com.baeldung.http.server;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.HashMap;
import java.util.Map;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpContentDecompressor;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http.cookie.ClientCookieEncoder;
import io.netty.handler.codec.http.cookie.DefaultCookie;
import io.netty.util.CharsetUtil;
//Ensure the server class - HttpServer.java is already started before running this test
public class HttpServerLiveTest {
private static final String HOST = "127.0.0.1";
private static final int PORT = 8080;
private Channel channel;
private EventLoopGroup group = new NioEventLoopGroup();
ResponseAggregator response = new ResponseAggregator();
@Before
public void setup() throws Exception {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new HttpClientCodec());
p.addLast(new HttpContentDecompressor());
p.addLast(new SimpleChannelInboundHandler<HttpObject>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
response = prepareResponse(ctx, msg, response);
}
});
}
});
channel = b.connect(HOST, PORT)
.sync()
.channel();
}
@Test
public void whenPostSent_thenContentReceivedInUppercase() throws Exception {
String body = "Hello World!";
DefaultFullHttpRequest request = createRequest(body);
channel.writeAndFlush(request);
Thread.sleep(200);
assertEquals(200, response.getStatus());
assertEquals("HTTP/1.1", response.getVersion());
assertTrue(response.getContent()
.contains(body.toUpperCase()));
}
@Test
public void whenGetSent_thenCookieReceivedInResponse() throws Exception {
DefaultFullHttpRequest request = createRequest(null);
channel.writeAndFlush(request);
Thread.sleep(200);
assertEquals(200, response.getStatus());
assertEquals("HTTP/1.1", response.getVersion());
Map<String, String> headers = response.getHeaders();
String cookies = headers.get("set-cookie");
assertTrue(cookies.contains("my-cookie"));
}
@After
public void cleanup() throws InterruptedException {
channel.closeFuture()
.sync();
group.shutdownGracefully();
}
private static DefaultFullHttpRequest createRequest(final CharSequence body) throws Exception {
DefaultFullHttpRequest request;
if (body != null) {
request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/");
request.content()
.writeBytes(body.toString()
.getBytes(CharsetUtil.UTF_8.name()));
request.headers()
.set(HttpHeaderNames.CONTENT_TYPE, "application/json");
request.headers()
.set(HttpHeaderNames.CONTENT_LENGTH, request.content()
.readableBytes());
} else {
request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/", Unpooled.EMPTY_BUFFER);
request.headers()
.set(HttpHeaderNames.COOKIE, ClientCookieEncoder.STRICT.encode(new DefaultCookie("my-cookie", "foo")));
}
request.headers()
.set(HttpHeaderNames.HOST, HOST);
request.headers()
.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
return request;
}
private static ResponseAggregator prepareResponse(ChannelHandlerContext ctx, HttpObject msg, ResponseAggregator responseAgg) {
if (msg instanceof HttpResponse) {
HttpResponse response = (HttpResponse) msg;
responseAgg.setStatus(response.status()
.code());
responseAgg.setVersion(response.protocolVersion()
.text());
if (!response.headers()
.isEmpty()) {
Map<String, String> headers = new HashMap<String, String>();
for (CharSequence name : response.headers()
.names()) {
for (CharSequence value : response.headers()
.getAll(name)) {
headers.put(name.toString(), value.toString());
}
}
responseAgg.setHeaders(headers);
}
if (HttpUtil.isTransferEncodingChunked(response)) {
responseAgg.setChunked(true);
} else {
responseAgg.setChunked(false);
}
}
if (msg instanceof HttpContent) {
HttpContent content = (HttpContent) msg;
String responseData = content.content()
.toString(CharsetUtil.UTF_8);
if (content instanceof LastHttpContent) {
responseAgg.setContent(responseData + "} End Of Content");
ctx.close();
}
}
return responseAgg;
}
}

View File

@ -0,0 +1,52 @@
package com.baeldung.http.server;
import java.util.Map;
public class ResponseAggregator {
int status;
String version;
Map<String, String> headers;
boolean isChunked;
String content;
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public Map<String, String> getHeaders() {
return headers;
}
public void setHeaders(Map<String, String> headers) {
this.headers = headers;
}
public boolean isChunked() {
return isChunked;
}
public void setChunked(boolean isChunked) {
this.isChunked = isChunked;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}