Design and implementation of general interface open platform -- (37) optimization of heartbeat mechanism of message service

Posted by [/Darthus] on Sat, 12 Feb 2022 02:49:48 +0100

In the first part, we optimized the sending time of heartbeat. The client sends heartbeat after successful handshake. It is adjusted to start heartbeat sending after successful login, so as to ensure that the heartbeat mechanism can truly establish and maintain the effectiveness of the logical business message channel between the client and the server.

Let's review the strategy of heartbeat mechanism:
The client sends the heartbeat, and the server responds to the heartbeat. When the client finds that the server does not respond to the heartbeat in time, it considers the channel abnormal and reconnects.

Specifically, the implementation is that the client sends heartbeat to the server every fixed time and frequency. According to the pingsocketframe agreed in the WebSocket protocol, the server will reply to the pangwebsocketframe immediately after receiving it. In case of channel failure or no response from the server, the client will be triggered to read idle.

Our original implementation is to set the timeout of idle reading through the IdleStateHandler processor built in netty

      // Add a read-write channel idle processor. When the idle meets the setting, the userEventTrigger will be triggered and obtained by the next processor
        pipeline.addLast(new IdleStateHandler(config.getReadIdleTimeOut(), 0,
                0, TimeUnit.SECONDS));
       
         // Heartbeat timeout processing
         pipeline.addLast(new HeartbeatTimeoutHandler());

The timeout event is handled by the HeartbeatTimeoutHandler processor implemented by ourselves

/**
 * Heartbeat timeout processor
 * @author wqliu
 * @date 2021-10-2 14:25
 **/
@Slf4j
public class HeartbeatTimeoutHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state().equals(IdleState.READER_IDLE)) {
                log.info("Read idle");
                //Close connection
                ctx.channel().close();

            }
        } else {
            //Non idle event, passed to the next processor
            super.userEventTriggered(ctx, evt);
        }

    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        //Empty connection channel
        MessageClientGlobalHolder.channel=null;
        log.info("The client detects that the channel is invalid and starts reconnection");
        MessageClient messageClient= SpringUtil.getBean(MessageClient.class);
        messageClient.reconnect();

    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("Establish a connection with the server and open the channel!");
        //The parent class method must be called to prevent the channelActive event of other processors from being triggered again
        super.channelActive(ctx);

    }


}

There is no problem with this implementation. In fact, netty also encapsulates a more traversal processor ReadTimeoutHandler. The source code is as follows:

public class ReadTimeoutHandler extends IdleStateHandler {
    private boolean closed;

    /**
     * Creates a new instance.
     *
     * @param timeoutSeconds
     *        read timeout in seconds
     */
    public ReadTimeoutHandler(int timeoutSeconds) {
        this(timeoutSeconds, TimeUnit.SECONDS);
    }

    /**
     * Creates a new instance.
     *
     * @param timeout
     *        read timeout
     * @param unit
     *        the {@link TimeUnit} of {@code timeout}
     */
    public ReadTimeoutHandler(long timeout, TimeUnit unit) {
        super(timeout, 0, 0, unit);
    }

    @Override
    protected final void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
        assert evt.state() == IdleState.READER_IDLE;
        readTimedOut(ctx);
    }

    /**
     * Is called when a read timeout was detected.
     */
    protected void readTimedOut(ChannelHandlerContext ctx) throws Exception {
        if (!closed) {
            ctx.fireExceptionCaught(ReadTimeoutException.INSTANCE);
            ctx.close();
            closed = true;
        }
    }
}

It can be seen that this processor actually inherits the IdleStateHandler we originally used, especially the setting of read timeout, which is actually implemented by calling the constructor of the parent class.

However, the default idle of this processor is to trigger an exception and close the channel, and our design is automatic reconnection. Therefore, we just need to define a class ourselves, inherit from ReadTimeoutHandler, and override readTimedOut method to implement our logic.

/**
 * Custom read timeout processor
 * @author wqliu
 * @date 2022-2-10 8:43
 **/
@Slf4j
public class CustomReadTimeoutHandler extends ReadTimeoutHandler {
    public CustomReadTimeoutHandler(int timeoutSeconds) {
        super(timeoutSeconds);
    }

    @Override
    protected void readTimedOut(ChannelHandlerContext ctx) throws Exception {
        log.info("Read idle");
        //Close connection
        ctx.channel().close();
    }


    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        //Empty connection channel
        MessageClientGlobalHolder.channel=null;
        log.info("The client detects that the channel fails and starts reconnection");
        MessageClient messageClient= SpringUtil.getBean(MessageClient.class);
        messageClient.reconnect();

    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("Establish a connection with the server and open the channel!");
        //The parent class method must be called to prevent the channelActive event of other processors from being triggered again
        super.channelActive(ctx);

    }
}

Then, in the process of processor assembly, you can cancel the previous two processors and only load a new processor to realize the processing of heartbeat timeout.

       // Add a read-write channel idle processor. When the idle meets the setting, the userEventTrigger will be triggered and obtained by the next processor
        // pipeline.addLast(new IdleStateHandler(config.getReadIdleTimeOut(), 0,
        //         0, TimeUnit.SECONDS));
        //
        // //Heartbeat timeout processing
        // pipeline.addLast(new HeartbeatTimeoutHandler());


        // Read timeout processing
        pipeline.addLast(new CustomReadTimeoutHandler(config.getReadIdleTimeOut()));

Similarly, the two processors of the original server can also be replaced by a new processor CustomReadTimeoutHandler.

/**
 * Custom read timeout processor
 * @author wqliu
 * @date 2022-2-10 8:43
 **/
@Slf4j
public class CustomReadTimeoutHandler extends ReadTimeoutHandler {
    public CustomReadTimeoutHandler(int timeoutSeconds) {
        super(timeoutSeconds);
    }

    @Override
    protected void readTimedOut(ChannelHandlerContext ctx) throws Exception {
        //The read idle timeout indicates that the client will no longer send heartbeat and close the connection
        ctx.channel().close();
        log.info("The server detects that the client will no longer send heartbeat and actively closes the connection");
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("Establish a connection with the client and open the channel!");

    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        log.info("The connection with the client fails, and the channel is closed!");
        MessageServerHolder.removeChannel(ctx.channel());

    }
}

Topics: Netty interface