关闭连接:本质是取消 Channel 在 Selelctor 的注册

关闭连接:本质是取消 Channel 在 Selelctor 的注册

Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html)

1. 主线分析

1.1 主线

关闭连接分两种:主动关闭(正常关闭)和被动关闭(异常关闭)。

  • 多路复用器(Selector)接收到 OP_READ 事件
  • 处理 OP_READ 事件:NioSocketChannel.NioSocketChannelUnsafe.read():
    • 接受数据
    • 判断接受的数据大小是否 < 0 , 如果是,说明是关闭,开始执行关闭
      • 关闭 channel(包含 cancel 多路复用器的key)
      • 清理消息:不接受新信息,fail 掉所有 queue 中消息。
      • 触发 fireChannelInactive 和 fireChannelUnregistered。
    • 读异常,同样开始执行关闭

1.2 知识点

(1)关闭连接本质

一句话概括:关闭连接的本质是取消 Channel 在 Selelctor 的注册。

  • java.nio.channels.spi.AbstractInterruptibleChannel#close
  • java.nio.channels.SelectionKey#cancel

(2)要点

  • 关闭连接,会触发 OP_READ 方法。读取字节数是 -1 代表关闭。
  • 数据读取进行时,强行关闭,触发 IO Exception,进而执行关闭。
  • Channel 的关闭包含了 SelectionKey 的 cancel。

补充1:NIO 中,如果一个客户端进程退出,为什么会触发服务器的 OP_READ 事件?

epoll 触发一个对断关闭然后在 jvm 层被包装成了一个读事件。因为 epoll 收到退出事件的时候要触发一个读操作,读到 -1 认为退出,所以 java 从实际操作角度认为 epoll 的退出事件也是读。所以简化了 java 层处理的事件数。但这个时候用 channel.read() 方法读的时候,会报 java.io.IOException: 远程主机强迫关闭了一个现有的连接。如果是主动关闭可以在触发读事件第一件事是判断是否有效,比如先读一个字节 看看是不是 -1,如果是 -1 就停止。 如果异常是 reset by peer,则表示被动关闭,一个流氓方法是所有和链接相关的异常都 catch,然后关闭这个链接,没有更好的做法了,Netty 自己也是这样做的。

转载自《关于netty你需要了解的二三事》:https://cloud.tencent.com/developer/article/1452395

2. 源码分析

连接关闭是会触发 OP_READ 事件,无论是正常还是异常关闭,都会调用 closeOnRead 关闭连接,最终调用 unsafe.close 关闭连接。

2.1 read

AbstractNioByteChannel.NioByteUnsafe#read
    -> closeOnRead
        -> AbstractUnsafe#close
    -> handleReadException
        -> closeOnRead

在前面分析 AbstractNioByteChannel.NioByteUnsafe#read 时,我们忽略了异常的处理。现在回过头再看一下代码:

(1)NioByteUnsafe#read

try {
    do {
        byteBuf = allocHandle.allocate(allocator);
        allocHandle.lastBytesRead(doReadBytes(byteBuf));
        if (allocHandle.lastBytesRead() <= 0) {
            byteBuf.release();
            byteBuf = null;
            // 1. 正常关闭,返回 -1
            close = allocHandle.lastBytesRead() < 0;
            if (close) {
                readPending = false;
            }
            break;
        }
    } while (allocHandle.continueReading());

    if (close) {
        closeOnRead(pipeline);
    }
} catch (Throwable t) {
    // 2. 如果异常是IOException,也需要关闭
    handleReadException(pipeline, byteBuf, t, close, allocHandle);
}

说明: 无论是正常关闭(allocHandle.lastBytesRead() = -1)还是异常关闭(IOException),都会调用 closeOnRead 关闭连接。

(2)closeOnRead

closeOnRead 方法调用 close 关闭连接。

private void closeOnRead(ChannelPipeline pipeline) {
    if (!isInputShutdown0()) {
        if (isAllowHalfClosure(config())) {
            // 特殊需求
            shutdownInput();
            pipeline.fireUserEventTriggered(ChannelInputShutdownEvent.INSTANCE);
        } else {
            // 基本上都是调用 close 方法关闭连接
            close(voidPromise());
        }
    } else {
        inputClosedSeenErrorOnRead = true;
        pipeline.fireUserEventTriggered(ChannelInputShutdownReadComplete.INSTANCE);
    }
}

2.2 close

unsafe.close 关闭连接,最终调用 NioSocketChannel#doClose(javaChannel.close) 或 NioEventLoop#cancel(key.cancel) 关闭连接,本质都会调用到 SelectionKey#cancel 取消注册。unsafe.close 做了如下工作:

  1. 预关闭:调用 prepareToClose 方法,实际上是判断 socket 是否配置了 soLinger。一旦配置了 soLinger 参数,socket 关闭就变成阻塞了,需要返回一个线程单独执行 doClose0 关闭任务。异步关闭连接,代码就不看了。
  2. 真正关闭连接:doClose0 方法会调用 javaChannel().close 来关闭连接。本质上也是调用 SelectionKey#cancel 取消注册。
  3. 清理 NioEventLoop 上的资源:重复调用 key.cancel(),但没有影响。同时清理 NioEventLoop 上资源。至于为什么 doDeregister 方法要重复取消注册?可能只调用 doDeregister 取消注册。
  4. 触发 ChannelInactive 和 ChannelUnregistered 事件。我们需要关注一下 head 有没有什么特殊的处理。
AbstractChannel.AbstractUnsafe#close
    -> prepareToClose
    -> doClose0
        -> NioSocketChannel#doClose             # √ javaChannel.close
    -> fireChannelInactiveAndDeregister
        -> deregister
            -> AbstractNioChannel#doDeregister
                -> NioEventLoop#cancel         # √ key.cancel()
            -> pipeline#fireChannelInactive
            -> pipeline#fireChannelUnregistered

(1)close

private void close(final ChannelPromise promise, final Throwable cause,
                   final ClosedChannelException closeCause, final boolean notify) {
    final boolean wasActive = isActive();
    this.outboundBuffer = null;                  // 清理资源,不允许再写数据到缓冲区
    Executor closeExecutor = prepareToClose();   // 1. 预关闭,设置soLinger后会阻塞关闭连接
    doClose0(promise);                           // 2. 真正关闭连接
    fireChannelInactiveAndDeregister(wasActive); // 3. 调用deregister,清理资源并触发事件
}

private void deregister(final ChannelPromise promise, final boolean fireChannelInactive) {
    try {
        doDeregister();                        // 4. 调用eventloop.cancel
    } catch (Throwable t) {
    } finally {
        pipeline.fireChannelInactive();        // 5. 触发channelInactive
        pipeline.fireChannelUnregistered();    // 6. 触发dhannelUnregistered
    }
}

(2)prepareToClose

prepareToClose 返回了一个线程用来单独执行关闭任务,因为开启 soLinger 后,关闭连接是阻塞的,需要异步关闭连接。NioSocketChannelUnsafe 中的实现如下:

@Override
protected Executor prepareToClose() {
    // 配置soLinger后会阻塞关闭连接,返回一个默认的连接池执行关闭任务
    if (javaChannel().isOpen() && config().getSoLinger() > 0) {
        doDeregister();
        return GlobalEventExecutor.INSTANCE;
    }
    return null;
}

(3)doClose

@Override
protected void doClose() throws Exception {
    super.doClose();
    javaChannel().close();   // 核心
}

(4)doDeregister

// AbstractNioChannel
@Override
protected void doDeregister() throws Exception {
    eventLoop().cancel(selectionKey());
}

void cancel(SelectionKey key) {
    key.cancel();             // 核心
    cancelledKeys ++;
    if (cancelledKeys >= CLEANUP_INTERVAL) {
        cancelledKeys = 0;
        needsToSelectAgain = true;
    }
}

每天用心记录一点点。内容也许不重要,但习惯很重要!

posted on 2020-04-06 19:35  binarylei  阅读(1771)  评论(0编辑  收藏  举报

导航