在网上查了很多netty server端实现的例子,感觉还是有很多坑,这里记录一下自己的实现,也把自己踩的坑记录一下,利人利己。
首先创建一个server类,用来初始化netty的server端
public class NettyHttpServer {
private Logger logger = LoggerFactory.getLogger(NettyHttpServer.class);
private int inetPort;
public NettyHttpServer(final int inetPort) {
this.inetPort = inetPort;
}
public int getInetPort() {
return inetPort;
}
public void init() throws Exception {
//根据自己机器的核心数设置parent和child数量,也可以不设定,netty会读取机器的核心数信息自行配置
EventLoopGroup parentGroup = new NioEventLoopGroup(8);
EventLoopGroup childGroup = new NioEventLoopGroup(16);
try {
ServerBootstrap server = new ServerBootstrap();
// 1. 绑定两个线程组分别用来处理客户端通道的accept和读写时间
server.group(parentGroup, childGroup)
// 2. 绑定服务端通道NioServerSocketChannel
.channel(NioServerSocketChannel.class)
//这句会在日志中打印netty日志,需要的可以加上
//.handler(new LoggingHandler(LogLevel.INFO))
// 3. 给读写事件的线程通道绑定handler去真正处理读写
// ChannelInitializer初始化通道SocketChannel
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline p = socketChannel.pipeline();
//这里要注意解码器和转码器的顺序,server端用下面的顺序,但是cilent端要反过来
//请求解码器
p.addLast("http-decoder", new HttpRequestDecoder());
// 响应转码器
p.addLast("http-encoder", new HttpResponseEncoder());
// 将HTTP消息的多个部分合成一条完整的HTTP消息,对于报文较长的请求需要设置,短请求可以不设置
p.addLast("http-aggregator", new HttpObjectAggregator(1024*1024));
// 自定义处理handler,这个handler中写自己的业务处理代码
p.addLast("http-server", new NettyHttpServerHandler());
}
});
//开启http的长连接,这个根据自己的请求场景来设置
server.option(ChannelOption.SO_KEEPALIVE, true)
//开启tcp立即响应,这样会影响整体性能,但是能保证单个响应不会延迟太高
.option(ChannelOption.TCP_NODELAY, true)
//开启请求缓存队列
.option(ChannelOption.SO_BACKLOG, 1024 * 1024)
//开启地址重用
.option(ChannelOption.SO_REUSEADDR, true);
// 4. 监听端口(服务器host和port端口),同步返回
logger.info("netty started");
Channel ch = server.bind(this.inetPort).sync().channel();
logger.info("netty started success");
// 当通道关闭时继续向后执行,这是一个阻塞方法
ch.closeFuture().sync();
} finally {
childGroup.shutdownGracefully();
parentGroup.shutdownGracefully();
logger.info("netty shutdown");
}
}
}
然后实现handler,完成自己的业务逻辑
//这边有多种类可以用来继承,server端一般使用ChannelInboundHandlerAdapter或者//SimpleChannelInboundHandler,不同之处在于后者会自动关闭conetxt,可能会导致通道在响应发送之前关//闭,所以我这边用ChannelInboundHandlerAdapter,需要手动关闭context
//client端一般使用ChannelOutboundHandlerAdapter或者SimpleChannelOutboundHandler
public class NettyHttpServerHandler extends ChannelInboundHandlerAdapter {
private static Logger logger = LoggerFactory.getLogger(NettyHttpServerHandler.class);
@Override
public void channelReadComplete(ChannelHandlerContext ctx){
ctx.flush();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception{
//这个方法中读取msg中的信息,进行自定义的业务逻辑处理
BusinessHandler.processRequest(ctx,msg);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception{
logger.error("handler error:" + cause);
ctx.close();
}
}
需要注意的是channelRead方法中的两个参数,第一个参数看名字就明白是上下文信息,第二个参数msg,如果使用SimpleChannelInboundHandler,可以指定msg的类型,例如
public class NettyHttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {}
那么就能在override的channelRead0()方法中指定msg的类型为FullHttpRequest,方便解析。
当然,这边的msg是可以有多种类型的,我这边使用的是FullHttpRequest,包含HttpRequest和FullHttpMessage两个类,http中的参数和内容等所有信息都在这两个类中。
然后实现BusinessHandler类
public class BusinessHandler {
public static void processRequest(ChannelHandlerContext ctx, Object msg){
FullHttpRequest fullHttpRequest = (FullHttpRequest)msg;
//请求内容在content中,通过ByteBuf可以转换成bytes
ByteBuf bb = fullHttpRequest.content();
//获取uri
fullHttpRequest.uri();
//获取paramteer
String sNow = getParameter(fullHttpRequest, "now");
/**
真·业务逻辑
**/
//判断是否是长连接
boolean close = fullHttpRequest.headers().contains(CONNECTION, HttpHeaders.Values.CLOSE, true)
|| fullHttpRequest.protocolVersion().equals(HttpVersion.HTTP_1_0)
&& !fullHttpRequest.headers().contains(CONNECTION, HttpHeaders.Values.KEEP_ALIVE, true);
// 返回
FullHttpResponse response = processor.doResponse();
if(close){
//使用netstat查看你的服务器连接情况,如果存在大量time-wait状态的连接,就可能是这个地方的问题
channelHandlerContext.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
} else {
channelHandlerContext.writeAndFlush(response);
}
//手动释放,防止内存泄露
fullHttpRequest.release();
}
public String getParameter(FullHttpRequest fullHttpRequest, String par) throws IOException{
HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(fullHttpRequest);
decoder.offer(fullHttpRequest);
for (InterfaceHttpData parm : decoder.getBodyHttpDatas()) {
Attribute data = (Attribute) parm;
if(par.equals(data.getName())){
return data.getValue();
}
}
return null;
}
}
如果自身的业务逻辑耗时较长,也可以使用线程池进行处理,这样能提升整体性能,如果追求单个响应的速度,就不要用线程池。
大致贴一下doResponse方法
public FullHttpResponse doResponse(){
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.valueOf(200));
//响应内容,内容的格式需要与下面设置的content-Type对应
String resp = "response content";
ByteBuf buffer = Unpooled.copiedBuffer(new StringBuilder(resp), CharsetUtil.UTF_8);
response.content().writeBytes(buffer);
//必须手动release,防止内存泄漏
buffer.release();
response.headers().set("Content-Type", "application/json;charset=utf-8");
//在返回内容不为空的情况下,Content-Length必须手动设置,否者通道会阻塞
response.headers().setInt("Content-Length", response.content().readableBytes());
return response;
}
这样netty server端就创建完成了
更新一下:
最近做了netty包版本的升级,具体从4.1.14.Final升级到4.1.56.Final
升级之后出现了对外内存溢出的问题,排查之后发现是bytebuf没有回收,所以在所有使用了bytebuf的地方手动进行了release,之后问题解决,这个问题可以通过在jvm启动参数中加入
java -jar -Dio.netty.leakDetection.level=advanced your-server.jar
来打印出可能存在内存泄露的地方来进行问题排查
另外一个问题就是可能会出现
io.netty.util.IllegalReferenceCountException: refCnt: 0, decrement:1
这样的报错,是因为同一个fullhttprequest的release次数大于了被引用次数,根据报错的地方修改一下release的次数就可以了
还有一个问题就是
channelHandlerContext.writeAndFlush(response);
fullHttpRequest.release();
在4.1.14.Final版本中,是不会报错的,但是在4.1.56.Final版本中会报错,就是上述IllegalReferenceCountException,这个问题没有细查,可能是在4.1.14.Final之后的版本中加入了retain的逻辑
另外有疑问可以参考官方文档https://netty.io/wiki/reference-counted-objects.html