Netty之心跳检测

版权声明: https://blog.csdn.net/qq_24313635/article/details/81116886

What

 顾名思义, 所谓 心跳, 即在 TCP 长连接中, 客户端和服务器之间定期发送的一种特殊的数据包, 通知对方自己还在线, 以确保 TCP 连接的有效性.

Why

因为网络的不可靠性, 有可能在 TCP 保持长连接的过程中, 由于某些突发情况, 例如网线被拔出, 突然掉电等, 会造成服务器和客户端的连接中断. 在这些突发情况下, 如果恰好服务器和客户端之间没有交互的话, 那么它们是不能在短时间内发现对方已经掉线的. 为了解决这个问题, 我们就需要引入 心跳 机制. 心跳机制的工作原理是: 在服务器和客户端之间一定时间内没有数据交互时, 即处于 idle 状态时, 客户端或服务器会发送一个特殊的数据包给对方, 当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文, 回应发送方, 此即一个 PING-PONG 交互. 自然地, 当某一端收到心跳消息后, 就知道了对方仍然在线, 这就确保 TCP 连接的有效性

How

我们可以通过两种方式实现心跳机制:

  • 使用 TCP 协议层面的 keepalive 机制.

  • 在应用层上实现自定义的心跳机制.

虽然在 TCP 协议层面上, 提供了 keepalive 保活机制, 但是使用它有几个缺点:

  1. 它不是 TCP 的标准协议, 并且是默认关闭的.

  2. TCP keepalive 机制依赖于操作系统的实现, 默认的 keepalive 心跳时间是 两个小时, 并且对 keepalive 的修改需要系统调用(或者修改系统配置), 灵活性不够.

  3. TCP keepalive 与 TCP 协议绑定, 因此如果需要更换为 UDP 协议时, keepalive 机制就失效了.

虽然使用 TCP 层面的 keepalive 机制比自定义的应用层心跳机制节省流量, 但是基于上面的几点缺点, 一般的实践中, 人们大多数都是选择在应用层上实现自定义的心跳.
既然如此, 那么我们就来大致看看在在 Netty 中是怎么实现心跳的吧. 在 Netty 中, 实现心跳机制的关键是 IdleStateHandler, 它可以对一个 Channel 的 读/写设置定时器, 当 Channel 在一定事件间隔内没有数据交互时(即处于 idle 状态), 就会触发指定的事件.

使用netty实现心跳

上面我们提到了, 在 Netty 中, 实现心跳机制的关键是 IdleStateHandler, 那么这个 Handler 如何使用呢? 我们来看看它的构造器: 

public IdleStateHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds) {this((long)readerIdleTimeSeconds, (long)writerIdleTimeSeconds, (long)allIdleTimeSeconds, TimeUnit.SECONDS);}

实例化一个 IdleStateHandler 需要提供三个参数:

  • readerIdleTimeSeconds, 读超时. 即当在指定的时间间隔内没有从 Channel 读取到数据时, 会触发一个 READER_IDLE 的 IdleStateEvent 事件.

  • writerIdleTimeSeconds, 写超时. 即当在指定的时间间隔内没有数据写入到 Channel 时, 会触发一个 WRITER_IDLE 的 IdleStateEvent 事件.

  • allIdleTimeSeconds, 读/写超时. 即当在指定的时间间隔内没有读或写操作时, 会触发一个 ALL_IDLE 的 IdleStateEvent 事件.

为了展示具体的 IdleStateHandler 实现的心跳机制, 下面我们来构造一个具体的EchoServer 的例子, 这个例子的行为如下:

  1. 在这个例子中, 客户端和服务器通过 TCP 长连接进行通信.

  2. TCP 通信的报文格式是:

+--------+-----+---------------+ 
| Length |Type |   Content     |
|   17   |  1  |   "Heartbeat" |
+--------+-----+---------------+
  1. 客户端每隔一个随机的时间后, 向服务器发送消息, 服务器收到消息后, 立即将收到的消息原封不动地回复给客户端.

  2. 若客户端在指定的时间间隔内没有读/写操作, 则客户端会自动向服务器发送一个 PING 心跳.

代码实现

服务端:

1.项目结构,如下图所示

2.pom.xml文件

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.newland</groupId>
	<artifactId>Netty</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>NettyServer</name>
	<url>http://maven.apache.org</url>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.5.2.RELEASE</version>
		<relativePath />
	</parent>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>io.netty</groupId>
			<artifactId>netty-all</artifactId>
			<version>4.1.12.Final</version>
		</dependency>
	</dependencies>
</project>

3.ServerApplication文件

package com.yj.netty;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ServerApplication {
	public static void main(String[] args) {
		SpringApplication.run(ServerApplication.class, args);
	}
}

4.HeartBeatServer文件

package com.yj.netty.server;

import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
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.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.timeout.IdleStateHandler;

@Component
public class HeartBeatServer {

	@Value("${netty.port}")
	private int port;
	
	@Autowired
    private AcceptorIdleStateTrigger idleStateTrigger;

	@PostConstruct
	public void start() throws Exception {
		EventLoopGroup group = new NioEventLoopGroup();
		try {
			ServerBootstrap b = new ServerBootstrap();
			b.group(group).channel(NioServerSocketChannel.class).localAddress(port)
					.childHandler(new ChannelInitializer<Channel>() {
						@Override
						protected void initChannel(Channel ch) throws Exception {
							ch.pipeline().addLast(new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));
                            ch.pipeline().addLast("decoder", new StringDecoder());
                            ch.pipeline().addLast(idleStateTrigger);
                            ch.pipeline().addLast("encoder", new StringEncoder());
						}
					}).option(ChannelOption.SO_BACKLOG, 128).childOption(ChannelOption.SO_KEEPALIVE, true);;
			ChannelFuture f = b.bind().sync();
			System.out.println("Netty服务端启动成功,开始监听端口:" + port);
			f.channel().closeFuture().sync();
		} finally {
			group.shutdownGracefully().sync();
		}
	}
}

5.AcceptorIdleStateTrigger文件

package com.yj.netty.server;

import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.stereotype.Component;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;

@Sharable
@Component
public class AcceptorIdleStateTrigger 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) {
				throw new Exception("状态异常");
			}
		} else {
			super.userEventTriggered(ctx, evt);
		}
	}

	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		System.out.println(getDate() + ",接收到客户端的心跳: " + msg.toString());
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		cause.printStackTrace();
		ctx.close();
	}
	
	private String getDate() {
		Date sysDate = new Date();
		SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String strDate = format.format(sysDate);
		return strDate;
	}
}

6.application.properties文件

server.port=8080

netty.port=8899
netty.host=127.0.0.1

客户端:

1.项目结构,如下图所示

2.pom.xml文件

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.yj</groupId>
	<artifactId>Netty</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>NettyClient</name>
	<url>http://maven.apache.org</url>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.5.2.RELEASE</version>
		<relativePath />
	</parent>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>io.netty</groupId>
			<artifactId>netty-all</artifactId>
			<version>4.1.12.Final</version>
		</dependency>
	</dependencies>
</project>

2.ClientApplication文件

package com.yj.netty;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ClientApplication {
	public static void main(String[] args) {
		SpringApplication.run(ClientApplication.class, args);
	}
}

3.HeartBeatClient文件

package com.yj.netty.client;

import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.HashedWheelTimer;

@Component
public class HeartBeatClient {

	@Value("${netty.host}")
	private String host;

	@Value("${netty.port}")
	private int port;
	
	@Value("${netty.reconnect}")
	private Boolean reconnect;
	
	@Value("${netty.attemptsNum}")
	private int attemptsNum;

	private final HashedWheelTimer timer = new HashedWheelTimer();

	@PostConstruct
	public void connect() throws Exception {
		EventLoopGroup group = new NioEventLoopGroup();
		Bootstrap boot = new Bootstrap();
		boot.group(group).channel(NioSocketChannel.class).handler(new LoggingHandler(LogLevel.INFO));
		final ConnectorIdleStateTrigger connectorIdleStateTrigger = new ConnectorIdleStateTrigger(boot, timer, port, host, reconnect,attemptsNum) {
			public ChannelHandler[] handlers() {
				return new ChannelHandler[] {new IdleStateHandler(0, 4, 0, TimeUnit.SECONDS),
						new StringDecoder(),this,new StringEncoder() };
			}
		};
		ChannelFuture future;
		// 进行连接
		try {
			synchronized (boot) {
				boot.handler(new ChannelInitializer<Channel>() {
					// 初始化channel
					@Override
					protected void initChannel(Channel ch) throws Exception {
						ch.pipeline().addLast(connectorIdleStateTrigger.handlers());
					}
				});
				future = boot.connect(host, port);
			}
			// 以下代码在synchronized同步块外面是安全的
			future.sync();
		} catch (Throwable t) {
			t.printStackTrace();
			throw new Exception("连接失败", t);
		}
	}
}

4.ConnectorIdleStateTrigger文件

package com.yj.netty.client;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.Timeout;
import io.netty.util.Timer;
import io.netty.util.TimerTask;

/**
 * 重连检测狗,当发现当前的链路不稳定关闭之后,进行重连
 */
@Sharable
public abstract class ConnectorIdleStateTrigger extends ChannelInboundHandlerAdapter
		implements TimerTask, ChannelHandlerHolder {

	private static final ByteBuf HEARTBEAT_SEQUENCE = Unpooled
			.unreleasableBuffer(Unpooled.copiedBuffer("Heartbeat", CharsetUtil.UTF_8));

	private int attemptsNum;

	private final Bootstrap bootstrap;

	private final Timer timer;

	private final int port;

	private final String host;

	private volatile boolean reconnect = true;

	private int attempts;

	public ConnectorIdleStateTrigger(Bootstrap bootstrap, Timer timer, int port, String host, boolean reconnect,
			int attemptsNum) {
		this.bootstrap = bootstrap;
		this.timer = timer;
		this.port = port;
		this.host = host;
		this.reconnect = reconnect;
		this.attemptsNum = attemptsNum;
	}

	/**
	 * channel链路每次active的时候,将其连接的次数重置为 0
	 */
	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		System.out.println("当前链路已经激活了,将重连尝试次数重置为0");
		attempts = 0;
		ctx.fireChannelActive();
	}

	@Override
	public void channelInactive(ChannelHandlerContext ctx) throws Exception {
		if (reconnect) {
			if (attempts < attemptsNum) {
				attempts++;
				System.out.println(getDate() + ",进行第" + attempts + "次重连");
				// 重连的间隔时间会越来越长
				int timeout = 2 << attempts;
				timer.newTimeout(this, timeout, TimeUnit.MILLISECONDS);
			}
		}
		ctx.fireChannelInactive();
	}

	@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(HEARTBEAT_SEQUENCE.duplicate());
			}
		} else {
			super.userEventTriggered(ctx, evt);
		}
	}

	public void run(Timeout timeout) throws Exception {
		ChannelFuture future;
		// bootstrap已经初始化好了,只需要将handler填入就可以了
		synchronized (bootstrap) {
			bootstrap.handler(new ChannelInitializer<Channel>() {
				@Override
				protected void initChannel(Channel ch) throws Exception {
					ch.pipeline().addLast(handlers());
				}
			});
			future = bootstrap.connect(host, port);
		}
		// future对象
		future.addListener(new ChannelFutureListener() {

			public void operationComplete(ChannelFuture f) throws Exception {
				boolean succeed = f.isSuccess();

				// 如果重连失败,则调用ChannelInactive方法,再次出发重连事件,一直尝试12次,如果失败则不再重连
				if (!succeed) {
					System.out.println("重连失败");
					f.channel().pipeline().fireChannelInactive();
				} else {
					System.out.println("重连成功");
				}
			}
		});
	}

	private String getDate() {
		Date sysDate = new Date();
		SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String strDate = format.format(sysDate);
		return strDate;
	}
}

5.ChannelHandlerHolder文件

package com.yj.netty.client;

import io.netty.channel.ChannelHandler;

public interface ChannelHandlerHolder {
    ChannelHandler[] handlers();
}

6.application.properties文件

server.port=8081

netty.port=8899
netty.host=127.0.0.1
#服务端恢复后,客户端是否进行重连
netty.reconnect=true
#服务端恢复后,客户端重连的尝试次数
netty.attemptsNum=12

实现效果

1.先开启服务端,再开启客户端

2.关闭服务端,客户端尝试重连

3.重新启动服务端,客户端重连成功

猜你喜欢

转载自blog.csdn.net/qq_24313635/article/details/81116886