最近有一个高并发的项目,经过反复思考,决定采用Netty框架,最后也给出了普通的springboot接口和Netty框架性能的对比。
springboot框架集成Netty框架比较简单,直接上代码
加入依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.25.Final</version>
</dependency>
追加java代码
package com.jndj.platform.netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetSocketAddress;
/**
* 实现客户端发送请求,服务器端会返回Hello Netty
*/
public class NettyServer {
private static final Logger logger = LoggerFactory.getLogger(NettyServer.class);
public void start(InetSocketAddress socketAddress) {
// 定义一对线程组(两个线程池)
// 主线程组,用于接收客户端的链接,但不做任何处理
EventLoopGroup bossGroup = new NioEventLoopGroup();
// 定义从线程组,主线程组会把任务转给从线程组进行处理
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 服务启动类,任务分配自动处理
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 需要去针对一个之前的线程模型(上面定义的是主从线程)
serverBootstrap.group(bossGroup, workerGroup)
// 设置NIO的双向通道
.channel(NioServerSocketChannel.class)
//子处理器,用于处理workerGroup
// 设置chanel初始化器
// 每一个chanel由多个handler共同组成管道(pipeline)
.childHandler(new NettyServerInitializer())
//.localAddress(socketAddress)
// 设置队列大小
.option(ChannelOption.SO_BACKLOG, 1024)
// 两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
.childOption(ChannelOption.SO_KEEPALIVE, true);
// 启动
// 绑定端口,并设置为同步方式,是一个异步的chanel
ChannelFuture future = serverBootstrap.bind(8888).sync();
logger.info("服务器启动开始监听端口: {}", socketAddress.getPort());
// 关闭
// 获取某个客户端所对应的chanel,关闭并设置同步方式
future.channel().closeFuture().sync();
}catch (Exception e) {
e.printStackTrace();
}finally {
//使用一种优雅的方式进行关闭
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
package com.jndj.platform.netty;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
/**
* 这是一个初始化器,chanel注册后会执行里面相应的初始化方法(也就是将handler逐个进行添加)
*/
public class NettyServerInitializer extends ChannelInitializer<SocketChannel>{
// 对chanel进行初始化
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 通过socketChannel去获得对应的管道
ChannelPipeline channelPipeline = socketChannel.pipeline();
// pipeline中会有很多handler类(也称之拦截器类)
// 获得pipeline之后,可以直接.add,添加不管是自己开发的handler还是netty提供的handler
// 一般来讲添加到last即可,添加了一个handler并且取名为HttpServerCodec
// 当请求到达服务端,要做解码,响应到客户端做编码
channelPipeline.addLast("HttpServerCodec", new HttpServerCodec());
// channelPipeline.addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
// channelPipeline.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
// 添加自定义的CustomHandler这个handler,返回Hello Netty
channelPipeline.addLast("NettyServerHandler", new NettyServerHandler());
}
}
package com.jndj.platform.netty;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.util.CharsetUtil;
/**
* 创建自定义助手类
* 处理的请求是:客户端向服务端发起送数据,先把数据放在缓冲区,服务器端再从缓冲区读取,类似于[ 入栈, 入境 ]
*/
public class NettyServerHandler extends SimpleChannelInboundHandler<HttpObject>{//Http请求,所以使用HttpObject
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
// System.out.println("channel注册");
super.channelRegistered(ctx);
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
// System.out.println("channel注册");
super.channelUnregistered(ctx);
}
/**
* 客户端连接会触发
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// System.out.println("channel活跃状态");
super.channelActive(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
// System.out.println("客户端与服务端断开连接之后");
super.channelInactive(ctx);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("channel读取数据完毕");
super.channelReadComplete(ctx);
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// System.out.println("用户事件触发");
super.userEventTriggered(ctx, evt);
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
// System.out.println("channel可写事件更改");
super.channelWritabilityChanged(ctx);
}
/**
* 发生异常触发
* channel发生异常,若不关闭,随着异常channel的逐渐增多,性能也就随之下降
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// System.out.println("捕获channel异常");
super.exceptionCaught(ctx, cause);
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// System.out.println("助手类添加");
super.handlerAdded(ctx);
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
// System.out.println("助手类移除");
super.handlerRemoved(ctx);
}
/**
* ChannelHandlerContext:上下文对象
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
//获取当前channel
Channel currentChannel = ctx.channel();
//判断msg是否为一个HttpRequest的请求类型
if(msg instanceof HttpRequest) {
// 客户端远程地址
// System.out.println(currentChannel.remoteAddress());
/**
*
* 未加判断类型,控制台打印的远端地址如下:
*
/0:0:0:0:0:0:0:1:5501
/0:0:0:0:0:0:0:1:5501
/0:0:0:0:0:0:0:1:5502
/0:0:0:0:0:0:0:1:5502
*
* 原因是接收的MSG没有进行类型判断
*
* 增加了判断,为何还会打印两次?
*
/0:0:0:0:0:0:0:1:5605
/0:0:0:0:0:0:0:1:5605
*
* 打开浏览器的network会发现,客户端对服务端进行了两次请求:
* 1、第一次是所需的
* 2、第二次是一个icon
* 因为没有加路由(相当于Springmvc中的requestMapping),只要发起请求,就都会到handler中去
*
*/
//在Linux中也可以通过CURL 本机Ip:端口号 发送请求(只打印一次,干净的请求)
// 定义发送的消息(不是直接发送,而是要把数据拷贝到缓冲区,通过缓冲区)
// Unpooed:是一个专门用于拷贝Buffer的深拷贝,可以有一个或多个
// CharsetUtil.UTF_8:Netty提供
ByteBuf content = Unpooled.copiedBuffer("Hello Netty", CharsetUtil.UTF_8);
// 构建一个HttpResponse,响应客户端
// HttpVersion.HTTP_1_1:默认开启keep-alive
FullHttpResponse response =
new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);
// 设置当前内容长度、类型等
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
// readableBytes:可读长度
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
// 通过长下文对象,把响应刷到客户端
ctx.writeAndFlush(response);
}
}
}
package com.jndj.platform;
import com.jndj.platform.netty.NettyServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import java.net.InetSocketAddress;
@SpringBootApplication
@EnableScheduling
public class PlatformApplication {
public static void main(String[] args) {
SpringApplication.run(PlatformApplication.class, args);
//启动服务端
NettyServer nettyServer = new NettyServer();
nettyServer.start(new InetSocketAddress("127.0.0.1", 8888));
}
}
运行后结果
使用postman测试一下高并发下的性能,并且也写了一个简单的springboot的api,进行了对比测试
10000次请求,间隔1ms的并发
Netty的性能结果,大部分响应时间在2ms,3ms,速度还是挺快的。
普通的springboot接口,接口响应速度大体在5ms到10ms之间
最后结论,明显看出使用了NIO的Netty框架速度更快