Netty 断开重连

1. 前言

上节,我们主要讲解了 Netty 的心跳检测机制,其中核心目的是提高性能。本节我们主要讲解的是 Netty 长连接的稳定性。

2. 学习目的

TCP 协议下,数据是可以双向传递,其实也就是全双工协议,通俗点来说就是长连接,只要客户端和服务端连接之后,双方可以正常通行,那么长连接是否存在什么不稳定性呢?

在长连接情况下,通常面临的情况就是网络问题,网络抖动造成的连接假死,举个例子:其实客户端和服务端的 TCP 连接已经断开,但是双方没有监听到,认为该连接仍然是有效的。

这样的问题会导致以下几个后果,如下所示:

  1. 客户端往服务端发送消息时,由于连接已经断开,会导致请求超时,影响用户体验;
  2. 服务端往客户端推送消息时,由于连接已经断开,导致连接推送失败;
  3. 每条连接都消耗 cpu 和内存资源,大量的假死会导致服务器资源消耗,导致服务器卡顿甚至宕机。

3. 连接面临问题及解决方案

图片描述

4. 连接假死

4.1 产生的原因

连接已经断开,但是程序没有捕捉的到,认为连接还存在,产生的原因大致如下:

  1. 应用程序内部线程堵塞,导致数据读写也会堵塞;
  2. 网络抖动,数据丢包等,发送方一种发送不出数据,接收方也收不到数据,连接就一直的耗着;
  3. 公网相对内网来说不是很稳定,受到的干扰更多,故障的概率也会增大。

4.2 解决办法

问题: 服务端 5 秒钟没用读取数据事件,那么是否一定是假死呢?

回答: 不一定,主要有两种情况,①连接假死;②连接空闲。

针对连接假死的解决方案

主要是通过心跳检测去监控,如果指定时间之内,服务端没有收到客户端的数据,则主动断开连接,杜绝了连接假死现象。

针对连接空闲状态的解决方案

情况一: 如果对通信的实时性要求不高,并且对性能要求很高的情况,可以直接断开连接,等待有需要的时候,再重新连接(这个是上节已经讲解过了,适合客户端主动的业务场景,比如:IM);
情况二: 如果对通信的实时性要求很高,则不能断开连接(比如:消息推送),为了保证连接能够存活而不被心跳检测机制自动断开。

针对情况二的解决方案如下:

  1. 定时发送空包,并且时间间隔小于心跳检测时间间隔,保证连接存活;
  2. 如果连接真的断开了,则客户端监听事件 channelInactive () 里面实现断开重连;

总结,这种模式的好处有两点,①保证连接能够长时间存活,避免错过重要消息;②避免连接空闲时,频繁的断开和重连,浪费资源。

其中,心跳检测上节以及详细讲解了,这里主要讲解一下发送空包数据和断开重连如何实现。

5. 代码实现

5.1 服务端心跳检测

实例:

ChannelPipeline pipeline = ch.pipeline();
//5秒钟之内没有 读事件 则断开连接
pipeline.addLast(new ReadTimeoutHandler(5, TimeUnit.SECONDS));

//字符串解码器
pipeline.addLast(new StringDecoder());

//字符串编码器
pipeline.addLast(new StringEncoder());

//业务Handler
pipeline.addLast(new HeartBeatHandler());

代码说明:

服务端主要是监听读事件,每隔 5 秒读取不到数据,则认为连接无效,主动断开连接。

5.2 客户端定时发送空包

实例:

public class HeartBeatTimerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //发送空包
        scheduleSendHeartBeat(ctx);
    }

    private void scheduleSendHeartBeat(ChannelHandlerContext ctx) {
        ctx.executor().schedule(() -> {
            if (ctx.channel().isActive()) {
                //发送空包(定义一个实体)
                ctx.writeAndFlush(Unpooled.EMPTY_BUFFER);
            }

        }, 3, TimeUnit.SECONDS);
    }
}

代码说明:

  1. 借助 EventLoop 的定时线程池去实现每隔 3 秒钟发送一个空包;
  2. 空包数据,自定义一个实体即可;
  3. 主要的是空包的时间间隔(3s)一定要小于心跳监听的时间间隔(5s)。

5.3 客户端断开重连

实例:

//字符串解码器
pipeline.addLast(new StringDecoder());
//字符串编码器
pipeline.addLast(new StringEncoder());
//业务Handler,需要传递“bootstrap”
pipeline.addLast(new ClientHandler(bootstrap));
public class ClientHandler extends ChannelInboundHandlerAdapter {
    private Bootstrap bootstrap;
    ClientHandler(Bootstrap bootstrap){
        this.bootstrap=bootstrap;
    }
	
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
		bootstrap.connect("127.0.0.1",80).sync();
    }
}

代码说明:

  1. 连接断开时,客户端的 Handler 的 channelInactive () 会监听的到,在该方法里面实现断开重连;
  2. Handler 必须传递 bootstrap。

6. 小结

本节主要讲解了基于心跳检测的基础上实现了空包发送和断开重连的功能,主要核心意图有两个

  1. 空包发送: 让连接能够长时间的存活,而避免空闲连接收到心跳检测的干扰;同时还避免了心跳检测导致的频繁的断开和重连,导致资源浪费;
  2. 断开重连: 让连接一直在线,保证了连接的稳定性。