![CDATA[
Introduction
In a previous Netty demo, we explored basic communication between a client and server. This article introduces heartbeat handling in Netty. Unlike Mina, where a KeepAliveMessageFactory was extended, Netty provides the IdleStateHandler class, which can detect three types of idle states: readerIdleTime, writerIdleTime, and allIdleTime.
These parameters are explained as follows:
readerIdleTime— the maximum period of inactivity during which no data was read from the remote peer.writerIdleTime— the maximum period of inactivity during which no data was written to the remote peer.allIdleTime— the maximum period of inactivity for any I/O operation (read or write).
By adding an IdleStateHandler to the ChannelPipeline, we can react to timeout events. In this demo, the server expects a heartbeat every 5 seconds, so its pipeline includes:
pipeline.addLast(new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));
Because the server tolerates a 5‑second read idle, the client sends a heartbeat every 4 seconds:
pipeline.addLast(new IdleStateHandler(0, 4, 0, TimeUnit.SECONDS));
When an idle state is detected, Netty fires a userEventTriggered event in the pipeline handler. By overriding this method we can implement custom logic, such as closing the connection after several missed heartbeats.
Server Implementation
The server bootstrap is straightforward and sets up the channel initializer.
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class HeartbeatServer {
private static final int PORT = 9090;
private static EventLoopGroup bossGroup = new NioEventLoopGroup();
private static ServerBootstrap bootstrap = new ServerBootstrap();
public static void main(String[] args) throws InterruptedException {
try {
bootstrap.group(bossGroup);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.childHandler(new HeartbeatServerInitializer());
ChannelFuture future = bootstrap.bind(PORT).sync();
System.out.println("Server started on port: " + PORT);
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
}
}
}
Server Channel Initializer
The pipeline is configured with a 5‑second read idle timeout, folllowed by string codecs and the business handler.
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.timeout.IdleStateHandler;
import java.util.concurrent.TimeUnit;
public class HeartbeatServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
pipeline.addLast("handler", new ServerHeartbeatHandler());
}
}
Server Business Handler
This handler counts missed heartbeats and closes the channel after three consecutive read timeouts. It also responds to "PING" messages with "PONG".
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
public class ServerHeartbeatHandler extends ChannelInboundHandlerAdapter {
private int missedHeartbeats = 0;
private int messageCount = 0;
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.READER_IDLE) {
System.out.println("No client heartbeat received for 5 seconds.");
missedHeartbeats++;
if (missedHeartbeats > 2) {
System.out.println("Closing inactive channel.");
ctx.channel().close();
}
}
} else {
super.userEventTriggered(ctx, evt);
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
messageCount++;
System.out.println("Message #" + messageCount + " received from client: " + msg);
String message = (String) msg;
if ("PING".equals(message)) {
ctx.writeAndFlush("PONG");
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
Client Implementation
The client connects to the server and sends an initial message. The heartbeat is drivan by the IdleStateHandler.
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
public class HeartbeatClient {
private static final String HOST = "127.0.0.1";
private static final int PORT = 9090;
private static EventLoopGroup group = new NioEventLoopGroup();
private static Bootstrap bootstrap = new Bootstrap();
private static Channel channel;
public static void main(String[] args) throws Exception {
System.out.println("Client starting...");
bootstrap.group(group);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new HeartbeatClientInitializer());
channel = bootstrap.connect(HOST, PORT).sync().channel();
sendInitialMessage();
}
public static void sendInitialMessage() {
String msg = "Hello Server";
channel.writeAndFlush(msg);
System.out.println("Client sent: " + msg);
}
}
Client Channel Initializer
The client uses a 4‑second write idle timeout to trigger heartbeat sending.
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.timeout.IdleStateHandler;
import java.util.concurrent.TimeUnit;
public class HeartbeatClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IdleStateHandler(0, 4, 0, TimeUnit.SECONDS));
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
pipeline.addLast("handler", new ClientHeartbeatHandler());
}
}
Client Business Handler
On write idle, the client sends a "PING" message (up to three times). It also logs received messages and connection state changes.
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.CharsetUtil;
import java.util.Date;
public class ClientHeartbeatHandler extends ChannelInboundHandlerAdapter {
private static final ByteBuf PING_MSG = Unpooled.unreleasableBuffer(
Unpooled.copiedBuffer("PING", CharsetUtil.UTF_8));
private int heartbeatsSent = 0;
private int messageCount = 0;
private int cycleCount = 0;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("Connection established at: " + new Date());
ctx.fireChannelActive();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("Connection closed at: " + new Date());
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
System.out.println("Heartbeat cycle: " + new Date() + ", count: " + cycleCount);
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.WRITER_IDLE) {
if (heartbeatsSent < 3) {
heartbeatsSent++;
ctx.channel().writeAndFlush(PING_MSG.duplicate());
} else {
System.out.println("Max heartbeats reached, no more PINGs.");
}
cycleCount++;
}
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
messageCount++;
System.out.println("Message #" + messageCount + " received from server: " + msg);
}
}
Example Output
Server Console
Client Console
]]>