【Netty】心跳机制 详解

shadowLogo

在我们学习了很多 中间件 的使用之后,我们会发现:

微服务、分布式 的架构下,注册中心 十分重要
注册中心,基本上都有 心跳机制,以 管理 以及 负载均衡 等功能的实现

那么,可能有没有接触过 分布式 架构 的同学有疑问了:
什么是 心跳机制 呢?

定义:

心跳机制,就是:

长连接 中,客户端服务器 之间、定期地 发送的一种 特殊的数据包,通知对方自己还 在线,以 确保 连接 的 有效性


有时候,也会将 客户端健康情况,通过 心跳数据包 发送给 服务器,以方便 之后的 负载均衡 等操作


Netty 中, 实现心跳机制的关键是 IdleStateHandler

IdleStateHandler:

那么,本人来展示下 使用 IdleStateHandler,实现 心跳机制

首先,本人来展示下 IdleStateHandler构造方法

构造方法:

构造方法
在上图中,我们可以看到:

IdleStateHandler 一共有 3个 构造方法

我们一般使用 四参构造方法

public IdleStateHandler(
            long readerIdleTime, 
            long writerIdleTime, 
            long allIdleTime,
            TimeUnit unit)

现在,本人来讲解下 各个参数意义

参数 意义
readerIdleTime 读超时 时限
即:当在 指定 时间间隔内 没有从 Channel 读取 到数据时, 会触发一个 READER_IDLEIdleStateEvent 事件
writerIdleTime 写超时 时限
即:当在 指定 时间间隔内 没有数据 写入到 Channel 时, 会触发一个 WRITER_IDLEIdleStateEvent 事件
allIdleTime 读/写超时 时限
即:当在 指定 时间间隔内 没有 读或写操作 时, 会触发一个 ALL_IDLEIdleStateEvent 事件
unit 时间单位

我们再来看下 真正实现逻辑构造方法
真实逻辑
我们可以看到:

超时时限 <= 0,则 不对 “0值”项 进行 心跳检测


那么,本人现在来展示下 心跳机制使用

使用展示:

服务端:

package edu.youzg.demo.heartbeat;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;

import java.util.concurrent.TimeUnit;

/**
 * @Author: Youzg
 * @CreateTime: 2021-05-06 17:37
 * @Description: 带你深究Java的本质!
 */
public class NettyServerDemo {

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

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            pipeline.addLast(new StringEncoder());
                            pipeline.addLast(new StringDecoder());
                            pipeline.addLast(new IdleStateHandler(3, 0, 0, TimeUnit.SECONDS));
                            pipeline.addLast(new ServerHandler());
                        }
                    });

            System.out.println("Netty Server start...");
            ChannelFuture channelFuture = bootstrap.bind(9000).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    
    static class ServerHandler extends SimpleChannelInboundHandler<String> {

        private int readIdleTimeCnt = 0;

        @Override
        protected void channelRead0(ChannelHandlerContext channelHandlerContext, String msg) throws Exception {
            System.out.println("客户端发来数据: " + msg);
            if ("Heartbeat Packet".equals(msg)) {
                channelHandlerContext.channel().writeAndFlush("ok");
            } else {
                System.out.println("业务请求处理...");
            }
        }

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

            String eventType = null;
            switch (event.state()) {
                case READER_IDLE:
                    eventType = "读空闲";
                    readIdleTimeCnt++;
                    break;
                case WRITER_IDLE:
                    eventType = "读空闲";
                    break;
                case ALL_IDLE:
                    eventType = "读空闲";
                    break;
            }

            System.out.println("发生 超时事件,事件类型为:[" + eventType + "]");
            if (readIdleTimeCnt >= 3) {
                System.out.println("客户端[读空闲]次数 超过3次,服务端关闭连接...");
                ctx.channel().writeAndFlush("idle close");
                ctx.close();
            }
        }

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

客户端:

package edu.youzg.demo.heartbeat;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.util.Random;

/**
 * @Author: Youzg
 * @CreateTime: 2021-05-06 17:41
 * @Description: 带你深究Java的本质!
 */
public class NettyClientDemo {

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

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            pipeline.addLast(new StringEncoder());
                            pipeline.addLast(new StringDecoder());
                            pipeline.addLast(new ClientHandler());
                        }
                    });

            System.out.println("Netty Client start...");
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 9000).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            group.shutdownGracefully();
        }
    }

    static class ClientHandler extends SimpleChannelInboundHandler<String> {

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
            System.out.println("服务端响应数据 为:" + msg);
            if (msg != null && msg.equals("idle close")) {
                System.out.println("超时次数过多,服务端强制关闭连接!");
                ctx.close();
            }
        }

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            String text = "Heartbeat Packet";
            Random random = new Random();
            int num = random.nextInt(8);
            Thread.sleep(num * 1000);
            ctx.writeAndFlush(text);
        }

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

}

现在,本人来展示下 运行结果

运行结果:

服务端:

服务端


客户端:

客户端


现在,本人来带同学们,深究下 Netty心跳机制核心源码

核心源码:

我们先来看一下 IdleStateHandler继承关系

继承关系:

继承关系
我们可以看到:

IdleStateHandler 既 继承了 ChannelInboundHandlerAdapter类,又 继承了 ChannelOutboundHandler类
因此,我们可以将其 当作一个 ChannelHandler,来处理 入站和出站 消息


接下来,我们来根据 一个Netty连接整个生命周期,来看看具体 是如何 执行的:

首先,我们来看看 channelActive()方法源码

channelActive()方法:

包装
我们可以看到:

channelActive()方法 只是调用了 初始化“心跳任务”initialize()方法

那么,我们 追下去,看看是 如何初始化 的:

初始化“心跳任务” —— initialize()方法:

初始化

我们可以看到:

initialize()方法,主要是 根据 参数值,执行 “定时任务”
并且 参数值 <= 0,则 不会触发 “心跳检测”
(这也照应了 上文 构造方法 的 逻辑)


我们继续跟踪进去,看看是 定时任务真实逻辑

定时任务 逻辑:

由于 三种超时事件,所触发的 定时任务类执行逻辑基本一致

那么,本人在这里,就通过讲解 读超时事件,来讲解下 心跳检测如何执行 的:
run
在上图中,我们基本上,只需要关注 本人标记的 四点

  • nextDelay 存储 下一次 心跳检测当前时刻间隔时间
    因为 当前定时任务执行时间 不一定是 设置的 心跳检测的时间,两个时间 有 偏差
    因此,nextDelay = 用户设置的读超时间隔 - (当前时间 - 上一次“读心跳”时间)
  • ② 如果 超时,则 重新 进行一次 “心跳检测”,防止 “网络抖动”等因素 造成 频繁 创建/销毁 连接 的情况
  • ③ 如果 超时,调用 用户编写的 userEventTriggered()方法 的 逻辑
  • ④ 如果 未超时,根据 步骤①计算结果,进行 下一次的 “心跳检测”

对于上述 第③点,可能有同学会产生疑惑:

为什么 channelIdle()方法 会调用 用户编写的 userEventTriggered()方法 的 逻辑 呢?

那么,我们来看看 channelIdle()方法 的 源码:

userEventTriggered逻辑的调用 —— channelIdle()方法:

channelIdle

我们可以看到:

channelIdle()方法 内部调用了 后续处理器userEventTriggered()方法


最后,本人来 总结Netty心跳机制巧妙实现

总结:

Netty 中,心跳检测 机制,并不是 采用 JDK提供的 Timer类 实现的
一时语塞

而是通过 类似于 递归调用 实现的:

定时
因为 schedule()方法 内部,只是简单地 向 内部线程池 提交 “一次性”延时任务

而 当 本次延时任务 执行期间,又会 提交 新的延时任务
新的定时任务

这样的 “心跳机制” 实现思想,真的让 初学的本人 眼前一亮!

恍然大悟
果然 没有固定的代码,只有死板的思想!

希望看到此处的同学们,也会有所感悟!

posted @ 2021-05-06 20:31  在下右转,有何贵干  阅读(935)  评论(0编辑  收藏  举报