Netty数据包的发送与心跳机制的实现

2,164 阅读10分钟

很感谢各位读者能够打开博主的这篇博客,博主在编写此博客时也是处于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学习路上提供帮助,再次感谢大家的用心阅读,再会。