Netty心跳(heartbeat)服务源码解析

源码解析目标
  • 理解Netty内部心跳服务源码实现。
源码解析使用案例
public class MyServer {

    public static void main(String[] args) {
        EventLoopGroup boosGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try{
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(boosGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new IdleStateHandler(7000, 7000, 10, TimeUnit.SECONDS));
//                            pipeline.addLast(new ReadTimeoutHandler(7));
//                            pipeline.addLast(new WriteTimeoutHandler(7));
                            pipeline.addLast(new MyServerHandler());
                        }
                    });
            //启动服务器
            ChannelFuture channelFuture = bootstrap.bind(7000).sync();
            channelFuture.channel().closeFuture().sync();
        }catch (Exception e){

        }finally {
            boosGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

/**
 * Created by jiamin5 on 2022/11/29.
 */
public class MyServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent){
            IdleStateEvent event = (IdleStateEvent) evt;
            String eventType = null;
            switch (event.state()){
                case ALL_IDLE:
                    eventType = "读写空闲";
                    break;
                case READER_IDLE:
                    eventType = "读空闲";
                    break;
                case WRITER_IDLE:
                    eventType = "写空闲";
                    break;
            }
            System.out.println(ctx.channel().remoteAddress()+ "--超时时间--" + eventType);
            System.out.println("服务器处理....");
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
       cause.printStackTrace();
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        super.channelReadComplete(ctx);
    }
}

  • 如上源码中我们对Netty服务器的(读/写)活动情况进行监听,利用IdleStateHandler 来完成,其中三个参数分别读,写,读/写 空闲时间
  • 方法效果总结如下表中
序号名称作用
1IdleStateHandler当连接的空闲时间(读/写 ) 太长时候,会触发IdleStateHandler事件,然后,可以通过我们在ChannelInboundHandler中重写 userEventTriggered 方法来处理对应的事件
2ReadTimeoutHandler如果在指定的时间内没有发生读事件,会抛出异常,并且自动关闭连接,我们可以在exceptionCaught 方法中处理这个异常
3WriteTimeoutHandler当一个写操作不能在一定的时间内完成时候,抛出此异常,并且关闭连接,同样可以在exceptionCaught 方法中处理这个异常
  • ReadTimeoutHandler,WriteTimeoutHandler 两个事件都会自动关闭连接,而且术语异常处理,我们以下中点介绍一下下IdleStateHandler 源码
源码分析
  • 首先当 调用addLast添加IdleStateHandler到pipeline中时候,会触发handlerAdd方法
//添加操作
 pipeline.addLast(new IdleStateHandler(7000, 7000, 10, TimeUnit.SECONDS));
//handlerAdder
@Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        if (ctx.channel().isActive() && ctx.channel().isRegistered()) {
            initialize(ctx);
        } else {
        }
    }
  • 关注initialize 方法,如下代码中,对IdleStateHandler的属性进行初始化过程,涉及到以下几个
private void initialize(ChannelHandlerContext ctx) {
        // Avoid the case where destroy() is called before scheduling timeouts.
        // See: https://github.com/netty/netty/issues/143
        switch (state) {
        case 1:
        case 2:
            return;
        }

        state = 1;
        initOutputChanged(ctx);

        lastReadTime = lastWriteTime = ticksInNanos();
        if (readerIdleTimeNanos > 0) {
            readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx),
                    readerIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
        if (writerIdleTimeNanos > 0) {
            writerIdleTimeout = schedule(ctx, new WriterIdleTimeoutTask(ctx),
                    writerIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
        if (allIdleTimeNanos > 0) {
            allIdleTimeout = schedule(ctx, new AllIdleTimeoutTask(ctx),
                    allIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
    }
  • private final boolean observeOutput; : 是否考虑出站时候较慢的情况,默认是false
  • private final long readerIdleTimeNanos; : 读事件空闲时间,0 则禁用事件
  • private final long writerIdleTimeNanos; : 写事件空闲时间,0 则禁用事件
  • private final long allIdleTimeNanos; : 读/写事件空闲时间,0 则禁用事件
  • 源码中,当参数 > 0 时候,就创建一个定时任务,每个事件都创建,同时将state设置为1 ,避免重复初始化,调用initOutputChanged方法初始化监控出站数据属性。
  • 创建定时任务源码如下, 利用channel中的EventLoop来添加一个Scheduler,看源码并debug,观察EvengLoop中Scheduler线程池中是否有新增任务,如下:
ScheduledFuture<?> schedule(ChannelHandlerContext ctx, Runnable task, long delay, TimeUnit unit) {
        return ctx.executor().schedule(task, delay, unit);
    }

在这里插入图片描述

  • 在ScheduledTaskQueue中的确有3个Task,因为我们addLast的方法中有三个参数都被初始化了,那么我们来看下对应的三个Task实现方法。
  • 三个Task都是ChannelDuplexHandler 中的内部类,并且有一个共同父类,AbstractIdleTask
private abstract static class AbstractIdleTask implements Runnable {
        private final ChannelHandlerContext ctx;
        AbstractIdleTask(ChannelHandlerContext ctx) {
            this.ctx = ctx;
        }
        @Override
        public void run() {
            if (!ctx.channel().isOpen()) {
                return;
            }

            run(ctx);
        }
        protected abstract void run(ChannelHandlerContext ctx);
    }
  • AbstractIdleTask内部类设计用来单例模式,模板模式UML如下
    请添加图片描述

  • 模板方法的通用流程:当通道关闭,就不执行任务,反之则执行抽象方法run方法,

读事件task分析
  • 先看ReaderIdleTimeoutTask 方法中的run方法的实现
 private final class ReaderIdleTimeoutTask extends AbstractIdleTask {

        ReaderIdleTimeoutTask(ChannelHandlerContext ctx) {
            super(ctx);
        }

        @Override
        protected void run(ChannelHandlerContext ctx) {
            long nextDelay = readerIdleTimeNanos;
            if (!reading) {
                nextDelay -= ticksInNanos() - lastReadTime;
            }

            if (nextDelay <= 0) {
                // Reader is idle - set a new timeout and notify the callback.
                readerIdleTimeout = schedule(ctx, this, readerIdleTimeNanos, TimeUnit.NANOSECONDS);

                boolean first = firstReaderIdleEvent;
                firstReaderIdleEvent = false;

                try {
                    IdleStateEvent event = newIdleStateEvent(IdleState.READER_IDLE, first);
                    channelIdle(ctx, event);
                } catch (Throwable t) {
                    ctx.fireExceptionCaught(t);
                }
            } else {
                // Read occurred before the timeout - set a new timeout with shorter delay.
                readerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
            }
        }
    }
@Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (readerIdleTimeNanos > 0 || allIdleTimeNanos > 0) {
            reading = true;
            firstReaderIdleEvent = firstAllIdleEvent = true;
        }
        ctx.fireChannelRead(msg);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        if ((readerIdleTimeNanos > 0 || allIdleTimeNanos > 0) && reading) {
            lastReadTime = ticksInNanos();
            reading = false;
        }
        ctx.fireChannelReadComplete();
    }
  • readerIdleTimeNanos 是我们在addLast方法的时候自定义的事件

  • reading 属性值在channelRead,channelReacComplete方法中设置,我们之前源码分析中已经解析过

    • 当读事件触发时候,首先会执行channelRead方法,此时我们将reading = true,并且设置firstReaderIdleEvent,firstAllIdleEvent第一次读,第一次读/写 事件标志设置为true
    • 当读事件完成时候,会执行channelReadComplete,此时我们将reading =false,并且设置最后一次读的时间lastReadTime
  • 我们在第二步骤中,读取操作结束,用当前时间ticksInNanos() 减去 最后一次读的时间lastReadTime,如果 < 0,就出发事件,反之,重新放入队列,并且间隔时间是重新计算后的时间

  • 事件触发逻辑:

    • 将任务再次放入队列,并且时间重置为我们最初设置的时间,也就是在一次监听读事件,此处返回的是一个promise,此处需要解释如下图类结构,schedule 方法返回的是ScheduledFuture 是一个接口,他的一个实现类是ScheduledFutureTask,这个实现类是promise的一个子类,因此此处返回的promise作用是做取消草足,
    • 接着设置first = firstReaderIdleEvent;,并且将firstReaderIdleEvent设置为false,也就是定义第一次的读事件结束了
    • 创建一个IdleStateEvent,是一个枚举,并且将对象传递给UserEventTriggered,
    • UserEventTriggered 就是我们自己实现的方法,这样就将事件传递到我们自己的方法,我们可以安返回事件类型做对应业务处理。

在这里插入图片描述

写事件task分析
  • 写事件的run方法WriterIdleTimeoutTask, 和读事件的差不多,如下有一点区别
 private final class WriterIdleTimeoutTask extends AbstractIdleTask {

        WriterIdleTimeoutTask(ChannelHandlerContext ctx) {
            super(ctx);
        }

        @Override
        protected void run(ChannelHandlerContext ctx) {

            long lastWriteTime = IdleStateHandler.this.lastWriteTime;
            long nextDelay = writerIdleTimeNanos - (ticksInNanos() - lastWriteTime);
            if (nextDelay <= 0) {
                // Writer is idle - set a new timeout and notify the callback.
                writerIdleTimeout = schedule(ctx, this, writerIdleTimeNanos, TimeUnit.NANOSECONDS);

                boolean first = firstWriterIdleEvent;
                firstWriterIdleEvent = false;

                try {
                    if (hasOutputChanged(ctx, first)) {
                        return;
                    }

                    IdleStateEvent event = newIdleStateEvent(IdleState.WRITER_IDLE, first);
                    channelIdle(ctx, event);
                } catch (Throwable t) {
                    ctx.fireExceptionCaught(t);
                }
            } else {
                // Write occurred before the timeout - set a new timeout with shorter delay.
                writerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
            }
        }
    }
  • 不通地方在于,run代码中多了一个判断hasOutputChanged 用来判断出站事件处理是否很慢,例如,我一个出站需要写的数据量大,需要处理20秒,但是我们执行写事件监测的时间是 5秒,此时我们只有第一次会触发,因为 hasOutputChanged 中有判断first 的时候还是会执行异常抛出
所有事件(读/写)事件task分析
  • 与写事件基本一致,不通地方在于事件判断
  • nextDelay -= ticksInNanos() - Math.max(lastReadTime, lastWriteTime); 当前时间 - 最后一次写/读 的时间 如果> 0说明超时了
  • 此处的时间判断是取读,写事件中的最大值,然后像写事件一样,判断是否发生了写很慢的情况
Netty心跳机制总结
  • IdleStateHandler实现心跳检测功能,当服务器和客户端没有任务读写,并且超过设置事件,会触发handler的userEentTriggered方法,用户可以在这个方法中实现自己的逻辑
  • IdleStateHandler的实现基于EventLoop的定时任务,每次读写都会记录一个最后读/写事件,定时任务执行的时候,根据最后读写事件与间隔时间的差值来判断是否执行
  • 内部有3 个定时任务,分别对应读,写,读/写事件,通常我们监听读/写事件就足够
  • IdleStateHandler 内部考虑了极端情况,例如客户端接收缓慢问题,一次接收数据的事件超过了我们设置的最长等待时间。Netty通过构造方法中 observeOutput 属性来决定是否对出站缓冲区的情况进行判断
  • 如果observeOutput = true情况,并且出现出站缓慢,Netty将不认为是空闲,也不会执行定时任务,除非是第一次写或者读/写事件,第一次无论如何也会触发,因为第一次无法判断是出站缓慢还是空闲,
  • 出站缓慢可能造成OOM,所以当我们应用出现OOM之类,并且写空闲极少发生,使用了observeOutput = true,那么可能需要注意是不是出站速度过慢导致的(源码中注释)
 // We can take this shortcut if the ChannelPromises that got passed into write()
            // appear to complete. It indicates "change" on message level and we simply assume
            // that there's change happening on byte level. If the user doesn't observe channel
            // writability events then they'll eventually OOME and there's clearly a different
            // problem and idleness is least of their concerns.
  • 以上都是对IdleStateHandler 源码的分析
  • ReadTimeoutHandler 继承自IdleStateHandler,当触发读空闲,触发 ctx.fireExceptionCaught,并给一个Exception,如下源码
 protected void readTimedOut(ChannelHandlerContext ctx) throws Exception {
        if (!closed) {
            ctx.fireExceptionCaught(ReadTimeoutException.INSTANCE);
            ctx.close();
            closed = true;
        }
    }
  • WriteTimeoutHandler 的实现不是基于IdleStateHandler,继承ChannelOutboundHandlerAdapter,当调用write方法时候,会创建一个定时任务,参数是promise ,定时任务根据promise 的完成情况来判断是否超出写事件,当定时任务发现task.scheduledFuture.isDone() 返回false,表示没写完,超时抛出异常,否则打断定时任务
 public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        if (timeoutNanos > 0) {
            promise = promise.unvoid();
            scheduleTimeout(ctx, promise);
        }
        ctx.write(msg, promise);
    }
 private void scheduleTimeout(final ChannelHandlerContext ctx, final ChannelPromise promise) {
        // Schedule a timeout.
        final WriteTimeoutTask task = new WriteTimeoutTask(ctx, promise);
        task.scheduledFuture = ctx.executor().schedule(task, timeoutNanos, TimeUnit.NANOSECONDS);

        if (!task.scheduledFuture.isDone()) {
            addWriteTimeoutTask(task);

            // Cancel the scheduled timeout if the flush promise is complete.
            promise.addListener(task);
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值