Netty ChannelHandler 性能优化

1. 前言

本节我们主要来继续讲解 ChannelHandler 的其它特性,主要讲解如何去进行 ChannelHandler 业务链表的常见性能优化。

2. 优化途径

通常情况下为了提高自定义业务 Handler 的性能需要进行一定的优化策略,常见的优化方案分别是缩短传播路径、Handler 单利等。

  1. 传播路径: 如果业务很复杂的情况,由很多的 Handler 组成的时候,链条过长会消耗性能,因此,一般都是动态的删除一些没用的 Handler。
  2. 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 注解即可。

主要目的:

  1. 统一把 Handler 交给 Spring 来管理;
  2. 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 性能上面的优化,分别是

  1. 热插拔: 在执行过程中动态的删除无用的 Handler, 缩短 Handler 的传播距离;
  2. 单例: 避免每个客户端的连接进来时都重复创建 Handler,使用单利的集中方式以及线程安全问题。