netty heartbeat mechanism and reconnection

netty heartbeat mechanism and reconnection

1. Theory and overview

1. What is a heartbeat

As the name implies, the so-called  heartbeat is a special data packet periodically sent between the client and the server in a long TCP connection to notify the other party that they are still online to ensure the validity of the TCP connection.

Why introduce the heartbeat mechanism and its principle?

Because of the unreliability of the network, it is possible that while TCP maintains a long connection, some unexpected situations, such as unplugged network cables, sudden power failures, etc., will cause the connection between the server and the client to be interrupted. In these unexpected situations Next, if there happens to be no interaction between the server and the client, then they cannot find that the other party has been disconnected in a short time. In order to solve this problem, we need to introduce a  heartbeat  mechanism. The working principle of the heartbeat mechanism is: on the server When there is no data interaction with the client for a certain period of time, that is, in the idle state, the client or server will send a special data packet to the other party. When the receiver receives this data packet, it will also immediately send a special data packet. The data message responds to the sender , which is a PING-PONG interaction. Naturally, when one end receives the heartbeat message, it knows that the other party is still online, which ensures the validity of the TCP connection

2. Netty realizes the principle of heartbeat

Realized by the processor of IdleStateHandler:

There are three parameters:

  • readerIdleTimeSeconds, read timeout. When no data is read to the channel within the specified time, an IdleStateEvent event of READER_IDLE will be triggered.

  • writerIdleTimeSeconds, write timeout. When no data is written to the channel within the specified time, an IdleStateEvent event of WRITER_IDLE will be triggered.

  • allIdleTimeSeconds, read/write timeout. That is, when there is no read or write operation within the specified time interval, an IdleStateEvent event of ALL_IDLE will be triggered.

For example, we configure on the client:

    p.addLast(new IdleStateHandler(0, 0, 5)); // 5 seconds read and write idle detection.

How does the processor receive the idle state after adding this IdleState? Our custom processor can receive the trigger of the idle state time in the userEventTriggered method.

    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent e = (IdleStateEvent) evt;
            switch (e.state()) {
                case READER_IDLE:  // 读空闲
                    handleReaderIdle(ctx);
                    break;
                case WRITER_IDLE: // 写空闲
                    handleWriterIdle(ctx);
                    break;
                case ALL_IDLE: // 读写空闲
                    handleAllIdle(ctx);
                    break;
                default:
                    break;
            }
        }
    }

3. The implementation process of sending heartbeat in this demo

First of all, the client and server are a simple echo program. The client configures IdleHandler to read and write for 5 seconds. The client sends messages to the server in an infinite loop, and the interval is 0-20 seconds . If the time is greater than 5 seconds If no information is sent, the idel event will be triggered (the client is the All event). At this time, the client sends a ping to the server, and the server returns pong to the client after receiving it.

So how to realize the breakpoint reconnection in this article?

First of all, why there is so much talk about heartbeat detection about breakpoint reconnection, because breakpoint reconnection relies on heartbeat detection to determine whether the connection has been lost. The following describes the method of breakpoint reconnection and how heartbeat monitoring judges the loss of connection and triggers the breakpoint reconnection.

(1) The way to reconnect at breakpoint:

Listener method (the method used in this demo)

/**
	 * 连接的封装 (抽取连接过程方法,节省bootstrap创建的时间)
	 */
	public void doConnect() {
		if (channel != null && channel.isActive()) {
			return;
		}
		ChannelFuture future = bootstrap.connect(address, port);
		future.addListener(new ChannelFutureListener() {
			public void operationComplete(ChannelFuture futureListener) throws Exception {
				if (futureListener.isSuccess()) {
					channel = futureListener.channel();
					System.out.println("Connect to server successfully!");
				} else {
					System.out.println("Failed to connect to server, try connect after 10s");

					futureListener.channel().eventLoop().schedule(new Runnable() {
						@Override
						public void run() {
							// 重连
							doConnect();
						}
					}, 1, TimeUnit.SECONDS);
				}
			}
		});
	}

Reconnect in inacitve

 

public void channelInactive(ChannelHandlerContext ctx) throws Exception {
		System.out.println("断开链接重连" + ctx.channel().localAddress().toString());
		new Thread(new Runnable() {
			@Override
			public void run() {
			    // 重连代码
			}
		}).start();
	}

2) Judgment and trigger reconnection

Because the server configuration above is to read 10 seconds to trigger idle, and the client configures to read and write idle in 5 seconds. If the server has not read any data in 10 seconds, then the client has not pinged twice, this time Initially determine that the client connection is lost, execute ChannelHandlerContext ctx.close(), and the client can trigger a reconnection. (This demo has not made an exception that triggers reconnection for the time being ---If you want to make it, you just need to make the client's ping not send every time)

4. Supplementary message format

[length, type, data] // Type 1 is ping, 2 is pong, and 3 is other messages.

This message can use LengthFieldBasedFrameDecoder to process semi-packet sticky packets.

 

Two, demo code

1. General purpose processor

public abstract class CustomHeartbeatHandler extends SimpleChannelInboundHandler<ByteBuf> {
  
    protected String name;
    private int heartbeatCount = 0;

    public CustomHeartbeatHandler(String name) {
        this.name = name;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext context, ByteBuf byteBuf) throws Exception {
    	// 第4角标是否是ping --服务端接收到ping,需要发送pong
        if (byteBuf.getByte(4) == Consts.PING_MSG) {
            sendPongMsg(context);
        // 客户端可以接收pong
        } else if (byteBuf.getByte(4) == Consts.PONG_MSG){
            System.out.println(name + " get pong msg from " + context.channel().remoteAddress());
        } else {
            handleData(context, byteBuf);
        }
    }

    protected void sendPingMsg(ChannelHandlerContext context) {
        ByteBuf buf = context.alloc().buffer(5);
        buf.writeInt(5);
        buf.writeByte(Consts.PING_MSG);
        context.writeAndFlush(buf);
        heartbeatCount++;
        System.out.println(name + " sent ping msg to " + context.channel().remoteAddress() + ", count: " + heartbeatCount);
    }

    private void sendPongMsg(ChannelHandlerContext context) {
        ByteBuf buf = context.alloc().buffer(5);
        buf.writeInt(5);
        buf.writeByte(Consts.PONG_MSG);
        context.channel().writeAndFlush(buf);
        heartbeatCount++;
        System.out.println(name + " sent pong msg to " + context.channel().remoteAddress() + ", count: " + heartbeatCount);
    }

    protected abstract void handleData(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf);

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent e = (IdleStateEvent) evt;
            switch (e.state()) {
                case READER_IDLE:  // 读空闲
                    handleReaderIdle(ctx);
                    break;
                case WRITER_IDLE: // 写空闲
                    handleWriterIdle(ctx);
                    break;
                case ALL_IDLE: // 读写空闲
                    handleAllIdle(ctx);
                    break;
                default:
                    break;
            }
        }
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.err.println("---" + ctx.channel().remoteAddress() + " is active---");
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.err.println("---" + ctx.channel().remoteAddress() + " is inactive---");
    }

    protected void handleReaderIdle(ChannelHandlerContext ctx) {
        System.err.println("---READER_IDLE---");
    }

    protected void handleWriterIdle(ChannelHandlerContext ctx) {
        System.err.println("---WRITER_IDLE---");
    }

    protected void handleAllIdle(ChannelHandlerContext ctx) {
        System.err.println("---ALL_IDLE---");
    }
}

2. Client

package netty.ping_pong.client;

import java.util.Random;
import java.util.concurrent.TimeUnit;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.timeout.IdleStateHandler;
import netty.ping_pong.domain.Consts;

public class PingClient {
	
	private String address;
	private int port;
	private NioEventLoopGroup workGroup = new NioEventLoopGroup(4);
	private Channel channel;
	private Bootstrap bootstrap;
	
	public PingClient(String address, int port) {
		super();
		this.address = address;
		this.port = port;
	}

	public static void main(String[] args) {
		PingClient client = new PingClient("127.0.0.1", 7000);
		client.start();
	}
	
	/**
	 * 启动
	 */
	public void start() {
		try {
			// 创建
			bootstrap = new Bootstrap();
			bootstrap.group(workGroup).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {
				protected void initChannel(SocketChannel socketChannel) throws Exception {
					ChannelPipeline p = socketChannel.pipeline();
					p.addLast(new IdleStateHandler(0, 0, 5)); // 5 秒读写idle检测
					p.addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, -4, 0));
					p.addLast(new PingHandler());
				}
			});
			
			// 连接和监听重连 
			doConnect();
			
			// 发送数据 -- 间隔时间是0-20秒随机
			Random random = new Random();
			for (int i = 0; i < 10000; i++) {
				if (channel != null && channel.isActive()) {
					String content = "client msg " + i;
					ByteBuf buf = channel.alloc().buffer(5 + content.getBytes().length);
					buf.writeInt(5 + content.getBytes().length);
					buf.writeByte(Consts.CUSTOM_MSG);
					buf.writeBytes(content.getBytes());
					channel.writeAndFlush(buf);
				}
				Thread.sleep(random.nextInt(20000));
			}
		} catch (Exception e) {
            throw new RuntimeException(e);
        }
	}

	
	/**
	 * 连接的封装 (抽取连接过程方法,节省bootstrap创建的时间)
	 */
	public void doConnect() {
		if (channel != null && channel.isActive()) {
			return;
		}
		ChannelFuture future = bootstrap.connect(address, port);
		future.addListener(new ChannelFutureListener() {
			public void operationComplete(ChannelFuture futureListener) throws Exception {
				if (futureListener.isSuccess()) {
					channel = futureListener.channel();
					System.out.println("Connect to server successfully!");
				} else {
					System.out.println("Failed to connect to server, try connect after 10s");

					futureListener.channel().eventLoop().schedule(new Runnable() {
						@Override
						public void run() {
							// 重连
							doConnect();
						}
					}, 10, TimeUnit.SECONDS);
				}
			}
		});
	}
	
	public void close() {
		if(channel!=null) {
			channel.close();
		}
		workGroup.shutdownGracefully();
	}

}
package netty.ping_pong.client;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import netty.ping_pong.server.common.CustomHeartbeatHandler;

public class PingHandler extends CustomHeartbeatHandler{

	public PingHandler() {
		super("client");
	}

	/**正常数据的处理**/
	@Override
	protected void handleData(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) {
		byte[] data = new byte[byteBuf.readableBytes() - 5];
		byteBuf.skipBytes(5);
		byteBuf.readBytes(data);
		String content = new String(data);
		System.out.println(name + " get content: " + content);
	}
	
	/**空闲all触发的时候,进行发送ping数据**/ 
	@Override
	protected void handleAllIdle(ChannelHandlerContext ctx) {
		super.handleAllIdle(ctx);
		sendPingMsg(ctx);
	}

}

3. Server

package netty.ping_pong.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleStateHandler;

public class PingServer {

	public static void main(String[] args) {
		PingServer server = new PingServer();
		server.bind(7000);
	}

	public void bind(int port) {
		EventLoopGroup bossGroup = new NioEventLoopGroup(1);
		EventLoopGroup workerGroup = new NioEventLoopGroup();
		try {
			ServerBootstrap b = new ServerBootstrap();
			b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 100)
					.handler(new LoggingHandler(LogLevel.INFO)).childHandler(new ChannelInitializer<SocketChannel>() {
						@Override
						protected void initChannel(SocketChannel ch) throws Exception {
							ch.pipeline().addLast(new IdleStateHandler(10, 0, 0));
							// lengthAdjustment: 总长-长度字段-长度字段描述 = -4表示长度字段描述就是总长
							ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, -4, 0));
							ch.pipeline().addLast(new PingHandler());
						}
					});

			ChannelFuture future = b.bind(port).sync();
			System.out.println("server start now");
			future.channel().closeFuture().sync();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			bossGroup.shutdownGracefully();
			workerGroup.shutdownGracefully();
		}
	}

}
package netty.ping_pong.server;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import netty.ping_pong.server.common.CustomHeartbeatHandler;

public class PingHandler extends CustomHeartbeatHandler{

	public PingHandler() {
		super("server");
	}

	@Override
	protected void handleData(ChannelHandlerContext channelHandlerContext, ByteBuf buf) {
		byte[] data = new byte[buf.readableBytes() - 5];
		ByteBuf responseBuf = Unpooled.copiedBuffer(buf);
		buf.skipBytes(5);
		buf.readBytes(data);
		String content = new String(data);
		System.out.println(name + " get content: " + content);
		channelHandlerContext.write(responseBuf);
	}

	// 服务端10秒没有读取到数据 (超时),关闭客户端连接。
	@Override
	protected void handleReaderIdle(ChannelHandlerContext ctx) {
		super.handleReaderIdle(ctx);
		System.err.println("---client " + ctx.channel().remoteAddress().toString() + " reader timeout, close it---");
		ctx.close();
	}
	
}

 

 

Guess you like

Origin blog.csdn.net/shuixiou1/article/details/115058019