基于Netty的简单HTTP服务例子
Contents
例子是基于 Netty 4.1.25.Final + Spring Boot 2 + JDK 1.8 + Maven
AppInitializer.java
package io.github.emacsist.netty.httpdemo.config;
import io.github.emacsist.netty.httpdemo.handler.AppHandler;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author emacsist
*/
@Component
@ChannelHandler.Sharable
public class AppInitializer extends ChannelInitializer {
private static final int MB = 1024 * 1024;
@Autowired
private AppHandler appHandler;
@Override
protected void initChannel(final Channel channel) {
final ChannelPipeline p = channel.pipeline();
p.addLast(new HttpServerCodec());
p.addLast(new HttpObjectAggregator(1 * MB));
p.addLast(appHandler);
}
}
AppHandler.java
package io.github.emacsist.netty.httpdemo.handler;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.util.AsciiString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* @author emacsist
*/
@Component
@ChannelHandler.Sharable
public class AppHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private static final Logger log = LoggerFactory.getLogger(AppHandler.class);
private static final AsciiString CONTENT_TYPE = AsciiString.cached("Content-Type");
private static final AsciiString TEXT_PLAIN = AsciiString.cached("text/plain; charset=utf-8");
private static final AsciiString CONTENT_LENGTH = AsciiString.cached("Content-Length");
@Override
protected void channelRead0(final ChannelHandlerContext ctx, final FullHttpRequest fullHttpRequest) {
final String uri = fullHttpRequest.uri();
final QueryStringDecoder queryStringDecoder = new QueryStringDecoder(uri);
final String requestPath = queryStringDecoder.path();
String body = "";
switch (requestPath) {
case "/hello":
log.info("in hello => {}", queryStringDecoder.parameters());
//请自行检测参数, 这里假设 /hello 是会带上 ?name=world 类似这参数值的
body = "Hello " + queryStringDecoder.parameters().get("name").get(0);
break;
case "/netty":
log.info("in netty => {}", queryStringDecoder.parameters());
body = "Hello Netty.";
break;
default:
break;
}
final DefaultFullHttpResponse defaultFullHttpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer(body.getBytes()));
defaultFullHttpResponse.headers().set(CONTENT_TYPE, TEXT_PLAIN);
defaultFullHttpResponse.headers().set(CONTENT_LENGTH, defaultFullHttpResponse.content().readableBytes());
ctx.write(defaultFullHttpResponse);
}
@Override
public void channelReadComplete(final ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) {
log.error(cause.getMessage(), cause);
ctx.close();
}
}
HttpDemoApplication.java
package io.github.emacsist.netty.httpdemo;
import io.github.emacsist.netty.httpdemo.config.AppInitializer;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author emacsist
*/
@SpringBootApplication
public class HttpDemoApplication implements CommandLineRunner {
@Value("${server.port}")
private int port;
private static final int KB = 1024;
@Autowired
private AppInitializer appInitializer;
public static void main(final String[] args) {
SpringApplication.run(HttpDemoApplication.class, args);
}
@Override
public void run(final String... args) {
final ServerBootstrap serverBootstrap = new ServerBootstrap();
final EventLoopGroup master = new NioEventLoopGroup();
final EventLoopGroup worker = new NioEventLoopGroup();
try {
serverBootstrap
.option(ChannelOption.SO_BACKLOG, 4 * KB)
.option(ChannelOption.TCP_NODELAY, true)
.group(master, worker)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.DEBUG))
.childHandler(appInitializer);
final Channel ch = serverBootstrap.bind(port).sync().channel();
System.out.println("start app ok...");
ch.closeFuture().sync();
} catch (final InterruptedException e) {
//ignore
} finally {
master.shutdownGracefully();
worker.shutdownGracefully();
System.out.println("stop app ok...");
}
}
}
注意
编写自己的HTTP业务时, 请记住千万不要阻塞EventLoop线程~, 不然会导致 Netty 的性能急剧下降.
当要处理一些阻塞操作时, 请用其他的线程池来处理.在Netty中一般有两种方式:
ChannelPipeline 指定 executor:
例如这样子:
/**
* @author emacsist
*/
@Component
@ChannelHandler.Sharable
public class AppInitializer extends ChannelInitializer {
private static final int MB = 1024 * 1024;
@Autowired
private AppHandler appHandler;
private static final DefaultEventExecutorGroup executorGroup = new DefaultEventExecutorGroup(Runtime.getRuntime().availableProcessors() * 2);
@Override
protected void initChannel(final Channel channel) {
final ChannelPipeline p = channel.pipeline();
p.addLast(new HttpServerCodec());
p.addLast(new HttpObjectAggregator(1 * MB));
p.addLast(executorGroup, appHandler);
}
}
创建一个全局的 ExecutorGroup , 自己手工创建任务
executorGroup.submit(() -> maxService.dealClick(queryStringDecoder));
这个是公司项目代码的一个片段, 要将HTTP参数 接收-> 解密 -> 进队和处理 Redis, 这些都是阻塞操作来的, 所以这里创建了一个异步任务来处理这些耗时的操作, 以免阻塞 EventLoop 线程.
性能
经过测试, 还是手工这种按需处理异步任务的性能更高点. 差不多高1倍. 当然, 具体问题要具体分析, 总之就是不要阻塞 EventLoop 线程就好, 毕竟Netty高性能的核心, 就在于不要阻塞 EventLoop.