前段时间用Netty搭了个Mqtt broker,初步实现了端到端的通信,Mqtt协议是基于发布/订阅(publish/subscribe)这种模型的,客户端之间是不知道彼此的存在,是解耦的,但在有些业务场景中,我们想要两个端知道彼此的存在以及状态,这就需要broker去维护两端的关系。
使用TCP协议层的Keeplive机制,但是该机制默认的心跳时间是2小时,依赖操作系统实现不够灵活;
应用层实现自定义心跳机制,比如Netty实现心跳机制;
Netty中自己有一套单独的心跳机制,靠IdleStateHandler这个类来监测,下面从我的理解出发介绍一下这个类以及心跳机制检测的实现过程。
一、IdleStateHandler的由来
首先来看下IdleStateHandler继承的父类以及实现的接口:
可以看到IdleStateHandler最终实现了ChannelHandler这个接口,这个接口中只定义如下三个方法:
- handlerAdded(ChannelHandlerContext ctx):当一个handler加入的时候调用;
- handlerRemoved(ChannelHandlerContext ctx):当一个handler销毁的时候调用;
- exceptionCaught(ChannelHandlerContext ctx, Throwable cause):当一个handler发生异常的时候调用。
handler可以理解为channel的执行者,发生在channel里的各种事务都由handler去执行,一个客户端连接到broker就会产生一个channel,可以看到三个方法每个都携带了一个参数ChannelHandlerContext
,这个就是理解为发生这些事务的客户端。
ChannelHandler接口只是定义了客户端最基本的三个操作,连接、断开和异常
,更丰富的功能由其子类以及实现类提供。
ChannelHandler接口有两个子类ChannelInboundHandler、ChannelOutboundHandler
,他们分别负责处理入站I/O事件以及出站I/O操作,对应的有三个实现类:
- ChannelInboundHandlerAdapter:实现具体的入站I/O事件逻辑;
- ChannelOutboundHandlerAdapter:实现具体的出战I/O操作逻辑;
- ChannelDuplexHandler:实现具体的入站以及出站I/O事件,算是上面两个的综合体。
从上面的继承关系图可以看到,本文将要着重介绍IdleStateHandler就是继承自ChannelDuplexHandler这个类,说明IdleStateHandler也是具备处理入站以及出站I/O事件的能力。
二、IdleStateHandler的创建及使用
IdleStateHandler有三个构造方法,构造方法里只是做了一些全局变量的赋值。
其余两个构造方法最终都是调用的这个方法,直接看这个,其中有5个参数:
- observeOutput:从字面意思看这个Boolean类型的参数决定是否考虑写空闲状态时的字节消耗,默认是false,具体作用还没用到过,这里先不过多考虑,就按默认值来;
- readerIdleTime:这个值决定,多长时间没有read操作发生,将触发IdleState.READER_IDLE状态,当这个参数值为0时,将一直不会触发;
- writerIdleTime:这个值决定,多长时间没有write操作发生,将触发IdleState.WRITER_IDLE状态,当这个参数值为0时,将一直不触发;
- allIdleTime:这个值决定,多长时间没有read和write操作发生,将触发IdleState.ALL_IDLE状态,当这个参数值为0时,将一直不触发;
- unit:时间单位
关于空闲状态,在IdleState
枚举类中定义了三种,分别是READER_IDLE、WRITER_IDLE、ALL_IDLE
,分别对应读空闲、写空闲、读写都空闲
。
具体使用方法,在这个类的顶部有个例子:
照上图那样,创建IdleStateHandler对象时,传入相关的参数,然后将IdleStateHandler对象放到通道管道中,这样这个管道就实现了心跳机制监测。
三、IdleStateHandler实现的原理
在IdleStateHandler类中有三个和handler连接时有关(入站)的回调,分别是channelActive(ChannelHandlerContext ctx)、channelRegistered(ChannelHandlerContext ctx)、handlerAdded(ChannelHandlerContext ctx)
,其中handlerAdded是ChannelHandler接口中定义的方法,另外两个则是ChannelInboundHandler接口中定义的方法,这三个回调中都调用了initialize
方法:
在这个方法中会过滤掉几个值小于或等于0的情况,所以在构造方法中会有当设置为0一直不会触发的介绍,就是在这里过滤掉了,当三个时间值都大于0的时候,调用了schedule(ChannelHandlerContext ctx, Runnable task, long delay, TimeUnit unit)
方法,一路追踪下去,会发现这个方法的最终实现是将传进来的task加到了一个任务队列中,队列这种数据结构,先进先出的特性保证了任务的顺序执行。
任务加入到了任务队列中,剩下的就是队列开始轮询,拿到任务后根据其delay时间调用其run()
方法开始执行任务,下面我们看看这个task里面实现了什么东西:
这个任务里面创建了一个IdleState.READER_IDLE状态的Event对象,调用channelIdle
方法将这个Event对象传了进去,下面看看channelIdle
方法的具体实现:
这里调用了ChannelHandlerContext的fireUserEventTriggered(final Object event)
方法。
最终可以看到,这个ReaderIdleTimeoutTask任务触发最终调用了ChannelInboundHandler接口中的userEventTriggered(ChannelHandlerContext ctx, Object evt)
方法,所以我们的childHandler只要实现了ChannelInboundHandler这个接口,就能拿到事件回调,就像IdleStateHandler类中顶部示例中那样:
到这里我们就理清楚了IdleStateHandler的整个脉络:
1、初始化的时候传入三个时间参数,分别表示读操作超时时间、写操作超时时间、读写操作超时时间;
2、handler连接的时候调用initialize方法,创建对应的计时任务,将任务放进任务队列中;
3、各自的计时任务中创建对应的状态事件,最后调用userEventTriggered方法,将event和ctx传递出去。
这样基于Netty框架搭建的mqtt broker拿到客户端的事件后,就能在某个客户端发生意外断连后,将这一情况告知需要响应的其他客户端。