netty writeAndFlush源码分析

​目录

          一、 背景

          二、源码


一、背景

writeAndFlush,顾名思义,就是写入(发送缓冲区)并且刷新,熟悉netty编码的同学对这个方法一定不会感到陌生, 这方法既能将数据写到发送缓存中, 也能刷新到channel中。

        二、源码

以在动态长度解码器那节中使用的例子为入口,我们进入到writeAndFlush看看其执行逻辑。

public class LengthFieldBasedFrameDecoderTestClient {
​
    public static void main(String[] args) throws Exception {
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(workerGroup)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.SO_KEEPALIVE, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) {
                            ch.pipeline()
                                    .addLast(new ChannelInboundHandlerAdapter() {
                                        public void channelActive(ChannelHandlerContext ctx) {
                                            ByteBuf byteBuf = Unpooled.copiedBuffer("hello world".getBytes());
                                            ctx.writeAndFlush(byteBuf);
                                        }
                                    });
                        }
                    });
            ChannelFuture f = b.connect("127.0.0.1", 9000).sync();
            System.out.println("Started LengthFieldBasedFrameDecoderTestClient...");
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}

     进入到ChannelHandlerContext的实现类AbstractChannelHandlerContext中。

   @Override
    public ChannelFuture writeAndFlush(Object msg) {
        return writeAndFlush(msg, newPromise());
    }

newPromise()是为了写成功或者失败后的异步回调通知(DefaultChannelPromise)。其中会注册listener监听器,对处理事件进行监听。

继续进入到实现了ChannelOutboundInvoker的writeAndFlush方法。

    @Override
    public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
        if (msg == null) {
            throw new NullPointerException("msg");
        }
​
        if (!validatePromise(promise, true)) {
            ReferenceCountUtil.release(msg);
            // cancelled
            return promise;
        }
​
        write(msg, true, promise);
​
        return promise;
    }

此时的msg就是发送的消息载体。promise是刚才创建的DefaultChannelPromise。

在这个writeAndFlush方法中首先对msg进行非空判断,然后对promise进行校验,核心逻辑为write方法,直接进入。

  private void write(Object msg, boolean flush, ChannelPromise promise) {  
        AbstractChannelHandlerContext next = findContextOutbound();
        final Object m = pipeline.touch(msg, next);
        EventExecutor executor = next.executor();
        if (executor.inEventLoop()) {
            if (flush) {
                next.invokeWriteAndFlush(m, promise);
            } else {
                next.invokeWrite(m, promise);
            }
        } else {
            AbstractWriteTask task;
            if (flush) {
                task = WriteAndFlushTask.newInstance(next, m, promise);
            }  else {
                task = WriteTask.newInstance(next, m, promise);
            }
            safeExecute(executor, task, promise, m);
        }
    }

findContextOutbound()用于以当前ctx为入口一直往前找,直到找到为出站的一个ctx为止,返回找到的ctx(此时为HeadContext,也就是第一个节点了),然后获取执行器NIOEventLoop,flush此时为true,直接进入invokeWriteAndFlush()方法。

  private void invokeWriteAndFlush(Object msg, ChannelPromise promise) {
        if (invokeHandler()) {
            //将消息放入输出缓冲区
            invokeWrite0(msg, promise);
            //将输出缓冲区中的数据通过socket发送到网络中
            invokeFlush0();
        } else {
            writeAndFlush(msg, promise);
        }
    }

到这里就很明显了,如果handler准备好了,依次调用invokeWrite0、invokeFlush0写入数据输出缓冲区中(ChannelOutboundBuffer)并且flush刷新缓冲区发送到网络中。

直接进入invokeWrite0方法。

此时将HeadContext转成出站Handler-ChannelOutboundHandler并且调用其write方法。

    private void invokeWrite0(Object msg, ChannelPromise promise) {
        try {
            ((ChannelOutboundHandler) handler()).write(this, msg, promise);
        } catch (Throwable t) {
            notifyOutboundHandlerException(t, promise);
        }
    }

再往下走就看到了熟悉的unsafe方法,netty所有io操作,底层都是依赖unsafe进行处理的。

        @Override
        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
            unsafe.write(msg, promise);
        }

此时的unsafe为NioSocketChannelUnsafe。

进入到其write方法逻辑中。

        @Override
        public final void write(Object msg, ChannelPromise promise) {
            assertEventLoop();
​
            ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
            if (outboundBuffer == null) {
                //如果outboundBuffer为null,则该通道已关闭,因此需要立即使将来失效。
                safeSetFailure(promise, WRITE_CLOSED_CHANNEL_EXCEPTION);
                // 立即释放消息以防止资源泄漏
                ReferenceCountUtil.release(msg);
                return;
            }
​
            int size;
            try {
                //过滤待发送的消息,只允许发送ByteBuf和FileRegion
                msg = filterOutboundMessage(msg);
                //预估待发送数据的大小
                size = pipeline.estimatorHandle().size(msg);
                if (size < 0) {
                    size = 0;
                }
            } catch (Throwable t) {
                safeSetFailure(promise, t);
                ReferenceCountUtil.release(msg);
                return;
            }
​
            outboundBuffer.addMessage(msg, size, promise);
        }

首先获得ChannelOutboundBuffer,如果outboundBuffer为null,说明通道已经关闭,立即释放消息以防止资源泄漏,然后直接return返回。

filterOutboundMessage(msg),过滤待发送的消息,只允许发送ByteBuf和FileRegion。

    @Override
    protected final Object filterOutboundMessage(Object msg) {
        if (msg instanceof ByteBuf) {
            ByteBuf buf = (ByteBuf) msg;
            if (buf.isDirect()) {
                return msg;
            }
​
            return newDirectBuffer(buf);
        }
        if (msg instanceof FileRegion) {
            return msg;
        }
​
        throw new UnsupportedOperationException(
                "unsupported message type: " + StringUtil.simpleClassName(msg) + EXPECTED_TYPES);
    }

在这里会对msg类型进行判断,只有ByteBuf以及 FileRegion可以进行网络传输,其他类型的数据是不支持的,如果是非ByteBuf或者FileRegion,直接抛出异常,消息类型是不支持的。

pipeline.estimatorHandle().size(msg)对待发送的消息大小进行预估,如果消息大小小于零,直接重置为零,然后最后通过addMessage(msg, size, promise)将消息添加到buffer中。

进入到addMessage(msg, size, promise)方法。

    public void addMessage(Object msg, int size, ChannelPromise promise) {
        Entry entry = Entry.newInstance(msg, size, total(msg), promise);
        if (tailEntry == null) {
            flushedEntry = null;
            tailEntry = entry;
        } else {
            Entry tail = tailEntry;
            tail.next = entry;
            tailEntry = entry;
        }
        if (unflushedEntry == null) {
            unflushedEntry = entry;
        }
​
        // increment pending bytes after adding message to the unflushed arrays.
        // See https://github.com/netty/netty/issues/1619
        incrementPendingOutboundBytes(size, false);
    }

首先构造一个Entry实例entry,这个Entry是ChannelOutboundBuffer中自定义的一个静态内部类,内部封装了一个Entry结点信息,核心属性如下:

        private final Handle<Entry> handle;
        Entry next;
        //原始消息
        Object msg;
        ByteBuffer[] bufs;
        ByteBuffer buf;
        //异步回调通知成功或者失败
        ChannelPromise promise;
        long progress;
        //对数据本身大小的记录
        long total;
        //msg+bufs+buf待发送数据总大小
        int pendingSize;
        int count = -1;
        boolean cancelled;

ChannelOutboundBuffer中几个核心属性如下:

    // flush操作时进行刷新的ByteBuf
    private Entry flushedEntry;
    // 还未刷新的ByteBuf
    private Entry unflushedEntry;
    // 始终指向最后一个Entry
    private Entry tailEntry;
    // 还未写入的刷新Entry对象数量
    private int flushed;

此时尾部结点tailEntry是为空的,所以,新创建的entry就放到了尾部作为tailEntry。unflushedEntry结点为entry。

然后incrementPendingOutboundBytes调整buffer的大小size。

    private void incrementPendingOutboundBytes(long size, boolean invokeLater) {
        if (size == 0) {
            return;
        }
​
        long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, size);
        if (newWriteBufferSize > channel.config().getWriteBufferHighWaterMark()) {
            setUnwritable(invokeLater);
        }
    }

至此,write写入一个信息就完成了,其实核心就是构造一个entry结点添加到buffer单向链表尾部。

然后继续回到之前invokeWriteAndFlush的invokeFlush0方法,invokeFlush0方法主要功能就两个:

①、将消息标记为待发送。

②、刷新发送缓冲区发送消息

直接进入方法内部。

来到unsafe操作。

        @Override
        public void flush(ChannelHandlerContext ctx) throws Exception {
            unsafe.flush();
        }

接着往下走。

        @Override
        public final void flush() {
            assertEventLoop();
​
            ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
            if (outboundBuffer == null) {
                return;
            }
            //消息标记为flushed
            outboundBuffer.addFlush();
            flush0();
        }

进入addFlush方法。

    public void addFlush() {
        // There is no need to process all entries if there was already a flush before and no new messages
        // where added in the meantime.
        //
        // See https://github.com/netty/netty/issues/2577
        Entry entry = unflushedEntry;
        if (entry != null) {
            if (flushedEntry == null) {
                // there is no flushedEntry yet, so start with the entry
                flushedEntry = entry;
            }
            do {
                //计数器
                flushed ++;
                if (!entry.promise.setUncancellable()) {
                    // 已取消,因此请确保我们释放内存并通知释放的字节
                    int pending = entry.cancel();
                    decrementPendingOutboundBytes(pending, false, true);
                }
                entry = entry.next;
            } while (entry != null);
​
            // 所有已刷新,因此重置unflushedEntry
            unflushedEntry = null;
        }
    }

将Entry entry = unflushedEntry意思是如果之前已经有刷新并且在此期间没有添加新消息,则无需处理所有entry,然后通过一个循环flushed记录还未刷新的entry数量。所有已刷新,因此重置unflushedEntry。

然后返回flush0,进入方法内部。

    protected void flush0() {
            if (inFlush0) {
                // 避免重复进入
                return;
            }
            // 没有数据,直接返回
            final ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
            if (outboundBuffer == null || outboundBuffer.isEmpty()) {
                return;
            }
            //正在刷新数据
            inFlush0 = true;
​
            //如果通道处于非活动状态,则将所有请求标记为失败。
            if (!isActive()) {
                try {
                    //Channel已经打开
                    if (isOpen()) {
                        outboundBuffer.failFlushed(FLUSH0_NOT_YET_CONNECTED_EXCEPTION, true);
                    } else {
                        // 不要触发channelWritabilityChanged,因为通道已经关闭
                        outboundBuffer.failFlushed(FLUSH0_CLOSED_CHANNEL_EXCEPTION, false);
                    }
                } finally {
                    inFlush0 = false;
                }
                return;
            }
​
            try {
                //将输出缓冲区中的数据通过socket传输
                doWrite(outboundBuffer);
            } catch (Throwable t) {
                if (t instanceof IOException && config().isAutoClose()) {
                    /**
                     * Just call {@link #close(ChannelPromise, Throwable, boolean)} here which will take care of
                     * failing all flushed messages and also ensure the actual close of the underlying transport
                     * will happen before the promises are notified.
                     *
                     * This is needed as otherwise {@link #isActive()} , {@link #isOpen()} and {@link #isWritable()}
                     * may still return {@code true} even if the channel should be closed as result of the exception.
                     */
                    close(voidPromise(), t, FLUSH0_CLOSED_CHANNEL_EXCEPTION, false);
                } else {
                    outboundBuffer.failFlushed(t, true);
                }
            } finally {
                inFlush0 = false;
            }
        }

进入到核心逻辑doWrite(outboundBuffer)。

    @Override
    protected void doWrite(ChannelOutboundBuffer in) throws Exception {
        for (;;) {
            int size = in.size();
            if (size == 0) {
                // All written so clear OP_WRITE
                clearOpWrite();
                break;
            }
            long writtenBytes = 0;
            boolean done = false;
            boolean setOpWrite = false;
​
            ByteBuffer[] nioBuffers = in.nioBuffers();
            int nioBufferCnt = in.nioBufferCount();
            long expectedWrittenBytes = in.nioBufferSize();
            SocketChannel ch = javaChannel();
​
            // Always us nioBuffers() to workaround data-corruption.
            // See https://github.com/netty/netty/issues/2761
            switch (nioBufferCnt) {
                case 0:
                    // 除了ByteBuffers之外,其他东西
                    super.doWrite(in);
                    return;
                case 1:
                    // 只有一个ByteBuf,非聚集写入
                    ByteBuffer nioBuffer = nioBuffers[0];
                    for (int i = config().getWriteSpinCount() - 1; i >= 0; i --) {
                        final int localWrittenBytes = ch.write(nioBuffer);
                        if (localWrittenBytes == 0) {
                            setOpWrite = true;
                            break;
                        }
                        expectedWrittenBytes -= localWrittenBytes;
                        writtenBytes += localWrittenBytes;
                        if (expectedWrittenBytes == 0) {
                            done = true;
                            break;
                        }
                    }
                    break;
                default:
                    // 多个ByteBuf,因此使用聚集写入 config().getWriteSpinCount()=16
                    for (int i = config().getWriteSpinCount() - 1; i >= 0; i --) {
                        final long localWrittenBytes = ch.write(nioBuffers, 0, nioBufferCnt);
                        if (localWrittenBytes == 0) {
                            setOpWrite = true;
                            break;
                        }
                        expectedWrittenBytes -= localWrittenBytes;
                        writtenBytes += localWrittenBytes;
                        //如果16次写入之后还有数据没有写入,那么done就位false
                        if (expectedWrittenBytes == 0) {
                            done = true;
                            break;
                        }
                    }
                    break;
            }
​
            // 释放完全写入的缓冲区,并更新部分写入的缓冲区的索引。
            in.removeBytes(writtenBytes);
​
            if (!done) {
                //没有完全写入所有缓冲区。
                incompleteWrite(setOpWrite);
                break;
            }
        }
    }

首先size方法(flushed的大小)获取到ChannelOutboundBuffer的未刷新的entry数量,如果为空,说明所有的都应刷新,那么就清空写事件clearOpWrite。writtenBytes为本次循环已经写出的字节数,done标识本次循环是否写出了所有待写出的数据,setOpWrite标识是否需要设置SelectionKey.OP_WRITE事件,in.nioBuffers确保待写的写操作仅有ByteBufs

    public ByteBuffer[] nioBuffers() {
        long nioBufferSize = 0;
        int nioBufferCount = 0;
        final InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
        ByteBuffer[] nioBuffers = NIO_BUFFERS.get(threadLocalMap);
        Entry entry = flushedEntry;
        while (isFlushedEntry(entry) && entry.msg instanceof ByteBuf) {
            if (!entry.cancelled) {
                ByteBuf buf = (ByteBuf) entry.msg;
                final int readerIndex = buf.readerIndex();
                final int readableBytes = buf.writerIndex() - readerIndex;
​
                if (readableBytes > 0) {
                    if (Integer.MAX_VALUE - readableBytes < nioBufferSize) {
                        // If the nioBufferSize + readableBytes will overflow an Integer we stop populate the
                        // ByteBuffer array. This is done as bsd/osx don't allow to write more bytes then
                        // Integer.MAX_VALUE with one writev(...) call and so will return 'EINVAL', which will
                        // raise an IOException. On Linux it may work depending on the
                        // architecture and kernel but to be safe we also enforce the limit here.
                        // This said writing more the Integer.MAX_VALUE is not a good idea anyway.
                        //
                        // See also:
                        // - https://www.freebsd.org/cgi/man.cgi?query=write&sektion=2
                        // - http://linux.die.net/man/2/writev
                        break;
                    }
                    nioBufferSize += readableBytes;
                    int count = entry.count;
                    if (count == -1) {
                        //noinspection ConstantValueVariableUse
                        entry.count = count =  buf.nioBufferCount();
                    }
                    int neededSpace = nioBufferCount + count;
                    if (neededSpace > nioBuffers.length) {
                        nioBuffers = expandNioBufferArray(nioBuffers, neededSpace, nioBufferCount);
                        NIO_BUFFERS.set(threadLocalMap, nioBuffers);
                    }
                    if (count == 1) {
                        ByteBuffer nioBuf = entry.buf;
                        if (nioBuf == null) {
                            // cache ByteBuffer as it may need to create a new ByteBuffer instance if its a
                            // derived buffer
                            entry.buf = nioBuf = buf.internalNioBuffer(readerIndex, readableBytes);
                        }
                        nioBuffers[nioBufferCount ++] = nioBuf;
                    } else {
                        ByteBuffer[] nioBufs = entry.bufs;
                        if (nioBufs == null) {
                            // cached ByteBuffers as they may be expensive to create in terms
                            // of Object allocation
                            entry.bufs = nioBufs = buf.nioBuffers();
                        }
                        nioBufferCount = fillBufferArray(nioBufs, nioBuffers, nioBufferCount);
                    }
                }
            }
            entry = entry.next;
        }
        this.nioBufferCount = nioBufferCount;
        this.nioBufferSize = nioBufferSize;
​
        return nioBuffers;
    }

此方法获取所有待写出的ByteBuf,然后构造出一个个ByteBuffer,因为底层还是利用jdk的ByteBuffer进行网络传输的,最终汇总成一个ByteBuffer数组进行返回。nioBufferCount为本次循环需要写出的ByteBuffer个数,然后对nioBufferCnt进行匹配。

如果为零,除了ByteBuffer,我们还有其他东西要写,所以执行doWrite(in)去写。

    @Override
    protected void doWrite(ChannelOutboundBuffer in) throws Exception {
        int writeSpinCount = -1;
​
        boolean setOpWrite = false;
        for (;;) {
            Object msg = in.current();
            if (msg == null) {
                // Wrote all messages.
                clearOpWrite();
                // Directly return here so incompleteWrite(...) is not called.
                return;
            }
​
            if (msg instanceof ByteBuf) {
                ByteBuf buf = (ByteBuf) msg;
                int readableBytes = buf.readableBytes();
                if (readableBytes == 0) {
                    in.remove();
                    continue;
                }
​
                boolean done = false;
                long flushedAmount = 0;
                if (writeSpinCount == -1) {
                    writeSpinCount = config().getWriteSpinCount();
                }
                for (int i = writeSpinCount - 1; i >= 0; i --) {
                    int localFlushedAmount = doWriteBytes(buf);
                    if (localFlushedAmount == 0) {
                        setOpWrite = true;
                        break;
                    }
​
                    flushedAmount += localFlushedAmount;
                    if (!buf.isReadable()) {
                        done = true;
                        break;
                    }
                }
​
                in.progress(flushedAmount);
​
                if (done) {
                    in.remove();
                } else {
                    // Break the loop and so incompleteWrite(...) is called.
                    break;
                }
            } else if (msg instanceof FileRegion) {
                FileRegion region = (FileRegion) msg;
                boolean done = region.transferred() >= region.count();
​
                if (!done) {
                    long flushedAmount = 0;
                    if (writeSpinCount == -1) {
                        writeSpinCount = config().getWriteSpinCount();
                    }
​
                    for (int i = writeSpinCount - 1; i >= 0; i--) {
                        long localFlushedAmount = doWriteFileRegion(region);
                        if (localFlushedAmount == 0) {
                            setOpWrite = true;
                            break;
                        }
​
                        flushedAmount += localFlushedAmount;
                        if (region.transferred() >= region.count()) {
                            done = true;
                            break;
                        }
                    }
​
                    in.progress(flushedAmount);
                }
​
                if (done) {
                    in.remove();
                } else {
                    // Break the loop and so incompleteWrite(...) is called.
                    break;
                }
            } else {
                // Should not reach here.
                throw new Error();
            }
        }
        incompleteWrite(setOpWrite);
    }

在doWrite中核心逻辑我们看doWriteBytes(buf)和doWriteFileRegion(region)两种方式,进入到方法内部会发现其实底层还是利用了JavaChannel进行数据写入。

    @Override
    protected int doWriteBytes(ByteBuf buf) throws Exception {
        final int expectedWrittenBytes = buf.readableBytes();
        return buf.readBytes(javaChannel(), expectedWrittenBytes);
    }
​
    @Override
    protected long doWriteFileRegion(FileRegion region) throws Exception {
        final long position = region.transferred();
        return region.transferTo(javaChannel(), position);
    }

返回来如果nioBufferCnt为1,那么就使用费非聚集写入,如果大于1,就使用循环进行写入,最终释放完全写入的缓冲区,并更新部分写入的缓冲区的索引。

最终如果经过循环之后还有数据剩余,那么就包装成task交给eventLoop进行处理。

    protected final void incompleteWrite(boolean setOpWrite) {
        // Did not write completely.
        if (setOpWrite) {
            setOpWrite();
        } else {
            // Schedule flush again later so other tasks can be picked up in the meantime
            Runnable flushTask = this.flushTask;
            if (flushTask == null) {
                flushTask = this.flushTask = new Runnable() {
                    @Override
                    public void run() {
                        flush();
                    }
                };
            }
            eventLoop().execute(flushTask);
        }
    }

个人才疏学浅、信手涂鸦,netty框架更多模块解读相关源码持续更新中,感兴趣的朋友请移步至个人公众号,谢谢支持😜😜......

公众号:wenyixicodedog

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值