Netty——心跳机制

 

前言

所谓心跳, 即在 TCP 长连接中, 客户端和服务器之间定期发送的一种特殊的数据包, 通知对方自己还在线, 以确保 TCP 连接的有效性。

心跳包还有另一个作用,经常被忽略,即:一个连接如果长时间不用,防火墙或者路由器就会断开该连接

 

操作系统内核心跳

Netty 是 基于 TCP 协议开发的,在四层协议 TCP 协议的实现中也提供了 keepalive 报文用来探测对端是否可用。

TCP 层将在定时时间到后发送相应的 KeepAlive 探针以确定连接可用性。

tcp-keepalive操作系统内核支持,但是默认不开启,应用需要自行开启,开启之后有三个参数会生效,来决定一个 keepalive 的行为。

net.ipv4.tcp_keepalive_time = 7200
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_intvl = 75
  • tcp_keepalive_time: 在 TCP 保活打开的情况下,最后一次数据交换到 TCP 发送第一个保活探测包的间隔,即允许的持续空闲时长,或者说每次正常发送心跳的周期,默认值为7200s(2h)
  • tcp_keepalive_probes: 在 tcp_keepalive_time 之后,没有接收到对方确认,继续发送保活探测包次数,默认值为9(次)
  • tcp_keepalive_intvl:在 tcp_keepalive_time 之后,没有接收到对方确认,继续发送保活探测包的发送频率,默认值为75s 

可以通过如下命令查看系统tcp-keepalive参数配置:

sysctl -a | grep keepalive

cat /proc/sys/net/ipv4/tcp_keepalive_time

sysctl net.ipv4.tcp_keepalive_time

Netty 中设置 tcp-keepalive :

bootstrap.group(boss, worker).channel(NioServerSocketChannel.class)
         .childHandler(new ChannelInitializer<SocketChannel>() {
               @Override
               protected void initChannel(SocketChannel channel) throws Exception {
                   ProtostuffCodecUtil util = new ProtostuffCodecUtil();
                   ChannelPipeline pipeline = channel.pipeline();
                   pipeline.addLast(new ProtostuffEncoder(util));
                   pipeline.addLast(new ProtostuffDecoder(util));
                   pipeline.addLast(handler);
               }
           }).option(ChannelOption.SO_BACKLOG, 1024)
             .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
             .childOption(ChannelOption.SO_KEEPALIVE, true)
             .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)

TCP KeepAlive 是用于检测连接的死活,而心跳机制则附带一个额外的功能:检测通讯双方的存活状态。两者听起来似乎是一个意思,但实际上却大相径庭。

考虑一种情况,某台服务器因为某些原因导致负载超高,CPU 100%,无法响应任何业务请求,但是使用 TCP 探针则仍旧能够确定连接状态,

这就是典型的连接活着但业务提供方已死的状态,对客户端而言,这时的最好选择就是断线后重新连接其他服务器,

而不是一直认为当前服务器是可用状态一直向当前服务器发送些必然会失败的请求。

所以基础协议对应用来说不是那么尽善尽美,一个 Netty 服务端可能会面临上万个连接,如何去维护这些连接是应用应该去处理的事情。

 

应用实现心跳

Netty心跳检测说明

1、在 Netty 中, 实现心跳机制的关键是 IdleStateHandler, 看下它的构造器:

public IdleStateHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds) {
    this((long)readerIdleTimeSeconds, (long)writerIdleTimeSeconds, (long)allIdleTimeSeconds, TimeUnit.SECONDS);
}

public IdleStateHandler(long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit) {
    this(false, readerIdleTime, writerIdleTime, allIdleTime, unit);
}

三个参数的含义如下:

  • readerIdleTimeSeconds: 读超时。即当在指定的时间间隔内没有从 Channel 读取到数据时, 会触发一个 READER_IDLE IdleStateEvent 事件。
  • writerIdleTimeSeconds: 写超时。 即当在指定的时间间隔内没有数据写入到 Channel 时, 会触发一个 WRITER_IDLE IdleStateEvent 事件。
  • allIdleTimeSeconds: 读/写超时。 即当在指定的时间间隔内没有读或写操作时, 会触发一个 ALL_IDLE IdleStateEvent 事件。
2、要实现Netty服务端心跳检测机制需要在服务器端的ChannelInitializer中加入如下的代码:

pipeline.addLast(new IdleStateHandler(3, 0, 0, TimeUnit.SECONDS));

 

Netty心跳检测代码示例

服务端Server类:

public class HeartBeatServer {

    public static void main(String[] args) throws Exception {
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup worker = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(boss, worker)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast("decoder", new StringDecoder());
                            pipeline.addLast("encoder", new StringEncoder());
                            //IdleStateHandler的readerIdleTime参数指定超过5秒还没收到客户端的连接,
                            //会触发IdleStateEvent事件并且交给下一个handler处理,下一个handler必须
                            //实现userEventTriggered方法处理对应事件
                            pipeline.addLast(new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));
                            pipeline.addLast(new HeartBeatServerHandler());
                        }
                    });
            System.out.println("netty server start。。");
            ChannelFuture future = bootstrap.bind(9000).sync();
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            worker.shutdownGracefully();
            boss.shutdownGracefully();
        }
    }
}

服务端Handler处理类:

@Slf4j
public class HeartBeatServerHandler extends SimpleChannelInboundHandler {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("server channelActive");
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        String message = (String) msg;
        if ("heartbeat".equals(message)) {
            log.info(ctx.channel().remoteAddress() + "===>server: " + message);
            ctx.write("heartbeat");
            ctx.flush();
        }
    }

    /**
     * 如果5s没有读请求,则向客户端发送心跳
     * @param ctx
     * @param evt
     * @throws Exception
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (IdleState.READER_IDLE.equals((event.state()))) {
                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);
        ctx.close();
    }
}

客户端Client类:

public class HeartBeatClient {
    public static void main(String[] args) throws Exception {
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast("decoder", new StringDecoder());
                            pipeline.addLast("encoder", new StringEncoder());
                 pipeline.addLast(new IdleStateHandler(0, 4, 0, TimeUnit.SECONDS)); pipeline.addLast(
new HeartBeatClientHandler()); } });         ChannelFuture future = bootstrap.connect("127.0.0.1", 9000).sync();
        future.channel().writeAndFlush("Hello world, i'm online");
        future.channel().closeFuture().sync();
}
catch (Exception e) { e.printStackTrace(); } finally { eventLoopGroup.shutdownGracefully(); } }

客户端Handler类:

@Slf4j
public class HeartBeatClientHandler extends SimpleChannelInboundHandler {

    /** 客户端请求的心跳命令 */
    private static final ByteBuf HEARTBEAT_SEQUENCE =
            Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("heartbeat", CharsetUtil.UTF_8));

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        String message = (String)msg;
        if("heartbeat".equals(message)) {
            log.info(ctx.channel().remoteAddress() + "===>client: " + msg);
        }
    }

    /**
     * 如果4s没有收到写请求,则向服务端发送心跳请求
     * @param ctx
     * @param evt
     * @throws Exception
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if(evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if(IdleState.WRITER_IDLE.equals(event.state())) {
                ctx.writeAndFlush(HEARTBEAT_SEQUENCE.duplicate()).addListener(ChannelFutureListener.CLOSE_ON_FAILURE) ;
            }
        }
        super.userEventTriggered(ctx, evt);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("client channelActive");
        ctx.fireChannelActive();
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        log.info("Client is close");
    }
}

解释一下代码的逻辑:

服务端添加了以下代码:每隔5s检查一下是否有读事件发生,如果没有就处罚 handler 中的 userEventTriggered(ChannelHandlerContext ctx, Object evt)逻辑。

pipeline.addLast(new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));

客户端添加了以下代码:每隔4s检查一下是否有写事件,如果没有就触发 handler 中的 userEventTriggered(ChannelHandlerContext ctx, Object evt)逻辑。

pipeline.addLast(new IdleStateHandler(0, 4, 0, TimeUnit.SECONDS));

 

Netty心跳源码分析

心跳检测也是一种 Handler,在启动时添加到 ChannelPipeline 管道中,当有读写操作时消息在其中传递。

1、首先我们看到 IdleStateHandler 继承了 ChannelDuplexHandler:

public class IdleStateHandler extends ChannelDuplexHandler {
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (this.readerIdleTimeNanos > 0L || this.allIdleTimeNanos > 0L) {
            this.reading = true;
            this.firstReaderIdleEvent = this.firstAllIdleEvent = true;
        }

        ctx.fireChannelRead(msg);
    }
}

表明 IdleStateHandler 也可以同时处理入站和出站事件,所以可以同时监控读事件和写事件。

2、然后IdleStateHandler 的 channelActive() 方法在 socket 通道建立时被触发:

public void channelActive(ChannelHandlerContext ctx) throws Exception {
    this.initialize(ctx);
    super.channelActive(ctx);
}

3、其中 channelActive() 方法调用 Initialize() 方法,根据配置的 readerIdleTimewriteIdleTIme 等超时事件参数往任务队列 taskQueue 中添加定时任务 task:

private void initialize(ChannelHandlerContext ctx) {
  // Avoid the case where destroy() is called before scheduling timeouts.
  // See: https://github.com/netty/netty/issues/143
  //这里判断状态,避免重复初始化
  switch (state) {
    case 1:
    case 2:
      return;
  }

  state = 1;

  EventExecutor loop = ctx.executor();
    //初始化最后一次读写时间
  lastReadTime = lastWriteTime = System.nanoTime();
  // 根据用户设置的读空闲时间启动一个定时任务,读空闲时间为频率执行
  // 这里的 schedule 方法会调用 eventLoop 的 schedule 方法,将定时任务添加进队列中
  if (readerIdleTimeNanos > 0) {
    readerIdleTimeout = loop.schedule(
      new ReaderIdleTimeoutTask(ctx),
      readerIdleTimeNanos, TimeUnit.NANOSECONDS);
  }
  // 根据用户设置的写空闲时间启动一个定时任务,写空闲时间为频率执行
  if (writerIdleTimeNanos > 0) {
    writerIdleTimeout = loop.schedule(
      new WriterIdleTimeoutTask(ctx),
      writerIdleTimeNanos, TimeUnit.NANOSECONDS);
  }
  // 根据用户设置的读写空闲时间启动一个定时任务,读写空闲时间为频率执行
  if (allIdleTimeNanos > 0) {
    allIdleTimeout = loop.schedule(
      new AllIdleTimeoutTask(ctx),
      allIdleTimeNanos, TimeUnit.NANOSECONDS);
  }
}

上面有一个 state 字段:

private byte state; 
0:初始状态,1:已经初始化, 2: 已经销毁。

上面的 switch 判断只有当前状态为 0 即初始化状态的时候才执行下面的操作,避免多次提交定时任务。

定时任务添加到对应线程 EventLoopExecutor 对应的任务队列 taskQueue 中,在对应线程的 run() 方法中循环执行:

  • 用当前时间减去最后一次 channelRead 方法调用的时间判断是否空闲超时;
  • 如果空闲超时则创建空闲超时事件并传递到 channelPipeline 中。

只要给定的参数大于0,就创建一个定时任务,每个事件都创建。同时,将 state 状态设置为 1,防止重复初始化。

读事件处理:ReaderIdleTimeoutTask

private final class ReaderIdleTimeoutTask implements Runnable {

  private final ChannelHandlerContext ctx;

  ReaderIdleTimeoutTask(ChannelHandlerContext ctx) {
    this.ctx = ctx;
  }

  @Override
  public void run() {
    if (!ctx.channel().isOpen()) {
      return;
    }
    // nextDelay = 当前时间-最后一次时间
    long nextDelay = readerIdleTimeNanos;
    if (!reading) {
      nextDelay -= System.nanoTime() - lastReadTime;
    }

    if (nextDelay <= 0) {
      // 重新定义readerIdleTimeout schedule,与initialize方法设置的相同,继续执行定时任务
      readerIdleTimeout =
        ctx.executor().schedule(this, readerIdleTimeNanos, TimeUnit.NANOSECONDS);
      try {
        // event = new IdleStateEvent(IdleState.READER_IDLE, true),将event设置为读空闲
        IdleStateEvent event = newIdleStateEvent(IdleState.READER_IDLE, firstReaderIdleEvent);
        if (firstReaderIdleEvent) {
          firstReaderIdleEvent = false;
        }
        //channelIdle的主要工作就是将evt传输给下一个Handler
        channelIdle(ctx, event);
      } catch (Throwable t) {
        ctx.fireExceptionCaught(t);
      }
    } else {
      // 如果nextDelay>0,则说明客户端在规定时间内已经写入数据了
      // 重新定义readerIdleTimeout schedule,以nextDelay为执行频率
      readerIdleTimeout = ctx.executor().schedule(this, nextDelay, TimeUnit.NANOSECONDS);
    }
  }
}

nextDelay的初始化值为超时秒数readerIdleTimeNanos,如果检测的时候没有正在读,就计算多久没读了:

nextDelay = nextDelay - 当前时间 - 上次读取时间

如果小于0,说明左边的 readerIdleTimeNanos 小于空闲时间(当前时间 - 上次读取时间),表示已经超时,
创建 IdleStateEvent 事件,IdleState 枚举值为 READER_IDLE,然后调用 channelIdle(ctx, event) 方法分发给下一个 ChannelInboundHandler。

总的来说,每次读取操作都会记录一个时间,定时任务时间到了,会计算当前时间和最后一次读的时间的间隔,如果间隔超过了设置的时间,就触发 UserEventTriggered() 方法。

 

Netty心跳机制总结

Netty 通过 IdleStateHandler 实现最常见的心跳机制不是一种双向心跳的 PING-PONG 模式,而是客户端发送心跳数据包,服务端接收心跳但不回复,

因为如果服务端同时有上千个连接,心跳的回复需要消耗大量网络资源。

如果服务端一段时间内一直没收到客户端的心跳数据包则认为客户端已经下线,将通道关闭避免资源的浪费。在这种心跳模式下服务端可以感知客户端的存活情况,

无论是宕机的正常下线还是网络问题的非正常下线,服务端都能感知到,而客户端不能感知到服务端的非正常下线。

要想实现客户端感知服务端的存活情况,需要进行双向的心跳;Netty 中的 channelInactive() 方法是通过 Socket 连接关闭时挥手数据包触发的

因此可以通过 channelInactive() 方法感知正常的下线情况,但是因为网络异常等非正常下线则无法感知。

上面的示例只做了客户端和服务端双向心跳测试,大家可以补充一下如果一段时间内都收到的是客户端的心跳包则判定连接无效关闭连接的逻辑。

 

 

引用:

posted on 2021-05-22 18:52  曹伟雄  阅读(2170)  评论(0编辑  收藏  举报

导航