最近研究了一下Netty的心跳和重连,在此和大家分享一下。
1.Netty的心跳机制实现
实现原理:客户端每隔一段时间都会发送一个消息到服务端,以此来通知服务端我还在线,处于正常运行状态,当一段时间内,服务端没有接收到客户端的消息,则视为该客户端已经下线,断开和他的连接。
实现的核心点是通过IdleStateHandler心跳检测处理器来实现心跳检测机制的
IdleStateHandler: 服务端添加IdleStateHandler心跳检测处理器,并添加自定义处理Handler类实现userEventTriggered()方法作为超时事件的逻辑处理。
该处理器有三个核心参数
readerIdleTime: 读超时时间(服务端达到该设定时间还没有接收到客户端的消息并未执行读操作,则会触发userEventTriggered方法)
writerIdleTime: 写超时时间(服务端达到该设定时间还没有接收到客户端的消息并未执行写操作,则会触发userEventTriggered方法)
allIdleTime: 读写超时时间(服务端达到该设定时间还没有接收到客户端的消息并未执行读或写操作,则会触发userEventTriggered方法)
TimeUnit: 时间单位
下面实战代码:
1.服务端的实现
- [1] 首先定义一个服务端
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;
/**
* <B>类名称</B>IntelliJ IDEA<BR/>
* <B>类描述</B><BR/>
*
* @author lt
* @date 2022/7/25 17:21
*/
public class HeartBeatServer {
int port;
public HeartBeatServer(int port) {
this.port = port;
}
public void start() {
ServerBootstrap bootstrap = new ServerBootstrap();
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
try {
bootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new HeartBeatInitializer());
ChannelFuture future = bootstrap.bind(port).sync();
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
worker.shutdownGracefully();
boss.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
HeartBeatServer server = new HeartBeatServer(8080);
server.start();
}
}
- [2] 接着定义初始化处理器链条
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
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;
/**
* <B>类名称</B>IntelliJ IDEA<BR/>
* <B>类描述</B><BR/>
*
* @author lt
* @date 2022/7/25 17:22
*/
public class HeartBeatInitializer extends ChannelInitializer<Channel> {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//防止出现粘包,定义一个特殊字符解码器
ByteBuf buf = Unpooled.buffer();
buf.writeBytes("*".getBytes());
pipeline.addLast(new DelimiterBasedFrameDecoder(1024, true, true, buf));
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
//此处设置读操作,超过5秒算超时情况
pipeline.addLast(new IdleStateHandler(5,0,0, TimeUnit.SECONDS));
pipeline.addLast(new HeaderBeatHandler());
}
}
- [3] 定义心跳超时处理器
/**
* <B>类名称</B>IntelliJ IDEA<BR/>
* <B>类描述</B><BR/>
*
* @author lt
* @date 2022/7/25 17:30
*/
public class HeaderBeatHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println("服务端接收到信息: " + msg);
if ("正常运行中。。。".equals(msg)){
//记得带*号,否则特殊字符处理器无法解析消息
ctx.channel().writeAndFlush("正常运行中。。。*");
}else {
System.out.println("其他信息处理。。。");
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state().equals(IdleState.READER_IDLE)) {
System.out.println("超过心跳时间,服务端即将断开和客户端的连接");
//记得带*号,否则特殊字符处理器无法解析消息
ctx.channel().writeAndFlush("心跳超时,断开客户端和服务端的连接*");
ctx.channel().close();
}
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.err.println("=== " + ctx.channel().remoteAddress() + " 已建立连接 ===");
}
}
2.客户端的实现
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
/**
* <B>类名称</B>IntelliJ IDEA<BR/>
* <B>类描述</B><BR/>
*
* @author lt
* @date 2022/7/25 17:44
*/
public class HeartBeatClient {
int port;
Channel channel;
int repeatTime;
public HeartBeatClient(int port){
this.port = port;
this.repeatTime = 0;
}
public static void main(String[] args) throws Exception{
HeartBeatClient client = new HeartBeatClient(8080);
client.start();
}
public void start() {
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try{
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
.handler(new HeartBeatClientInitializer());
connect(bootstrap,port);
String text = "正常运行中。。。*";
while (channel.isActive()){
sendMsg(text);
}
}catch(Exception e){
// ignore
}finally {
eventLoopGroup.shutdownGracefully();
}
}
public void connect(Bootstrap bootstrap,int port) throws Exception{
channel = bootstrap.connect("localhost",8080).sync().channel();
}
public void sendMsg(String text) throws Exception{
repeatTime++;
//第4次发送消息时模拟超时情况,用来触发服务端的超时回调方法
if (repeatTime > 3) {
Thread.sleep(6 * 1000);
}else {
Thread.sleep(2 * 1000);
}
channel.writeAndFlush(text);
}
static class HeartBeatClientInitializer extends ChannelInitializer<Channel> {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//特殊字符解码器
ByteBuf buf = Unpooled.buffer();
buf.writeBytes("*".getBytes());
pipeline.addLast(new DelimiterBasedFrameDecoder(1024, true, true, buf));
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
pipeline.addLast(new HeartBeatClientHandler());
}
}
static class HeartBeatClientHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println(" 客户端接收到信息 :" +msg);
if(msg!= null && msg.equals("心跳超时,断开客户端和服务端的连接")) {
System.out.println(" 服务器已经和客户端断开连接 , 客户端自己也要关闭链接了");
ctx.channel().closeFuture();
}
}
}
}
代码运行结果如下:
服务端:
客户端:
可以看到第四次发送消息的时候,因为超过了服务端设定的超时时间,因此触发了userEventTriggered方法,服务端断开了和客户端的连接。
那么断开连接之后,我们该如何实现Netty的重新连接呢?
2.Netty重新连接的实现
Netty重连实现原理:
首先你要了解Netty生命周期及其不同时期的回调方法:
1.handlerAdded:
新建立的连接会按照初始化策略,把handler添加到该channel的pipeline里面,也就是channel.pipeline.addLast(new LifeCycleInBoundHandler)执行完成后的回调
channelRegistered:
当该连接分配到具体的worker线程后,该回调会被调用
2.channelActive:
channel的准备工作已经完成,所有的pipeline添加完成,并分配到具体的线上上,说明该channel准备就绪,可以使用了
3.channelRead:
客户端向服务端发来数据,每次都会回调此方法,表示有数据可读
4.channelReadComplete:
服务端每次读完一次完整的数据之后,回调该方法,表示数据读取完毕
5.channelInactive:
当连接断开时,该回调会被调用,说明这时候底层的TCP连接已经被断开了
6.channelUnReg stered:
对应channelRegistered,当连接关闭后,释放绑定的workder线程
7.handlerRemoved:
对应handlerAdded,将handler从该channel的pipeline移除后的回调方法
当服务端和客户端断开连接的时候,首先会调用channelInactive方法,我们就可以在这里面调用重新连接的方法,用来恢复连接。
下面是具体实现代码,服务端不变,下面的都是客户端的代码:
-[1] 首先是客户端启动代码
import io.netty.channel.Channel;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import lombok.SneakyThrows;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
public class NettyClient {
private static final InternalLogger log = InternalLoggerFactory.getInstance(NettyClient.class);
private static final ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
public static Integer time = 1;
public static void main(String[] args) {
NettyClientProcess nettyClient = new NettyClientProcess();
boolean connect = false;
//启动时尝试连接5次
for (int i = 0; i < 5; i++) {
connect = nettyClient.connect();
if (connect) {
break;
}
//连接不成功,隔2s之后重新尝试连接
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (connect) {
log.info("定时发送数据");
while (nettyClient.getChannel().isActive()) {
send(nettyClient);
}
}
}
/**
* 定时发送数据
*/
@SneakyThrows
static void send(NettyClientProcess client) {
time++;
Channel channel = client.getChannel();
//此处为了模拟超时情况,每8秒超时一次,用来触发服务端的超时回调方法
if ((time%5) == 0) {
Thread.sleep(6000);
} else {
Thread.sleep(2000);
}
channel.writeAndFlush("正常运行中。。。*");
}
}
-[2] 客户端初始化
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import java.net.ConnectException;
import java.nio.channels.ClosedChannelException;
import java.util.concurrent.TimeUnit;
public class NettyClientProcess {
private static final InternalLogger log = InternalLoggerFactory.getInstance(NettyClientProcess.class);
private EventLoopGroup workerGroup;
private Bootstrap bootstrap;
private volatile Channel clientChannel;
public volatile Integer repeatTime;
public NettyClientProcess() {
this(-1);
}
public NettyClientProcess(int threads) {
this.repeatTime = 0;
workerGroup = threads > 0 ? new NioEventLoopGroup(threads) : new NioEventLoopGroup();
bootstrap = new Bootstrap();
bootstrap.group(workerGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.SO_KEEPALIVE, false)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 30000)
.handler(new ClientHandlerInitializer(this));
}
public boolean connect() {
log.info("尝试连接到服务端: 127.0.0.1:8088");
try {
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8080);
boolean notTimeout = channelFuture.awaitUninterruptibly(20, TimeUnit.SECONDS);
clientChannel = channelFuture.channel();
if (notTimeout) {
if (clientChannel != null && clientChannel.isActive()) {
log.info("客户端重新连接成功!!! {} ,已经同服务端建立连接。。。", clientChannel.localAddress());
NettyClient.send(this);
NettyClient.time = 1;
return true;
}
Throwable cause = channelFuture.cause();
if (cause != null) {
exceptionHandler(cause);
}
} else {
log.warn("connect remote host[{}] timeout {}s", clientChannel.remoteAddress(), 30);
}
} catch (Exception e) {
exceptionHandler(e);
}
clientChannel.close();
return false;
}
private void exceptionHandler(Throwable cause) {
if (cause instanceof ConnectException) {
log.error("连接异常:{}", cause.getMessage());
} else if (cause instanceof ClosedChannelException) {
log.error("connect error:{}", "client has destroy");
} else {
log.error("connect error:", cause);
}
}
public Channel getChannel() {
return clientChannel;
}
}
-[3] 客户端处理器链条组装
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
/**
* <B>类名称</B>IntelliJ IDEA<BR/>
* <B>类描述</B><BR/>
*
* @author lt
* @date 2022/7/26 18:54
*/
public class ClientHandlerInitializer extends ChannelInitializer<SocketChannel> {
private static final InternalLogger log = InternalLoggerFactory.getInstance(NettyClientProcess.class);
private NettyClientProcess client;
public ClientHandlerInitializer(NettyClientProcess client) {
this.client = client;
}
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//防止出现粘包,定义一个特殊字符解码器
ByteBuf buf = Unpooled.buffer();
buf.writeBytes("*".getBytes());
pipeline.addLast(new DelimiterBasedFrameDecoder(1024, true, true, buf));
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
pipeline.addLast(new CustomClientHandler(client));
}
}
-[4] 重新连接触发处理器
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import java.io.IOException;
public class CustomClientHandler extends ChannelInboundHandlerAdapter {
private static final InternalLogger log = InternalLoggerFactory.getInstance(CustomClientHandler.class);
private NettyClientProcess client;
public CustomClientHandler(NettyClientProcess client) {
this.client = client;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("客户端接收到消息:{}", msg);
}
/**
* 和服务端断开连接会回调此方法
* @param ctx
* @throws Exception
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.warn("客户端断开连接,IP地址:{}", ctx.channel().localAddress());
reconnection(ctx);
client.repeatTime = 0;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if (cause instanceof IOException) {
log.warn("异常情况: 客户端[{}]和远程断开连接", ctx.channel().localAddress());
} else {
log.error(cause);
}
reconnection(ctx);
}
private void reconnection(ChannelHandlerContext ctx) {
log.info("5s之后重新建立连接");
boolean connect = client.connect();
if (connect) {
log.info("重新连接成功");
}
}
}
代码运行结果如下:
服务端:
客户端: