1. 概述
本文将带你用 Netty 实现一个简单的 HTTP 大写转换服务器。Netty 是一个高性能的异步网络框架,适合在 Java 中构建各种网络应用。相比传统阻塞 I/O,它在高并发场景下优势明显,是很多中间件(如 Dubbo、RocketMQ)底层通信的首选。
我们的目标很明确:接收 HTTP 请求,把请求参数和正文内容转为大写后返回。虽然功能简单,但足以展示 Netty 处理 HTTP 协议的核心机制。
2. 服务启动与引导配置
在动手写业务逻辑前,先回顾 Netty 的核心概念:Channel
、Handler
、Pipeline
、编解码器等。这些是构建 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
:将原始字节流解码成HttpRequest
和HttpContent
对象HttpResponseEncoder
:将HttpResponse
对象编码回字节流发送CustomHttpServerHandler
:我们自定义的业务处理器
⚠️ 注意:HTTP 是基于请求-响应模型的,因此必须同时添加解码器和编码器,顺序不能错。
3. 自定义处理器 CustomHttpServerHandler
这是整个服务的核心,负责处理请求、构建响应。
3.1. 基本结构
继承 SimpleChannelInboundHandler
,泛型留空(因为可能收到 HttpRequest
或 HttpContent
):
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);
}
}
}
⚠️ 踩坑提醒:HttpRequest
和 HttpContent
可能分开发,不能假设一次 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-Type
和charset
- 根据
keep-alive
决定是否关闭连接 - 使用
ChannelFutureListener.CLOSE
在发送完成后优雅关闭
4. 服务测试
启动 HttpServer
后,使用 curl
测试。
4.1. GET 请求测试
curl "http://127.0.0.1:8080?param1=one¶m2=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 服务器。核心在于:
- ✅ 正确配置
HttpRequestDecoder
和HttpResponseEncoder
- ✅ 理解 HTTP 消息的分段特性(
HttpRequest
+HttpContent
+LastHttpContent
) - ✅ 在
LastHttpContent
到达时才发送完整响应 - ✅ 正确处理连接复用(keep-alive)
这套模式可扩展性强,适合构建高性能的 REST API 网关、反向代理等中间件。
扩展阅读:Netty 官方 HTTP/2 示例
完整源码已托管至 GitHub:https://github.com/yourname/netty-http-demo