今天在在搭建的netty框架中添加心跳机制,特此记录一下;
1.什么是心跳机制?
- 心跳是在TCP长连接中,客户端和服务端定时向对方发送数据包通知对方自己还在线,保证连接的有效性的一种机制
- 在服务器和客户端之间一定时间内没有数据交互时, 即处于 idle 状态时, 客户端或服务器会发送一个特殊的数据包给对方, 当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文, 回应发送方, 此即一个 PING-PONG 交互. 自然地, 当某一端收到心跳消息后, 就知道了对方仍然在线, 这就确保 TCP 连接的有效性
2.如何实现?
- 使用TCP协议层的Keeplive机制,但是该机制默认的心跳时间是2小时,依赖操作系统实现不够灵活;
- 应用层实现自定义心跳机制,比如Netty实现心跳机制;
3.心跳检测的实现
(1)服务端
- 服务端添加IdleStateHandler心跳检测处理器,并添加自定义处理Handler类实现userEventTriggered()方法作为超时事件的逻辑处理;
- 设定IdleStateHandler心跳检测每五秒进行一次读检测,如果五秒内ChannelRead()方法未被调用则触发一次userEventTrigger()方法
1 ServerBootstrap b= new ServerBootstrap();
2 b.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class)
3 .option(ChannelOption.SO_BACKLOG,1024)
4 .childHandler(new ChannelInitializer<SocketChannel>() {
5 @Override
6 protected void initChannel(SocketChannel socketChannel) throws Exception {
7 socketChannel.pipeline().addLast(new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));
8 socketChannel.pipeline().addLast(new StringDecoder());
9 socketChannel.pipeline().addLast(new HeartBeatServerHandler());
10 }
11 });
自定义处理类Handler继承ChannlInboundHandlerAdapter,实现其userEventTriggered()方法,在出现超时事件时会被触发,包括读空闲超时或者写空闲超时;
1 class HeartBeatServerHandler extends ChannelInboundHandlerAdapter {
2 private int lossConnectCount = 0;
3
4 @Override
5 public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
6 System.out.println("已经5秒未收到客户端的消息了!");
7 if (evt instanceof IdleStateEvent){
8 IdleStateEvent event = (IdleStateEvent)evt;
9 if (event.state()== IdleState.READER_IDLE){
10 lossConnectCount++;
11 if (lossConnectCount>2){
12 System.out.println("关闭不活跃通道!");
13 ctx.channel().close();
14 }
15 }
16 }else {
17 super.userEventTriggered(ctx,evt);
18 }
19 }
20
21 @Override
22 public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
23 lossConnectCount = 0;
24 System.out.println("client says: "+msg.toString());
25 }
26
27 @Override
28 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
29 ctx.close();
30 }
31 }
(2)客户端
- 客户端添加IdleStateHandler心跳检测处理器,并添加自定义处理Handler类实现userEventTriggered()方法作为超时事件的逻辑处理;
- 设定IdleStateHandler心跳检测每四秒进行一次写检测,如果四秒内write()方法未被调用则触发一次userEventTrigger()方法,实现客户端每四秒向服务端发送一次消息;
1 Bootstrap b = new Bootstrap();
2 b.group(group).channel(NioSocketChannel.class)
3 .handler(new ChannelInitializer<SocketChannel>() {
4 @Override
5 protected void initChannel(SocketChannel socketChannel) throws Exception {
6 socketChannel.pipeline().addLast(new IdleStateHandler(0,4,0, TimeUnit.SECONDS));
7 socketChannel.pipeline().addLast(new StringEncoder());
8 socketChannel.pipeline().addLast(new HeartBeatClientHandler());
9 }
10 });
11
- 自定义处理类Handler继承ChannlInboundHandlerAdapter,实现自定义userEventTrigger()方法,如果出现超时时间就会被触发,包括读空闲超时或者写空闲超时;
1 public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
2 System.out.println("客户端循环心跳监测发送: "+new Date());
3 if (evt instanceof IdleStateEvent){
4 IdleStateEvent event = (IdleStateEvent)evt;
5 if (event.state()== IdleState.WRITER_IDLE){
6 if (curTime<beatTime){
7 curTime++;
8 ctx.writeAndFlush("biubiu");
9 }
10 }
11 }
12 }
13
3.IdleStateHandler源码分析
- IdleStateHandler构造器
- readerIdleTime读空闲超时时间设定,如果channelRead()方法超过readerIdleTime时间未被调用则会触发超时事件调用userEventTrigger()方法;
- writerIdleTime写空闲超时时间设定,如果write()方法超过writerIdleTime时间未被调用则会触发超时事件调用userEventTrigger()方法;
- allIdleTime所有类型的空闲超时时间设定,包括读空闲和写空闲;
- unit时间单位,包括时分秒等;
1 public IdleStateHandler(
2 long readerIdleTime, long writerIdleTime, long allIdleTime,
3 TimeUnit unit) {
4 this(false, readerIdleTime, writerIdleTime, allIdleTime, unit);
5 }
- 心跳检测也是一种Handler,在启动时添加到ChannelPipeline管道中,当有读写操作时消息在其中传递;
socketChannel.pipeline().addLast(new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));
- IdleStateHandler的channelActive()方法在socket通道建立时被触发
1 @Override
2 public void channelActive(ChannelHandlerContext ctx) throws Exception {
3 initialize(ctx);
4 super.channelActive(ctx);
5 }
- channelActive()方法调用Initialize()方法,根据配置的readerIdleTime,WriteIdleTIme等超时事件参数往任务队列taskQueue中添加定时任务task ;
1 private void initialize(ChannelHandlerContext ctx) {
2 switch (state) {
3 case 1:
4 case 2:
5 return;
6 }
7
8 state = 1;
9 initOutputChanged(ctx);
10
11 lastReadTime = lastWriteTime = ticksInNanos();
12 if (readerIdleTimeNanos > 0) {
13 readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx),
14 readerIdleTimeNanos, TimeUnit.NANOSECONDS);
15 }
16 if (writerIdleTimeNanos > 0) {
17 writerIdleTimeout = schedule(ctx, new WriterIdleTimeoutTask(ctx),
18 writerIdleTimeNanos, TimeUnit.NANOSECONDS);
19 }
20 if (allIdleTimeNanos > 0) {
21 allIdleTimeout = schedule(ctx, new AllIdleTimeoutTask(ctx),
22 allIdleTimeNanos, TimeUnit.NANOSECONDS);
23 }
24 }
- 定时任务添加到对应线程EventLoopExecutor对应的任务队列taskQueue中,在对应线程的run()方法中循环执行
- 用当前时间减去最后一次channelRead方法调用的时间判断是否空闲超时;
- 如果空闲超时则创建空闲超时事件并传递到channelPipeline中;
1 protected void run(ChannelHandlerContext ctx) {
2 long nextDelay = readerIdleTimeNanos;
3 if (!reading) {
4 nextDelay -= ticksInNanos() - lastReadTime;
5 }
6
7 if (nextDelay <= 0) {
8 // Reader is idle - set a new timeout and notify the callback.
9 readerIdleTimeout = schedule(ctx, this, readerIdleTimeNanos, TimeUnit.NANOSECONDS);
10
11 boolean first = firstReaderIdleEvent;
12 firstReaderIdleEvent = false;
13
14 try {
15 IdleStateEvent event = newIdleStateEvent(IdleState.READER_IDLE, first);
16 channelIdle(ctx, event);
17 } catch (Throwable t) {
18 ctx.fireExceptionCaught(t);
19 }
20 } else {
21 // Read occurred before the timeout - set a new timeout with shorter delay.
22 readerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
23 }
24 }
- 在管道中传递调用自定义的userEventTrigger()方法
1 protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
2 ctx.fireUserEventTriggered(evt);
3 }
4.总结
- IdleStateHandler心跳检测主要是通过向线程任务队列中添加定时任务,判断channelRead()方法或write()方法是否调用空闲超时,如果超时则触发超时事件执行自定义userEventTrigger()方法;
- Netty通过IdleStateHandler实现最常见的心跳机制不是一种双向心跳的PING-PONG模式,而是客户端发送心跳数据包,服务端接收心跳但不回复,因为如果服务端同时有上千个连接,心跳的回复需要消耗大量网络资源;如果服务端一段时间内内有收到客户端的心跳数据包则认为客户端已经下线,将通道关闭避免资源的浪费;在这种心跳模式下服务端可以感知客户端的存活情况,无论是宕机的正常下线还是网络问题的非正常下线,服务端都能感知到,而客户端不能感知到服务端的非正常下线;
- 要想实现客户端感知服务端的存活情况,需要进行双向的心跳;Netty中的channelInactive()方法是通过Socket连接关闭时挥手数据包触发的,因此可以通过channelInactive()方法感知正常的下线情况,但是因为网络异常等非正常下线则无法感知;