我为 Netty 贡献源码 |(一)

justtouch
发布于 2022-8-9 17:03
浏览
0收藏

写在前面.....


本文是笔者肉眼盯 Bug 系列的第三弹,前两弹分别是:

  ●    抓到Netty一个Bug,顺带来透彻地聊一下Netty是如何高效接收网络连接的 ,在这篇文章中盯出了一个在 Netty 接收网络连接时,影响吞吐量的一个 Bug。
  ●    抓到Netty一个隐藏很深的内存泄露Bug | 详解Recycler对象池的精妙设计与实现,在这篇文章中盯出了一个 Netty 对象池在多线程并发回收对象时可能导致内存            泄露的一个 Bug。
而在本篇文章中笔者又用肉眼盯出了 Netty 在处理 TCP 连接半关闭时的一个 Bug。

我为 Netty 贡献源码 |(一)-鸿蒙开发者社区

我为 Netty 贡献源码 |(一)-鸿蒙开发者社区
那么在接下来的内容中,笔者会随着源码深入的解读慢慢的为大家一层一层地拨开迷雾,带大家来一步一步分析这个 Bug 产生的原因以及造成的影响,并逐步带大家把这个 Bug 修复掉。

下面就让我们一起带着怀疑,审视,欣赏,崇敬,敬畏的态度来一起品读世界顶级程序员编写出的代码。由衷的感谢他们在这一领域做出的贡献。

我为 Netty 贡献源码 |(一)-鸿蒙开发者社区
在笔者前边关于 Netty Reactor 的系列文章中,我们详细的分析了 Reactor 的创建启动运行,以及接收网络连接接收网络数据,然后通过 pipeline 对 IO 事件的编排处理,最后到发送网络数据的一整套流程实现。相信大家通过对这一系列文章的阅读思考,已经对 Reactor 在 Netty 中的实现有了一个全面并且深刻的认识。

那么现在就到了关闭连接的时刻了,在本文中笔者将带大家一起剖析下关闭连接在 Netty 中的整个实现逻辑。

在 Netty 中对于用户关闭连接的处理分为三大模块:

 1. 处理正常的 TCP 连接关闭。
 2. 处理异常的 TCP 连接关闭。
 3. 处理 TCP 连接半关闭的场景。
接下来,笔者就带大家从这三个连接关闭场景来全面分析下 Netty 是如何处理连接关闭的。

首先我们来看下最简单的场景 --- 正常的TCP连接关闭。

1. 正常 TCP 连接关闭

在进入源码实现之前,我们先来回顾下 TCP 连接关闭的整个流程,其实 Netty 中针对连接关闭的整个源码实现流程也是按照图中 TCP 连接关闭的四次挥手步骤进行的。

我为 Netty 贡献源码 |(一)-鸿蒙开发者社区
1. 首先 Netty 客户端在对应的 ChannelHandler 中调用 ctx.channel().close() 方法主动关闭连接,内核会向服务端发送一个 FIN 包,随即客户端连接进入 FIN_WAIT1 状态。

public class EchoClientHandler extends ChannelInboundHandlerAdapter {

   @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
       // 客户端连接进入 FIN_WAIT1 状态
       ctx.channel().close();
    }
}

2. 服务端内核协议栈在接收到客户端发送过来的 FIN 包后,会自动回复客户端一个 ACK 包,随后会将文件结束符 EOF 插入到 Socket 接收缓冲区中的末尾。服务端连接状态进入 CLOSE_WAIT ,客户端接收到 ACK 包后进入FIN_WAIT2 状态。
3. 当服务端内核协议栈将 EOF 插入到 Socket 的接收缓冲区时,这时 OP_READ 事件活跃,Reactor 线程随即会处理 channel 上的 OP_READ 事件,只不过此时从 channel 中读取到的字节数为 -1 ,表示对端发起了 channel 关闭请求。服务端开始执行连接关闭流程。
4. 由于客户端调用的是 ctx.channel().close() 方法来关闭连接,相当于将 TCP 连接的读写通道同时关闭,所以客户端在 FIN_WAIT2 状态下无法在接收服务端发送的数据,但此时服务端处于 CLOSE_WAIT 状态下仍可向客户端发送数据,只不过客户端在接收到数据后会丢弃并发送 RST 报文给服务端。
4. 服务端在 CLOSE_WAIT 状态下,调用 ctx.channel().close() 向客户端发送 FIN 包,随即进入 LAST_ACK 状态。
6. 客户端在收到来自服务端的 FIN 包后,回复 ACK 包给服务端,完成四次挥手,随即进入 TIME_WAIT 状态,服务端在收到客户端的 ACK 包后结束 LAST_ACK 状态进入 CLOSE 状态。
Netty 中对于连接关闭的处理主要在第 3 步和第 5 步,剩下的逻辑均由内核协议栈处理完成。

从上述 TCP 关闭连接的四次挥手步骤中,我们可以看出 Netty 对于关闭连接的响应是通过处理 OP_READ 事件来完成的,而对于 OP_READ 事件的处理,笔者已经在 Netty如何高效接收网络数据 一文中详细介绍过了,这里我们直接来到 OP_READ 事件的处理函数中,聚焦于连接关闭逻辑的处理。

我为 Netty 贡献源码 |(一)-鸿蒙开发者社区
当 Reactor 线程轮询到 Channel 上有 OP_READ 事件活跃时,就会来到 NioEventLoop#processSelectedKey 函数中去处理活跃的 IO 事件,在本文的语义中 OP_READ 事件就表示连接关闭事件。

public final class NioEventLoop extends SingleThreadEventLoop {

   private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
        final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
      
                  .................省略..............

        try {
            int readyOps = k.readyOps();

                  .................省略..............

            if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                //处理 OP_READ 事件,本文中表示连接关闭事件
                unsafe.read();
            }
        } catch (CancelledKeyException ignored) {
            unsafe.close(unsafe.voidPromise());
        }
    }
}

最终会在 AbstractNioByteChannel#read 方法中完成对 OP_READ 事件的处理,下图中置灰的逻辑处理模块即为 Netty 在整个 OP_READ 事件处理中关于连接关闭事件的处理位置。

Netty 中关于 OP_READ 事件的处理一共分为两大模块,一块是针对接收连接上网络数据的处理。另一块则是本文的主题,针对连接关闭事件的处理。

我为 Netty 贡献源码 |(一)-鸿蒙开发者社区

public abstract class AbstractNioByteChannel extends AbstractNioChannel {

        @Override
        public final void read() {
            final ChannelConfig config = config();

            ..........省略连接半关闭处理........

            ..........省略获取allocHandle过程.......

            ByteBuf byteBuf = null;
            boolean close = false;
            try {
                do {
                    byteBuf = allocHandle.allocate(allocator);
                    //记录本次读取了多少字节数
                    allocHandle.lastBytesRead(doReadBytes(byteBuf));
                    //如果本次没有读取到任何字节,则退出循环 进行下一轮事件轮询
                    // -1 表示客户端主动关闭了连接close或者shutdownOutput 这里均会返回-1
                    if (allocHandle.lastBytesRead() <= 0) {
                        // nothing was read. release the buffer.
                        byteBuf.release();
                        byteBuf = null;
                        //当客户端主动关闭连接时(客户端发送fin1),会触发read就绪事件,这里从channel读取的数据会是-1
                        close = allocHandle.lastBytesRead() < 0;
                        if (close) {
                            // There is nothing left to read as we received an EOF.
                            readPending = false;
                        }
                        break;
                    }

                    .........省略.............

                } while (allocHandle.continueReading());

                allocHandle.readComplete();
                pipeline.fireChannelReadComplete();

                if (close) {
                    //此时客户端发送fin1(fi_wait_1状态)主动关闭连接,服务端接收到fin,并回复ack进入close_wait状态
                    //在服务端进入close_wait状态 需要调用close 方法向客户端发送fin_ack,服务端才能结束close_wait状态
                    closeOnRead(pipeline);
                }
            } catch (Throwable t) {
                 ............省略...............
            } finally {
                 ............省略...............
            }
        }
    }

}

在前边 TCP 连接关闭的步骤 3 中我们提到,当服务端的内核协议栈接收到来自客户端的 FIN 包后,内核协议栈会向 Socket 的接收缓冲区插入文件结束符 EOF ,表示客户端已经主动发起了关闭连接流程,这时 NioSocketChannel 上的 OP_READ 事件活跃,随即 Reactor 线程会在 AbstractNioByteChannel#read 方法中处理 OP_READ 事件。

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

    @Override
    protected int doReadBytes(ByteBuf byteBuf) throws Exception {
        final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
        allocHandle.attemptedBytesRead(byteBuf.writableBytes());
        //读到EOF后,这里会返回-1
        return byteBuf.writeBytes(javaChannel(), allocHandle.attemptedBytesRead());
    }

}

Reactor 线程会通过 ByteBuf#writeBytes 方法读取 NioSocketChannel 中的数据,由于此时底层 Socket 接收缓冲区中只有一个 EOF 并没有其他接收数据,所以这里的 ByteBuf#writeBytes 方法会返回 -1。表示客户端已经发起了连接关闭流程,此时服务端连接状态为 CLOSE_WAIT ,客户端连接状态为 FIN_WAIT2 。

我为 Netty 贡献源码 |(一)-鸿蒙开发者社区

     boolean close = false;
     close = allocHandle.lastBytesRead() < 0;
     if (close) {
           closeOnRead(pipeline);
     }

当本次 read loop 从 Channel 中读取到的字节数为 -1 时,则进入 closeOnRead 方法,服务端开始关闭连接流程。

从上述 Netty 处理 TCP 正常关闭流程( Socket 接收缓冲区中只有 EOF ,没有其他正常接收数据)可以看出,这种情况下只会触发 ChannelReadComplete 事件而不会触发 ChannelRead 事件。

2. Netty 对 TCP 连接正常关闭的处理

       private void closeOnRead(ChannelPipeline pipeline) {
           //判断服务端连接接收方向是否关闭,这里肯定是没有关闭的
           if (!isInputShutdown0()) {
                if (isAllowHalfClosure(config())) {
                      .....省略TCP连接半关闭处理逻辑.......
                } else {
                    //如果不支持半关闭,则服务端直接调用close方法向客户端发送fin,结束close_wait状态进如last_ack状态
                    close(voidPromise());
                }
            } else {
                    .....省略TCP连接半关闭处理逻辑.......
            }
        }

众所周知 TCP 是一个面向连接的、可靠的、基于字节流的全双工传输层通信协议,既然它是全双工的,那就意味着 TCP 连接同时有一个读通道和写通道。

我为 Netty 贡献源码 |(一)-鸿蒙开发者社区
这里的 isInputShutdown0 方法是用来判断 TCP 连接上的读通道是否关闭,那么在当前情况下,服务端的读通道肯定还没有关闭,因为目前 Netty 还没有调用任何关闭连接的系统调用。

    @Override
    protected boolean isInputShutdown0() {
        return isInputShutdown();
    }

    @Override
    public boolean isInputShutdown() {
        return javaChannel().socket().isInputShutdown() || !isActive();
    }

至于这里为什么要对读通道是否关闭进行判断,笔者会在本文 TCP  连接半关闭相关处理章节为大家详细解释。

由于本小节介绍的是 TCP 连接正常关闭的场景,并不是半关闭,所以这里的 isAllowHalfClosure = false 。Reactor 线程进入 close 方法,执行真正的关闭流程。

2.1 close 方法发起 TCP 连接关闭流程

public abstract class AbstractChannel extends DefaultAttributeMap implements Channel {

      @Override
      public void close(final ChannelPromise promise) {
            assertEventLoop();

            ClosedChannelException closedChannelException =
                    StacklessClosedChannelException.newInstance(AbstractChannel.class, "close(ChannelPromise)");

            close(promise, closedChannelException, closedChannelException, false);
      }

      private void close(final ChannelPromise promise, final Throwable cause,
                           final ClosedChannelException closeCause, final boolean notify) {

                      .........省略...........

      }

}

这里正是 netty 关闭 channel 的核心逻辑所在,而关闭 channel 的行为又分为主动关闭和被动关闭,如本例中,客户端主动调用 ctx.channel().close() 发起关闭流程为主动关闭方,而服务端则是被动关闭方。

而主动关闭方和被动关闭方在这里的传参是不一样的,我们先来看被动关闭方也就是本例中服务端在调用 close 方法的传参。

        @Override
        public void close(final ChannelPromise promise) {
            assertEventLoop();

            ClosedChannelException closedChannelException =
                    StacklessClosedChannelException.newInstance(AbstractChannel.class, "close(ChannelPromise)");
            close(promise, closedChannelException, closedChannelException, false);
        }

ChannelPromise promise:服务端作为被动关闭方,这里传入的 ChannelPromise 类型为 VoidChannelPromise ,表示调用方对处理结果并不关心,VoidChannelPromise 不可添加 Listener ,不可修改操作结果状态。

public final class VoidChannelPromise extends AbstractFuture<Void> implements ChannelPromise {

    @Override
    public VoidChannelPromise addListener(GenericFutureListener<? extends Future<? super Void>> listener) {
        fail();
        return this;
    }

    @Override
    public boolean isDone() {
        return false;
    }

   @Override
    public boolean setUncancellable() {
        return true;
    }

    @Override
    public VoidChannelPromise setFailure(Throwable cause) {
        fireException0(cause);
        return this;
    }

    @Override
    public boolean trySuccess() {
        return false;
    }
   
}

而作为主动关闭方的客户端则需要监听 Channel 关闭的结果,所以这里传递的 ChannelPromise 参数为 DefaultChannelPromise 。

        ChannelFuture channelFuture = ctx.channel().close();
        channelFuture.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                  ...........省略.......
            }
        });
    @Override
    public ChannelFuture close() {
        return close(newPromise());
    }

    @Override
    public ChannelPromise newPromise() {
        return new DefaultChannelPromise(channel(), executor());
    }

  ●  Throwable cause:当 Channel 关闭之后,需要清理 Channel 写入缓冲队列 ChannelOutboundBuffer 中的待发送数据,这里会将异常 cause 传递给用户的 writePromise ,通知用户 Channel 已经关闭,write 操作失败。这里传入的异常类型为 StacklessClosedChannelException 。
我为 Netty 贡献源码 |(一)-鸿蒙开发者社区

如图中所示,当用户调用 ctx.writeAndFlush(msg) 发送数据时,由于是异步发送 Netty 会在图中的第 2 步直接返回一个  ChannelFuture 给用户,发送成功或者发送失败都会通知这个 ChannelFuture 。如果在数据发送之前连接就关闭了,那么 Netty 就会把 StacklessClosedChannelException 异常通知给用户持有的这个 ChannelFuture。相关数据的发送细节,感兴趣的读者可以在回顾下笔者的 一文搞懂 Netty 发送数据全流程 这篇文章。

  ●   ClosedChannelException closeCause:这个参数和 Throwable cause 参数的作用差不多,都是用于在连接关闭的时候如果此时还有待发送数据未发送。就通知用户这里在参数中指定的异常。唯一不同的是 Throwable cause 负责通知给 Channel 发送数据缓冲队列 ChannelOutboundBuffer 中的 flushedEntry 队列。ClosedChannelException closeCause 负责通知给 ChannelOutboundBuffer 中的 unflushedEntry 队列。
我为 Netty 贡献源码 |(一)-鸿蒙开发者社区

这里大家只需要理解个大概,稍微有个印象就行,笔者后面还会详细介绍。

  ●  boolean notify:由于在关闭 Channel 之后,会清理 Channel 对应的发送缓冲队列 ChannelOutboundBuffer 中存储的待发送数据,同时也会释放其中用于存储待发送数据用的 ByteBuffer ,当 ChannelOutboundBuffer 中的内存占用低于低水位线的时候,会触发 ChannelWritabilityChanged 事件。这里的参数 boolean notify 决定是否触发 ChannelWritabilityChanged 事件,由于当前是关闭操作,所以 notify = false ,不需要触发 ChannelWritabilityChanged 事件。
在介绍完 close 方法的各个参数之后,接下来我们来看一下具体的关闭逻辑:

2.1.1 连接关闭之前的校验工作

      // channel的关闭流程是否已经开始
      private boolean closeInitiated;

      // 关闭channel操作的指定future,来判断关闭流程进度 每个channel对应一个CloseFuture
      // 连接关闭之后,netty 会通知这个CloseFuture
      private final CloseFuture closeFuture = new CloseFuture(this);

      private void close(final ChannelPromise promise, final Throwable cause,
                           final ClosedChannelException closeCause, final boolean notify) {
            if (!promise.setUncancellable()) {
                //关闭操作如果被取消则直接返回
                return;
            }

            if (closeInitiated) {
                //如果此时channel已经开始关闭流程,则进入这里
                if (closeFuture.isDone()) {               
                    //如果channel已经关闭 则设置promise为success,如果promise是voidPromise类型则会跳过
                    safeSetSuccess(promise);
                } else if (!(promise instanceof VoidChannelPromise)) { 
                    //如果promise不是voidPromise,则会在关闭完成后 通过closeFuture设置promise success
                    closeFuture.addListener(new ChannelFutureListener() {
                        @Override
                        public void operationComplete(ChannelFuture future) throws Exception {
                            promise.setSuccess();
                        }
                    });
                }
                // 直接返回,防止重复关闭
                return;
            }
  
            //当前channel现在开始进入正在关闭状态
            closeInitiated = true;

            .......关闭channel.........
        }

Netty 这里使用一个 boolean closeInitiated 变量来防止 Reactor 线程来重复执行关闭流程,因为 Channel 的关闭操作可以在多个业务线程中发起,这样就会导致多个业务线程向 Reactor 线程提交多个关闭 Channel 的任务。

除此之外,Netty 还为每一个 Channel 创建了一个 CloseFuture closeFuture,用来表示 Channel 关闭的相关进度状态。当 Channel 完成关闭后,Netty 会设置 closeFuture 为 success 状态,并通知 closeFuture 上注册的 listener 。

如果 closeInitiated == true 说明当前 Channel 的关闭操作已经开始,如果有多个业务线程先后提交过来多个关闭任务,Reactor 线程则会首先通过 closeFuture.isDone() 判断当前 Channel 是否已经完成关闭 ,如果 Channel 已经关闭,则会在 closeFuture 上注册的 listener 中设置关闭任务对应的 Promie 为 success ,进而通知到业务线程。

     protected final void safeSetSuccess(ChannelPromise promise) {
            if (!(promise instanceof VoidChannelPromise) && !promise.trySuccess()) {
                logger.warn("Failed to mark a promise as success because it is done already: {}", promise);
            }
    }

 

从这里也可以看出 VoidChannelPromise 表示一个空的 Promise ,不能对其设置 success 或者 fail , 更不能对其添加 listener 。一般用于不关心操作结果的场景。
如果此时 Channel 的关闭流程虽然已经开始但还未完成的情况下,则将关闭任务对应 Promise (在业务线程中持有)的通知动作封装成 ChannelFutureListener  并添加到 closeFuture 中。当 Channel 关闭后,closeFuture 会被设置为 success ,并通知其中注册的 ChannelFutureListener 。

我为 Netty 贡献源码 |(一)-鸿蒙开发者社区
2.1.2 Channel关闭前的准备工作

       private void close(final ChannelPromise promise, final Throwable cause,
                           final ClosedChannelException closeCause, final boolean notify) {
            
            ...........省略连接关闭之前的校验工作........

            //当前channel是否active,这里肯定是active的
            final boolean wasActive = isActive();
            final ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
            //将channel对应的写缓冲区channelOutboundBuffer设置为null 表示channel要关闭了,不允许继续发送数据
            //此时如果还在write数据,则直接释放bytebuffer,并立马 fail 相关writeFuture 并抛出newClosedChannelException异常
            //此时如果执行flush,则会直接返回
            this.outboundBuffer = null; 
            //如果开启了SO_LINGER,则需要先将channel从reactor中取消掉。避免reactor线程空转浪费cpu
            Executor closeExecutor = prepareToClose();

            .............省略关闭Channel逻辑流程.......
        }

通过 isActive() 获取 Channel 的状态 boolean wasActive ,由于此时我们还没有关闭 Channel,所以 Channel 现在的状态肯定是 active 的。之所以在关闭流程的一开始就获取 Channel 是否 active 的状态,是因为当我们关闭 Channel 之后,需要通过这个状态来判断 channel 是否是第一次从 active 变为 inactive ,如果是第一次,则会触发 ChannelInactive 事件在 Channel 对应的 pipeline 中传播。

在 Channel 关闭之前,还会将 Channel 对应的写入缓冲队列 ChannelOutboundBuffer 设置为 null ,表示 Channel 即将要关闭了,不允许业务线程在继续发送数据。

一文搞懂 Netty 发送数据全流程 一文中我们提到过,如果 Channel 准备关闭的时候,用户还在向 Channel 写入数据,则直接释放 bytebuffer ,并立马 fail 掉相关 ChannelPromise 并抛出 newClosedChannelException 异常。

       @Override
        public final void write(Object msg, ChannelPromise promise) {
            assertEventLoop();
            //获取当前channel对应的待写入数据缓冲队列(支持用户异步写入的核心关键)
            ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
            // outboundBuffer == null说明channel准备关闭了,直接标记发送失败。
            if (outboundBuffer == null) {
                try {
                    ReferenceCountUtil.release(msg);
                } finally {
                    safeSetFailure(promise,
                            newClosedChannelException(initialCloseCause, "write(Object, ChannelPromise)"));
                }
                return;
            }

            .............省略.........
         }

如果此时用户还在执行 Channel 的 flush 操作发送数据,那么发送流程直接会 return 掉,停止发送数据。

     

  @Override
        public final void flush() {
            assertEventLoop();

            ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
            //channel以关闭
            if (outboundBuffer == null) {
                return;
            }

            .........省略........
       }

2.1.3 针对 SO_LINGER 选项的处理

       @Override
        protected Executor prepareToClose() {
            try {
                if (javaChannel().isOpen() && config().getSoLinger() > 0) {
                    //在设置SO_LINGER后,channel会延时关闭,在延时期间我们仍然可以进行读写,这样会导致io线程eventloop不断的循环浪费cpu资源
                    //所以需要在延时关闭期间 将channel注册的事件全部取消。
                    doDeregister();

                    /**
                     * 设置了SO_LINGER,不管是阻塞socket还是非阻塞socket,在关闭的时候都会发生阻塞,所以这里不能使用Reactor线程来
                     * 执行关闭任务,否则Reactor线程就会被阻塞。
                     * */
                    return GlobalEventExecutor.INSTANCE;
                }
            } catch (Throwable ignore) {
            }
            //在没有设置SO_LINGER的情况下,可以使用Reactor线程来执行关闭任务
            return null;
        }
    }

要理解这段逻辑,首先我们需要理解 SO_LINGER 这个 Socket 选项,他会影响 Socket 的关闭行为。

在默认情况下,当我们调用 Socket 的 close 方法后 ,close 方法会立即返回,剩下的事情会交给内核协议栈帮助我们处理,如果此时 Socket 对应的发送缓冲区还有数据待发送,接下来内核协议栈会将 Socket 发送缓冲区的数据发送出去,随后会向对端发送 FIN 包关闭连接。注意:此时应用程序是无法感知到这些数据是否已经发送到对端的,因为应用程序在调用 close 方法后就立马返回了,剩下的这些都是内核在替我们完成。接着主动关闭方就进入了 TCP 四次挥手的关闭流程最后进入TIME_WAIT状态。

我为 Netty 贡献源码 |(一)-鸿蒙开发者社区
而 SO_LINGER 选项会控制调用 close 方法关闭 Socket 的行为。

  struct linger {
      int l_onoff;   // linger active
      int l_linger;  // how many seconds to linger for
  };

  ●   l_onoff :表示是否开启 SO_LINGER 选项。0 表示关闭。默认情况下是关闭的。

  ●   int l_linger:如果开启了 SO_LINGER 选项,则该参数表示应用程序调用 close 方法后需要阻塞等待多长时间。单位为秒。
这两个参数的不同组合会影响到 Socket 的关闭行为:

  ●   l_onoff = 0 时 l_linger 的值会被忽略,属于我们上边讲述的默认关闭行为。

  ●   l_onoff = 1,l_linger > 0:这种情况下,应用程序调用 close 方法后就不会立马返回,无论 Socket 是阻塞模式还是非阻塞模式,应用程序都会阻塞在这里。直到以下两个条件其中之一发生,才会解除阻塞返回。随后进行正常的四次挥手关闭流程。
  ●   当 Socket 发送缓冲区的数据全部发送出去,并等到对端 ACK 后,close 方法返回。
  ●   应用程序在 close 方法上的阻塞时间到达 l_linger 设置的值后,close 方法返回。
我为 Netty 贡献源码 |(一)-鸿蒙开发者社区

  ●  l_onoff = 1,l_linger = 0:这种情况下,当应用程序调用 close 方法后会立即返回,随后内核直接清空 Socket 的发送缓冲区,并向对端发送 RST 包,主动关闭方直接跳过四次挥手进入 CLOSE 状态,注意这种情况下是不会有 TIME_WAIT 状态的。
我为 Netty 贡献源码 |(一)-鸿蒙开发者社区
Netty 也提供了 SO_LINGER 选项的设置,由于一般关闭连接的行为都是由客户端发起,我们以 Netty 客户端代码为例说明:

public final class EchoClient {

        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
             .channel(NioSocketChannel.class)
             .option(ChannelOption.SO_LINGER, 2)
              ..........省略........
        }
}
public class DefaultSocketChannelConfig extends DefaultChannelConfig
                                        implements SocketChannelConfig {

    @Override
    public SocketChannelConfig setSoLinger(int soLinger) {
        try {
            if (soLinger < 0) {
                javaSocket.setSoLinger(false, 0);
            } else {
                javaSocket.setSoLinger(true, soLinger);
            }
        } catch (SocketException e) {
            throw new ChannelException(e);
        }
        return this;
    }

}

默认情况下 SO_LINGER 选项是关闭的,在 JDK 底层设置 SO_LINGER 选项的方法 setSoLinger  中,参数 on 对应 l_onoff ,参数 linger 对应 l_linger ,单位为秒。

public void setSoLinger(boolean on, int linger) throws SocketException 

 
当我们理解了 SO_LINGER 选项的工作原理及其应用之后,现在回过头来在看 prepareToClose 方法的逻辑就很容易理解了。

 

     @Override
        protected Executor prepareToClose() {
            try {
                if (javaChannel().isOpen() && config().getSoLinger() > 0) {
                    //在设置SO_LINGER后,channel会延时关闭,在延时期间我们仍然可以进行读写,这样会导致io线程eventloop不断的循环浪费cpu资源
                    //所以需要在延时关闭期间 将channel注册的事件全部取消。
                    doDeregister();

                    /**
                     * 设置了SO_LINGER,不管是阻塞socket还是非阻塞socket,在关闭的时候都会发生阻塞,所以这里不能使用Reactor线程来
                     * 执行关闭任务,否则Reactor线程就会被阻塞。
                     * */
                    return GlobalEventExecutor.INSTANCE;
                }
            } catch (Throwable ignore) {
            }
            //在没有设置SO_LINGER的情况下,可以使用Reactor线程来执行关闭任务
            return null;
        }

首先我们来关注下 prepareToClose 方法的返回值,它会返回一个 Executor  ,这个 Executor 用于执行真正的 Channel 关闭任务。

大家这里可能会有疑问,Channel 上的 IO 操作之前不都是由 Reactor 线程负责执行吗?为什么这里需要用一个单独的 Executor 来执行呢?

原因就是如果我们设置了 SO_LINGER 选项 config().getSoLinger() > 0 ,如果继续采用 Reactor 线程执行 Channel 关闭的动作,那么在这种情况下底层Socket 的 close 方法会阻塞 Reactor 线程,直到 Socket 发送缓冲区中的数据全部发送出去并收到对端 ACK ,或者 linger 指定的超时时间到达。

由于 Reactor 线程负责多个 Channel 上的 IO 处理,如果被阻塞在这里,就会影响其他 Channel 上的 IO 处理,降低吞吐。所以当我们设置了 SO_LINGER 选项时,就不能使用 Reactor 线程来执行 Channel 关闭的动作,而是用GlobalEventExecutor.INSTANCE来负责执行 Channel 的关闭动作。

如果我们没有设置 SO_LINGER 选项,底层 Socket 的 close 方法会立即返回并不会阻塞,所以这种情况下,依然会使用 Reactor 线程来执行 Channel 的关闭动作。

prepareToClose 方法这种情况下会返回 null ,表示默认采用 Reactor 线程来执行 Channel 的关闭。

这里还有一个重要的点需要和大家强调的是,当我们设置了 SO_LINGER 选项之后,Channel 的关闭动作会被阻塞并延时关闭,在延时关闭期间,Reactor 线程依然可以响应 OP_READ 事件和 OP_WRITE 事件,这可能会导致 Reactor 线程不断的自旋循环浪费 CPU 资源,所以基于这个原因,netty 这里需要将 Channel 从 Reactor 上注销掉。这样 Reactor 线程就不会在响应 Channel 上的 IO 事件了。

2.1.4 doDeregister 注销 Channel

public abstract class AbstractNioChannel extends AbstractChannel {

   //channel注册到Selector后获得的SelectKey
    volatile SelectionKey selectionKey;

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

    protected SelectionKey selectionKey() {
        assert selectionKey != null;
        return selectionKey;
    }
}
public final class NioEventLoop extends SingleThreadEventLoop {
    //记录socketChannel从Selector上注销的个数 达到256个 则需要将无效selectKey从SelectedKeys集合中清除掉
    private int cancelledKeys;

    private static final int CLEANUP_INTERVAL = 256;
    /**
     * 将socketChannel从selector中注销 取消监听IO事件
     * */
    void cancel(SelectionKey key) {
        key.cancel();
        cancelledKeys ++;
        // 当从selector中注销的socketChannel数量达到256个,设置needsToSelectAgain为true
        // 在io.netty.channel.nio.NioEventLoop.processSelectedKeysPlain 中重新做一次轮询,将失效的selectKey移除,
        // 以保证selectKeySet的有效性
        if (cancelledKeys >= CLEANUP_INTERVAL) {
            cancelledKeys = 0;
            needsToSelectAgain = true;
        }
    }

}

Channel 在向 Reactor 中的 Selector 注册成功后,会得到一个 SelectionKey 。这个 SelectionKey 可以理解成 Channel 在 Selector 中的模型。

当 Channel 需要将自己从 Selector 中注销掉时,直接可以通过调用对应的 SelectionKey#cancel 方法。此时调用 SelectionKey#isValid 将会返回 false 。

SelectionKey#cancel 方法调用后,Selector 会将要取消的这个 SelectionKey 加入到 Selector 中的 cancelledKeys 集合中。

public abstract class AbstractSelector extends Selector {

    private final Set<SelectionKey> cancelledKeys = new HashSet<SelectionKey>();

    void cancel(SelectionKey k) {                      
        synchronized (cancelledKeys) {
            cancelledKeys.add(k);
        }
    }
}

随后在 Selector 的下一次轮询过程中,会将 cancelledKeys 集合中的 SelectionKey 从 Selector 中所有的 KeySet 中移除。这里的 KeySet 包括Selector用于存放 IO 就绪 SelectionKey 的 selectedKeys 集合,以及用于存放所有在 Selector 上注册的 Channel 对应 SelectionKey 的 keys 集合。

public abstract class SelectorImpl extends AbstractSelector {

    protected Set<SelectionKey> selectedKeys = new HashSet();
    protected HashSet<SelectionKey> keys = new HashSet();
    
     .....................省略...............
}

 

这里需要注意的是当我们调用 SelectionKey#cancel 方法后,该 SelectionKey 并不会立马从 Selector 中删除,只不过此时调用 SelectionKey#isValid 方法会返回 false 。需要等到下次轮询 selector.selectNow() 的时候,被取消掉的 SelectionKey 才会从 Selector 中被删除掉。

当在本次轮询期间,假如有大量的 Channel 从 Selector 中注销,就绪集合 selectedKeys 中依然会保存这些 Channel 对应 SelectionKey 直到下次轮询。那么当然会影响本次轮询结果 selectedKeys 的有效性,增加了许多不必要的遍历开销。

所以 netty 在 NioEventLoop#cancel 方法中做了一个优化来保证 Selector 中的 IO 就绪集合 selectedKeys 的有效性,当 Selector 中注销的 Channel 数量 cancelledKeys 超过 CLEANUP_INTERVAL = 256 个时,就会将 needsToSelectAgain 标志设置为 true 。

    private void processSelectedKeysOptimized() {
        for (int i = 0; i < selectedKeys.size; ++i) {

            ......循环处理Selector中的IO就绪集合selectedKeys.....

            if (needsToSelectAgain) {
                selectedKeys.reset(i + 1);
                selectAgain();
                i = -1;
            }
        }
    }

当 Reactor 线程在循环遍历处理 Selector 中的 IO 活跃 Channel 时,如果 needsToSelectAgain = true ,那么就会立马执行一次 selector.selectNow() ,目的就是为了清除 Selector 中已经注销的 Selectionkey ,从而保证IO就绪集合 selectedKeys 的有效性。

    private void selectAgain() {
        needsToSelectAgain = false;
        try {
            selector.selectNow();
        } catch (Throwable t) {
            logger.warn("Failed to update SelectionKeys.", t);
        }
    }

分类
标签
已于2022-8-9 17:03:24修改
收藏
回复
举报
回复
    相关推荐