概述
什么是心跳
不管是客户端和服务端建立的长连接还是集群(比如zookeeper集群、mongoDB集群等),其节点之间要形成TCP形式的节点间通讯,不管是通过什么方式,节点间的数据同步可能不是实时一致性的,而是最终一致性,通过定时的拉取或者说是同步请求来完成数据通讯或者同步,这其中是会有一定时间是数据不一致的,但是最终是会将数据拉平的。如何去保持节点间健康的感知呢,这就需要节点间定时的通过心跳包告知其他节点或者组件自己是“活着”的。这个心跳包的内容并不重要,主要是起到告知自我健康的作用,可能心跳通讯过程中会应为网络等各种各样的原因导致心跳包没有按照预期到达并返回,需要存在心跳重发确认的机制,当一个节点发给另一个节点心跳包之后并没有收到返回内容,就需要再次发送心跳包过去,知道达到设计的阈值,这时候可能就认为另一个节点已经挂掉了,这时候就可以做一些当前情况下的业务处理,比如踢出这个非健康节点。可能有些集群中会一直发送心跳包,当非健康节点又能返回响应的时候,再触发恢复机制。这种场景可以类比即时通讯类APP。
示例
由于本次心跳的demo可以复用Netty学习(3):多客户端与服务端的连接和通讯的客户端,本篇只给出服务端demo
服务端:
package com.leolee.netty.fourthExample;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
/**
* @ClassName MyServer
* @Description: 心跳实现
* @Author LeoLee
* @Date 2020/8/27
* @Version V1.0
**/
public class MyServer {
public static void main(String[] args) throws InterruptedException {
//定义线程组 EventLoopGroup为死循环
//boss线程组一直在接收客户端发起的请求,但是不对请求做处理,boss会将接收到的请i交给worker线程组来处理
//实际可以用一个线程组来做客户端的请求接收和处理两件事,但是不推荐
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//启动类定义
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
//子处理器,自定义处理器,服务端可以使用childHandler或者handler,handlerr对应接收线程组(bossGroup),childHandler对应处理线程组(workerGroup)
.handler(new LoggingHandler(LogLevel.INFO))//日志处理器
.childHandler(new MyServerInitializer());
//绑定监听端口
ChannelFuture channelFuture = serverBootstrap.bind(8899).sync();
//定义关闭监听
channelFuture.channel().closeFuture().sync();
} finally {
//Netty提供的优雅关闭
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
package com.leolee.netty.fourthExample;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.timeout.IdleStateHandler;
import java.util.concurrent.TimeUnit;
/**
* @ClassName MyServerInitializer
* @Description: TODO
* @Author LeoLee
* @Date 2020/8/27
* @Version V1.0
**/
public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//IdleStateHandler是空闲时间处理器,在一定时间没有读或者写操作,就会被触发
pipeline.addLast(new IdleStateHandler(5, 7, 10, TimeUnit.SECONDS));
pipeline.addLast(new MyServerHandler());
}
}
package com.leolee.netty.fourthExample;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleStateEvent;
/**
* @ClassName MyServerHandler
* @Description: 继承ChannelInboundHandlerAdapter适配器
* @Author LeoLee
* @Date 2020/8/27
* @Version V1.0
**/
public class MyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 功能描述: <br> 当某一个事件触发后,userEventTriggered会调用ChannelHandlerContext这个上下文对象的fireUserEventTriggered(Object),转发给pipeline中下一个ChannelInboundHandler(处理器)
* 〈〉
* @Param: [ctx, evt]
* @Return: void
* @Author: LeoLee
* @Date: 2020/8/27 17:28
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
//捕获空闲事件
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
String eventType = null;
switch (event.state()) {
case READER_IDLE:
eventType = "读空闲";
break;
case WRITER_IDLE:
eventType = "写空闲";
break;
case ALL_IDLE:
eventType = "读写空闲";
break;
}
System.out.println(ctx.channel().remoteAddress() + ":" + eventType);
//关闭空闲连接
ctx.close();
}
}
}
运行服务端和客户端结果如下
服务端:
八月 27, 2020 6:02:36 下午 io.netty.handler.logging.LoggingHandler channelRegistered
信息: [id: 0x54e8e20f] REGISTERED
八月 27, 2020 6:02:36 下午 io.netty.handler.logging.LoggingHandler bind
信息: [id: 0x54e8e20f] BIND: 0.0.0.0/0.0.0.0:8899
八月 27, 2020 6:02:36 下午 io.netty.handler.logging.LoggingHandler channelActive
信息: [id: 0x54e8e20f, L:/0:0:0:0:0:0:0:0:8899] ACTIVE
由于我们引入了LoggingHandler处理器的原因,打印了服务端启动的关键过程日志,注册管道,绑定端口,管道活跃状态
启动客户端再观察服务端输出如下:
八月 27, 2020 6:04:19 下午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0x54e8e20f, L:/0:0:0:0:0:0:0:0:8899] READ: [id: 0xccf3079e, L:/127.0.0.1:8899 - R:/127.0.0.1:56351]
八月 27, 2020 6:04:19 下午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0x54e8e20f, L:/0:0:0:0:0:0:0:0:8899] READ COMPLETE
打印了客户端于服务端建立连接过程的日志
当5秒钟后客户端没有再次写入数据到服务端,服务端打印如下:
/127.0.0.1:56351:读空闲
可以修改一下IdleStatHandler的读写时间参数,来看看服务端会有什么输出。
总结
这一篇其实我们主要关注IdleStatHandler是监控连接空闲状态的处理器就可以了,已经写了4个demo了,现在对于Netty的Handler应该是很有印象的,所有关于请求的处理都是通过一个一个Netty自带的处理器和自定义处理器来完成的,Netty处理器提供了很多方便的、通用的处理能力,自定义处理器主要是实现了业务的处理。
需要代码的来这里拿嗷:demo项目地址