nettyハートビートメカニズムと再接続
1.理論と概要
1.ハートビートとは
名前が示すように、いわゆる ハートビートは、長いTCP接続でクライアントとサーバー間で定期的に送信される特別なデータパケットであり、TCP接続の有効性を確保するために相手がまだオンラインであることを相手に通知します。
なぜハートビートメカニズムとその原理を導入するのですか?
ネットワークの信頼性が低いため、TCPが長い接続を維持しているときに、ネットワークケーブルが抜かれたり、突然の停電などの予期しない状況が発生すると、サーバーとクライアント間の接続が中断される可能性があります。このような予期せぬ状況で次に、サーバーとクライアントの間に相互作用がない場合、相手が短時間で切断されたことを見つけることができません。この問題を解決するには、ハートビート メカニズムを導入する必要があります。 ハートビートメカニズムの動作原理は次のとおりです。サーバー上一定期間クライアントとのデータ対話がない場合、つまりアイドル状態の場合、クライアントまたはサーバーは特別なデータパケットを他のサーバーに送信します。受信者はデータパケットを受信すると、すぐに特別なデータパケットも送信します。データメッセージは送信者に応答します。これはPING-PONGインタラクションです。当然、一方の端がハートビートメッセージを受信すると、相手はまだオンラインであり、TCP接続の有効性を保証します
2.Nettyはハートビートの原理を実現します
IdleStateHandlerのプロセッサによって実現されます:
3つのパラメータがあります。
-
readerIdleTimeSeconds、読み取りタイムアウト。指定された時間内にチャネルにデータが読み取られない場合、READER_IDLEのIdleStateEventイベントがトリガーされます。
-
writerIdleTimeSeconds、書き込みタイムアウト。指定された時間内にチャネルにデータが書き込まれない場合、WRITER_IDLEのIdleStateEventイベントがトリガーされます。
-
allIdleTimeSeconds、読み取り/書き込みタイムアウト。つまり、指定された時間間隔内に読み取りまたは書き込み操作がない場合、ALL_IDLEのIdleStateEventイベントがトリガーされます。
たとえば、クライアントで次のように構成します。
p.addLast(new IdleStateHandler(0、0、5)); // 5秒の読み取りおよび書き込みアイドル検出。
この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;
}
}
}
3.このデモでハートビートを送信する実装プロセス
まず、クライアントとサーバーは単純なエコープログラムです。クライアントはIdleHandlerを5秒間読み書きするように構成します。クライアントは無限ループでサーバーにメッセージを送信し、間隔は0〜20秒です。が5秒を超えている情報が送信されない場合、idelイベントがトリガーされます(クライアントはAllイベントです)。このとき、クライアントはサーバーにpingを送信し、サーバーは受信後にpongをクライアントに返します。それ。
では、この記事でブレークポイントの再接続を実現するにはどうすればよいでしょうか。
まず、ブレークポイントの再接続は、接続が失われたかどうかを判断するためにハートビートの検出に依存しているため、ブレークポイントの再接続についてハートビートの検出について多くの話題があるのはなぜですか。ブレークポイントの再接続をトリガーします。
(1)ブレークポイントで再接続する方法:
リスナーメソッド(このデモで使用されるメソッド)
/**
* 连接的封装 (抽取连接过程方法,节省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);
}
}
});
}
非アクティブで再接続します
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秒を読み取ることであり、クライアントは5秒でアイドルの読み取りと書き込みを構成するためです。サーバーが10秒以内にデータを読み取らなかった場合、クライアントは2回pingを実行していません。クライアント接続が失われたと判断し、ChannelHandlerContext ctx.close()を実行すると、クライアントは再接続をトリガーできます。(このデモでは、当面の間、再接続をトリガーする例外は発生していません---作成したい場合は、クライアントのpingを毎回送信しないようにする必要があります)
4.補足メッセージ形式
[長さ、タイプ、データ] //タイプ1はping、2はポン、3はその他のメッセージです。
このメッセージは、LengthFieldBasedFrameDecoderを使用して、セミパケットのスティッキーパケットを処理できます。
2、デモコード
1.汎用プロセッサー
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.クライアント
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.サーバー
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();
}
}