前言
在我们启动MyServer
之后可以通过TCP
连接到我们的Netty
服务端, 但是如果我们十秒没有发送消息的话就会自动的失去连接
这就是Netty
的心跳机制(keepalive
), 因为我们在进行TCP
连接的时候是占用着服务器资源的, 如果大量连接一直保持着还会造成服务器的宕机
此时此刻心跳机制的作用就体现出来了, 心跳机制能够让服务器及时的释放掉占用的资源
项目来源于本次
源码阅读
活动, 可以通过下面命令来拉取代码
git clone https://github.com/arthur-zhang/netty-study.git 复制代码
TCP
的keep-alive
什么是keepalive
, 简单的来讲, 就是客户端和服务端通过TCP
来连接, 假设: 客户端突然挂掉了, 由于在这之后客户端和服务端的连接不会再有消息传输, 所以服务端永远不会发现客户端已经挂掉了, 连接会一直保持
, 造成资源浪费
所以在TCP
层面引入了keepalive
, 假如某一方在规定时间内没有发送探测包或者网络不通之后就会关闭连接, 避免资源的浪费
keepalive 的三个核心参数
TCP
中的keepalive
核心参数如下所示:
tcp_keepalive_time
:每次发送心跳的周期, 默认为 7200stcp_keepalive_intvl
:探测包的发送间隔, 默认为 75stcp_kepalive_probes
:在一个心跳周期之内没有接收到对方确认, 继续发送探测包的时间间隔, 默认为 9(次)
keepalive
默认是不启动的, 在Netty
中, 我们可以通过ServerBootstrap.option(ChannelOption.SO_KEEPALIVE, true)
来进行设置
有了TCP
层面的keep-alive
为什么还需要应用层keepalive
TCP
的keep-alive
只能保证一段时间内连接的两端有数据相互发送, 但是如果碰到网络不佳的情况, 可能会导致心跳收发不及时而断开连接, 这也就是我们为什么需要应用层的keepalive
, 也就是空闲检测策略
空闲检测策略
: 如果某个连接在规定时间内没有数据流动, 那么就认为该连接是空闲的, 就会将其连接进行关闭. 空闲检测策略在Netty
中为我们提供好了, 接下来我们就对其进行详细的讲解
Netty 的 Idle 检测如何实现, 是用 HashedWheelTimer 时间轮吗
还记得我们在前言
说的吗, 当我们使用TCP
调试工具和Netty
服务端连接之后, 如果十秒内没有消息发送的话就会自动的断开连接, 接下来我们从如何配置讲起
Netty 的空闲检测机制配置
在Netty
中有一个类IdleStateHandler
, 这个类是Netty
团队为我们准备的的检测连接是否处于空闲状态的双向handler
处理器(读和写), 在上篇文章, 我们专门学习过 Netty的六大组件, 其中就有handler
相关介绍, 对handler
陌生的可以看一下
在这个类继承关系图上也可以看到:
- 该类继承了
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
的空闲检测机制有关
设置超时时间的构造方法
直接点进 super()
方法
继续往下走就会进入到下面的这个方法
可以看到最后就是将几个参数对属性进行赋值罢了, 接下来我们直接说这个方法的作用
我们从构造器开始分析, 直接就可以看到, 我们的空闲十秒断开连接
是通过当前类的构造器方法设置的, 两个非0的参数直接肉眼就能看出来了吧, 那么剩下两个参数只能根据名称来判断了
readerIdleTime
: 读空闲时间, 超过规定时间还没有通过连接读取到数据, 那么就 game over 连接断开
- 10肯定是十秒了, 也就是
writerIdleTime
: 写空闲时间, 如果规定时间内没有写数据, 那么连接断开, 为0表示不关心allIdleTime
: 读写空闲时间, 如果规定时间内没有读数据或者写数据则连接断开, 0表示不关心unit
: 肯定就是时间的单位了
TimeUnit.SECONDS
肯定是秒
构造器方法执行结束之后, 这个空闲检测的handler
就创建好了
初始化操作
经过上面的分析, 在构造方法的过程中, 我们就将心跳
给构造好了, 但是我们是在什么时候调用的呢
我们直接去找哪里调用了initialize()
相关方法, 别问为什么是这个方法, 问就是未卜先知
如上图所示, 根据名称和超简单的判断方法可以判断出分别是以下三种情况会调用initialize()
方法, 主要的作用就是保证该初始化方法一定会被触发
- 连接建立时
active
事件触发之前active
事件已触发, 同时连接建立
initialize() 方法
首先可以看到, 这边有个 bug 修复, 即为该类添加了一个标记state
属性:
- 0: 表示已关闭
- 1: 表示已经开始工作
- 2: 表示
handler
已被销毁
在早期的
Netty
中, 如果在任务调度之前关闭了资源会导致NPE
空指针异常
initOutputChanged(ctx)
方法我们下面在看
ticksInNanos()
方法是用来获取纳秒时间的API
然后跟了三个启动定时任务的方法schedule
- 检测读空闲
- 检测写空闲
- 检测所有
在这里, 我们可以直接看到它调用的类
点进上图三个类中随便的一个类就可以看到这三个类是AbstractIdleTask
的实现类, 同时他们都是IdleStateHandler
的内部类
我们直接看ReaderIdleTimeoutTask
的实现run()
方法
首先, 我们看当前类的变量reading
, 可以看到这个变量只有两个地方对其进行了赋值, 我们继续看
这两个地方分别是channelRead()
方法和channelReadComplete()
方法, 这两个方法都是用来判断当前Channel
中是否有数据可读, 如果有reading == true
, 如果没有数据可读之后会更新上一次的开始读取时间, 同时reading == false
所以第一个判断我们解析完了, 如果当前Channel
有数据被读到, reading == true
则下面判断直接进入else
, 重新调度该读空闲检测的定时任务,并更新检测周期
如果当前处于读空闲状态, 继续判断是否在指定空闲检测的时间内触发读空闲时间(nextDelay <= 0
)
若满足上述条件, 则进入下述流程
schedule()
方法是一个回调方法, 直接略过
我们会指定到 try 内的newIdleStateEvent()
方法, 创建一个新的事件, 最后通过channelIdle(ctx, event);
方法将该事件对象回调通知给用户
initOutputChanged(ctx);
这个方法是Netty
对IdleStateHandler
写空闲检测的优化, 默认为 false
点进这个属性查看调用情况, 可以看到只有一处调用是给该属性进行赋值操作的
兜兜转转, 最后又回到了构造器方法, 有没有感觉到熟悉. 我们最开始就调用了该方法
在调用该方法的时候我们默认给这个值传递了 false
该方法暂时就分析到这里, 对本次流程并不重要