基于Netty的IdleStateHandler实现Mqtt心跳
基于Netty的IdleStateHandler实现Mqtt心跳
IdleStateHandler
解析
最近研究jetlinks
编写的基于Netty
的mqtt-client
(https://github.com/jetlinks/netty-mqtt-client),总结若干知识点.
Netty
中,实现心跳机制较为简单,主要依赖于IdleStateHandler
判断channel
的读写超时.
/**
* Creates a new instance firing {@link IdleStateEvent}s.
*
* @param readerIdleTimeSeconds
* an {@link IdleStateEvent} whose state is {@link IdleState#READER_IDLE}
* will be triggered when no read was performed for the specified
* period of time. Specify {@code 0} to disable.
* @param writerIdleTimeSeconds
* an {@link IdleStateEvent} whose state is {@link IdleState#WRITER_IDLE}
* will be triggered when no write was performed for the specified
* period of time. Specify {@code 0} to disable.
* @param allIdleTimeSeconds
* an {@link IdleStateEvent} whose state is {@link IdleState#ALL_IDLE}
* will be triggered when neither read nor write was performed for
* the specified period of time. Specify {@code 0} to disable.
*/
public IdleStateHandler(
int readerIdleTimeSeconds,
int writerIdleTimeSeconds,
int allIdleTimeSeconds) {
this(readerIdleTimeSeconds, writerIdleTimeSeconds, allIdleTimeSeconds,
TimeUnit.SECONDS);
}
以上是IdleStateHandler
的构造函数,主要依赖于三个参数readerIdleTimeSeconds
,writerIdleTimeSeconds
以及allIdleTimeSeconds
.
如果难于理解英文注释,可参考<<浅析 Netty 实现心跳机制与断线重连>>https://segmentfault.com/a/1190000006931568一文中的解释:
- readerIdleTimeSeconds, 读超时. 即当在指定的时间间隔内没有从 Channel 读取到数据时, 会触发一个 READER_IDLE 的 IdleStateEvent 事件.
- writerIdleTimeSeconds, 写超时. 即当在指定的时间间隔内没有数据写入到 Channel 时, 会触发一个 WRITER_IDLE 的 IdleStateEvent 事件.
- allIdleTimeSeconds, 读/写超时. 即当在指定的时间间隔内没有读或写操作时, 会触发一个 ALL_IDLE 的 IdleStateEvent 事件.
在IdleStateHandler
中,分别通过如下函数实现对channel
读写操作事件的跟踪:
@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();
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
// Allow writing with void promise if handler is only configured for read timeout events.
if (writerIdleTimeNanos > 0 || allIdleTimeNanos > 0) {
ctx.write(msg, promise.unvoid()).addListener(writeListener);
} else {
ctx.write(msg, promise);
}
}
// Not create a new ChannelFutureListener per write operation to reduce GC pressure.
private final ChannelFutureListener writeListener = new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
lastWriteTime = ticksInNanos();
firstWriterIdleEvent = firstAllIdleEvent = true;
}
};
其中:
channelRead
: 判断channel
是否有数据可读取;channelReadComplete
: 判断channel
是否有数据可读取;write
: 判断channel
是否有数据写(通过writeListener
判断当前写操作是否执行成功).
IdleStateHandler
在channel
激活或注册时,会执行initialize
函数,根据读写超时时间创建对应的定时任务.
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
// Initialize early if channel is active already.
if (ctx.channel().isActive()) {
initialize(ctx);
}
super.channelRegistered(ctx);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// This method will be invoked only if this handler was added
// before channelActive() event is fired. If a user adds this handler
// after the channelActive() event, initialize() will be called by beforeAdd().
initialize(ctx);
super.channelActive(ctx);
}
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);
}
}
此处,我们将剖析AllIdleTimeoutTask
任务.
此任务,会判断在超时时间段内,是否有读写操作:
- 有读或者写操作,则重新创建定时任务,等待下次执行;
- 没有读或者写操作,则创建
IdleStateEvent
对象,通过ChannelHandlerContext
通知注册了用户事件触发器的handler
(即handler
重载了userEventTriggered
函数).
private final class AllIdleTimeoutTask extends AbstractIdleTask {
AllIdleTimeoutTask(ChannelHandlerContext ctx) {
super(ctx);
}
@Override
protected void run(ChannelHandlerContext ctx) {
long nextDelay = allIdleTimeNanos;
if (!reading) {
nextDelay -= ticksInNanos() - Math.max(lastReadTime, lastWriteTime);
}
if (nextDelay <= 0) {
// Both reader and writer are idle - set a new timeout and
// notify the callback.
allIdleTimeout = schedule(ctx, this, allIdleTimeNanos, TimeUnit.NANOSECONDS);
boolean first = firstAllIdleEvent;
firstAllIdleEvent = false;
try {
if (hasOutputChanged(ctx, first)) {
return;
}
IdleStateEvent event = newIdleStateEvent(IdleState.ALL_IDLE, first);
channelIdle(ctx, event);
} catch (Throwable t) {
ctx.fireExceptionCaught(t);
}
} else {
// Either read or write occurred before the timeout - set a new
// timeout with shorter delay.
allIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
}
}
}
了解了IdleStateHandler
,我们接下来学习如何编写Mqtt
的心跳handler
.
Mqtt
心跳handler
以下是jetlinks
编写的Mqtt
心跳handler
代码,我们截取部分代码学习.
final class MqttPingHandler extends ChannelInboundHandlerAdapter {
private final int keepaliveSeconds;
private ScheduledFuture<?> pingRespTimeout;
MqttPingHandler(int keepaliveSeconds) {
this.keepaliveSeconds = keepaliveSeconds;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (!(msg instanceof MqttMessage)) {
ctx.fireChannelRead(msg);
return;
}
MqttMessage message = (MqttMessage) msg;
if (message.fixedHeader().messageType() == MqttMessageType.PINGREQ) {
this.handlePingReq(ctx.channel());
} else if (message.fixedHeader().messageType() == MqttMessageType.PINGRESP) {
this.handlePingResp();
} else {
ctx.fireChannelRead(ReferenceCountUtil.retain(msg));
}
}
/**
* IdleStateHandler,在连接处于idle状态超过设定时间后,会发送IdleStateEvent
* 接收到IdleStateEvent,当前类会发送心跳包至server,保持连接
*
* @param ctx 上下文
* @param evt 事件
* @throws Exception 异常
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
super.userEventTriggered(ctx, evt);
// 确认监听事件为IdleStateEvent,即发送心跳包至server
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.WRITER_IDLE) {
this.sendPingReq(ctx.channel());
}
}
}
/**
* 发送心跳包至server端,并建立心跳超时断开连接任务
* 此处,先行创建心跳超时任务,后续再发送心跳包(避免收到心跳响应时,心跳超时任务未建立完成)
*
* @param channel 连接
*/
private void sendPingReq(Channel channel) {
// 创建心跳超时,断开连接任务
if (this.pingRespTimeout == null) {
this.pingRespTimeout = channel.eventLoop().schedule(() -> {
MqttFixedHeader disconnectHeader =
new MqttFixedHeader(MqttMessageType.DISCONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0);
channel.writeAndFlush(new MqttMessage(disconnectHeader)).addListener(ChannelFutureListener.CLOSE);
//TODO: what do when the connection is closed ?
}, this.keepaliveSeconds, TimeUnit.SECONDS);
}
// 创建心跳包,并发送至Mqtts Server
MqttFixedHeader pingHeader = new MqttFixedHeader(MqttMessageType.PINGREQ, false, MqttQoS.AT_MOST_ONCE, false, 0);
channel.writeAndFlush(new MqttMessage(pingHeader));
}
/**
* 处理ping resp,取消ping超时任务(断开连接)
*/
private void handlePingResp() {
if (this.pingRespTimeout != null && !this.pingRespTimeout.isCancelled() && !this.pingRespTimeout.isDone()) {
this.pingRespTimeout.cancel(true);
this.pingRespTimeout = null;
}
}
}
函数解析:
(1) 接收超时事件,发送心跳请求
MqttPingHandler
中重载了userEventTriggered
函数,用以接收ChannelHandlerContext
传递的事件,代码中会判断事件是否为IdleStateEvent
.
如果当前接收事件为IdleStateEvent
,则说明当前channel
在超时时间内未发生读写事件,则客户端发送Mqtt
心跳请求.
(2) 发送心跳请求,建立请求响应超时关闭连接任务
sendPingReq
函数中(以下两步操作,顺序可任意安排):
- 建立心跳请求响应超时判断任务,如果在一定时长内未接收到心跳响应,则会关闭连接;
- 构建
Mqtt
心跳包,发送至远端服务器.
(3) 取消心跳响应超时关闭连接任务
channelRead
读取数据,判断是否是Mqtt
的心跳响应包.
如果是,则执行handlePingResp
函数,取消心跳响应超时关闭连接任务.
handler
添加
ch.pipeline().addLast("idleStateHandler",
new IdleStateHandler(keepAliveTimeSeconds, keepAliveTimeSeconds, 0));
ch.pipeline().addLast("mqttPingHandler",
new MqttPingHandler(MqttClientImpl.this.clientConfig.getKeepAliveTimeSeconds()));
只需要以上两句代码,就可以完成Mqtt
心跳维持功能.
PS:
如果您觉得我的文章对您有帮助,请关注我的微信公众号,谢谢!
基于Netty的IdleStateHandler实现Mqtt心跳的更多相关文章
- 基于netty实现的长连接,心跳机制及重连机制
技术:maven3.0.5 + netty4.1.33 + jdk1.8 概述 Netty是由JBOSS提供的一个java开源框架.Netty提供异步的.事件驱动的网络应用程序框架和工具,用以快速 ...
- 适合新手:从零开发一个IM服务端(基于Netty,有完整源码)
本文由“yuanrw”分享,博客:juejin.im/user/5cefab8451882510eb758606,收录时内容有改动和修订. 0.引言 站长提示:本文适合IM新手阅读,但最好有一定的网络 ...
- 基于netty轻量的高性能分布式RPC服务框架forest<下篇>
基于netty轻量的高性能分布式RPC服务框架forest<上篇> 文章已经简单介绍了forest的快速入门,本文旨在介绍forest用户指南. 基本介绍 Forest是一套基于java开 ...
- 基于Netty与RabbitMQ的消息服务
Netty作为一个高性能的异步网络开发框架,可以作为各种服务的开发框架. 前段时间的一个项目涉及到硬件设备实时数据的采集,采用Netty作为采集服务的实现框架,同时使用RabbitMQ作为采集服务和各 ...
- 基于Netty的私有协议栈的开发
基于Netty的私有协议栈的开发 书是人类进步的阶梯,每读一本书都使自己得以提升,以前看书都是看了就看了,当时感觉受益匪浅,时间一长就又还回到书本了!所以说,好记性不如烂笔头,以后每次看完一本书都写一 ...
- 开源IM项目-InChat登录接口设计与实现(基于Netty)
- 一个基于netty的websocket聊天demo
这里,仅仅是一个demo,模拟客户基于浏览器咨询卖家问题的场景,但是,这里的demo中,卖家不是人,是基于netty的程序(我就叫你uglyRobot吧),自动回复了客户问的问题. 项目特点如下: 1 ...
- webcat——基于netty的http和websocket框架
代码地址如下:http://www.demodashi.com/demo/12687.html Webcat是一个基于netty的简单.高性能服务端框架,目前提供http和websocket两种协议的 ...
- 基于Netty和SpringBoot实现一个轻量级RPC框架-协议篇
基于Netty和SpringBoot实现一个轻量级RPC框架-协议篇 前提 最近对网络编程方面比较有兴趣,在微服务实践上也用到了相对主流的RPC框架如Spring Cloud Gateway底层也切换 ...
随机推荐
- 如何隐藏WooCommerce Shop Page页面的标题
有时我们不想显示WooCommerce Shop Page页面标题,如下图所示,需要如何操作呢?随ytkah一起来看看吧.在主题function.php文件中添加下面的代码就可以隐藏了 add_fil ...
- matlab的plot3()函数、mesh()函数和surf()函数
1.plot3()函数 例1:绘制一条空间折线. x=[0.2,1.8,2.5]; y=[1.3,2.8,1.1]; z=[0.4,1.2,1.6]; figure(1);plot3(x,y,z); ...
- leetcode752. 打开转盘锁
我们可以将 0000 到 9999 这 10000 状态看成图上的 10000 个节点,两个节点之间存在一条边,当且仅当这两个节点对应的状态只有 1 位不同,且不同的那位相差 1(包括 0 和 9 也 ...
- iptables学习2
Firewall:工作在主机或网络边缘,对进出的报文按事先定义的规则进行检查, 并且由匹配到的规则进行处理的一组硬件或软件,甚至可能是两者的组合 隔离用户访问,只允许访问指定的服务 通过ADSL ...
- 【可视化】Vue基础
作者 | Jeskson 来源 | 达达前端小酒馆 Vue简介 Vue框架,框架的作者,尤雨溪,组件化,快速开发的特点. 生命周期 beforeCreate:组件刚刚被创建 created:组件创建完 ...
- centos6服务启动脚本及开机启动过程
centos6服务启动脚本 centos6的服务启动脚本都放在/etc/rc.d/init.d/下,/etc/init.d/是/etc/rc.d/init.d/的软链接: centos6的服务启动脚本 ...
- 粘包和拆包及Netty解决方案
在RPC框架中,粘包和拆包问题是必须解决一个问题,因为RPC框架中,各个微服务相互之间都是维系了一个TCP长连接,比如dubbo就是一个全双工的长连接.由于微服务往对方发送信息的时候,所有的请求都是使 ...
- Vagrant 安装Oracle19c RAC测试环境的简单学习
1. 学习自网站: https://xiaoyu.blog.csdn.net/article/details/103135158 简单学习了下 能够将oracle RAC开起来了 但是 对后期的维护和 ...
- MonkeyDev安装--逆向开发
MonkeyDev是原有iOS OpenDev的升级,非越狱插件的开发集成神器! 可以使用Xcode开发CaptainHook Tweak.Logos Tweak 和 Command-line Too ...
- Redis(三)数据类型
之前的文章中说了Redis的常见应用场景和特性,在特性章节中也大致说了数据结构契合场景.因为我想在更深入.更全面的学习Redis之前,了解场景和特性,才能在学习时更加全面且理解更透彻: redis的什 ...