Netty的writeAndFlush源码解析

530 阅读4分钟

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

一、 源码入口

上篇文章我们讲述了几个典型的内置解码器的的源码,本节课我们将讲述关于数据通信过程中数据写入的过程.

我们以之前的一个例子的代码为例:

image-20210509165529927

这段代码相信大家都记得,我们直接进入主题,分析以下他的源码:

ctx.writeAndFlush(byteBuf);

image-20210509165652555

@Override
public ChannelFuture writeAndFlush(Object msg) {
    return writeAndFlush(msg, newPromise());
}
@Override
public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
    write(msg, true, promise);
    return promise;
}

这里注意个小细节,write(msg, true, promise); 的第二个参数是true,为什么是true呢?因为我们现在调用的是writeAndFlush方法,是存在flush的,所以为true,不带flush为false!

private void write(Object msg, boolean flush, ChannelPromise promise) {
    ...............忽略..............
    //寻找对应的handler 根据掩码
    final AbstractChannelHandlerContext next = findContextOutbound(flush ?
                                                                   (MASK_WRITE | MASK_FLUSH) : MASK_WRITE);
    final Object m = pipeline.touch(msg, next);
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        //是否调用了 writeAndFlush
        if (flush) {
            next.invokeWriteAndFlush(m, promise);
        } else {
            //是否调用了 write
            next.invokeWrite(m, promise);
        }
    } else {
        ...........忽略............
    }
}

这里为了方便分析,我们还是按照同步的方式进行分析:

final AbstractChannelHandlerContext next = findContextOutbound(flush ? (MASK_WRITE | MASK_FLUSH) : MASK_WRITE);

从当前的节点开始向上寻找实现了 write方法和flush方法的,默认是HeadContext来实现的,所以这里找到的必定是HeadContext!

二、源码分析

我们调用的是writeAndflush方法,所以flush为true,所以会进入到if逻辑里面:

next.invokeWriteAndFlush(m, promise);
void invokeWriteAndFlush(Object msg, ChannelPromise promise) {
    if (invokeHandler()) {
        //加入写队列
        invokeWrite0(msg, promise);
        //刷新缓冲区
        invokeFlush0();
    } else {
        writeAndFlush(msg, promise);
    }
}

invokeHandler() 一般返回为true, 所以我们进入到 invokeWrite0:

1. 追加写队列

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

我们前面分析过,这个handler必定是HeadContext的,所以我们进入到 io.netty.channel.DefaultChannelPipeline.HeadContext#write:

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

image-20210509171910532

@Override
public final void write(Object msg, ChannelPromise promise) {
    assertEventLoop();

    ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
    ..........忽略................

    int size;
    try {
        //转换bytebuf为堆外内存
        //io.netty.channel.nio.AbstractNioByteChannel.filterOutboundMessage
        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);
}
  • Netty默认的就是使用堆外内存,即使你创建的是一个堆内内存Netty也会强行转换为堆外内存(代码如下):

    msg = filterOutboundMessage(msg);
    

    image-20210509172312632

    @Override
    protected final Object filterOutboundMessage(Object msg) {
        if (msg instanceof ByteBuf) {
            ByteBuf buf = (ByteBuf) msg;
            //判断是不是堆外内存
            if (buf.isDirect()) {
                //堆外内存无需转换直接返回
                return msg;
            }
    		//创建一个堆外内存,将数据写进堆外内存
            return newDirectBuffer(buf);
        }
    	....................................................
    }
    
  • 判断可读字节数:

    size = pipeline.estimatorHandle().size(msg);
    
  • 开始向写队列追加数据

    //将数据插入到写队列
    outboundBuffer.addMessage(msg, size, promise);
    
    public void addMessage(Object msg, int size, ChannelPromise promise) {
        //将BuytBuf封装为Entity
        Entry entry = Entry.newInstance(msg, size, total(msg), promise);
        //当tailEntity为Null的时候
        if (tailEntry == null) {
            //初始化 flushedEntry
            flushedEntry = null;
        } else {
            Entry tail = tailEntry;
            //将当前的尾节点指向新节点
            tail.next = entry;
        }
        //把当前的新节点赋值为尾节点
        tailEntry = entry;
        if (unflushedEntry == null) {
            //unflushedEntry也指向entry新节点
            unflushedEntry = entry;
        }
    
        // 将消息添加到未刷新的数组后,增加待处理字节。
        // See https://github.com/netty/netty/issues/1619
        incrementPendingOutboundBytes(entry.pendingSize, false);
    }
    

    首先进行节点的追加,该次追加是为刷新节点的追加,我们看一动图的演示:

    写队列入队过程

    事实上说白了就是,数据在这一步并不会被写进通道,而是被追加到了Netty设计的一个链表上,只要不调用flush方法,该链表的数据就一致被追加;

    • tailEntry永远指向最新的一个数据,unflushedEnrty永远指向头节点!

    • 每次追加一个节点,这个节点都会被追加到上一个节点next的引用!

    每次追加数据完毕后,记录以下当前待刷新的字节的数量:

    incrementPendingOutboundBytes(entry.pendingSize, false);
    
    private void incrementPendingOutboundBytes(long size, boolean invokeLater) {
        if (size == 0) {
            return;
        }
        //当前缓冲区有多少代写字节 TOTAL_PENDING_SIZE_UPDATER
        long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, size);
        //待写字节 > 64 * 1024的话
        if (newWriteBufferSize > channel.config().getWriteBufferHighWaterMark()) {
            setUnwritable(invokeLater);
        }
    }
    

    这里会判断当前的代写字节是否超过了 64 * 1024

    private void setUnwritable(boolean invokeLater) {
        for (;;) {
            final int oldValue = unwritable;
            final int newValue = oldValue | 1;
            if (UNWRITABLE_UPDATER.compareAndSet(this, oldValue, newValue)) {
                if (oldValue == 0 && newValue != 0) {
                    //传播 ChannelWritabilityChanged(invokeLater) 事件
                    fireChannelWritabilityChanged(invokeLater);
                }
                break;
            }
        }
    }
    

    主要逻辑就是当待写入的字节数量大于 64K的时候们就会传播一个 ChannelWritabilityChanged事件!

2. 追加到刷新队列

我们回到任务的主线: io.netty.channel.AbstractChannelHandlerContext#invokeWriteAndFlush

invokeFlush0();
private void invokeFlush0() {
    try {
        ((ChannelOutboundHandler) handler()).flush(this);
    } catch (Throwable t) {
        notifyHandlerException(t);
    }
}

同样同上面一样,这里的handler同样是 HeadContext,我们进入到:io.netty.channel.DefaultChannelPipeline.HeadContext#flush:

image-20210509174724941

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

    ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
    if (outboundBuffer == null) {
        return;
    }
    //添加到刷新缓冲区
    outboundBuffer.addFlush();
    //开始刷新到管道
    flush0();
}

这里做了两件事:

  1. 将写队列的数据转移到刷新队列
  2. 将刷新队列的数据写到Socket通道

I. 将写队列的数据转移到刷新队列

//添加到刷新缓冲区
outboundBuffer.addFlush();
public void addFlush() {
    //取出第一个 等待刷新队列的节点
    Entry entry = unflushedEntry;
    if (entry != null) {
        //当第一次添加
        if (flushedEntry == null) {
            // 还没有flushedEntry,所以从条目开始
            //将刷新队列等于当前的待刷新的节点
            flushedEntry = entry;
        }
        do {
            //自增
            flushed ++;
            if (!entry.promise.setUncancellable()) {
                // 已取消,因此请确保我们释放内存并通知释放的字节
                int pending = entry.cancel();
                //减少将要写入的待处理字节。
                decrementPendingOutboundBytes(pending, false, true);
            }
            //不断将数据处理完毕
            entry = entry.next;
        } while (entry != null);

        // 所有已刷新,因此重置为未刷新
        unflushedEntry = null;
    }
}

这个逻辑,起始细看来并不难,我们之前写队列的结构如下:

image-20210509175512631

我们看这个逻辑:

Entry entry = unflushedEntry;
if (entry != null) {
    //当第一次添加
    if (flushedEntry == null) {
        // 还没有flushedEntry,所以从条目开始
        //将刷新队列等于当前的待刷新的节点
        flushedEntry = entry;
    }

根据上图,flushedEntry确实为Null,就会进入到if逻辑,使 flushedEntry指向entry, 与是乎,结构变成了下图这样:

image-20210509175817042

开始循环消费带待刷新队列:

do {
    //自增
    flushed ++;
    if (!entry.promise.setUncancellable()) {
        // 已取消,因此请确保我们释放内存并通知释放的字节
        int pending = entry.cancel();
        //减少将要写入的待处理字节。
        decrementPendingOutboundBytes(pending, false, true);
    }
    //不断将数据处理完毕
    entry = entry.next;
} while (entry != null);
  • 这里会不断的消费 ,等到数据量小于32K的时候,同样会触发ChannelWritabilityChanged

  • decrementPendingOutboundBytes(pending, false, true);
    
  • private void decrementPendingOutboundBytes(long size, boolean invokeLater, boolean notifyWritability) {
        if (size == 0) {
            return;
        }
    
        long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, -size);
        //待处理的节点 小于 32*1024
        if (notifyWritability && newWriteBufferSize < channel.config().getWriteBufferLowWaterMark()) {
            //ChannelWritabilityChanged(invokeLater);
            setWritable(invokeLater);
        }
    }
    
  • 这个源码可以自己跟一下试试,和上面的基本一致,一个是+一个是-

// 所有已刷新,因此重置为未刷新
unflushedEntry = null;

此时的结构图变为:

image-20210509180748785

总体的刷新队列如下:

fulsh_querey

II. 将数据写进刷新队列

//开始刷新到管道
flush0();

image-20210509180936660

@Override
protected final void flush0() {
    if (!isFlushPending()) {
        super.flush0();
    }
}
protected void flush0() {
    .................忽略..................

    try {
        //开始写入
        doWrite(outboundBuffer);
    } catch (Throwable t) {
        .................忽略..................
    }
}

这里我们重点关注:doWrite(outboundBuffer);

image-20210509181249349

@Override
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
    int writeSpinCount = config().getWriteSpinCount();
    do {
        //获取当前刷新队列的头数据
        Object msg = in.current();
        ..................................
        //不断循环写入
        //开始向jdk管道写入数据
        writeSpinCount -= doWriteInternal(in, msg);
    } while (writeSpinCount > 0);

    incompleteWrite(writeSpinCount < 0);
}

这里开始循环消费之前的flush队列,调用 doWriteInternal写入:

private int doWriteInternal(ChannelOutboundBuffer in, Object msg) throws Exception {
    if (msg instanceof ByteBuf) {
        ByteBuf buf = (ByteBuf) msg;
        ...................................
        //开始写入
        final int localFlushedAmount = doWriteBytes(buf);
        ...................................
        //磁盘写入
    } else if (msg instanceof FileRegion) {
        ...................................
    }
    return WRITE_STATUS_SNDBUF_FULL;
}

我们进入到:doWriteBytes(buf);方法:

image-20210509181633595

@Override
protected int doWriteBytes(ByteBuf buf) throws Exception {
    //返回可读字节
    final int expectedWrittenBytes = buf.readableBytes();
    //将 buf写入管道
    return buf.readBytes(javaChannel(), expectedWrittenBytes);
}

获取当前的可读字节数,然后通过 javaChannel()返回的channel对象写入通道,至此一个entity的数据就被写入到Socket通道里面了!

三、总结

  1. 先将数据写进write队列
  2. 将write队列的Entity转移到flush队列
  3. 将flush队列里面的Entity里面的ByteBuf通过JDK底层的Channel写进Socket通道!