zoukankan      html  css  js  c++  java
  • 关闭连接:本质是取消 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;
        }
    }
    

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

  • 相关阅读:
    jvisualvm工具使用
    Java四种引用包括强引用,软引用,弱引用,虚引用。
    <实战> 通过分析Heap Dump 来了解 Memory Leak ,Retained Heap,Shallow Heap
    什么是GC Roots
    Memory Analyzer tool(MAT)分析内存泄漏---理解Retained Heap、Shallow Heap、GC Root
    JDK自带工具之问题排查场景示例
    websocket协议握手详解
    ssh 登陆服务器原理
    新版本macos无法安装mysql-python包
    如何将多个小值存储进一个值中
  • 原文地址:https://www.cnblogs.com/binarylei/p/12643807.html
Copyright © 2011-2022 走看看