1. 概述

本文将带你用 Netty 实现一个简单的 HTTP 大写转换服务器。Netty 是一个高性能的异步网络框架,适合在 Java 中构建各种网络应用。相比传统阻塞 I/O,它在高并发场景下优势明显,是很多中间件(如 Dubbo、RocketMQ)底层通信的首选。

我们的目标很明确:接收 HTTP 请求,把请求参数和正文内容转为大写后返回。虽然功能简单,但足以展示 Netty 处理 HTTP 协议的核心机制。

2. 服务启动与引导配置

在动手写业务逻辑前,先回顾 Netty 的核心概念:ChannelHandlerPipeline、编解码器等。这些是构建 Netty 应用的基石。

服务启动的关键是 ServerBootstrap,配置方式与普通 TCP 服务类似,但针对 HTTP 协议,childHandler 中的编解码器是重点

public class HttpServer {

    private int port;
    private static Logger logger = LoggerFactory.getLogger(HttpServer.class);

    public HttpServer(int port) {
        this.port = port;
    }

    public static void main(String[] args) throws Exception {
        new HttpServer(8080).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();
            logger.info("HTTP Server started on port " + port);
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

✅ 关键点说明:

  • HttpRequestDecoder:将原始字节流解码成 HttpRequestHttpContent 对象
  • HttpResponseEncoder:将 HttpResponse 对象编码回字节流发送
  • CustomHttpServerHandler:我们自定义的业务处理器

⚠️ 注意:HTTP 是基于请求-响应模型的,因此必须同时添加解码器和编码器,顺序不能错。

3. 自定义处理器 CustomHttpServerHandler

这是整个服务的核心,负责处理请求、构建响应。

3.1. 基本结构

继承 SimpleChannelInboundHandler,泛型留空(因为可能收到 HttpRequestHttpContent):

public class CustomHttpServerHandler extends SimpleChannelInboundHandler<Object> {
    private HttpRequest request;
    private StringBuilder responseData = new StringBuilder();

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
        // 核心逻辑
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}
  • channelReadComplete:处理完一批数据后刷新输出缓冲区
  • exceptionCaught:捕获异常并关闭连接,避免资源泄漏
  • channelRead0:真正的业务入口,Netty 会自动分发不同类型的 HttpObject

3.2. 请求处理与数据读取

HTTP 请求可能被拆分成多个事件(如:HttpRequest + 多个 HttpContent),所以我们需要分段处理。

@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
    if (msg instanceof HttpRequest) {
        HttpRequest request = this.request = (HttpRequest) msg;

        // 处理 100 Continue
        if (HttpUtil.is100ContinueExpected(request)) {
            writeResponse(ctx);
        }
        responseData.setLength(0);            
        responseData.append(RequestUtils.formatParams(request));
    }

    responseData.append(RequestUtils.evaluateDecoderResult(request));

    if (msg instanceof HttpContent) {
        HttpContent httpContent = (HttpContent) msg;
        responseData.append(RequestUtils.formatBody(httpContent));
        responseData.append(RequestUtils.evaluateDecoderResult(request));

        if (msg instanceof LastHttpContent) {
            LastHttpContent trailer = (LastHttpContent) msg;
            responseData.append(RequestUtils.prepareLastResponse(request, trailer));
            writeResponse(ctx, trailer, responseData);
        }
    }
}

⚠️ 踩坑提醒:HttpRequestHttpContent 可能分开发,不能假设一次 channelRead 就收到完整请求。必须用 LastHttpContent 判断是否结束。

辅助工具类:RequestUtils

public class RequestUtils {

    // 格式化查询参数,转为大写
    public static StringBuilder formatParams(HttpRequest request) {
        StringBuilder sb = new StringBuilder();
        QueryStringDecoder decoder = new QueryStringDecoder(request.uri());
        Map<String, List<String>> params = decoder.parameters();
        if (!params.isEmpty()) {
            for (Map.Entry<String, List<String>> entry : params.entrySet()) {
                String key = entry.getKey();
                for (String val : entry.getValue()) {
                    sb.append("Parameter: ")
                      .append(key.toUpperCase())
                      .append(" = ")
                      .append(val.toUpperCase())
                      .append("\r\n");
                }
            }
            sb.append("\r\n");
        }
        return sb;
    }

    // 处理请求体,转为大写
    public static StringBuilder formatBody(HttpContent httpContent) {
        StringBuilder sb = new StringBuilder();
        ByteBuf content = httpContent.content();
        if (content.isReadable()) {
            sb.append(content.toString(CharsetUtil.UTF_8).toUpperCase())
              .append("\r\n");
        }
        return sb;
    }

    // 处理结束标志,添加尾部信息
    public static StringBuilder prepareLastResponse(HttpRequest request, LastHttpContent trailer) {
        StringBuilder sb = new StringBuilder();
        sb.append("Good Bye!\r\n");

        if (!trailer.trailingHeaders().isEmpty()) {
            sb.append("\r\n");
            for (CharSequence name : trailer.trailingHeaders().names()) {
                for (CharSequence value : trailer.trailingHeaders().getAll(name)) {
                    sb.append("P.S. Trailing Header: ")
                      .append(name)
                      .append(" = ")
                      .append(value)
                      .append("\r\n");
                }
            }
            sb.append("\r\n");
        }
        return sb;
    }

    // 检查解码结果(可用于记录错误)
    public static StringBuilder evaluateDecoderResult(HttpRequest request) {
        StringBuilder sb = new StringBuilder();
        DecoderResult result = request.decoderResult();
        if (result.isSuccess()) {
            return sb;
        }
        sb.append("Decoder Result: ").append(result.toString()).append("\r\n");
        return sb;
    }
}

3.3. 发送响应

当收到 LastHttpContent 时,说明请求已完整接收,可以构建响应:

private void writeResponse(ChannelHandlerContext ctx) {
    FullHttpResponse response = new DefaultFullHttpResponse(
        HttpVersion.HTTP_1_1,
        HttpResponseStatus.CONTINUE,
        Unpooled.EMPTY_BUFFER
    );
    ctx.write(response);
}

private void writeResponse(ChannelHandlerContext ctx, LastHttpContent trailer, StringBuilder responseData) {
    boolean keepAlive = HttpUtil.isKeepAlive(request);
    HttpResponseStatus status = ((HttpObject) trailer).decoderResult().isSuccess() ? 
        HttpResponseStatus.OK : HttpResponseStatus.BAD_REQUEST;

    FullHttpResponse httpResponse = new DefaultFullHttpResponse(
        HttpVersion.HTTP_1_1,
        status,
        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);
    }

    ChannelFuture future = ctx.writeAndFlush(httpResponse);
    if (!keepAlive) {
        future.addListener(ChannelFutureListener.CLOSE);
    }
}

✅ 关键点:

  • 设置 Content-Typecharset
  • 根据 keep-alive 决定是否关闭连接
  • 使用 ChannelFutureListener.CLOSE 在发送完成后优雅关闭

4. 服务测试

启动 HttpServer 后,使用 curl 测试。

4.1. GET 请求测试

curl "http://127.0.0.1:8080?param1=one&param2=two"

✅ 输出:

Parameter: PARAM1 = ONE
Parameter: PARAM2 = TWO

Good Bye!

浏览器访问同理。

4.2. POST 请求测试

curl -d "hello netty" -X POST http://127.0.0.1:8080

✅ 输出:

HELLO NETTY
Good Bye!

5. 总结

本文通过一个简单的“大写转换”服务,演示了如何使用 Netty 构建 HTTP 服务器。核心在于:

  • ✅ 正确配置 HttpRequestDecoderHttpResponseEncoder
  • ✅ 理解 HTTP 消息的分段特性(HttpRequest + HttpContent + LastHttpContent
  • ✅ 在 LastHttpContent 到达时才发送完整响应
  • ✅ 正确处理连接复用(keep-alive)

这套模式可扩展性强,适合构建高性能的 REST API 网关、反向代理等中间件。

扩展阅读:Netty 官方 HTTP/2 示例

完整源码已托管至 GitHub:https://github.com/yourname/netty-http-demo


原始标题:HTTP Server with Netty