Netty

214 阅读3分钟

参考:spring boot整合长连接心跳机制

简介

Netty 是一个高性能的 NIO (非堵塞)网络框架

Netty能实现的功能

  • 客户端每隔N秒检测是否需要发送心跳
  • 服务端也是每隔N秒检测是否需要发送心跳
  • 服务器可以主动push消息到客户端
  • 基于spring Boot的监控,可以查看实时连接及应用的其他信息

IdleStateHandler

Netty通过IdleStateHandler来实现连接管理,当空闲时间太长时会触发一个事件,我们便可在此事件中实现心跳机制

客户端心跳

当客户端空闲了N秒没有给服务器发送消息时会自动向服务器发送一个心跳来保持连接

核心代码如下:

public class EchoClientHandle extends SimpleChannelInboundHandler<ByteBuf> {

    private final static Logger LOGGER = LoggerFactory.getLogger(EchoClientHandle.class);

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent){
            IdleStateEvent idleStateEvent = (IdleStateEvent) evt ;

            if (idleStateEvent.state() == IdleState.WRITER_IDLE){
                LOGGER.info("已经 10 秒没有发送信息!");
                //向服务端发送消息
                CustomProtocol heartBeat = SpringBeanFactory.getBean("heartBeat", CustomProtocol.class);
                ctx.writeAndFlush(heartBeat).addListener(ChannelFutureListener.CLOSE_ON_FAILURE) ;
            }
        }
        super.userEventTriggered(ctx, evt);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf in) throws Exception {
        //从服务端收到消息时被调用
        LOGGER.info("客户端收到消息={}",in.toString(CharsetUtil.UTF_8)) ;
    }
}    

实现时只需要在事件回调中发一条消息即可。

由于整合了spring Boot,所以发送心跳的是一个单例的bean

@Configuration
public class HeartBeatConfig {

    @Value("${channel.id}")
    private long id ;

    @Bean(value = "heartBeat")
    public CustomProtocol heartBeat(){
        return new CustomProtocol(id,"ping") ;
    }
}

启动引导程序

@Component
public class HeartbeatClient {

    private final static Logger LOGGER = LoggerFactory.getLogger(HeartbeatClient.class);

    private EventLoopGroup group = new NioEventLoopGroup();

    @Value("${netty.server.port}")
    private int nettyPort;

    @Value("${netty.server.host}")
    private String host;

    private SocketChannel channel;

    @PostConstruct
    public void start() throws InterruptedException {
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(group)
                .channel(NioSocketChannel.class)
                .handler(new CustomerHandleInitializer());

        ChannelFuture future = bootstrap.connect(host, nettyPort).sync();
        if (future.isSuccess()) {
            LOGGER.info("启动 Netty 成功");
        }
        channel = (SocketChannel) future.channel();
    }
}

public class CustomerHandleInitializer extends ChannelInitializer<Channel> {
    @Override
    protected void initChannel(Channel ch) throws Exception {
        ch.pipeline()
                //10 秒没发送消息 将IdleStateHandler 添加到 ChannelPipeline 中
                .addLast(new IdleStateHandler(0, 10, 0))
                .addLast(new HeartbeatEncode())
                .addLast(new EchoClientHandle())
        ;
    }
}   

服务器心跳

服务器的心跳利用的也是IdleStateHandler

注意:有多个客户端连接上来的时候,服务端需要对客户端进行区分,不然消息就会混乱。所以在每次有客户端连上来的时候,我们都将当前的 Channel 与连上的客户端 ID 进行关联(每个客户端的ID都必须唯一)

可以利用一个MAP来保存这个关系

自定义协议

从上文可以看出,服务器与客户端之间使用的是自定义的POJO进行通讯的。所以需要在客户端进行编码,服务器端进行解码。

实现原理

IdleStateHandler的实现原理:存在一个定时任务的线程去处理这些消息

spring boot中实现监控

使用actuator监控功能,他可以暴露很多端点供我们使用,如应用中的统计数据,存在的bean等

使用SBA

使用spring-boot-admin可以很方便,美观的查看多个应用

扩展

堵塞与非堵塞的区别

传统的堵塞:从内存读取数据,然后写到磁盘,而CPU一直等到磁盘写入完成,磁盘的写入速度是非常慢的,这段时间内CPU是被堵塞的,不能发挥效率

JAVA NIO: CPU只是发出写操作这样的指令,做一些初始化工作,DMA具体执行,从内存中读取数据,然后写到磁盘,当完成写后发出一个中断事件给CPU。这段时间CPU是空闲的,可以做别的事情。这个原理称为Zero.copy零拷贝。Netty底层就是基于上述JAVA NIO原理实现的

Netty,node.js,Tomcat比较

  • tomcat是一个Web服务器,它是采取一个请求一个线程,当有1000客户端时,会耗费很多内存。通常一个线程将花费 256kb到1mb的stack空间。
  • Node.js是一个线程服务于所有请求,在错误处理上有限制
  • Netty是一个线程服务于很多请求