Netty「源码分析」之 Idle 检测

1,335 阅读6分钟

「我正在参与掘金会员专属活动-源码共读第一期,点击参与

前言

在我们启动MyServer之后可以通过TCP连接到我们的Netty服务端, 但是如果我们十秒没有发送消息的话就会自动的失去连接

image.png

这就是Netty的心跳机制(keepalive), 因为我们在进行TCP连接的时候是占用着服务器资源的, 如果大量连接一直保持着还会造成服务器的宕机

此时此刻心跳机制的作用就体现出来了, 心跳机制能够让服务器及时的释放掉占用的资源

项目来源于本次源码阅读活动, 可以通过下面命令来拉取代码

git clone https://github.com/arthur-zhang/netty-study.git

TCPkeep-alive

什么是keepalive, 简单的来讲, 就是客户端和服务端通过TCP来连接, 假设: 客户端突然挂掉了, 由于在这之后客户端和服务端的连接不会再有消息传输, 所以服务端永远不会发现客户端已经挂掉了, 连接会一直保持 , 造成资源浪费

所以在TCP层面引入了keepalive, 假如某一方在规定时间内没有发送探测包或者网络不通之后就会关闭连接, 避免资源的浪费

keepalive 的三个核心参数

TCP中的keepalive核心参数如下所示:

  • tcp_keepalive_time:每次发送心跳的周期, 默认为 7200s
  • tcp_keepalive_intvl:探测包的发送间隔, 默认为 75s
  • tcp_kepalive_probes:在一个心跳周期之内没有接收到对方确认, 继续发送探测包的时间间隔, 默认为 9(次)

keepalive默认是不启动的, 在Netty中, 我们可以通过ServerBootstrap.option(ChannelOption.SO_KEEPALIVE, true)来进行设置

image.png

有了TCP层面的keep-alive为什么还需要应用层keepalive

TCPkeep-alive只能保证一段时间内连接的两端有数据相互发送, 但是如果碰到网络不佳的情况, 可能会导致心跳收发不及时而断开连接, 这也就是我们为什么需要应用层的keepalive, 也就是空闲检测策略

空闲检测策略: 如果某个连接在规定时间内没有数据流动, 那么就认为该连接是空闲的, 就会将其连接进行关闭. 空闲检测策略在Netty中为我们提供好了, 接下来我们就对其进行详细的讲解

Netty 的 Idle 检测如何实现, 是用 HashedWheelTimer 时间轮吗

还记得我们在前言说的吗, 当我们使用TCP调试工具和Netty服务端连接之后, 如果十秒内没有消息发送的话就会自动的断开连接, 接下来我们从如何配置讲起

Netty 的空闲检测机制配置

Netty中有一个类IdleStateHandler, 这个类是Netty团队为我们准备的的检测连接是否处于空闲状态的双向handler处理器(读和写), 在上篇文章, 我们专门学习过 Netty的六大组件, 其中就有handler相关介绍, 对handler陌生的可以看一下

image.png

在这个类继承关系图上也可以看到:

  • 该类继承了ChannelDuplexHandler
  • ChannelDuplexHandler即继承了Inbound的入站类, 也继承了Outbound出站类

IdleStateHandler文件中, Netty团队为我们准备的该类对应的实现案例

分析项目中的ServerIdleCheckHandler

接下来我们分析一下在我们的示例项目中是怎么实现Netty的心跳检测机制的

没拉取代码的, 可以通过下述命令拉取代码

git clone https://github.com/arthur-zhang/netty-study.git

首先, 我们用ServerIdleCheckHandler类来继承了IdleStateHandler类, 实现ServerIdleCheckHandler()方法和channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt)方法

可以看到这两个方法中都带有Idle单词, 也足以证明这两个方法均和Netty的空闲检测机制有关

image.png

设置超时时间的构造方法

image.png

直接点进 super()方法

image.png

继续往下走就会进入到下面的这个方法

image.png

可以看到最后就是将几个参数对属性进行赋值罢了, 接下来我们直接说这个方法的作用

我们从构造器开始分析, 直接就可以看到, 我们的空闲十秒断开连接是通过当前类的构造器方法设置的, 两个非0的参数直接肉眼就能看出来了吧, 那么剩下两个参数只能根据名称来判断了

  • readerIdleTime: 读空闲时间, 超过规定时间还没有通过连接读取到数据, 那么就 game over 连接断开
    • 10肯定是十秒了, 也就是
  • writerIdleTime: 写空闲时间, 如果规定时间内没有写数据, 那么连接断开, 为0表示不关心
  • allIdleTime: 读写空闲时间, 如果规定时间内没有读数据或者写数据则连接断开, 0表示不关心
  • unit: 肯定就是时间的单位了
    • TimeUnit.SECONDS肯定是秒

构造器方法执行结束之后, 这个空闲检测的handler就创建好了

初始化操作

经过上面的分析, 在构造方法的过程中, 我们就将心跳给构造好了, 但是我们是在什么时候调用的呢

我们直接去找哪里调用了initialize()相关方法, 别问为什么是这个方法, 问就是未卜先知

image.png

如上图所示, 根据名称和超简单的判断方法可以判断出分别是以下三种情况会调用initialize()方法, 主要的作用就是保证该初始化方法一定会被触发

  • 连接建立时
  • active事件触发之前
  • active事件已触发, 同时连接建立

initialize() 方法

image.png

首先可以看到, 这边有个 bug 修复, 即为该类添加了一个标记state属性:

  • 0: 表示已关闭
  • 1: 表示已经开始工作
  • 2: 表示handler已被销毁

image.png

在早期的Netty中, 如果在任务调度之前关闭了资源会导致NPE空指针异常

initOutputChanged(ctx)方法我们下面在看

ticksInNanos()方法是用来获取纳秒时间的API

image.png

然后跟了三个启动定时任务的方法schedule

  • 检测读空闲
  • 检测写空闲
  • 检测所有

在这里, 我们可以直接看到它调用的类

image.png

点进上图三个类中随便的一个类就可以看到这三个类是AbstractIdleTask的实现类, 同时他们都是IdleStateHandler的内部类

image.png

我们直接看ReaderIdleTimeoutTask的实现run()方法

image.png

首先, 我们看当前类的变量reading, 可以看到这个变量只有两个地方对其进行了赋值, 我们继续看

image.png

这两个地方分别是channelRead()方法和channelReadComplete()方法, 这两个方法都是用来判断当前Channel中是否有数据可读, 如果有reading == true, 如果没有数据可读之后会更新上一次的开始读取时间, 同时reading == false

image.png

所以第一个判断我们解析完了, 如果当前Channel有数据被读到, reading == true则下面判断直接进入else, 重新调度该读空闲检测的定时任务,并更新检测周期

image.png

如果当前处于读空闲状态, 继续判断是否在指定空闲检测的时间内触发读空闲时间(nextDelay <= 0)

若满足上述条件, 则进入下述流程

image.png

schedule()方法是一个回调方法, 直接略过

我们会指定到 try 内的newIdleStateEvent()方法, 创建一个新的事件, 最后通过channelIdle(ctx, event);方法将该事件对象回调通知给用户

initOutputChanged(ctx);

这个方法是NettyIdleStateHandler写空闲检测的优化, 默认为 false

image.png

点进这个属性查看调用情况, 可以看到只有一处调用是给该属性进行赋值操作的

image.png

兜兜转转, 最后又回到了构造器方法, 有没有感觉到熟悉. 我们最开始就调用了该方法

image.png

在调用该方法的时候我们默认给这个值传递了 false

image.png

该方法暂时就分析到这里, 对本次流程并不重要




本文内容到此结束了

如有收获欢迎点赞👍收藏💖关注✔️,您的鼓励是我最大的动力。

如有错误❌疑问💬欢迎各位大佬指出。

我是 宁轩 , 我们下次再见