Building HTTP Servers with Netty 4

Netty provides a robust foundation for implementing custom HTTP servers without relying on traditional web frameworks like Spring MVC or Jakarta Servlet. Its channnel-based architecture allows fine-grained control over request parsing, response generation, and protocol handling.

Core Pipeline Configuration

To enable HTTP support, the server pipeline must include appropriate encoders and decoders:

pipeline.addLast(new HttpResponseEncoder());
pipeline.addLast(new HttpRequestDecoder());
pipeline.addLast(new HttpObjectAggregator(10 * 1024 * 1024));
pipeline.addLast(new HttpServerHandler());

The HttpObjectAggregator consolidates fragmetned HTTP messages (e.g., chunked transfers) into complete FullHttpRequest and FullHttpResponse objects, simplifying access to headers and content.

Handling Complete HTTP Messages

Unlike HttpRequest, which only exposes headers and metadata, FullHttpRequest implements both HttpMessage and HttpContent, enabling direct access to the entire request body as a ByteBuf. This eliminates manual aggregation logic in handlers.

Example usage inside a handler:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    if (!(msg instanceof FullHttpRequest)) {
        sendError(ctx, HttpResponseStatus.BAD_REQUEST);
        return;
    }

    FullHttpRequest request = (FullHttpRequest) msg;
    String targetPath = request.uri();
    HttpMethod verb = request.method();
    String payload = extractPayload(request);

    try {
        String responseText = handleRequest(verb, targetPath, payload);
        sendSuccess(ctx, responseText);
    } finally {
        request.release(); // Prevent memory leaks
    }
}

private String extractPayload(FullHttpRequest req) {
    return req.content().toString(CharsetUtil.UTF_8);
}

private void sendSuccess(ChannelHandlerContext ctx, String content) {
    ByteBuf buffer = Unpooled.copiedBuffer(content, CharsetUtil.UTF_8);
    FullHttpResponse response = new DefaultFullHttpResponse(
        HttpVersion.HTTP_1_1,
        HttpResponseStatus.OK,
        buffer
    );
    response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
    ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}

private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
    FullHttpResponse response = new DefaultFullHttpResponse(
        HttpVersion.HTTP_1_1,
        status,
        Unpooled.EMPTY_BUFFER
    );
    ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}

private String handleRequest(HttpMethod method, String path, String body) {
    switch (method) {
        case GET:
            return "Handled GET request to " + path;
        case POST:
            return "Received POST data: " + body;
        case PUT:
            return "Applied PUT update with payload length " + body.length();
        case DELETE:
            return "Deleted resource at " + path;
        default:
            return "Unsupported method: " + method;
    }
}

Server Bootstrap Setup

The server is initialized using ServerBootstrap, configured with NIO event loops and a custom channel initializer:

public class HttpServer {
    private static final int PORT = 6789;

    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 128)
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childHandler(new HttpServerInitializer());

            ChannelFuture future = bootstrap.bind(PORT).sync();
            System.out.println("HTTP server started on port " + PORT);
            future.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

Channel Initializer

The initializer configures the pipeline per accepted connection:

public class HttpServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) {
        ChannelPipeline p = ch.pipeline();
        p.addLast(new HttpResponseEncoder());
        p.addLast(new HttpRequestDecoder());
        p.addLast(new HttpObjectAggregator(10 * 1024 * 1024));
        p.addLast(new HttpServerHandler());
    }
}

This configuration supports standard HTTP methods (GET, POST, PUT, DELETE), handles UTF-8 encoded payloads, and enforces proper resource cleanup via release() calls on pooled buffers.

Tags: Netty http-server java network-programming asynchronous-io

Posted on Sat, 12 Sep 2026 16:19:04 +0000 by salmanshafiq