很感谢各位读者能够打开博主的这篇博客,博主在编写此博客时也是处于Netty框架初学阶段,在学习Netty框架之前已具备Mina框架基本使用经验,以下关于Netty心跳机制的讲解也是全部出自于自己对于Netty框架的理解,希望能够帮助到更多的和博主一样在初学Netty时愁于找不到称心如意的学习文档的小白们,如果有博主理解不到位或者编写错误的地方,也希望评论区里的大佬们能够多多包涵,不吝赐教
心跳机制
心跳机制的具体概念这里不多赘述,网上的一些教程讲的非常详细,博主这里主要利用自己写的一个小Demo来与大家简单探讨一下Netty的心跳机制,在上代码之前,还是希望读者在阅读本博客之前,有一定的网络框架使用经验,最好能够了解Netty框架的基础概念及基本的使用
好了废话有点多了,现在我们就直接点,上代码!
1.认识userEventTriggered
userEventTriggered是继承自ChannelInboundHandlerAdapter类中的一个方法,它的主要作用类似于Mina框架中的sessionIdle方法,用来处理当读写空闲时长超过设置的时间范围的回调,它的源码如下:
public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelInboundHandler {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
ctx.fireUserEventTriggered(evt);
}
}
我们在使用时只需在ChannelInboundHandlerAdapter的实现类中重写该方法就能实现定制化的效果
public class KpClientHandler extends SimpleChannelInboundHandler<DataFrame> {
private static final Logger log = LoggerFactory.getLogger(KpClientHandler.class);
// 当出现空闲时触发该方法(不管是读空闲还是写空闲)
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if(evt instanceof IdleStateEvent) {
// 将evt向下转型为IdleStateEvent
IdleStateEvent event = (IdleStateEvent) evt;
// 当出现读空闲时处理下列逻辑
if(event.state() == IdleState.READER_IDLE) {
log.warn("【服务端】服务端5秒内没有收到客户端心跳");
ctx.channel().close();
}
}
super.userEventTriggered(ctx, evt);
}
}
单单是重写处理器还不能达到我们想要的超时进行回调的效果,我们还需将该超时处理器配置进我们的pipeline中
注意:这里我们需要手动配置IdleStateHandler,用来配置读空闲时间、写空闲时间及读写空闲时间
public class ServerChannelInitializer extends ChannelInitializer<NioSocketChannel>{
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// TODO Auto-generated method stub
ChannelPipeline pipeline = ch.pipeline();
/* 这里我们设置5秒的读空闲时间
* IdleStateHandler
*
* readerIdleTimeSecond:指定读超时时间,指定 0 表明为禁用。
* writerIdleTimeSecond:指定写超时时间,指定 0 表明为禁用。
* allIdleTimeSecond:在指定读写超时时间,指定 0 表明为禁用。
*/
pipeline.addLast(new IdleStateHandler(5, 0 , 0 , TimeUnit.SECONDS));
pipeline.addLast(new StringEncoder());
pipeline.addLast(new StringDecoder());
// 将我们的超时处理器配置进pipeline
pipeline.addLast(new KpServerHandler());
}
}
到这里我们的userEventTriggered触发器的基本使用就完成了,下一步我们就需要使用超时处理器来学习Netty心跳机制的处理
2.简单心跳的发送
在该模块我们的心跳直接使用"heartbeat"字符串作为心跳进行发送,没有使用数据包进行网络传递,不需要使用协议进行解析数据包
服务端消息处理器
KpServerHandler.java
/**
* 心跳包处理器
* @ClassName KpServerHandler
* @Description TODO
* @author wuxiangyi
* @date 2022年4月7日 下午7:18:42
*/
public class KpServerHandler extends SimpleChannelInboundHandler {
private static final Logger log = LoggerFactory.getLogger(KpServerHandler.class);
// 有客户端消息时回调
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
// TODO Auto-generated method stub
String info = msg.toString();
log.info("【服务端接受消息】{}" , info);
// 判定为心跳
if(info.equals("heartbeat")) {
// 写回心跳给客户端
ctx.writeAndFlush("heartbeat").addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
}
}
/**
* 当出现空闲时间的触发器
* @title: userEventTriggered
* @Description TODO
* @date 2022年4月7日 下午7:22:04
* @param ctx
* @param evt
* @throws Exception
* @see io.netty.channel.ChannelInboundHandlerAdapter#userEventTriggered(io.netty.channel.ChannelHandlerContext, java.lang.Object)
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if(evt instanceof IdleStateEvent) {
// 将evt向下转型为IdleStateEvent
IdleStateEvent event = (IdleStateEvent) evt;
// 服务端在乎的是客户端有没有发过来心跳,所以这里关注读空闲
if(event.state() == IdleState.READER_IDLE) {
log.warn("【服务端】服务端5秒内没有收到客户端心跳");
ctx.channel().close();
}
}
super.userEventTriggered(ctx, evt);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
log.info("【服务端】发现异常");
ctx.close();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.error("【服务端】连接断开");
super.channelInactive(ctx);
}
}
服务端Channel初始化器
ServerChannelInitializer.java
public class ServerChannelInitializer extends ChannelInitializer<NioSocketChannel>{
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// TODO Auto-generated method stub
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IdleStateHandler(5, 0 , 0 , TimeUnit.SECONDS));
// 因为直接传递的是字符串类型,这里必须使用默认的字符串编解码器,否则将接收不到返回数据
pipeline.addLast(new StringEncoder());
pipeline.addLast(new StringDecoder());
pipeline.addLast(new KpServerHandler());
}
}
服务端启动类
KpServer.java
@Component
public class KpServer implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
// TODO Auto-generated method stub
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup(2);
ChannelFuture future;
try {
future = new ServerBootstrap()
.group(boss , worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ServerChannelInitializer())
.bind(8001).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
客户端消息处理器
KpClientHandler.java
public class KpClientHandler extends SimpleChannelInboundHandler {
private static final Logger log = LoggerFactory.getLogger(KpClientHandler.class);
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
// TODO Auto-generated method stub
String info = msg.toString();
if(info.equals("heartbeat")) {
log.info("【客户端】接受服务端心跳");
}
}
/**
* 当出现空闲时间的触发器
* @title: userEventTriggered
* @Description TODO
* @date 2022年4月7日 下午7:22:04
* @param ctx
* @param evt
* @throws Exception
* @see io.netty.channel.ChannelInboundHandlerAdapter#userEventTriggered(io.netty.channel.ChannelHandlerContext, java.lang.Object)
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if(evt instanceof IdleStateEvent) {
// 将evt向下转型为IdleStateEvent
IdleStateEvent event = (IdleStateEvent) evt;
// 服务端在指定的空闲时间内没有收到客户端心跳,向客户端发送心跳
// 注意因为客户端在乎的是有没有向服务端写入数据,所以这里关注的是写空闲
if(event.state() == IdleState.WRITER_IDLE) {
log.warn("【客户端】客户端发送心跳");
ctx.writeAndFlush("heartbeat").addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
}
}
super.userEventTriggered(ctx, evt);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
log.info("【客户端】发现异常");
ctx.close();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.error("【客户端】连接断开");
super.channelInactive(ctx);
}
}
客户端Channel初始化器
ClientChannelInitializer.java
public class ClientChannelInitializer extends ChannelInitializer<NioSocketChannel> {
private static final Logger log = LoggerFactory.getLogger(ClientChannelInitializer.class);
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// TODO Auto-generated method stub
ChannelPipeline pipeline = ch.pipeline();
// 为了避免客户端发送心跳到服务端时服务端处理器读空闲已经超时,这里的写空闲时间应小于服务端读空闲
pipeline.addLast(new IdleStateHandler(0 , 3 , 0 , TimeUnit.SECONDS));
pipeline.addLast(new StringEncoder());
pipeline.addLast(new StringDecoder());
pipeline.addLast(new LoggingHandler());
pipeline.addLast(new KpClientHandler());
}
}
3.复杂心跳的发送
这里的心跳使用的数据包进行发送,数据包中根据字段"type"来判别该心跳包实例是心跳包还是一般数据包,依据包类型的不同进行相应的处理。这里需要使用协议对数据包解析,如果读者对Netty协议不了解,望读者自行查看Netty协议相关技术文档
心跳包实例
DataFrame.java
/**
* 数据包
* @ClassName DataFrame
* @Description TODO
* @author wuxiangyi
* @date 2022年4月8日 上午11:10:46
*/
public class DataFrame implements Serializable {
/**
* @Fields serialVersionUID TODO
*/
private static final long serialVersionUID = 1L;
// 数据包类型:心跳包
public static int HEART_PACKAGE = 0;
// 数据包类型:数据包
public static int DATA_PACKAGE = 1;
// 数据包内容:心跳请求
public static String HEART_BEAT_REQUEST = "ping";
// 数据包内容:心跳响应
public static String HEART_BEAT_RESPONSE = "pong";
// 数据包类型:0-心跳,1-数据包
private int type;
// 内容
private String content;
public DataFrame(int type , String content) {
this.type = type;
this.content = content;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@Override
public String toString() {
return "DataFrame [type=" + type + ", content=" + content + "]";
}
}
协议解析器
MessageCodec.java
编码格式:FE【包头(byte类型)】 序列化对象长度(int) 序列化对象(byte数组) 01【包尾(byte类型)】
/**
* 消息编码器
* @ClassName MessageCodec
* @Description TODO
* @author wuxiangyi
* @date 2022年4月8日 下午12:58:20
*/
public class MessageCodec extends ByteToMessageCodec<DataFrame> {
private static final Logger log = LoggerFactory.getLogger(MessageCodec.class);
// 在每次有DataFrame数据包发送时会调用该编码函数,对
@Override
protected void encode(ChannelHandlerContext ctx, DataFrame msg, ByteBuf out) throws Exception {
// TODO Auto-generated method stub
log.info("【编码器】数据包开始编码");
int type = msg.getType();
String content = msg.getContent();
// 协议:FE-序列化对象长度-序列化对象-01
// 包头以FE开头
out.writeByte((byte) 254);
// 序列化对象
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(baos);
outputStream.writeObject(msg);
byte[] contentBytes = baos.toByteArray();
// 由于包尾还有一个字节数据,如果这里不对序列化对象字节数组长度加一
// 在通过LengthFieldBasedFrameDecoder对粘包半包处理时拿不到包尾,我们在解析时就无法解析包尾格式是否正确
out.writeInt(contentBytes.length + 1);
out.writeBytes(contentBytes);
// 包尾以01结尾
out.writeByte((byte) 1);
outputStream.close();
baos.close();
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
// TODO Auto-generated method stub
log.info("【解码器】数据包开始解码");
byte header = in.readByte();
if(header != (byte) 254) {
in.clear();
throw new Exception("包头不为FE,包丢弃");
}
int len = in.readInt();
byte[] contentBytes = new byte[len - 1];
in.readBytes(contentBytes);
// 反序列化成对象
ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(contentBytes));
DataFrame frame = (DataFrame) inputStream.readObject();
byte tail = in.readByte();
if(tail != (byte) 1) {
in.clear();
throw new Exception("包尾不为01,包丢弃");
}
// 将解析出来的对象写入out,我们在读取消息回调中才能拿到该对象
out.add(frame);
}
}
服务端处理器
KpServerHandler.java
/**
* 心跳包处理器
* @ClassName KpServerHandler
* @Description TODO
* @date 2022年4月7日 下午7:18:42
*/
public class KpServerHandler extends SimpleChannelInboundHandler<DataFrame> {
private static final Logger log = LoggerFactory.getLogger(KpServerHandler.class);
@Override
protected void channelRead0(ChannelHandlerContext ctx, DataFrame msg) throws Exception {
// TODO Auto-generated method stub
log.info("【服务端】接受数据包:{}" , msg);
// 判定为心跳
if(msg.getType() == DataFrame.HEART_PACKAGE && msg.getContent().equals(DataFrame.HEART_BEAT_REQUEST)) {
log.info("【服务端】认定数据包为心跳包");
// 写回心跳给客户端
ctx.writeAndFlush(new DataFrame(DataFrame.HEART_PACKAGE , DataFrame.HEART_BEAT_RESPONSE)).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
}
}
/**
* 当出现空闲时间的触发器
* @title: userEventTriggered
* @Description TODO
* @date 2022年4月7日 下午7:22:04
* @param ctx
* @param evt
* @throws Exception
* @see io.netty.channel.ChannelInboundHandlerAdapter#userEventTriggered(io.netty.channel.ChannelHandlerContext, java.lang.Object)
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if(evt instanceof IdleStateEvent) {
// 将evt向下转型为IdleStateEvent
IdleStateEvent event = (IdleStateEvent) evt;
// 服务端在指定的空闲时间内没有收到客户端心跳,连接断开
if(event.state() == IdleState.READER_IDLE) {
log.warn("【服务端】服务端5秒内没有收到客户端心跳");
ctx.channel().close();
}
}
super.userEventTriggered(ctx, evt);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
log.info("【服务端】发现异常");
ctx.close();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.error("【服务端】连接断开");
super.channelInactive(ctx);
}
}
服务端channel初始化器
ServerChannelInitializer.java
public class ServerChannelInitializer extends ChannelInitializer<NioSocketChannel>{
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// TODO Auto-generated method stub
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IdleStateHandler(5, 0 , 0 , TimeUnit.SECONDS));
// 1024 最大接收内容大小
// 1 内容长度起始索引
// 4 内容长度所占字节数
// 0 长度字段为基准,还有几个字节是内容
// 0 从头开始跳过几个字节不读取
pipeline.addLast(new LengthFieldBasedFrameDecoder(1024 , 1 , 4 , 0 , 0));
pipeline.addLast(new LoggingHandler());
pipeline.addLast(new MessageCodec());
pipeline.addLast(new KpServerHandler());
}
}
服务端启动类
KpServer.java
@Component
public class KpServer implements CommandLineRunner {
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup(2);
ChannelFuture future;
try {
future = new ServerBootstrap()
.group(boss , worker)
.childOption(ChannelOption.SO_KEEPALIVE , true)
.channel(NioServerSocketChannel.class)
.childHandler(new ServerChannelInitializer())
.bind(8001).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* 服务端发布到远程服务器上运行如下代码,本地测试直接运行上面启动类即可
* @title: run
* @Description TODO
* @date 2022年4月8日 下午1:01:34
* @param args
* @throws Exception
* @see org.springframework.boot.CommandLineRunner#run(java.lang.String[])
*/
@Override
public void run(String... args) throws Exception {
// TODO Auto-generated method stub
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup(2);
ChannelFuture future;
try {
future = new ServerBootstrap()
.group(boss , worker)
.childOption(ChannelOption.SO_KEEPALIVE , true)
.channel(NioServerSocketChannel.class)
.childHandler(new ServerChannelInitializer())
.bind(8001).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
客户端处理器
KpClientHandler.java
public class KpClientHandler extends SimpleChannelInboundHandler<DataFrame> {
private static final Logger log = LoggerFactory.getLogger(KpClientHandler.class);
@Override
protected void channelRead0(ChannelHandlerContext ctx, DataFrame msg) throws Exception {
// TODO Auto-generated method stub
log.info("【客户端】接受数据包:{}" , msg);
if(msg.getType() == DataFrame.HEART_PACKAGE && msg.getContent().equals(DataFrame.HEART_BEAT_RESPONSE)) {
log.info("【客户端】认定该数据包为服务端心跳响应");
}
}
/**
* 当出现空闲时间的触发器
* @title: userEventTriggered
* @Description TODO
* @date 2022年4月7日 下午7:22:04
* @param ctx
* @param evt
* @throws Exception
* @see io.netty.channel.ChannelInboundHandlerAdapter#userEventTriggered(io.netty.channel.ChannelHandlerContext, java.lang.Object)
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if(evt instanceof IdleStateEvent) {
// 将evt向下转型为IdleStateEvent
IdleStateEvent event = (IdleStateEvent) evt;
// 服务端在指定的空闲时间内没有收到客户端心跳,向客户端发送心跳
if(event.state() == IdleState.WRITER_IDLE) {
log.warn("【客户端】客户端发送心跳");
ctx.writeAndFlush(new DataFrame(DataFrame.HEART_PACKAGE , DataFrame.HEART_BEAT_REQUEST)).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
}
}
super.userEventTriggered(ctx, evt);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
log.info("【客户端】发现异常");
ctx.close();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.error("【客户端】连接断开");
super.channelInactive(ctx);
}
}
客户端channel初始化器
ClientChannelInitializer.java
public class ClientChannelInitializer extends ChannelInitializer<NioSocketChannel> {
private static final Logger log = LoggerFactory.getLogger(ClientChannelInitializer.class);
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// TODO Auto-generated method stub
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IdleStateHandler(1000 , 3 , 1000 , TimeUnit.SECONDS));
pipeline.addLast(new LengthFieldBasedFrameDecoder(1024 , 1 , 4 , 0 , 0));
pipeline.addLast(new LoggingHandler());
pipeline.addLast(new MessageCodec());
pipeline.addLast(new KpClientHandler());
}
}
客户端启动类
KpClient.java
public class KpClient {
public static void main(String[] args) {
try {
ChannelFuture future = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ClientChannelInitializer())
.connect(new InetSocketAddress("localhost" , 8001))
.sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
4.结语
到这里博主的博客就已至尾声,希望博主的博客能够给大家netty学习路上提供帮助,再次感谢大家的用心阅读,再会。