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:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user