Netty实战:心跳检测

文章目录

        • 1. 相关网络知识
          • 1.1 TCP长连接和短连接
          • 1.2 TCP Keepalive
        • 2. Netty Heart Beat
        • 3. 基于Netty实现应用层心跳检测、断线重连
        • 4. 总结

1. 相关网络知识

1.1 TCP长连接和短连接

TCP区别于UDP最重要的特点是TCP必须建立在可靠的连接之上,连接的建立和释放就是握手和挥手的过程。三次握手为连接的建立过程,握手失败则连接建立失败。四次挥手为连接的完整释放过程,也会发生某个消息丢失或者超时的情况,有一方主动发送FIN消息即表示连接即将释放。

长连接,也叫持久连接,在TCP层握手成功后,不立即断开连接,并在此连接的基础上进行多次消息(包括心跳)交互,直至连接的任意一方(客户端OR服务端)主动断开连接,此过程称为一次完整的长连接。HTTP 1.1相对于1.0最重要的新特性就是引入了长连接

短连接,顾名思义,与长连接的区别就是,客户端收到服务端的响应后,立刻发送FIN消息,主动释放连接。也有服务端主动断连的情况,凡是在一次消息交互(发请求-收响应)之后立刻断开连接的情况都称为短连接。注:短连接是建立在TCP协议上的,有完整的握手挥手流程,区别于UDP协议。

在默认情况下,在建立TCP长连接之后,空闲时刻客户端和服务端不会互相发送数据包确认连接。假如有一端发生异常而掉线(如死机、防火墙拦截包、服务器爆炸),另一端若不进行连接确认,则会一直消耗资源。所以服务器端要做到快速感知失败,减少无效链接操作,这就有了TCP的Keepalive(保活探测)机制。

1.2 TCP Keepalive

TCP Keepalive通过定时发送Keepalive探测包来探测连接的对端是否存活。

简单的说当客户端等待超过一定时间后自动给服务端发送一个空的报文,如果对方回复了这个报文证明连接还存活着,如果对方没有报文返回且进行了多次尝试都是一样,那么就认为连接已经丢失,客户端就没必要继续保持连接了。如果没有这种机制就会有很多空闲的连接占用着系统资源。但Keepalive会额外产生一些网络数据包外,这些包将加大网络流量,对路由器和防火墙造成一定的负担。

KeepAlive并不是TCP协议规范的一部分,但在几乎所有的TCP/IP协议栈(不管是Linux还是Windows)中,都实现了KeepAlive功能。

KeepAlive默认不是开启的,如果想使用KeepAlive,需要在你的应用中设置SO_KEEPALIVE才可以生效。

Linux中 /etc/sysctl.conf 的全局配置:

net.ipv4.tcp_keepalive_time=7200
net.ipv4.tcp_keepalive_intvl=75
net.ipv4.tcp_keepalive_probes=9

keepalive 保活机制, 但是使用它有几个缺点:

  • 它不是 TCP 的标准协议, 并且是默认关闭的.

  • TCP keepalive 机制依赖于操作系统的实现, 默认的 keepalive 心跳时间是 两个小时, 并且对 keepalive 的修改需要系统调用(或者修改系统配置), 灵活性不够.

  • TCP keepalive 与 TCP 协议绑定, 因此如果需要更换为 UDP 协议时, keepalive 机制就失效了.

虽然使用 TCP 层面的 keepalive 机制比自定义的应用层心跳机制节省流量, 但是基于上面的几点缺点, 一般的实践中, 人们大多数都是选择在应用层上实现自定义的心跳.

Http keep-alive
每个http请求都要求打开一个tpc socket连接,并且使用一次之后就断开这个tcp连接。
Http 协议中有一个keep-alive的状态,使用keep-alive可以改善这种状态,即在一次TCP连接中可以持续发送多份数据而不会断开连接。通过使用keep-alive机制,http 中keep-alive的作用就是复用tcp,可以减少tcp连接建立次数,也意味着可以减少TIME_WAIT状态连接,以此提高性能和提高httpd服务器的吞吐率(更少的tcp连接意味着更少的系统内核调用,socket的accept()和close()调用)。

其原理是OS层面的连接池技术,在一个TCP连接上进行多次的HTTP请求从而提高性能。

浏览器在HTTP1.1下的keep-alive都是默认开启的。

HTTP位于网络协议栈的应用层,而TCP位于网络协议栈的传输层,两者的KEEP-ALIVE虽然名称相同,但是作用不同。HTTP是为了重用TCP,避免每次请求,都重复创建TCP;而TCP的KEEP-ALIVE是一种保活机制,检测对端是否依然存活。

2. Netty Heart Beat

正是因为TCP的keepalive的缺点,大多数情况下都是选择在应用层上实现自定义的心跳。应用层心跳包不依赖于传输层协议,无论传输层协议是TCP还是UDP都可以用。并且心跳包可以定制,可以应对更复杂的情况或传输一些额外信息。原理和TCP的keepalive类似。

工作原理:在服务器和客户端之间一定时间内没有数据交互时, 即处于 idle(No read/No write) 状态时, 客户端或服务器会发送一个特殊的数据包给对方, 当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文, 回应发送方, 此即一个 PING-PONG 交互. 自然地, 当某一端收到心跳消息后, 就知道了对方仍然在线, 这就确保 TCP 连接的有效性。

有以下几个问题:

  1. 当连接处于空闲后, 如何对连接的空闲时间进行计时,多久发送心跳包?
  2. 心跳包的发起者是谁?
  3. 如何判断某一端已经关闭了连接?

Netty提供了IdleStateHandler空闲连接处理器,IdleStateHandler会对关联的Channel进行空闲计时,如果空闲的时间超过阈值,则会触发userEventTriggered()方法,在此方法内部实现心跳包的发送,也就解答了上面的第一个问题。至于IdleStateHandler的实现原理后面再说.

IdleStateHandler提供了三种类型的空闲连接计时,io.netty.handler.timeout.IdleState枚举类型定义如下:

public enum IdleState {
    /** * No data was received for a while. */
    READER_IDLE,
    /** * No data was sent for a while. */
    WRITER_IDLE,
    /** * No data was either received or sent for a while. */
    ALL_IDLE
}

IdleStateHandler类中对应三个超时阈值设定:

  • readerIdleTimeNanos:读超时. 即当在指定的时间间隔内没有从 Channel 读取到数据时, 会触发一个 READER_IDLE 的 IdleStateEvent 事件
  • writerIdleTimeNanos:写超时. 即当在指定的时间间隔内没有数据写入到 Channel 时, 会触发一个 WRITER_IDLE 的 IdleStateEvent 事件.
  • allIdleTimeNanos:读/写超时. 即当在指定的时间间隔内没有读或写操作时, 会触发一个 ALL_IDLE 的 IdleStateEvent 事件.

上面的所说的事件,也就是触发userEventTriggered()方法。我们心跳包的发送逻辑放在此方法内部,那么在客户端,还是服务端?如果是放在服务端,那么需要服务端进行一次写操作,当客户端有回应,服务端在进行一次确认。因此决定将发送心跳包放在客户端。主要是可以减轻服务端的压力。

交互的过程如下:

  1. 客户端成功连接服务端。

  2. 在客户端中的ChannelPipeline中加入IdleStateHandler,设置写事件触发时间为5s.

  3. 客户端超过5s未写数据,触发写事件,向服务端发送心跳包

  4. 同样,服务端要对心跳包做出响应

  5. 超过三次,如果15s内没有收到来自对方心跳信息,可以认为对方端挂了,可以close链路。

  6. 客户端恢复正常,发现链路已断,重新连接服务端。

3. 基于Netty实现应用层心跳检测、断线重连

客户端添加IdleStateHandler心跳检测处理器,并添加自定义处理Handler类实现userEventTriggered()方法作为超时事件的逻辑处理。

客户端:

public class HeartBeatClientHandler extends SimpleChannelInboundHandler<String> {

  private ClientStarter clientStarter;

  public HeartBeatClientHandler(ClientStarter clientStarter) {
    this.clientStarter = clientStarter;
  }

  @Override
  protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
    if (msg.equals("PONG")) {
      System.out.println("receive form server PONG");
    }
    ReferenceCountUtil.release(msg);
  }

  @Override
  public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
    if(evt instanceof IdleStateEvent) {
      IdleState state = ((IdleStateEvent)evt).state();
      if(state == IdleState.ALL_IDLE) {
        ctx.writeAndFlush("PING");
        System.out.println("send PING");
      }
    }
    super.userEventTriggered(ctx, evt);
  }
  @Override
  public void channelInactive(ChannelHandlerContext ctx) throws Exception {
    super.channelInactive(ctx);
    System.err.println("客户端与服务端断开连接,断开的时间为:"+(new Date()).toString());
    // 定时线程 断线重连
    final EventLoop eventLoop = ctx.channel().eventLoop();
    eventLoop.schedule(() -> clientStarter.connect(), 2, TimeUnit.SECONDS);
  }

  @Override
  public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    if(cause instanceof IOException) {
      System.out.println("server "+ctx.channel().remoteAddress()+"关闭连接");
    }
  }
}
public class ClientStarter {

    private Bootstrap bootstrap;
    private int times = 0;

    public ClientStarter(Bootstrap bootstrap) {
        this.bootstrap = bootstrap;
        ClientStarter clientStarter = this;
        bootstrap.group(new NioEventLoopGroup())
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new StringEncoder());
                        ch.pipeline().addLast(new StringDecoder());
                        ch.pipeline().addLast(new IdleStateHandler(0, 0, 4));
                        ch.pipeline().addLast(new HeartBeatClientHandler(clientStarter));
                    }
                });
    }

    public static void main(String[] args) {
        ClientStarter starter = new ClientStarter(new Bootstrap());
        starter.connect();
    }

    public void connect() {
        ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("localhost", 1111));
        channelFuture.addListener(future ->{
                if (future.isSuccess()) {
                    System.out.println("connect to server success");
                } else {
                    System.out.println("connect to server failed,try times:" + ++times);
                    connect();
                }
        });
    }
}

服务端实现:

public  class HeartBeatServerHandller extends SimpleChannelInboundHandler {
  //连续超过N次未收到client的ping消息,那么关闭该通道,等待client重连
  private static final int MAX_UN_REC_PING_TIMES = 3 ;
  private int failTimes = 0;
  //收到一个client的ping消息的个数
  private int allPings = 0;
  public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
    if (evt instanceof IdleStateEvent) {
      IdleStateEvent event = (IdleStateEvent) evt;
      if (event.state()== IdleState.READER_IDLE){
        System.out.println("5秒内没有收到"+ctx.channel().remoteAddress()+" PING");
        // 失败计数器次数大于等于3次的时候,关闭链接,等待client重连
        if (failTimes >= MAX_UN_REC_PING_TIMES) {
          System.out.println("15秒内没有收到"+ctx.channel().remoteAddress()+"PING ,即将关闭连接!");
          ctx.close();
        } else {
          // 失败计数器加1
          failTimes++;
        }
      }else {
        super.userEventTriggered(ctx,evt);
      }
    }
  }

  @Override
  protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
    if (msg!=null && msg.equals("PING")){
      System.out.println("客户端"+ctx.channel().remoteAddress()+"第 "+(++allPings)+" 个PING");
      ctx.writeAndFlush("PONG");
    }
  }
  @Override
  public void channelActive(ChannelHandlerContext ctx) throws Exception {
    super.channelActive(ctx);
    System.out.println(ctx.channel().remoteAddress()+"客户端已连接");

  }

  @Override
  public void channelInactive(ChannelHandlerContext ctx) throws Exception {
    System.out.println(ctx.channel().remoteAddress()+"客户端已断开");
  }
  @Override
  public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    if(cause instanceof IOException) {
      System.out.println("client "+ctx.channel().remoteAddress()+"强制关闭连接");
    }
  }
}
public class ServerStarter {

  public static void main(String[] args) {
    EventLoopGroup bossgroup = new NioEventLoopGroup();
    EventLoopGroup workergroup = new NioEventLoopGroup();
    try {
      ServerBootstrap bootstrap = new ServerBootstrap();
      bootstrap.group(bossgroup, workergroup)
          .channel(NioServerSocketChannel.class)
          .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
              ch.pipeline().addLast(new IdleStateHandler(5, 0, 0));
              ch.pipeline().addLast(new StringDecoder());
              ch.pipeline().addLast(new StringEncoder());
              ch.pipeline().addLast(new HeartBeatServerHandller());
            }
          });
      // 服务器绑定端口监听
      ChannelFuture future = bootstrap.bind(1111).sync();
      System.out.println("server start ,port: "+1111);
      // 监听服务器关闭监听,此方法会阻塞
      future.channel().closeFuture().sync();
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      bossgroup.shutdownGracefully();
      workergroup.shutdownGracefully();
    }
  }
}

4. 总结

本文从TCP的长连接和短连接开始说起,接着讲到TCP自带Keepalive机制,以及它的缺陷。顺便提到了HTTP keep-alive的机制。最后基于Netty IdleStateHandler实现基于应用的心跳检测功能。下一篇文章分析IdleStateHandler的原理。

你可能感兴趣的:(Netty系列,Netty源码解读)