Implementation of Mqtt Heart Rate Based on Netty IdleStateHandler

Keywords: Java Netty github

IdleStateHandler parsing

Recent research on Netty-based mqtt-client written by jetlinks( https://github.com/jetlinks/netty-mqtt-client Summarize some knowledge points.
In Netty, the realization of heartbeat mechanism is relatively simple, which mainly depends on IdleStateHandler's judgment of channel's read-write timeout.

    /**
     * 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);
    }

These are the constructors of IdleStateHandler, which depend on three parameters: reader IdleTime Seconds, writer IdleTime Seconds and allIdleTime Seconds.

If it is difficult to understand English annotations, please refer to <>. https://segmentfault.com/a/1190000006931568 Explanation in the article:

  • Reader IdleTime Seconds, read timeout. When data is not read from Channel within a specified time interval, a READER_IDLE IdleStateEvent event is triggered.
  • Writer IdleTime Seconds, write timeout. When no data is written to Channel within a specified time interval, a WRITER_IDLE IdleStateEvent event is triggered.
  • All IdleTime Seconds, read/write timeouts. When there is no read or write operation within a specified time interval, an IdleStateEvent event of ALL_IDLE is triggered.

In IdleStateHandler, the following functions are used to track channel read-write events:

    @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;
        }
    };

Among them:

  • channelRead: Determine whether the channel has data to read;
  • channelReadComplete: Determine whether the channel has data to read;
  • Write: Determine whether the channel has data written (through the writeListener to determine whether the current write operation is successful).

IdleStateHandler executes initialize function when channel is activated or registered, and creates corresponding timing tasks based on read-write timeout time.

    @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) {
            // Create read timeout judgment timing tasks
            readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx),
                    readerIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
        if (writerIdleTimeNanos > 0) {
            // Create a write timeout judgment timer task
            writerIdleTimeout = schedule(ctx, new WriterIdleTimeoutTask(ctx),
                    writerIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
        if (allIdleTimeNanos > 0) {
            // Create read-write timeout judgment timeout tasks
            allIdleTimeout = schedule(ctx, new AllIdleTimeoutTask(ctx),
                    allIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
    }

Here, we will dissect the AllIdleTimeoutTask task.
This task determines whether there is a read-write operation during the timeout period:

  • If there is a read or write operation, the timed task is recreated and waited for the next execution.
  • Without a read or write operation, an IdleStateEvent object is created, and the handler registered with the user event trigger is notified through the ChannelHandlerContext (that is, the handler overloads the userEventTriggered function).
  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);
            }
        }
    }

With IdleStateHandler in mind, let's learn how to write Mqtt's heartbeat handler.

Mqtt Heart Rate handler

Following is the Mqtt heartbeat handler code written by jetlinks. We intercept part of the code to learn.

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,IdleStateEvent is sent when the connection is idle beyond the set time
     * Receiving the IdleStateEvent, the current class sends a heartbeat packet to the server to maintain the connection
     *
     * @param ctx context
     * @param evt Event
     * @throws Exception abnormal
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        super.userEventTriggered(ctx, evt);

        // Confirm that the listening event is IdleStateEvent, which sends the heartbeat packet to the server
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state() == IdleState.WRITER_IDLE) {
                this.sendPingReq(ctx.channel());
            }
        }
    }

    /**
     * Send heartbeat packet to server and set up heartbeat timeout disconnection task
     * Here, the heartbeat timeout task is created first, and then the heartbeat packet is sent (to avoid that the heartbeat timeout task is not completed when the heartbeat response is received).
     *
     * @param channel Connect
     */
    private void sendPingReq(Channel channel) {

        // Create heartbeat timeout and disconnect tasks
        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);
        }

        // Create a heartbeat package and send it to Mqtts Server
        MqttFixedHeader pingHeader = new MqttFixedHeader(MqttMessageType.PINGREQ, false, MqttQoS.AT_MOST_ONCE, false, 0);
        channel.writeAndFlush(new MqttMessage(pingHeader));
    }

    /**
     * Deal with ping resp, cancel ping timeout task (disconnect)
     */
    private void handlePingResp() {
        if (this.pingRespTimeout != null && !this.pingRespTimeout.isCancelled() && !this.pingRespTimeout.isDone()) {
            this.pingRespTimeout.cancel(true);
            this.pingRespTimeout = null;
        }
    }
}

Functional analysis:

(1) Receiving timeout events and sending heartbeat requests

MqttPingHandler overloads the userEventTriggered function to receive events passed by ChannelHandlerContext, and the code determines whether the event is an IdleStateEvent.
If the current receiving event is IdleStateEvent, the client sends Mqtt heartbeat request if the current channel does not have a read-write event within the timeout period.

(2) Send heartbeat requests, establish request response timeout closing connection tasks

In the sendPingReq function (the following two steps can be arranged in any order):

  • Establish a heartbeat request response timeout judgment task. If the heartbeat response is not received within a certain period of time, the connection will be closed.
  • Build Mqtt heartbeat packet and send it to remote server.

(3) Cancel heartbeat response timeout closing connection task

channelRead reads the data to determine whether it is Mqtt's heart-beat response package.
If so, the handlePingResp function is executed to cancel the heartbeat response timeout and close the connection task.

handler add

    ch.pipeline().addLast("idleStateHandler",
        new IdleStateHandler(keepAliveTimeSeconds, keepAliveTimeSeconds, 0));
    ch.pipeline().addLast("mqttPingHandler",
        new MqttPingHandler(MqttClientImpl.this.clientConfig.getKeepAliveTimeSeconds()));

The Mqtt heartbeat maintenance function can be completed with only the above two sentences of code.

Posted by NICKKKKK on Fri, 27 Sep 2019 05:41:49 -0700