我为Netty贡献源码|且看Netty如何应对TCP连接的正常关闭(三)

lemonvita
发布于 2022-7-19 10:39
浏览
0收藏

2.1.5 Channel 的关闭

我为Netty贡献源码|且看Netty如何应对TCP连接的正常关闭(三)-鸿蒙开发者社区 image.png


prepareToClose 方法返回的 closeExecutor 是用来执行 Channel 关闭操作的,当我们开启了 SO_LINGER 选项时,closeExecutor = GlobalEventExecutor.INSTANCE ,避免了 Reactor 线程的阻塞。

 

由 GlobalEventExecutor 负责执行 doClose0 方法关闭 Channel 底层的 Socket,并通知 closeFuture 关闭结果。

     

private void close(final ChannelPromise promise, final Throwable cause,
                           final ClosedChannelException closeCause, final boolean notify) {
            
            ...........省略重进入关闭流程处理........

            ...........省略Channel关闭前的准备工作........

            Executor closeExecutor = prepareToClose();
            if (closeExecutor != null) {
                closeExecutor.execute(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            // 在GlobalEventExecutor中执行channel的关闭任务,设置closeFuture,promise success
                            doClose0(promise);
                        } finally {
                            // reactor线程中执行
                            invokeLater(new Runnable() {
                                @Override
                                public void run() {
                                    if (outboundBuffer != null) {
                                        // cause = closeCause = ClosedChannelException, notify = false
                                        // 此时channel已经关闭,需要清理对应channelOutboundBuffer中的待发送数据flushedEntry
                                        outboundBuffer.failFlushed(cause, notify);
                                        //循环清理channelOutboundBuffer中的unflushedEntry
                                        outboundBuffer.close(closeCause);
                                    }
                                    //这里的active = true
                                    //关闭channel后,会将channel从reactor中注销,首先触发ChannelInactive事件,然后触发ChannelUnregistered
                                    fireChannelInactiveAndDeregister(wasActive);
                                }
                            });
                        }
                    }
                });
            } else {
                 ...........省略在Reactor中Channel关闭的逻辑........
            }
        }

当 Channel 的关闭操作在 closeExecutor 线程中执行完毕之后,此时 Channel 从物理上就已经关闭了,但是 Channel 中还有一些遗留的东西需要清理,比如 Channel 对应的写入缓冲队列 ChannelOutboundBuffer 中的待发送数据需要被清理掉,并通知用户线程由于 Channel 已经关闭,导致数据发送失败。

 

同时 Netty 也需要让用户感知到 Channel 已经关闭的事件,所以还需要在关闭 Channel 对应的 pipeline 中触发 ChannelInactive 事件和 ChannelUnregistered 事件。

 

而以上列举的这两点清理 Channel 的相关工作则需要在 Reactor 线程中完成,不能在 closeExecutor 线程中完成。这是处于线程安全的考虑,因为在 Channel 关闭之前,对于 ChannelOutboundBuffer 以及 pipeline 的操作均是由 Reactor 线程来执行的,Channel 关闭之后相关的清理工作理应继续由 Reactor 线程负责,避免多线程执行产生线程安全问题。

 

2.1.5.1 doClose0 关闭 Channel
     

 // 关闭channel操作的指定future,来判断关闭流程进度 每个channel一个
        private final CloseFuture closeFuture = new CloseFuture(this);

        private void doClose0(ChannelPromise promise) {
            try {
                // 关闭channel,此时服务端向客户端发送fin2,服务端进入last_ack状态,客户端收到fin2进入time_wait状态
                doClose();
                // 设置clostFuture的状态为success,表示channel已经关闭
                // 调用shutdownOutput则不会通知closeFuture
                closeFuture.setClosed();
                // 通知用户promise success,关闭操作已经完成
                safeSetSuccess(promise);
            } catch (Throwable t) {
                closeFuture.setClosed();
                // 通知用户线程关闭失败
                safeSetFailure(promise, t);
            }
        }

 

首先调用 doClose() 方法关闭底层 JDK 中的 SocketChannel 。

public class NioSocketChannel extends AbstractNioByteChannel implements io.netty.channel.socket.SocketChannel {

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

}

这里大家需要注意的一个点是,在 JDK 底层 SocketChannel 的关闭方法中,同样也会将该 Channel 关联的所有 SelectionKey 取消掉。因为在 prepareToClose 方法中我们提到,只有我们设置了 SO_LINGER 选项时,才会在 prepareToClose 方法中调用 doDeregister 方法将 Channel 关联的 SelectionKey 从 Selector 中取消掉。

 

而当我们没有设置 SO_LINGER 选项时,则不会提前调用 doDeregister 方法取消。所以需要在这里真正关闭 Channel 的地方,将其关联的所有 SelectionKey 取消掉。

 

public final void close() throws IOException {
        synchronized (closeLock) {
            if (!open)
                return;
            open = false;
            implCloseChannel();
        }
    }

    protected final void implCloseChannel() throws IOException {
        implCloseSelectableChannel();
        synchronized (keyLock) {
            int count = (keys == null) ? 0 : keys.length;
            //关闭与该Channel相关的所有SelectionKey
            for (int i = 0; i < count; i++) {
                SelectionKey k = keys[i];
                if (k != null)
                    k.cancel();
            }
        }
    }

 

当我们调用了 doClose() 方法后,此时服务端的内核协议栈就会向客户端发出 FIN 包,服务端结束 CLOSE_WAIT 状态进入 LAST_ACK 状态。客户端收到服务端的 FIN 包后,向服务端回复 ACK 包,随后客户端进入 TIME_WAIT 状态。服务端收到客户端的 ACK 包后结束 LAST_ACK 状态进入 CLOSE 状态。

我为Netty贡献源码|且看Netty如何应对TCP连接的正常关闭(三)-鸿蒙开发者社区

TCP连接关闭.png


当调用 doClose() 完成 Channel 的关闭后,就会调用 closeFuture.setClosed() 通知 Channel 的 closeFuture 关闭成功。

static final class CloseFuture extends DefaultChannelPromise {

        boolean setClosed() {
            return super.trySuccess();
        }

}

随后调用 safeSetSuccess(promise) 通知用户的 promise 关闭成功。

我为Netty贡献源码|且看Netty如何应对TCP连接的正常关闭(三)-鸿蒙开发者社区

 image.png


2.1.5.2 清理 ChannelOutboundBuffer


这里大家需要注意:清空 ChannelOutboundBuffer 的操作是在 Reactor 线程中执行的。

我为Netty贡献源码|且看Netty如何应对TCP连接的正常关闭(三)-鸿蒙开发者社区 ChannelOutboundBuffer结构.png

 

       if (outboundBuffer != null) {
                // Fail all the queued messages
                // cause = closeCause = ClosedChannelException, notify = false
                // 此时channel已经关闭,需要清理对应channelOutboundBuffer中的待发送数据flushedEntry
                outboundBuffer.failFlushed(cause, notify);
                //循环清理channelOutboundBuffer中的unflushedEntry
                outboundBuffer.close(closeCause);
       }

当 Channel 关闭之后,此时 Channel 中的写入缓冲队列 ChannelOutboundBuffer 中可能会有一些待发送数据,这时就需要将这些待发送数据从 ChannelOutboundBuffer 中清除掉。

 

通过调用 ChannelOutboundBuffer#failFlushed 方法,循环遍历 flushedEntry 指针到 tailEntry 指针之间的 Entry 对象,将其从 ChannelOutboundBuffer 链表中删除,并释放 Entry 对象中封装的 byteBuffer ,通知用户的 promise 写入失败。并回收 Entry 对象实例。

public final class ChannelOutboundBuffer {

    void failFlushed(Throwable cause, boolean notify) {
        if (inFail) {
            return;
        }

        try {
            inFail = true;
            for (;;) {
                // 循环清除channelOutboundBuffer中的待发送数据
                // 将entry从buffer中删除,并释放entry中的bytebuffer,通知promise failed
                if (!remove0(cause, notify)) {
                    break;
                }
            }
        } finally {
            inFail = false;
        }
    }

    private boolean remove0(Throwable cause, boolean notifyWritability) {
        Entry e = flushedEntry;
        if (e == null) {
            //清空当前reactor线程缓存的所有待发送数据
            clearNioBuffers();
            return false;
        }
        Object msg = e.msg;

        ChannelPromise promise = e.promise;
        int size = e.pendingSize;
        //从channelOutboundBuffer中删除该Entry节点
        removeEntry(e);

        if (!e.cancelled) {
            // only release message, fail and decrement if it was not canceled before.
            //释放msg所占用的内存空间
            ReferenceCountUtil.safeRelease(msg);
            //编辑promise发送失败,并通知相应的Lisener
            safeFail(promise, cause);
            //由于msg得到释放,所以需要降低channelOutboundBuffer中的内存占用水位线,并根据notifyWritability决定是否触发ChannelWritabilityChanged事件
            decrementPendingOutboundBytes(size, false, notifyWritability);
        }

        // recycle the entry
        //回收Entry实例对象
        e.recycle();

        return true;
    }
}

在 remove0 方法中 netty 会将已经关闭的 Channel 对应的 ChannelOutboundBuffer 中还没来得及 flush 进 Socket 发送缓存区中的数据全部清除掉。这部分数据就是上图中 flushedEntry 指针到 tailEntry 指针之间的 Entry对象。

 

Entry 对象中封装了用户待发送数据的 ByteBuffer,以及用于通知用户发送结果的 promise 实例。

 

这里需要将这些还未来得及 flush 的 Entry 节点从 ChannelOutboundBuffer 中全部清除,并释放这些 Entry 节点中包裹的发送数据 msg 所占用的内存空间。并标记对应的 promise 为失败同时通知对应的用户 listener 。

 

以上的清理逻辑主要是应对在 Channel 即将关闭之前,用户极限调用 flush 操作想要发送数据的情况。

 

另外还有一种情况 Netty 这里需要考虑处理,由于在关闭 Channel 之前,用户可能还会向 ChannelOutboundBuffer 中 write 数据,但还未来得及调用 flush 操作,这就导致了 ChannelOutboundBuffer 中在 unflushedEntry 指针与 tailEntry 指针之间还可能会有数据。

 

之前我们清理的是 flushedEntry 指针与 tailEntry 指针之间的数据,这里大家需要注意区分。

 

所以还需要调用 ChannelOutboundBuffer#close 方法将这一部分数据全部清理掉。

public final class ChannelOutboundBuffer {

  void close(final Throwable cause, final boolean allowChannelOpen) {
        if (inFail) {
            channel.eventLoop().execute(new Runnable() {
                @Override
                public void run() {
                    close(cause, allowChannelOpen);
                }
            });
            return;
        }

        inFail = true;

        if (!allowChannelOpen && channel.isOpen()) {
            throw new IllegalStateException("close() must be invoked after the channel is closed.");
        }

        if (!isEmpty()) {
            throw new IllegalStateException("close() must be invoked after all flushed writes are handled.");
        }

        // Release all unflushed messages.
        //循环清理channelOutboundBuffer中的unflushedEntry,因为在执行关闭之前有可能用户有一些数据write进来,需要清理掉
        try {
            Entry e = unflushedEntry;
            while (e != null) {
                // Just decrease; do not trigger any events via decrementPendingOutboundBytes()
                int size = e.pendingSize;
                TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, -size);

                if (!e.cancelled) {
                    //释放unflushedEntry中的bytebuffer
                    ReferenceCountUtil.safeRelease(e.msg);
                    //通知unflushedEntry中的promise failed
                    safeFail(e.promise, cause);
                }
                e = e.recycleAndGetNext();
            }
        } finally {
            inFail = false;
        }
        //清理channel用于缓存JDK nioBuffer的 threadLocal缓存NIO_BUFFERS
        clearNioBuffers();
    }

}

当我们清理完 ChannelOutboundBuffer 中的残留数据之后,ChannelOutboundBuffer 中的内存水位线就会下降,由于当前是关闭操作,所以这里的 notifyWritability = false ,不需要触发 ChannelWritabilityChanged 事件。

 

关于对 ChannelOutboundBuffer 的详细操作,笔者已经在 一文搞懂 Netty 发送数据全流程 一文中详细介绍过了,忘记的同学可以在回顾下这篇文章。

 

2.1.5.3  触发 ChannelInactive 事件和 ChannelUnregistered 事件


在 Channel 关闭之后并清理完 ChannelOutboundBuffer 中遗留的待发送数据,就该在 Channel 的 pipeline 中触发 ChannelInactive 事件和 ChannelUnregistered 事件了。同样以下的这些操作也都是在 Reactor 线程中执行的。

       private void fireChannelInactiveAndDeregister(final boolean wasActive) {
            //wasActive && !isActive() 条件表示 channel的状态第一次从active变为 inactive
            //这里的wasActive = true  isActive()= false
            deregister(voidPromise(), wasActive && !isActive());
        }

这里传递进来的参数 wasActive = true ,在我们关闭 Channel 之前会通过 isActive() 先获取一次,在该方法中通过 wasActive && !isActive() 判断 Channel 是否是第一次从 active 状态变为 inactive 状态。如果是,则触发后续的 ChannelInactive 事件。

     

 private void deregister(final ChannelPromise promise, final boolean fireChannelInactive) {
            if (!promise.setUncancellable()) {
                return;
            }

            if (!registered) {
                safeSetSuccess(promise);
                return;
            }

            invokeLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        //将channel从reactor中注销,reactor不在监听channel上的事件
                        doDeregister();
                    } catch (Throwable t) {
                        logger.warn("Unexpected exception occurred while deregistering a channel.", t);
                    } finally {
                        if (fireChannelInactive) {
                            //当channel被关闭后,触发ChannelInactive事件
                            pipeline.fireChannelInactive();
                        }

                        if (registered) {
                            //如果channel没有注册,则不需要触发ChannelUnregistered
                            registered = false;
                            //随后触发ChannelUnregistered
                            pipeline.fireChannelUnregistered();
                        }
                        //通知deRegisterPromise
                        safeSetSuccess(promise);
                    }
                }
            });
        }

注意这里又会调用 doDeregister() 方法将 Channel 从 Reactor 上注销,到目前为止,我们已经看到有三个地方执行注销 Channel 的操作了。

 

 •第一次是在 prepareToClose() 方法中,当我们设置了 SO_LINGER 选项后,为了防止 Reactor 线程在延时关闭期间,还在不停的自旋循环响应 OP_READ 事件和 OP_WRITE 事件从而造成浪费 CPU 资源,我们需要 doDeregister() 方法将 Channel 从 Reactor 上取消。

 

 •第二次是在真正的关闭 Channel 的时候,JDK 底层在关闭 SocketChannel 的时候又会将 Channel 从 Selector 上取消。应对关闭 SO_LINGER 选项的情况

 

 •第三次就是在本小节中,触发 ChannelUnregistered 事件之前,又会调用 doDeregister() 方法将 Channel 从 Reactor 上取消。

 

这里大家可能会有疑问,这第三次注销操作是应对哪种情况呢?

 

首先 JDK NIO 底层在将 Channel 从 Selector 上注销的时候做了防重处理,多次调用注销操作是没有影响的。

 

另外这个方法可能会在用户的 ChannelHandler 中被调用,因为用户的行为我们无法预知,用户可能在 Channel 关闭前调用,所以这里还是需要调用一次 doDeregister() 方法。为的就是应对用户在 ChannelHandler 中主动注销 Channel 同时不希望 Channel 关闭的场景。

        // 仅仅是注销 Channel,但是 Channel 不会关闭
        ctx.deregister();
        ctx.channel().deregister();

在调用完 doDeregister() 方法之后,netty 紧接着就会在 Channel 的 pipeline 中触发 ChannelInactive 事件以及 ChannelUnregistered 事件,并且这两个事件只会被触发一次。

 

在接收连接的时候,当 Channel 向 Reactor 注册成功之后,是先触发 ChannelRegistered 事件后触发 ChannelActive 事件。
在关闭连接的时候,当 Channel 从 Reactor 中取消注册之后,是先触发 ChannelInactive 事件后触发 ChannelUnregistered 事件

 

这里大家还需要注意的一个点是,以上的逻辑会封装在 Runnable 中被提交到 Reactor 的任务队列中延迟执行。那么这里为什么要延迟执行呢

 

这里延后 deRegister 操作的原因是用于处理一种极端的异常情况,前边我们提到 Channel 的 deregister() 操作是可以在用户的 ChannelHandler 中执行的,用户行为是不可预知的。

 

我们想象一下这样的一个场景:假如当前 pipeline 中还有事件传播(比如正在处理编码解码),与此同时 deregister() 方法可能会在某个事件回调中被用户调用,导致其它事件在传播的过程中,Channel 被从 Reactor 上注销掉了。

 

并且同时 channel 又注册到新的 Reactor 上。如果此时旧的 Reactor 正在处理 pipeline 上的事件而旧 Reactor 还未处理完的数据理应继续在旧的 Reactor 中处理,如果此时我们立马执行 deRegister ,未处理完的数据就会在新的 Reactor 上处理,这样就会导致一个 handler 被多个 Reactor 线程处理导致线程安全问题。所以需要延后 deRegister 的操作。

 
到这里呢,关于 netty 如何处理 TCP 连接正常关闭的逻辑,笔者就为大家全部介绍完了,不过还留了一个小小的尾巴,就是当我们未设置 SO_LINGER 选项时,Channel 的关闭操作会直接在 Reactor 线程中执行。closeExecutor 这种情况下会是 null 。

     

private void close(final ChannelPromise promise, final Throwable cause,
                           final ClosedChannelException closeCause, final boolean notify) {
            
            ...........省略重进入关闭流程处理........

            ...........省略Channel关闭前的准备工作........

            Executor closeExecutor = prepareToClose();
            if (closeExecutor != null) {
                ...........省略在closeExecutor中Channel关闭的逻辑........
            } else {
                try {
                    // Close the channel and fail the queued messages in all cases.
                    doClose0(promise);
                } finally {
                    if (outboundBuffer != null) {
                        // Fail all the queued messages.
                        outboundBuffer.failFlushed(cause, notify);
                        outboundBuffer.close(closeCause);
                    }
                }

                // 此时 Channel 已经关闭,如果此时用户还在执行 flush 操作
                // netty 则会在 flush 方法的处理中处理 Channel 关闭的情况
                // 所以这里 deRegister 操作需要延后到 flush 方法处理完之后
                if (inFlush0) {
                    invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            fireChannelInactiveAndDeregister(wasActive);
                        }
                    });
                } else {
                    fireChannelInactiveAndDeregister(wasActive);
                }                 
            }
        }

这里可以看到其实逻辑都是一样的。都是先调用 doClose0 关闭 JDK NIO 底层的 SocketChannel ,然后清理 ChannelOutboundBuffer 中遗留的待发送数据,最后触发 ChannelInactive 事件和 ChannelUnregistered 事件。

我为Netty贡献源码|且看Netty如何应对TCP连接的正常关闭(三)-鸿蒙开发者社区

 image.png

标签
已于2022-7-19 10:39:07修改
收藏
回复
举报
回复
    相关推荐