本文的示例代码参考NettyHeartBeat
目录
Introduce
心跳与空闲监测是为了解决连接假死的问题
- 连接假死的定义
在某一端(服务端或者客户端)看来 底层TCP连接已经断开 但是应用程序并没有捕获到而认为连接仍然存在
- 连接假死的危害
对服务端来说 连接假死会耗费CPU和内存资源
对客户端来说 连接假死会造成发送数据超时 影响用户体验
- 连接假死的原因
应用程序出现线程堵塞 无法进行数据的读写
客户端或者服务端网络相关的设备出现故障 比如网卡、机房故障
公网丢包、网络抖动等
Prepare
cp -R NettySticky NettyHeartBeat
cd NettyHeartBeat
关于NettySticky详细参考教程Netty粘拆包
Server Idle Handler
mkdir -p src/main/java/handler
vim src/main/java/handler/IdleHandler.java
package handler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;
import java.util.concurrent.TimeUnit;
public class IdleHandler extends IdleStateHandler {
private static final int READER_IDLE_TIME = 15;
public IdleHandler() {
super(READER_IDLE_TIME, 0, 0, TimeUnit.SECONDS);
}
@Override
protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
System.out.println(READER_IDLE_TIME + "秒内未读到数据,关闭连接");
ctx.channel().close();
}
}
vim src/main/java/server/Server.java
// 省略了未修改的代码
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new IdleHandler());
}
});
// 省略了未修改的代码
sed -i "" "s/client.Client/server.Server/g" build.gradle && gradle run
# 端口[8888] 绑定成功!
# 15秒内未读到数据,关闭连接
telnet 127.0.0.1 8888
# Connected to localhost.
# Connection closed by foreign host.
Protocol
vim src/main/java/protocol/command/Command.java
package protocol.command;
public interface Command {
Byte LOGIN = 1;
Byte HEARTBEAT_REQUEST = 21;
}
vim src/main/java/protocol/request/HeartBeatRequestPacket.java
package protocol.request;
import protocol.Packet;
import protocol.command.Command;
public class HeartBeatRequestPacket extends Packet {
@Override
public Byte getCommand() {
return Command.HEARTBEAT_REQUEST;
}
}
Client Heart Beat Timer
vim src/main/java/client/ClientHeartBeatTimerHandler.java
package client;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import protocol.request.HeartBeatRequestPacket;
import java.util.concurrent.TimeUnit;
public class ClientHeartBeatTimerHandler extends ChannelInboundHandlerAdapter {
private static final int HEARTBEAT_INTERVAL = 5;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
scheduleSendHeartBeat(ctx);
super.channelActive(ctx);
}
private void scheduleSendHeartBeat(ChannelHandlerContext ctx) {
ctx.executor().schedule(() -> {
if (ctx.channel().isActive()) {
System.out.println("发送心跳包");
ctx.writeAndFlush(PacketCodec.INSTANCE.encode(new HeartBeatRequestPacket()));
scheduleSendHeartBeat(ctx);
}
}, HEARTBEAT_INTERVAL, TimeUnit.SECONDS);
}
}
vim src/main/java/client/Client.java
// 省略了未修改的代码
Bootstrap bootstrap = new Bootstrap();
bootstrap
.group(workerGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ClientHeartBeatTimerHandler());
}
});
// 省略了未修改的代码
sed -i "" "s/client.Client/server.Server/g" build.gradle && gradle run
# 端口[8888] 绑定成功!
sed -i "" "s/server.Server/client.Client/g" build.gradle && gradle run
# 连接成功!
# 发送心跳包 (间隔5s)
网友评论