IdleStateHandler 心跳机制源码详解

优质博文:IT-BLOG-CN

一、心跳机制

Netty支持心跳机制,可以检测远程服务端是否存活或者活跃。心跳是在TCP长连接中,客户端和服务端定时向对方发送数据包通知对方自己还在线,保证连接的有效性的一种机制。在服务器和客户端之间一定时间内没有数据交互时, 即处于idle状态时, 客户端或服务器会发送一个特殊的数据包给对方, 当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文, 回应发送方, 即一个PING-PONG交互。当某一端收到心跳消息后, 就知道了对方仍然在线, 这就确保TCP连接的有效性。

Netty提供了IdleStateHandler可以对三种类型心跳进行检测,是用来监测连接的空闲情况。然后我们就可以根据心跳情况,来实现具体的处理逻辑,比如说断开连接、重新连接等等。同时,还提供了ReadTimeoutHandlerWriteTimeoutHandler检测连接的有效性。

名称作用
IdleStateHandler当连接的空闲时间(读或者写)太长时,将会触发一个IdleStateEvent事件。然后,你可以通过你的ChannelInboundHandler中重写userEventTrigged方法来处理该事件。
ReadTimeoutHandler如果在指定的事件没有发生读事件,就会抛出这个异常,并自动关闭这个连接。你可以在exceptionCaught方法中处理这个异常。
WriteTimeoutHandler当一个写操作不能在一定的时间内完成时,抛出此异常,并关闭连接。你同样可以在exceptionCaught方法中处理这个异常。

二、IdleStateHandler 简介

IdleStateHandler也是一个ChannelHandler,也需要被载入到ChannelPipeline中,加入我们在服务器端的ChannelInitializer中。我们在channel链中加入了IdleSateHandler,第一个参数是5,单位是秒,那么这样做的意思就是:在服务器端会每隔5秒来检查一下channelRead方法被调用的情况,如果在5秒内该链上的channelRead方法都没有被触发,就会调用userEventTriggered方法:

//创建服务类
ServerBootstrap serverBootstrap = new ServerBootstrap();

//boss和worker
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();

try {
    //设置线程池
    serverBootstrap.group(boss,worker);
    //设置socket工厂,Channel 是对 Java 底层 Socket 连接的抽象
    serverBootstrap.channel(NioServerSocketChannel.class);
    //设置管道工厂
    serverBootstrap.childHandler(new ChannelInitializer<Channel>() {

        @Override
        protected void initChannel(Channel ch) throws Exception {
            //设置后台转换器(二进制转换字符串)
            ch.pipeline().addLast(new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));
            ch.pipeline().addLast(new StringDecoder());
            ch.pipeline().addLast(new StringEncoder());
            ch.pipeline().addLast(new ServerSocketHandler());
        }
    });

并且还是个ChannelInboundHandler,是用来处理入站事件的。看下它的构造器:

public IdleStateHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds) {
    this((long)readerIdleTimeSeconds, (long)writerIdleTimeSeconds, (long)allIdleTimeSeconds, TimeUnit.SECONDS);
}
名称作用
readerIdleTimeSeconds读超时。即当在指定的时间间隔内没有从Channel读取到数据时,会触发一个READER_IDLEIdleStateEvent事件
writerIdleTimeSeconds写超时。即当在指定的时间间隔内没有数据写入到Channel时,会触发一个WRITER_IDLEIdleStateEvent事件
allIdleTimeSeconds读/写超时。即当在指定的时间间隔内没有读或写操作时,会触发一个ALL_IDLEIdleStateEvent事件

三、IdleStateHandler 源码

【1】handlerAddedhandlerRemovedIdleStateHandler是在创建IdleStateHandler实例并添加到ChannelPipeline时添加定时任务来进行定时检测的,具体在initialize(ctx)方法实现;同时在从ChannelPipeline移除或Channel关闭时,移除这个定时检测,具体在destroy()实现。IdleStateHandlerchannelActive()方法在socket通道建立时被触发。

public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
    if (ctx.channel().isActive() && ctx.channel().isRegistered()) {
        this.initialize(ctx);
    }

}

public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
    this.destroy();
}

public void channelInactive(ChannelHandlerContext ctx) throws Exception {
    this.destroy();
    super.channelInactive(ctx);
}

【2】initialize:根据配置的readerIdleTimeWriteIdleTIme等超时事件参数往任务队列taskQueue中添加定时任务task。我先先查看ReaderIdleTimeoutTask的作用。

private void initialize(ChannelHandlerContext ctx) {
    switch(this.state) {
    case 1:
    case 2:
        return;
    default:
        this.state = 1;
        this.initOutputChanged(ctx);
        this.lastReadTime = this.lastWriteTime = this.ticksInNanos();
        if (this.readerIdleTimeNanos > 0L) {
            // 这里的 schedule 方法会调用 eventLoop 的 schedule 方法,将定时任务添加进队列中
            this.readerIdleTimeout = this.schedule(ctx, new IdleStateHandler.ReaderIdleTimeoutTask(ctx), this.readerIdleTimeNanos, TimeUnit.NANOSECONDS);
        }

        if (this.writerIdleTimeNanos > 0L) {
            this.writerIdleTimeout = this.schedule(ctx, new IdleStateHandler.WriterIdleTimeoutTask(ctx), this.writerIdleTimeNanos, TimeUnit.NANOSECONDS);
        }

        if (this.allIdleTimeNanos > 0L) {
            this.allIdleTimeout = this.schedule(ctx, new IdleStateHandler.AllIdleTimeoutTask(ctx), this.allIdleTimeNanos, TimeUnit.NANOSECONDS);
        }

    }
}

定时任务添加到对应线程EventLoopExecutor对应的任务队列taskQueue中,在对应线程的run()方法中循环执行。只要给定的参数大于0,就创建一个定时任务,每个事件都创建。同时,将state状态设置为1,防止重复初始化。调用initOutputChanged方法,初始化监控出站数据属性

private void initOutputChanged(ChannelHandlerContext ctx) {
    if (this.observeOutput) {
        Channel channel = ctx.channel();
        Unsafe unsafe = channel.unsafe();
        ChannelOutboundBuffer buf = unsafe.outboundBuffer();
        // 记录了出站缓冲区相关的数据,buf 对象的 hash 码,和 buf 的剩余缓冲字节数
        if (buf != null) {
            this.lastMessageHashCode = System.identityHashCode(buf.current());
            this.lastPendingWriteBytes = buf.totalPendingWriteBytes();
            this.lastFlushProgress = buf.currentProgress();
        }
    }
}

这边会触发一个ReaderIdleTimeoutTasknextDelay的初始化值为超时秒数readerIdleTimeNanos。如果检测的时候没有正在读,且计算多久没读了,nextDelay -= 当前时间 - 上次读取时间,假如这个结果是6s,说明最后一次调用channelRead已经是6s之前的事情了,你设置的是5s,那么nextDelay则为-1,说明超时了。则创建IdleStateEvent事件,IdleState枚举值为READER_IDLE,然后调用channelIdle方法分发给下一个ChannelInboundHandler,通常由用户自定义一个ChannelInboundHandler来捕获并处理。

private final class ReaderIdleTimeoutTask extends IdleStateHandler.AbstractIdleTask {
    ReaderIdleTimeoutTask(ChannelHandlerContext ctx) {
        super(ctx);
    }

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

        if (nextDelay <= 0L) {
            IdleStateHandler.this.readerIdleTimeout = IdleStateHandler.this.schedule(ctx, this, IdleStateHandler.this.readerIdleTimeNanos, TimeUnit.NANOSECONDS);
            boolean first = IdleStateHandler.this.firstReaderIdleEvent;
            IdleStateHandler.this.firstReaderIdleEvent = false;

            try {
                IdleStateEvent event = IdleStateHandler.this.newIdleStateEvent(IdleState.READER_IDLE, first);
                IdleStateHandler.this.channelIdle(ctx, event);
            } catch (Throwable var6) {
                ctx.fireExceptionCaught(var6);
            }
        } else {
            IdleStateHandler.this.readerIdleTimeout = IdleStateHandler.this.schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
        }

    }
}

firstxxxxIdleEvent作用:假设当你的客户端应用每次接收数据是30秒,而你的写空闲时间是25秒,那么,当你数据还没有写出的时候,写空闲时间触发了。实际上是不合乎逻辑的。因为你的应用根本不空闲。

Netty的解决方案是:记录最后一次输出消息的相关信息,并使用一个值firstXXXXIdleEvent表示是否再次活动过,每次读写活动都会将对应的first值更新为true,如果是false,说明这段时间没有发生过读写事件。同时如果第一次记录出站的相关数据和第二次得到的出站相关数据不同,则说明数据在缓慢的出站,就不用触发空闲事件。

总的来说,这个字段就是用来对付 “客户端接收数据奇慢无比,慢到比空闲时间还多” 的极端情况。所以,Netty默认是关闭这个字段的。

总的来说,每次读取操作都会记录一个时间,定时任务时间到了,会计算当前时间和最后一次读的时间的间隔,如果间隔超过了设置的时间,就触发·UserEventTriggered`方法:

protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
    ctx.fireUserEventTriggered(evt);
}

【3】写任务的run方法逻辑基本和读任务的逻辑一样,唯一不同的就是有一个针对 出站较慢数据的判断。

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

如果这个方法返回true,就不执行触发事件操作了,即使时间到了。看看该方法实现:

private boolean hasOutputChanged(ChannelHandlerContext ctx, boolean first) {
    if (observeOutput) {
        // 如果最后一次写的时间和上一次记录的时间不一样,说明写操作进行过了,则更新此值
        if (lastChangeCheckTimeStamp != lastWriteTime) {
            lastChangeCheckTimeStamp = lastWriteTime;
            // 但如果,在这个方法的调用间隙修改的,就仍然不触发事件
            if (!first) { // #firstWriterIdleEvent or #firstAllIdleEvent
                return true;
            }
        }
        Channel channel = ctx.channel();
        Unsafe unsafe = channel.unsafe();
        ChannelOutboundBuffer buf = unsafe.outboundBuffer();
        // 如果出站区有数据
        if (buf != null) {
            // 拿到出站缓冲区的 对象 hashcode
            int messageHashCode = System.identityHashCode(buf.current());
            // 拿到这个 缓冲区的 所有字节
            long pendingWriteBytes = buf.totalPendingWriteBytes();
            // 如果和之前的不相等,或者字节数不同,说明,输出有变化,将 "最后一个缓冲区引用" 和 “剩余字节数” 刷新
            if (messageHashCode != lastMessageHashCode || pendingWriteBytes != lastPendingWriteBytes) {
                lastMessageHashCode = messageHashCode;
                lastPendingWriteBytes = pendingWriteBytes;
                // 如果写操作没有进行过,则任务写的慢,不触发空闲事件
                if (!first) {
                    return true;
                }
            }
        }
    }
    return false;
}

如果用户没有设置需要观察出站情况。就返回false,继续执行事件。如果设置了观察出站的情况,且最后一次写的时间和上一次记录的时间不一样,说明写操作刚刚做过了,则更新此值,但仍然需要判断这个first的值,如果这个值还是false,说明在这个写事件是在两个方法调用间隙完成的,或者是第一次访问这个方法,就仍然不触发事件。如果不满足上面的条件,就取出缓冲区对象,如果缓冲区没对象了,说明没有发生写的很慢的事件,就触发空闲事件。反之,记录当前缓冲区对象的hashcode和剩余字节数,再和之前的比较,如果任意一个不相等,说明数据在变化,或者说数据在慢慢的写出去。那么就更新这两个值,留在下一次判断。继续判断first,如果是fasle,说明这是第二次调用,就不用触发空闲事件了。

【4】所有事件的run方法:这个类叫做AllIdleTimeoutTask,表示这个监控着所有的事件。当读写事件发生时,都会记录。代码逻辑和写事件的的基本一致,除了这里:

long nextDelay = allIdleTimeNanos;
if (!reading) {
   // 当前时间减去 最后一次写或读 的时间 ,若大于0,说明超时了
   nextDelay -= ticksInNanos() - Math.max(lastReadTime, lastWriteTime);
}

这里的时间计算是取读写事件中的最大值来的。然后像写事件一样,判断是否发生了写的慢的情况。最后调用ctx.fireUserEventTriggered(evt)方法。

通常这个使用的是最多的。构造方法一般是:

pipeline.addLast(new IdleStateHandler(0, 0, 30, TimeUnit.SECONDS));

读写都是0表示禁用,30表示30秒内没有任务读写事件发生,就触发事件。注意,当不是0的时候,这三个任务会重叠。

四、总结

【1】IdleStateHandler心跳检测主要是通过向线程任务队列中添加定时任务,判断channelRead()方法或write()方法是否调用空闲超时,如果超时则触发超时事件执行自定义userEventTrigger()方法;
【2】Netty通过IdleStateHandler实现最常见的心跳机制不是一种双向心跳的PING-PONG模式,而是客户端发送心跳数据包,服务端接收心跳但不回复,因为如果服务端同时有上千个连接,心跳的回复需要消耗大量网络资源;如果服务端一段时间内没有收到客户端的心跳数据包则认为客户端已经下线,将通道关闭避免资源的浪费;在这种心跳模式下服务端可以感知客户端的存活情况,无论是宕机的正常下线还是网络问题的非正常下线,服务端都能感知到,而客户端不能感知到服务端的非正常下线;
【3】要想实现客户端感知服务端的存活情况,需要进行双向的心跳;Netty中的channelInactive()方法是通过Socket连接关闭时挥手数据包触发的,因此可以通过channelInactive()方法感知正常的下线情况,但是因为网络异常等非正常下线则无法感知;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/213830.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++实现DFS、BFS、Kruskal算法和Prim算法、拓扑排序、Dijkstra算法

背景&#xff1a; 实现要求&#xff1a; 根据图的抽象数据类型的定义&#xff0c;请采用邻接矩阵来存储图1&#xff0c;采用邻接表来存储图2&#xff0c;并完成如下操作&#xff1a;对图1无向图进行深度优先遍历和广度优先遍历。对图1无向图采用Kruskal算法和Prim算法得出最小…

<蓝桥杯软件赛>零基础备赛20周--第8周第2讲--排序的应用

报名明年4月蓝桥杯软件赛的同学们&#xff0c;如果你是大一零基础&#xff0c;目前懵懂中&#xff0c;不知该怎么办&#xff0c;可以看看本博客系列&#xff1a;备赛20周合集 20周的完整安排请点击&#xff1a;20周计划 每周发1个博客&#xff0c;共20周&#xff08;读者可以按…

c语言-归并排序

目录 1、归并排序基本思想 2、归并排序的实现&#xff08;递归法&#xff09; 2.1 代码实现递归法归并排序 3、归并排序的实现&#xff08;非递归法&#xff09; 3.1 修正边界问题 3.2 代码实现非递归法归并排序 结语&#xff1a; 前言&#xff1a; 归并排序是一种把数…

万界星空科技灯具行业MES介绍

中国是LED照明产品最大的生产制造国&#xff0c;如今&#xff0c;我国初步形成了包括LED外延片的生产、LED芯片的制备、LED芯片的封装以及LED产品应用在内的较为完超为产业链&#xff0c;随着LED照明市场渗诱率的快速警升&#xff0c;LED下游应用市场将会越来越广阔。这也将推动…

硬件基础:二极管

基本定义 二极管的内部其实就是一个PN结。 把PN结封装起来&#xff0c;两边加上两个电极&#xff0c;就组成了半导体二极管。简称二极管&#xff08;Diode&#xff09; 二极管和PN结一样&#xff0c;具有单向导通性&#xff1a; 外观和正负极 常见芯片封装如下&#xff1a; 一般…

MDETR 论文翻译及理解

题目Abstract1. Introduction2. Method2.1. Background2.2. MDETR2.2.1 Architecture2.2.2 Training 3. Experiments3.1. Pre-training Modulated Detection 预训练调制检测3.2. Downstream Tasks3.2.1 Few-shot transfer for long-tailed detection 4. Related work5. Conclus…

飞行员兄弟

飞行员兄弟 思路&#xff1a; 这里一共有16个格子&#xff0c;如果暴力的话也就是2^16次方种排列组合。 这题和之前的开关不一样&#xff0c;这题是会影响到周围很多格子&#xff0c;而开关那题可以利用上方只改变一个的操作来解题&#xff0c;这题我想到的就是暴搜&#xff…

Prefix-Tuning 论文概述

Prefix-Tuning 论文概述 前缀调优&#xff1a;优化生成的连续提示前言摘要论文十问实验数据集模型实验结论摘要任务泛化性能 前缀调优&#xff1a;优化生成的连续提示 前言 大规模预训练语言模型(PLM)在下游自然语言生成任务中广泛采用fine-tuning的方法进行adaptation。但是f…

Redis 数据结构详解

分类 编程技术 Redis 数据类型分为&#xff1a;字符串类型、散列类型、列表类型、集合类型、有序集合类型。 Redis 这么火&#xff0c;它运行有多块&#xff1f;一台普通的笔记本电脑&#xff0c;可以在1秒钟内完成十万次的读写操作。 原子操作&#xff1a;最小的操作单位&a…

基于Java SSM框架实现实现四六级英语报名系统项目【项目源码+论文说明】

基于java的SSM框架实现四六级英语报名系统演示 摘要 本论文主要论述了如何使用JAVA语言开发一个高校四六级报名管理系统&#xff0c;本系统将严格按照软件开发流程进行各个阶段的工作&#xff0c;采用B/S架构&#xff0c;面向对象编程思想进行项目开发。在引言中&#xff0c;作…

HarmonyOS开发(九):数据管理

1、概述 1.1、功能简介 数据管理为开发者提供数据存储、数据管理能力。 它分为两个部分&#xff1a; 数据存储&#xff1a;提供通用数据持久化能力&#xff0c;根据数据特点&#xff0c;分为用户首选项、键值型数据库和关系型数据库。数据管理&#xff1a;提供高效的数据管…

LeedCode刷题---子数组问题

顾得泉&#xff1a;个人主页 个人专栏&#xff1a;《Linux操作系统》 《C/C》 《LeedCode刷题》 键盘敲烂&#xff0c;年薪百万&#xff01; 一、最大子数组和 题目链接&#xff1a;最大子数组和 题目描述 给你一个整数数组 nums &#xff0c;请你找出一个具有最大和的连…

Microsoft Expression Web - 网页布局

在本章中&#xff0c;我们将介绍网页的基本布局。在创建我们的网页布局之前&#xff0c;我们需要考虑我们的内容&#xff0c;然后设计我们希望如何呈现该内容&#xff0c;因为它是在我们的网站上可见的内容。 由我们如何呈现我们的内容&#xff0c;以便我们的观众找到我们的网…

网络运维与网络安全 学习笔记2023.12.3

网络运维与网络安全 学习笔记 第三十三天 今日目标 目录-文件基本管理、vim文本编辑、用户账号管理 组账号管理、归属控制、权限控制 目录-文件基本管理 ls 列目录及文档属性 ls - List 格式:ls[选项]…[目录或文件路径] 1.如果不以/开始,表示相对路径(省略了当前所在位置…

不得不说,HelpLook真的是一个很懂用户的文档管理工具

在当今互联网时代&#xff0c;信息的爆炸性增长使得有效管理和组织文档变得至关重要。随着企业规模的扩大和团队协作的增加&#xff0c;如何高效地存储、共享和访问关键知识和文档成为了一个难题。不过&#xff0c;我早之前有幸发现&#xff0c;HelpLook这个文档工具是真正懂得…

【计算机视觉】基于OpenCV计算机视觉的摄像头测距技术设计与实现

基于计算机视觉的摄像头测距技术 文章目录 基于计算机视觉的摄像头测距技术导读引入技术实现原理技术实现细节Python-opencv实现方案获取目标轮廓步骤 1&#xff1a;图像处理步骤 2&#xff1a;找到轮廓步骤完整代码 计算图像距离前置技术背景与原理步骤 1&#xff1a;定义距离…

【tensorflow学习-选择动作】 学习tensorflow代码调用过程

a actor.choose_action(s) def choose_action(self, s):s s[np.newaxis, :]return self.sess.run(self.action, {self.s: s}) # get probabilities for all actions输入&#xff1a;s 输出&#xff1a;self.sess.run(self.action, {self.s: s}) &#xff1a;a

【云原生Prometheus篇】Prometheus PromQL语句详解 1.0

文章目录 一、前言1.1 Prometheus的时间序列1.1.1 指标名称1.1.2 标签1.1.3 使用的注意事项 1.2 样本数据格式1.3 Prometheus 的聚合函数 二 、PromQL 理论部分2.1 PromQL简介2.2 PromQL的数据类型2.3 时间序列选择器2.3.1 瞬时向量选择器 &#xff08;Instant Vector Selector…

React使用TailwindCSS

React中使用TailwindCSS TailwindCSS是 下载及初始化 可以查看官网对照自己使用的框架进行配置 npm install -D tailwindcss postcss autoprefixer下载完毕后执行如下命令 npx tailwindcss init -p可以发现项目中多了两个文件 其中默认已经进行了配置&#xff0c;我们需要将…

JSP格式化标签 parseNumber指定格式字符串转数字类型

好 我们继续来说格式化标签 parseNumber 它的作用是讲一个字符串 转换为指定格式的数值型 老实说 这东西 作为了解把 实际开发中都不是用得少 我建议还是在java端就处理好 不建议在jsp中高这种类型转换的操作 基本格式如下 这几个属性都是我们这几期jsp标签的老朋友了 我们…