netty心跳机制和重连
顾名思义, 所谓 心跳, 即在 TCP 长连接中, 客户端和服务器之间定期发送的一种特殊的数据包, 通知对方自己还在线, 以确保 TCP 连接的有效性。
为什么引入心跳机制和其原理?
因为网络的不可靠性, 有可能在 TCP 保持长连接的过程中, 由于某些突发情况, 例如网线被拔出, 突然掉电等, 会造成服务器和客户端的连接中断. 在这些突发情况下, 如果恰好服务器和客户端之间没有交互的话, 那么它们是不能在短时间内发现对方已经掉线的. 为了解决这个问题, 我们就需要引入 心跳 机制. 心跳机制的工作原理是: 在服务器和客户端之间一定时间内没有数据交互时, 即处于 idle 状态时, 客户端或服务器会发送一个特殊的数据包给对方, 当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文, 回应发送方, 此即一个 PING-PONG 交互. 自然地, 当某一端收到心跳消息后, 就知道了对方仍然在线, 这就确保 TCP 连接的有效性
通过IdleStateHandler 的处理器进行实现:
它的参数有三个:
readerIdleTimeSeconds, 读超时. 当指定时间内没有读数据到channel时, 会触发一个 READER_IDLE 的 IdleStateEvent 事件。
writerIdleTimeSeconds, 写超时. 当指定时间内没有写数据到channel时, 会触发一个 WRITER_IDLE 的 IdleStateEvent 事件。
allIdleTimeSeconds, 读/写超时. 即当在指定的时间间隔内没有读或写操作时, 会触发一个 ALL_IDLE 的 IdleStateEvent 事件。
比如我们在客户端配置:
p.addLast(new IdleStateHandler(0, 0, 5)); // 5 秒读写idle检测.
加了这个IdleState处理器如何接收空闲状态呢?我们自定义处理器可以在userEventTriggered的方法接收空闲状态时间的触发。
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;
}
}
}
首先,客户端和服务端就是一个简单echo的程序,客户端配置IdleHandler为读写5秒时间,客户端无限循环给服务端发送信息,并且这个间隔时间是0-20秒,如果时间大于5秒没有发送信息,那么就会触发idel事件(客户端是All事件),此时客户端发送ping给服务端,服务端收到后,返回pong给客户端。
那么本篇文章断点重连是怎么实现呢?
首先为什么断点重连说心跳检测说这么多,因为断点重连依赖心跳检测判断是否已经失去连接,下面说下断点重连的方式和心跳监测如何判断失去连接和触发断点重连。
(1) 断点重连的方式:
监听器方式 (本次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);
}
}
});
}
在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)判断和触发重连
因为上面我们服务端配置的是读10秒触发idle,客户端配置的是5秒读写idle,如果服务端10秒还没有读到任何数据,那么说明客户端2次没有ping通,这个时候就初步判断客户端连接丢失,执行ChannelHandlerContext ctx.close(),客户端就可以触发重连。 (本demo暂时未做触发重连的异常---如果想制造就是让客户端的ping不是每次发送即可)
[length, type, data] // type为1是ping,为2是pong,为3就是其他消息。
本报文可用LengthFieldBasedFrameDecoder处理半包黏包。
public abstract class CustomHeartbeatHandler extends SimpleChannelInboundHandler {
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---");
}
}
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() {
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);
}
}
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() {
@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();
}
}