网络问题

      下面是网络应用程序中普遍会遇到的一个问题:连接假死。
      连接假死的现象是:在某一端(服务端或者客户端)看来,底层的 TCP 连接已经断开,但是应用程序并没有捕获到,因此会认为这条连接仍然是存在的。从 TCP 层面来说,只有收到四次握手数据包或者一个 RST 数据包,才标识连接的状态已断开。

      连接假死会带来以下两大问题。

  1. 对于服务端来说,因为每个连接都会耗费 CPU 和内存资源,大量假死的连接会逐渐耗光服务器的资源,最终导致性能逐渐下降,程序崩溃;
  2. 对于客户端来说,连接假死会造成发送数据超时,影响用户体验。

通常,连接假死由以下几个原因造成。

  1. 应用程序出现线程堵塞,无法进行数据的读写。
  2. 客户端或者服务端网络相关的设备出现故障,比如网卡、机房故障。
  3. 公网丢包。公网环境相对内网而言,非常容易出现丢包、网络抖动等现象,如果在一段时间内用户接入的网络连续出现丢包现象,那么对客户端来说,数据一直发送不出去;而服务端也一直收不到客户端的数据,连接就一直耗着。

服务端空闲检测

      对于服务端来说,客户端的连接如果出现假死,那么服务端将无法收到客户端的数据。也就是说,如果能一直收到客户端发来的数据,则说明这个连接时活的。因此,服务端对于连接假死的应对策略就是空闲检测。

空闲检测就是每隔一段时间,检测这段时间内是否有数据读写。服务端只需要检测一点时间内,是否收到过客户端发来的数据即可,Netty 自带的 IdleStateHandler 就可以实现这个功能。

      首先,我们写一个继承自IdleStateHandler的类,来定义检测到假死连接之后的逻辑。

@ChannelHandler.Sharable
public class ImIdleStateHandler extends IdleStateHandler {
    private static final int READER_IDLE_TIME = 15;

    public ImIdleStateHandler() {
        /**
         * 第一个参数:读空闲时间,指的是这段时间内如果没有读到数据,就表示连接假死;
         * 第二个参数:写空闲时间,指的是这段时间如果没有写数据,就表示连接假死;
         * 第三个参数:读写空闲时间,指的是这段时间内如果没有产生数据读或者写,就表示连接假死;
         * 第四个参数:时间单位
         */
        super(READER_IDLE_TIME, 0, 0, TimeUnit.SECONDS);
    }

    @Override
    protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
        System.out.println(READER_IDLE_TIME + "秒内未读到数据,关闭连接");
        ctx.channel().close();
    }
}

      连接假死之后会回调channelIdle()方法,可以在此处关闭连接,同时我们需要将此handler添加到服务端 Pipeline 的最前面。

为什么需要插到最前面?因为假如插到最后面,如果这个连接读到了数据,但是在 inbound 传播的过程中出错了或者数据处理完毕就不往后传递了,那么最终 ImIdleStateHandler 就不会读到数据,会导致误判。

客户端定时发送心跳包

      服务端在一段时间内没有收到客户端的数据,这个现象产生的原因可以分为以下两种:

  1. 连接假死;
  2. 非假死状态下确实没有发送数据。

      我们只需要排除第二种可能,那么连接自然就是假死的。要排查第二种情况,我们可以在客户端定期发送数据包到服务端,通常这个数据包被称为心跳数据包。我们定义一个 Handler,定期发送心跳数据包给服务端。

public class HeartBeatTimerHandler extends ChannelInboundHandlerAdapter {

    private static final int HEARTBEAT_INTERVAL = 5;

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        scheduleSendHeartBeat(ctx);
        super.channelActive(ctx);
    }

    private void scheduleSendHeartBeat(ChannelHandlerContext ctx) {
        ctx.executor().schedule(() -> {
            if (ctx.channel().isActive()) {
                System.out.println("客户端发送心跳包");
                ctx.writeAndFlush(new HeartBeatRequestPacket());
                scheduleSendHeartBeat(ctx);
            }
        }, HEARTBEAT_INTERVAL, TimeUnit.SECONDS);
    }
}

      ctx.executor() 方法返回的是当前 Channel 绑定的 NIO 线程。NIO 线程有一个 schedule()方法,类似 JDK 的延时任务机制,可以隔一段时间执行一个任务。

服务端回复心跳与客户端空闲检测

      实现服务端心跳请求处理器:

@ChannelHandler.Sharable
public class HeartBeatRequestHandler extends SimpleChannelInboundHandler<HeartBeatRequestPacket> {

    public static final HeartBeatRequestHandler INSTANCE = new HeartBeatRequestHandler();

    protected HeartBeatRequestHandler(){}

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HeartBeatRequestPacket msg) throws Exception {
        ctx.writeAndFlush(new HeartBeatResponsePacket());
    }
}

      添加处理器至Server端Pipeline。
      将客户端心跳发送处理器添加至Client端Pipeline,同时添加空闲检测处理器:

bootstrap.group(workerGroup)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline()
                                .addLast(new ImIdleStateHandler())
                                // 其他处理器
                                .addLast(new HeartBeatTimerHandler());
Last modification:May 26th, 2024 at 12:58 am
如果觉得我的文章对你有用,请随意赞赏