Netty 进阶:write流程全解析

1. ChannelOutboundBuffer
1.1 ChannelOutboundBuffer概述

在分析write前,有必要介绍一下ChannelOutboundBuffer,顾名思义该类是用来缓存写向Socket的数据。每个 ChannelSocket 的 Unsafe 都有一个绑定的 ChannelOutboundBuffer , Netty 向站外输出数据的过程统一通过 ChannelOutboundBuffer 类进行封装,目的是为了提高网络的吞吐量,在外面调用 write 的时候,数据并没有写到 Socket,而是写到了 ChannelOutboundBuffer 这里,当调用 flush 的时候,才真正的向 Socket 写入
下面看它定义的代码:

protected abstract class AbstractUnsafe implements Unsafe {
	private volatile ChannelOutboundBuffer outboundBuffer = new ChannelOutboundBuffer(AbstractChannel.this);
}

它被定义在AbstractUnsafe类中,AbstractUnsafe是AbstractChannel的内部类。在ChannelOutboundBuffer的构造方法中传入了AbstractChannel.this对象,就是把自己传入。可以猜到ChannelOutboundBuffer内部有一个Channel的属性。而且每一个Channel实例化时,都会调用父类AbstractChannel的构造函数,代码如下。

protected AbstractChannel(Channel parent) {
    this.parent = parent;
    id = newId();
    unsafe = newUnsafe();//新建unsafe
    pipeline = newChannelPipeline();
}

可见每一个Channel都有一个unsafe属性,因此可以说每一个Channe都关联了一个ChannelOutboundBuffer。引用关系如下:

/**
 * 箭头表示:起点持有终点的应用
 * 
 * Channel → unsafe
 *   ↑         ↓
 * ChannelOutboundBuffer
 *
 */

当我们清楚了这层关系之后,下面来进入ChannelOutboundBuffer内部看看它的具体实现,先看它的几个重要属性:

//所绑定的Channel
private final Channel channel;

// Entry(flushedEntry) --> ... Entry(unflushedEntry) --> ... Entry(tailEntry)
// 表示要lush到socket的Entry起点
private Entry flushedEntry;
// 表示不flush到socket的Entry起点
private Entry unflushedEntry;
// 链表的尾节点
private Entry tailEntry;
// 将要写入socket的entry个数
private int flushed;
//是否可写的标志,0为可以写,1不能写入
private volatile int unwritable;

ChannelOutboundBuffer中维护了节点元素为Entry的单向链表。Entry为待发送数据的一层封装,Entry 里面包含了待写出ByteBuf 以及消息回调 promise,实际待发送数据保存在Entry的Object msg中。列表的结果如下图所示:
Netty
看起来像是两个头结点的列表,为了flushedEntry是用了虚线。当 addFlush 方法的时候会将 unflushedEntry 赋值给 flushedEntry。表示即将从这里开始刷新到socket。

1.2 AddMessage

当保存数据的结构定义好了,那么如何添加数据?addMessage方法就是添加数据到此列表。

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.
    incrementPendingOutboundBytes(entry.pendingSize, false);
}

先是将数据封装为一个Entry对象,第一次调用三者都为空,则将新的entry赋值给tailEntry和unflushedEntry,并且将flushedEntry置为空。如果不是第一次调用,则将新的entry添加到tailEntry的后面,并且将tailEntry指向新的entry。另外两个指针不变。最后一行是通过CAS增加buffer的总字节数。

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

如果总字节数超过默认的64KB,则设置通过CAS修改unwritable标志位。

private void setUnwritable(boolean invokeLater) {
	for (;;) {
	    final int oldValue = unwritable;//初始值0
	    final int newValue = oldValue | 1;
	    if (UNWRITABLE_UPDATER.compareAndSet(this, oldValue, newValue)) {
	        if (oldValue == 0 && newValue != 0) {
	            fireChannelWritabilityChanged(invokeLater);
	        }
	        break;
	    }
	}
}

修改成功后,激活fireChannelWritabilityChanged事件,这是一个Inbound事件,从Head节点开始传播。关于unwritable状态为非常重要,在Channel的isWriteable方法就是通过这个状态为来判断Channel是否可以写入。

@Override
public boolean isWritable() {
    ChannelOutboundBuffer buf = unsafe.outboundBuffer();
    return buf != null && buf.isWritable();
}

另外这里有一个点很重要Entry.newInstance:

static Entry newInstance(Object msg, int size, long total, ChannelPromise promise) {
    Entry entry = RECYCLER.get();
    entry.msg = msg;
    entry.pendingSize = size + CHANNEL_OUTBOUND_BUFFER_ENTRY_OVERHEAD;
    entry.total = total;
    entry.promise = promise;
    return entry;
}

Recycler是一个Netty实现的基于线程局部堆栈的轻量级对象池,先不管它内部的实现,只需要知道不是每次添加数据都需要new一个Entry对象,而是通过对象池技术复用,只是取出来之后将数据以及promise赋值给Entry。这样做的目的不多说。必然能降低开销。

1.3 addFlush

addMessage的作用是将数据添加到ChannelOutboundBuffer维护的列表中,但是addFlush并不是消费列表中元素,它的作用是确定将要flush的元素。更准确的说是更改flushedEntry和unflushedEntry的值。

 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()) {
                    // Was cancelled so make sure we free up memory and notify about the freed bytes
                    int pending = entry.cancel();
                    decrementPendingOutboundBytes(pending, false, true);
                }
                entry = entry.next;
            } while (entry != null);

            // All flushed so reset unflushedEntry
            unflushedEntry = null;
        }
    }

首先拿到未刷新的头节点。将这个 unflushedEntry 赋值给 flushedEntry,循环尝试设置这些节点,能做取消操作了,如果尝试失败了,就将这个节点取消。同时将 totalPendingSize相应的减小。设置之后,promise 调用 cancel 方法就会返回 false。
至于真正的将数据flush到socket是在具体的SocketChannel中实现。

2. write

清楚了上面的内容之后,开始分析write,一般我们调用channel.write、ctx.write方法,其实都是ChannelOutboundInvoker接口声明的方法。文档中讲的很到位

Request to write a message via this ChannelHandlerContext through the ChannelPipeline. This method will not request to actual flush, so be sure to call flush() once you want to request to flush all pending data to the actual transport.

写入消息通过通过此ChannelHandlerContext传播在ChannelPipeline。 此方法不会请求实际刷新,因此一旦要请求将所有待处理数据刷新到实际传输,请务必调用flush()。

也就是说此write方法不会把数据刷到socket中,需要手动的调用flush函数。其实两种channel.write、ctx.write没有啥区别,只是开始传播的Context节点不同,前者是从tail节点开始传播,而后者是从当前Handler的下一个OutBoundHandler开始传播,流过解码器、编码器最终都会流向Head节点,统一处理。这里有一个细节就是业务handler应该放在编解码器的后面,编解码器的作用是ByteBuf<——>Message,如果放在业务handler后面,运行会报错。如果不使用编解码器,那么只能ctx.write(bytebuf),就是说入参只能byteBuf类型。本文重点不是编解码器,因此跳过这部分内容,我们主要分析HeadConext的内容。

调用过程见下图:
Netty
unsafe.write的实现是在AbstractChannel.AbstractUnsafe中,代码如下:

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

     ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
     int size;
     try {
         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);
}
  1. 获取和Channel关联的outboundBuffer
  2. 将要写的数据转换为directbuf,如果是FileRegion类型,直接返回。意味着通过socket传输的对象只能是非堆的ByteBuf和FileRegion。
 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;
     }
 }
  1. 将ByteBuf msg存入ChannelOutboundBuffer中,这个在前面已经详细分析过

小结:不管是调用chanel.write还是ctx.write方法都只是将数据放入ChannelOutboundBuffer,当调用flush时,才统一的写入Socket。至此write已经分析完毕

3. flush

正如write方法的文档所言,write方法不会将数据刷到socket,需要程序员手动调用ctx或者channel的flush方法,这两者的区别和前面分析的write的区别类似。只是Handler的起点不同而已。同样最终真正的逻辑都是在HeadContext中实现,即调用unsafe.flush方法。下面我们逐步分析。

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

unsafe.flush方法的实现是在AbstractChannel.AbstractUnsafe#flush,

@Override
public final void flush() {
    assertEventLoop();
    ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
    outboundBuffer.addFlush();
    flush0();
}

看到熟悉的addFlush,它的作用是确定要入socket的数据,从outboundBuffer总字节数减去将要写的数据的字节数

flush0的实现是在.AbstractNioChannel.AbstractNioUnsafe中:

 @Override
protected final void flush0() {
	// Flush immediately only when there's no pending flush.
	// If there's a pending flush operation, event loop will call forceFlush() later,
	// and thus there's no need to call it now.
	if (!isFlushPending()) {
	super.flush0();
	}
}

如果未注册过OP_WRITE事件,表示Socket缓冲区未满,可以进行写操作,我们看一下他的判断逻辑:

private boolean isFlushPending() {
    SelectionKey selectionKey = selectionKey();
    return selectionKey.isValid() && (selectionKey.interestOps() & SelectionKey.OP_WRITE) != 0;
}

这里有一个细节,为什么注册了OP_WRITE事件就不能进行写操作了呢?我们一般会主动的注册OP_READ事件是对读感兴趣,包括Netty或者普通的NIO程序,但是很少去注册一个OP_WRITE事件。首先要明白OP_WRITE的意义,向多路复用器注册OP_WRITE事件,当Socket可以写的时候返回,但是Socket绝大部分情况下是可以写的,因此注册此事件的作用就不打了。那么什么时候注册改事件合适?当Socket的缓冲区繁忙、网络因素等不可写的时候注册此事件,其实在之前的EventLoop文章中提到过,到轮训到OP_WRITE事件时,会调用flush方法,把ChannelOutboundBuffer的数据刷到socket中取。另外要注意:一般不要随便想多路复用器注册OP_WRITE事件。

继续调用父类AbstractChannel.AbstractUnsafe:

protected void flush0() {
	// 防止重复调用
	if (inFlush0) {
	   return;
	}
	//如果没有数据要flush就返回
	final ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
	if (outboundBuffer == null || outboundBuffer.isEmpty()) {
	   return;
	}
	inFlush0 = true;
	// Mark all pending write requests as failure if the channel is inactive.
	if (!isActive()) {
	   try {
	       if (isOpen()) {
	           outboundBuffer.failFlushed(FLUSH0_NOT_YET_CONNECTED_EXCEPTION, true);
	       } else {
	           // Do not trigger channelWritabilityChanged because the channel is closed already.
	           outboundBuffer.failFlushed(FLUSH0_CLOSED_CHANNEL_EXCEPTION, false);
	       }
	   } finally {
	       inFlush0 = false;
	   }
	   return;
	}
	doWrite(outboundBuffer);
}
  1. 判断Channel的输出缓冲区是否为null或待发送的数据个数为0,如果是则直接返回,因为此时并没有数据需要发送。
  2. 判断当前的NioSocketChannel是否是Inactive状态,如果是,则会标识所有等待写请求为失败(即所有的write操作的promise都会是失败完成),并且如果NioSocketChannel已经关闭了,失败的原因是“FLUSH0_CLOSED_CHANNEL_EXCEPTION”且不会回调注册到promise上的listeners;但如果NioSocketChannel还是open的,则失败的原始是“FLUSH0_NOT_YET_CONNECTED_EXCEPTION”并且会回调注册到promise上的listeners。
  3. 调用doWrite(outboundBuffer);方法将Channel输出缓冲区中的数据通过socket传输给对端。

doWrite方法是根据不同的Channel有多种实现,我们分析NioSocketChannel#doWrite方法。
netty write
netty write
在这里插入图片描述
方法内部比较复杂,将其分解为四部分内容,一个很重要的细节:
writeSpinCount是循环次数,默认为16次,作用数如果数据量过大,一次性(16次循环)写完过于可能耗尽socket缓冲区,当循环了16次还是写不完,那么注册OP_WRITE事件,当socket可以写的时候,继续flush。
下面分析flush的流程:

  1. 将ChannelOutboundBuffer中的ByteBuf复制到Java NIO ByteBuffer数组中
  2. 如何ByteBuffer数组的长度为1,那么取出数组的第一位,调用原生Java NIO 写入Socket,如果实际写入的字节数小于等于0,代表Socket不可写。调用incompleteWrite函数,表明没有写进去,向多路复用器注册写事件。如果写进去数据,调整maxBytesPerGatheringWrite的值。并且清除缓冲区的数据。
  3. 如果是多个ByteBuffer,逻辑和上面一样,只不过写的是ByteBuffer数组。
  4. 是否写完的处理函数,根据writeSpinCount判断。如果写完,清除写事件,这一点非常重要。如果没写完,注册写事件。

我们重点分析第一步

 public ByteBuffer[] nioBuffers(int maxCount, long maxBytes) {
        assert maxCount > 0;
        assert maxBytes > 0;
        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 (maxBytes - readableBytes < nioBufferSize && nioBufferCount != 0) {
                        break;
                    }
                    nioBufferSize += readableBytes;
                    int count = entry.count;
                    if (count == -1) {
                        //noinspection ConstantValueVariableUse
                        entry.count = count = buf.nioBufferCount();
                    }
                    int neededSpace = min(maxCount, 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) {
                            entry.bufs = nioBufs = buf.nioBuffers();
                        }
                        for (int i = 0; i < nioBufs.length && nioBufferCount < maxCount; ++i) {
                            ByteBuffer nioBuf = nioBufs[i];
                            if (nioBuf == null) {
                                break;
                            } else if (!nioBuf.hasRemaining()) {
                                continue;
                            }
                            nioBuffers[nioBufferCount++] = nioBuf;
                        }
                    }
                    if (nioBufferCount == maxCount) {
                        break;
                    }
                }
            }
            entry = entry.next;
        }
        this.nioBufferCount = nioBufferCount;
        this.nioBufferSize = nioBufferSize;
        return nioBuffers;
    }
  1. 先是从当前ThreadLocal里获取ByteBuffer数组,不需要每次创建。
  2. 在循环中遍历每个Entry,将Entry的的Direct ByteBuf转换为一个或者多个 NIO Direct ByteBuffer
总结
  1. write只是将数据放到Netty的缓冲区ChannelOutboundBuffer中,内部实现是一个链表
  2. flush的时候,先判断当前是否监听了OP_WRITE事件,如果监听了则不进行flush操作,由selector异步轮询到OP_WRITE事件的时候调用foreceFlush进行flush
  3. 当SocketChannel.write返回的字节数小于等于0的时候代表当前通过不可写,则监听OP_WRITE事件,由selector触发flush操作
  4. 由于没法一次性将所有数据写入,Netty使用了类似自旋锁的操作,重复一定次数进行SocketChannel.write操作,如果最后还是没能全部将数据写入,则监听OP_WRITE事件
  5. 当把数据写入了Socket将已经完全处理完毕的Entry移除,flushedEntry不停向后移动
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值