【Netty】心跳机制 详解
在我们学习了很多 中间件
的使用之后,我们会发现:
在 微服务、分布式 的架构下,
注册中心
十分重要
而注册中心
,基本上都有心跳机制
,以 管理 以及 负载均衡 等功能的实现
那么,可能有没有接触过 分布式
架构 的同学有疑问了:
什么是 心跳机制
呢?
定义:
心跳机制
,就是:
在 长连接 中,客户端 和 服务器 之间、定期地 发送的一种 特殊的数据包,通知对方自己还 在线,以 确保 连接 的 有效性
有时候,也会将 客户端 的 健康情况,通过 心跳数据包 发送给 服务器,以方便 之后的 负载均衡 等操作
在 Netty
中, 实现心跳机制的关键是 IdleStateHandler
类
IdleStateHandler:
那么,本人来展示下 使用 IdleStateHandler
类,实现 心跳机制
:
首先,本人来展示下 IdleStateHandler
类 的 构造方法:
构造方法:
在上图中,我们可以看到:
IdleStateHandler
类 一共有 3个 构造方法
我们一般使用 四参 的 构造方法:
public IdleStateHandler(
long readerIdleTime,
long writerIdleTime,
long allIdleTime,
TimeUnit unit)
现在,本人来讲解下 各个参数 的 意义:
参数 | 意义 |
---|---|
readerIdleTime | 读超时 时限 即:当在 指定 时间间隔内 没有从 Channel 读取 到数据时, 会触发一个 READER_IDLE 的 IdleStateEvent 事件 |
writerIdleTime | 写超时 时限 即:当在 指定 时间间隔内 没有数据 写入到 Channel 时, 会触发一个 WRITER_IDLE 的 IdleStateEvent 事件 |
allIdleTime | 读/写超时 时限 即:当在 指定 时间间隔内 没有 读或写操作 时, 会触发一个 ALL_IDLE 的 IdleStateEvent 事件 |
unit | 时间单位 |
我们再来看下 真正实现逻辑 的 构造方法:
我们可以看到:
若
超时时限 <= 0
,则 不对 “0值”项 进行 心跳检测
那么,本人现在来展示下 心跳机制
的 使用:
使用展示:
服务端:
package edu.youzg.demo.heartbeat;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;
import java.util.concurrent.TimeUnit;
/**
* @Author: Youzg
* @CreateTime: 2021-05-06 17:37
* @Description: 带你深究Java的本质!
*/
public class NettyServerDemo {
public static void main(String[] args) {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new StringEncoder());
pipeline.addLast(new StringDecoder());
pipeline.addLast(new IdleStateHandler(3, 0, 0, TimeUnit.SECONDS));
pipeline.addLast(new ServerHandler());
}
});
System.out.println("Netty Server start...");
ChannelFuture channelFuture = bootstrap.bind(9000).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
static class ServerHandler extends SimpleChannelInboundHandler<String> {
private int readIdleTimeCnt = 0;
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, String msg) throws Exception {
System.out.println("客户端发来数据: " + msg);
if ("Heartbeat Packet".equals(msg)) {
channelHandlerContext.channel().writeAndFlush("ok");
} else {
System.out.println("业务请求处理...");
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent event = (IdleStateEvent) evt;
String eventType = null;
switch (event.state()) {
case READER_IDLE:
eventType = "读空闲";
readIdleTimeCnt++;
break;
case WRITER_IDLE:
eventType = "读空闲";
break;
case ALL_IDLE:
eventType = "读空闲";
break;
}
System.out.println("发生 超时事件,事件类型为:[" + eventType + "]");
if (readIdleTimeCnt >= 3) {
System.out.println("客户端[读空闲]次数 超过3次,服务端关闭连接...");
ctx.channel().writeAndFlush("idle close");
ctx.close();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
}
客户端:
package edu.youzg.demo.heartbeat;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import java.util.Random;
/**
* @Author: Youzg
* @CreateTime: 2021-05-06 17:41
* @Description: 带你深究Java的本质!
*/
public class NettyClientDemo {
public static void main(String[] args) {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new StringEncoder());
pipeline.addLast(new StringDecoder());
pipeline.addLast(new ClientHandler());
}
});
System.out.println("Netty Client start...");
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 9000).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
static class ClientHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println("服务端响应数据 为:" + msg);
if (msg != null && msg.equals("idle close")) {
System.out.println("超时次数过多,服务端强制关闭连接!");
ctx.close();
}
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
String text = "Heartbeat Packet";
Random random = new Random();
int num = random.nextInt(8);
Thread.sleep(num * 1000);
ctx.writeAndFlush(text);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
}
现在,本人来展示下 运行结果:
运行结果:
服务端:
客户端:
现在,本人来带同学们,深究下 Netty
的 心跳机制 的 核心源码:
核心源码:
我们先来看一下 IdleStateHandler
类 的 继承关系:
继承关系:
我们可以看到:
IdleStateHandler
类 既 继承了 ChannelInboundHandlerAdapter类,又 继承了 ChannelOutboundHandler类
因此,我们可以将其 当作一个 ChannelHandler,来处理 入站和出站 消息
接下来,我们来根据 一个Netty连接 的 整个生命周期,来看看具体 是如何 执行的:
首先,我们来看看 channelActive()方法 的 源码:
channelActive()方法:
我们可以看到:
channelActive()方法 只是调用了 初始化“心跳任务” 的 initialize()方法
那么,我们 追下去,看看是 如何初始化 的:
初始化“心跳任务” —— initialize()方法:
我们可以看到:
initialize()方法,主要是 根据 参数值,执行 “定时任务”
并且 参数值 <= 0,则 不会触发 “心跳检测”
(这也照应了 上文 构造方法 的 逻辑)
我们继续跟踪进去,看看是 定时任务 的 真实逻辑:
定时任务 逻辑:
由于 三种超时事件,所触发的 定时任务类 的 执行逻辑 是 基本一致 的
那么,本人在这里,就通过讲解 读超时
事件,来讲解下 心跳检测
是 如何执行 的:
在上图中,我们基本上,只需要关注 本人标记的 四点:
- ①
nextDelay
存储 下一次 心跳检测 和 当前时刻 的 间隔时间
因为 当前定时任务 的 执行时间 不一定是 设置的 心跳检测的时间,两个时间 有 偏差
因此,nextDelay = 用户设置的读超时间隔 - (当前时间 - 上一次“读心跳”时间)- ② 如果 超时,则 重新 进行一次 “心跳检测”,防止 “网络抖动”等因素 造成 频繁 创建/销毁 连接 的情况
- ③ 如果 超时,调用 用户编写的
userEventTriggered()方法
的 逻辑- ④ 如果 未超时,根据 步骤① 的 计算结果,进行 下一次的 “心跳检测”
对于上述 第③点,可能有同学会产生疑惑:
为什么 channelIdle()方法 会调用 用户编写的
userEventTriggered()方法
的 逻辑 呢?
那么,我们来看看 channelIdle()方法 的 源码:
userEventTriggered逻辑的调用 —— channelIdle()方法:
我们可以看到:
channelIdle()方法 内部调用了 后续处理器 的
userEventTriggered()方法
最后,本人来 总结 下 Netty
的 心跳机制 的 巧妙实现:
总结:
在 Netty
中,心跳检测 机制,并不是 采用 JDK提供的 Timer类 实现的
而是通过 类似于 递归调用 实现的:
因为 schedule()方法 内部,只是简单地 向 内部线程池 提交 “一次性”延时任务
而 当 本次延时任务 执行期间,又会 提交 新的延时任务:
这样的 “心跳机制” 实现思想,真的让 初学的本人 眼前一亮!
果然 没有固定的代码,只有死板的思想!
希望看到此处的同学们,也会有所感悟!