Appearance
配置文件设置心跳超时时间
yaml
xim:
tcpPort: 9000
webSocketPort: 19000
bossThreadSize: 1
workThreadSize: 4
heartBeatTime: 30000 # 心跳超时时间,单位毫秒
# ……
配置类对象
java
@Data
public class BootstrapConfig {
private TcpConfig xim;
@Data
public static class TcpConfig {
private Integer tcpPort;
private Integer webSocketPort;
private Integer bossThreadSize;
private Integer workThreadSize;
private Long heartBeatTime;
private RedisConfig redis;
}
// ……
}
开启Netty心跳检测
首先开启心跳检测,添加IdleStateHandler
,当Netty检测到心跳不活跃时,会交给下一个Handler的userEventTriggered
方法
java
public class XimServer {
// ……
public XimServer(BootstrapConfig.TcpConfig config) {
// ……
server.group(bossGroup, workerGroup)
// ……
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline().addLast(new MessageDecoder());
sc.pipeline().addLast(new IdleStateHandler(0, 0, 10)); //开启心跳检测
sc.pipeline().addLast(new NettyServerHandler());
}
});
}
// ……
}
记录心跳跳动时间
如果是心跳消息,记录心跳最后一次跳动时间到channel连接对象中,后面在channel中取出时间与当前时间对比,如果超过设置时间则改为离线状态。
java
public class NettyServerHandler extends SimpleChannelInboundHandler<Message> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Message msg) throws Exception {
Integer command = msg.getMessageHeader().getCommand();
if (command == SystemCommand.LOGIN.getCommand()) {
login(ctx, msg);
} else if (command == SystemCommand.LOGOUT.getCommand()) {
// 用户主动退出,清空用户登录Session信息
SessionSocketHolder.removeUserSession((NioSocketChannel) ctx.channel());
} else if (command == SystemCommand.PING.getCommand()) {
// 如果是心跳消息,设置心跳最后一次跳动时间
ctx.channel().attr(AttributeKey.valueOf(Constants.ReadTime)).set(System.currentTimeMillis());
}
}
}
心跳不活跃设置登录状态为离线
当Netty检测到心跳不活跃时,会交给下一个Handler的userEventTriggered
方法,我们创建一个新的Handler专门处理心跳不活跃时的情况。
- 判断是否是心跳消息
- 分别判断读空闲、写空闲、读写空闲,本项目只处理读写空间
java
@Slf4j
public class HearBeatHandler extends ChannelInboundHandlerAdapter {
private Long heartBeatTime;
public HearBeatHandler(Long heartBeatTime) {
this.heartBeatTime = heartBeatTime;
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt; // 强制类型转换
if (event.state() == IdleState.READER_IDLE) {
log.info("读空闲");
} else if (event.state() == IdleState.WRITER_IDLE) {
log.info("进入写空闲");
} else if (event.state() == IdleState.ALL_IDLE) {
// 1.获取最后一次心跳跳动时间
Long lastReadTime = (Long) ctx.channel().attr(AttributeKey.valueOf(Constants.ReadTime)).get();
long now = System.currentTimeMillis();
if (lastReadTime != null && now - lastReadTime > heartBeatTime) {
// 离线,清除channel连接信息,redis信息状态改为离线但不删除
SessionSocketHolder.offlineUserSession((NioSocketChannel) ctx.channel());
}
}
}
}
}
在Netty启动时添加心跳处理Handler
HearBeatHandler
处理心跳,IdleStateHandler
设置每10s没有读写触发一次HearBeatHandler
的userEventTriggered
方法。
- 第一次触发时,当前时间-channel记录的时间≈1000ms
- 第二次触发时,当前时间-channel记录的时间≈2000ms
- 第三次触发时,当前时间-channel记录的时间≈3000ms
- 第三次触发时发现3000ms大于等于配置文件中记录的3000ms,设置当前channel为离线状态。
java
public class XimServer {
// ……
public XimServer(BootstrapConfig.TcpConfig config) {
// ……
server.group(bossGroup, workerGroup)
// ……
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline().addLast(new MessageDecoder());
sc.pipeline().addLast(new IdleStateHandler(0, 0, 10)); //开启心跳检测
sc.pipeline().addLast(new HearBeatHandler(config.getHeartBeatTime())); // 处理心跳
sc.pipeline().addLast(new NettyServerHandler());
}
});
}
// ……
}