Backend Development 19 min read

Implementing Heartbeat Mechanism and Reconnection Logic with Netty

This article explains how to implement a TCP heartbeat mechanism using Netty's IdleStateHandler, details client and server handler implementations, and demonstrates a robust reconnection strategy with customizable retry policies, providing complete Java code examples for each component.

Top Architect
Top Architect
Top Architect
Implementing Heartbeat Mechanism and Reconnection Logic with Netty

The article introduces the concept of a TCP heartbeat, a periodic packet sent between client and server to keep the connection alive and detect idle links.

In Netty, the core component for heartbeat is IdleStateHandler . Its constructor IdleStateHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds) configures read, write, and all‑idle timeouts, all measured in seconds.

Three parameters are explained:

readerIdleTimeSeconds – triggers a READER_IDLE event when no data is read within the interval.

writerIdleTimeSeconds – triggers a WRITER_IDLE event when no data is written within the interval.

allIdleTimeSeconds – triggers an ALL_IDLE event when neither read nor write occurs.

Client‑side implementation uses a custom ClientIdleStateTrigger that overrides userEventTriggered to send a heartbeat string when a WRITER_IDLE event occurs:

public class ClientIdleStateTrigger extends ChannelInboundHandlerAdapter {
    public static final String HEART_BEAT = "heart beat!";
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleState state = ((IdleStateEvent) evt).state();
            if (state == IdleState.WRITER_IDLE) {
                ctx.writeAndFlush(HEART_BEAT);
            }
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }
}

The Pinger handler schedules random‑interval heartbeat sends, checks channel activity before sending, and reschedules itself on success:

public class Pinger extends ChannelInboundHandlerAdapter {
    private Random random = new Random();
    private int baseRandom = 8;
    private Channel channel;
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        super.channelActive(ctx);
        this.channel = ctx.channel();
        ping(ctx.channel());
    }
    private void ping(Channel channel) {
        int second = Math.max(1, random.nextInt(baseRandom));
        System.out.println("next heart beat will send after " + second + "s.");
        ScheduledFuture
future = channel.eventLoop().schedule(() -> {
            if (channel.isActive()) {
                System.out.println("sending heart beat to the server...");
                channel.writeAndFlush(ClientIdleStateTrigger.HEART_BEAT);
            } else {
                System.err.println("The connection had broken, cancel the task that will send a heart beat.");
                channel.closeFuture();
                throw new RuntimeException();
            }
        }, second, TimeUnit.SECONDS);
        future.addListener(f -> { if (f.isSuccess()) ping(channel); });
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

Server‑side uses ServerIdleStateTrigger to close the connection when no inbound data is received within a configured period (e.g., 5 seconds):

public class ServerIdleStateTrigger extends ChannelInboundHandlerAdapter {
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleState state = ((IdleStateEvent) evt).state();
            if (state == IdleState.READER_IDLE) {
                ctx.disconnect();
            }
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }
}

The server business handler simply logs received messages:

@ChannelHandler.Sharable
public class ServerBizHandler extends SimpleChannelInboundHandler
{
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String data) throws Exception {
        System.out.println("receive data: " + data);
    }
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("Established connection with the remote client.");
        ctx.fireChannelActive();
    }
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("Disconnected with the remote client.");
        ctx.fireChannelInactive();
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

Initializers assemble pipelines. The client initializer now adds ReconnectHandler for automatic reconnection, while the server initializer adds the idle handler and business handler.

public class ClientHandlersInitializer extends ChannelInitializer
{
    private ReconnectHandler reconnectHandler;
    public ClientHandlersInitializer(TcpClient tcpClient) {
        this.reconnectHandler = new ReconnectHandler(tcpClient);
    }
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(reconnectHandler);
        pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
        pipeline.addLast(new LengthFieldPrepender(4));
        pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
        pipeline.addLast(new Pinger());
    }
}

Reconnection logic is encapsulated in RetryPolicy and its default implementation ExponentialBackOffRetry , which determines whether to retry and calculates exponential back‑off sleep times, capping at a maximum.

public interface RetryPolicy {
    boolean allowRetry(int retryCount);
    long getSleepTimeMs(int retryCount);
}

public class ExponentialBackOffRetry implements RetryPolicy {
    private final Random random = new Random();
    private final long baseSleepTimeMs;
    private final int maxRetries;
    private final int maxSleepMs;
    public ExponentialBackOffRetry(int baseSleepTimeMs, int maxRetries, int maxSleepMs) {
        this.baseSleepTimeMs = baseSleepTimeMs;
        this.maxRetries = maxRetries;
        this.maxSleepMs = maxSleepMs;
    }
    @Override
    public boolean allowRetry(int retryCount) { return retryCount < maxRetries; }
    @Override
    public long getSleepTimeMs(int retryCount) {
        if (retryCount < 0) throw new IllegalArgumentException("retries count must greater than 0.");
        long sleepMs = baseSleepTimeMs * Math.max(1, random.nextInt(1 << Math.min(retryCount, 29)));
        return Math.min(sleepMs, maxSleepMs);
    }
}

ReconnectHandler listens for channel inactivity, uses the retry policy to schedule reconnection attempts, and resets the retry counter on successful connection.

public class ReconnectHandler extends ChannelInboundHandlerAdapter {
    private int retries = 0;
    private final TcpClient tcpClient;
    private RetryPolicy retryPolicy;
    public ReconnectHandler(TcpClient tcpClient) { this.tcpClient = tcpClient; }
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("Successfully established a connection to the server.");
        retries = 0;
        ctx.fireChannelActive();
    }
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        if (retries == 0) {
            System.err.println("Lost the TCP connection with the server.");
            ctx.close();
        }
        boolean allow = getRetryPolicy().allowRetry(retries);
        if (allow) {
            long sleep = getRetryPolicy().getSleepTimeMs(retries);
            System.out.println(String.format("Try to reconnect after %dms. Retry count: %d.", sleep, ++retries));
            ctx.channel().eventLoop().schedule(() -> {
                System.out.println("Reconnecting ...");
                tcpClient.connect();
            }, sleep, TimeUnit.MILLISECONDS);
        }
        ctx.fireChannelInactive();
    }
    private RetryPolicy getRetryPolicy() {
        if (retryPolicy == null) retryPolicy = tcpClient.getRetryPolicy();
        return retryPolicy;
    }
}

The TcpClient class now holds a RetryPolicy , creates the bootstrap with the client initializer, and adds a connection listener that triggers channelInactive on failure.

public class TcpClient {
    private String host;
    private int port;
    private Bootstrap bootstrap;
    private RetryPolicy retryPolicy;
    private Channel channel;
    public TcpClient(String host, int port) {
        this(host, port, new ExponentialBackOffRetry(1000, Integer.MAX_VALUE, 60 * 1000));
    }
    public TcpClient(String host, int port, RetryPolicy retryPolicy) {
        this.host = host;
        this.port = port;
        this.retryPolicy = retryPolicy;
        init();
    }
    public void connect() {
        synchronized (bootstrap) {
            ChannelFuture future = bootstrap.connect(host, port);
            future.addListener(getConnectionListener());
            this.channel = future.channel();
        }
    }
    private ChannelFutureListener getConnectionListener() {
        return future -> {
            if (!future.isSuccess()) {
                future.channel().pipeline().fireChannelInactive();
            }
        };
    }
    private void init() {
        EventLoopGroup group = new NioEventLoopGroup();
        bootstrap = new Bootstrap();
        bootstrap.group(group)
                 .channel(NioSocketChannel.class)
                 .handler(new ClientHandlersInitializer(this));
    }
    public RetryPolicy getRetryPolicy() { return retryPolicy; }
    public static void main(String[] args) {
        TcpClient client = new TcpClient("localhost", 2222);
        client.connect();
    }
}

Testing steps are described: start the client alone to observe heartbeat logs, then start the server to see connection establishment and disconnection after missed heartbeats, and finally test reconnection behavior with the exponential back‑off policy.

The article also contains promotional sections encouraging readers to reply with keywords to receive gift packages, but the technical core remains a practical guide for implementing reliable TCP heartbeat and reconnection using Netty.

JavaNettyTCPnetworkingheartbeatreconnection
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.