Netty ChannelHandler 性能优化
1. 前言
本节我们主要来继续讲解 ChannelHandler 的其它特性,主要讲解如何去进行 ChannelHandler 业务链表的常见性能优化。
2. 优化途径
通常情况下为了提高自定义业务 Handler 的性能需要进行一定的优化策略,常见的优化方案分别是缩短传播路径、Handler 单利等。
- 传播路径: 如果业务很复杂的情况,由很多的 Handler 组成的时候,链条过长会消耗性能,因此,一般都是动态的删除一些没用的 Handler。
- Handler 单利: 每个客户端进来,都会为每个 Channel 创建一轮 Handler 并且加入到 Pipeline 进行管理,new 的过程是消耗性能的。
3. 热插拔
上节我们学习了 ChannelHandler 的生命周期,其中有一个关键的方法是 handlerRemoved (),在 handler 被移除的时候触发该事件,针对该事件,其实我们可以灵活的扩展自己的业务功能。
需求:客户端和服务端之间通信,必须需要先认证。
实例:
serverBootstrap
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
//1.登录认证Handler
ch.pipeline().addLast(new LoginHandler());
//2.其他业务Handler
ch.pipeline().addLast(new OtherHandler());
}
});
通过以上的代码,我们就能很好的解决了客户端登录认证问题,但是我们会发现,在登录认证成功之后,客户端发起其他类型请求的时候,每次请求 LoginHandler 都会被执行,那么应该怎么去解决这个问题呢?
解决思路:在客户端第一次连接服务端时,进行账号认证,认证成功之后,把 LoginHandler 给移除掉。
实例:
public class LoginHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//1.省略了部分代码(转换ByteBuf,对象流反序列化)
//2.获取Map
Map<String,String> map=(Map<String,String>)iss.readObject();
//3.认证账号、密码,并且响应
String username=map.get("username");
String password=map.get("password");
if(username.equals("admin")&&password.equals("123456")){
//3.1.给客户端响应
ctx.channel().writeAndFlush(Unpooled.copiedBuffer("success".getBytes()));
//3.2.移除该Handler,这样下次请求就不会再执行该Handler了
ctx.pipeline().remove(this);
}else{
ctx.channel().writeAndFlush(Unpooled.copiedBuffer("error".getBytes()));
ctx.channel().closeFuture();
}
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
System.out.println("LoginHandler被移除");
}
}
总结,动态新增和移除 Handler,也称之为热插拔,在真实项目开发当中其实非常的有用。
4. Handler 单利
4.1 @Shareable
ch.pipeline().addLast(new LoginHandler());
添加链表节点的时候,我们是手工 new 一个对象,其实也就是说,每个客户端连接进来的时候,都需要组建一条双向链表,并且都是 new 每个节点的对象,我们都知道每次 new 性能肯定是不高。
Spring 的 IOC 其实就是解决手工 new 对象的,项目启动的时候把所有对象创建完放到 Spring 容器,后面每次使用的时候无需再创建,而是直接从容器里面获取,这种方式可以提高性能。同样道理,Netty 也提供类似的功能,那就是 @Shareable
注解修饰的 Handler,只要用该注解修饰之后,那么该 Handler 就会变成共享,也就是说被所有的客户端所共享,无需每次都创建,自然性能会得到提升。
实例:
//使用注解修饰
@ChannelHandler.Sharable
public class ServerLoginHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
}
}
public class NettyServer {
public static void main(String[] args) {
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
//提前创建好
final ServerLoginHandler serverLoginHandler=new ServerLoginHandler();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
//这里无需再创建,只需要传递实例即可
ch.pipeline().addLast(serverLoginHandler);
}
});
serverBootstrap.bind(80);
}
}
4.2 @Shareable 线程不安全
对于共享的 Handler,很容易就会出现线程安全问题,多个线程同时访问同一个对象不会出现任何的线程安全问题,但是有读有写,则就会产生线程安全问题,因此需要特别注意,因此,如果使用了 @Shareable 修饰了 Handler,那么千万不要包含全局变量、全局静态变量,否则就会出现线程安全问题。
实例:
@ChannelHandler.Sharable
public class ServerLoginHandler extends ChannelInboundHandlerAdapter {
//全局变量
private int count;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//递增
count++;
}
}
疑问:为什么以上的代码在并发情况下是不安全的呢?
原因是,每个线程内部都会开辟一个内存空间,从主内存中拷贝 count 值,在线程中递增之后,再把结果写到主内存当中。并发情况下,多个线程之间可能取得的值是一样,然后线程之间又不可见性,因此就会导致线程不安全。
解决:如果开发过程中遇到类似的问题,应该如何解决呢?
直接使用 AtomicXxx
去代替,AtomicXxx 是 J.U.C 下提供的工具类,底层是通过 CAS 无锁机制去控制,保证线程安全。
4.3 集成 Spring 容器
其实,在真实开发项目当中,一般都是把 Handler 直接交给 Spring 容器进行管理,也就是说在 Handler 类上添加 Spring 提供的 @Component 注解即可。
主要目的:
- 统一把 Handler 交给 Spring 来管理;
- Handler 一般都是需要和底层的数据库进行交互的,真实项目当中一般都是使用 Spring 来管理 ORM 组件,如果 Handler 不交给 Spring 管理,那么操作数据库的时候就会相对麻烦。
实例:
//交给Spring容器管理
@Component
public class ServerLoginHandler extends ChannelInboundHandlerAdapter {
//注入dao
@Autowired
private UserDao userDao;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
}
}
@Autowired
private ServerLoginHandler serverLoginHandler;
//这里无需再创建,只需要传递实例即可
ch.pipeline().addLast(serverLoginHandler);
5. 小结
本内容主要是从两个方面去进行业务 Handler 性能上面的优化,分别是
- 热插拔: 在执行过程中动态的删除无用的 Handler, 缩短 Handler 的传播距离;
- 单例: 避免每个客户端的连接进来时都重复创建 Handler,使用单利的集中方式以及线程安全问题。