Netty4.1源码阅读——核心(ChannelOutboundBuffer)

1,028 阅读4分钟

前言

前面介绍了EventLoopGroup和ChannelPipeline的源码,这一期ChannelOutboundBuffer出站缓存。这篇文章结束后,有关于Channel周边的组建就分析完成,将这些实现类组合在一起,就形成了完整的netty处理流程。

正文

ChannelOutboundBuffer是Netty的发送缓存,当Netty调用write时数据不会真正的去发送而是写入到ChannelOutboundBuffer中,这样做的目的是为了减少TCP缓存的压力,提高系统吞吐率。

成员

image.png

ChannelOutboundBuffer绑定了一个Channel,并且内部有3个Entry他们之间的关系是:

  • fulshedEntry到unflushedEntry:是待发送的数据
  • unflushedEntry到tailEntry:是暂存还没有准备好发送的数据

看一下Entry源码

image.png

Entry使用了之前介绍的对象回收池,由于系统会不断发送数据从而创建很多Entry,重复利用Entry对象减少GC压力。msg存放实际对象,将对象的信息一并存储后续调用。

 public void addMessage(Object msg, int size, ChannelPromise promise) {
        Entry entry = Entry.newInstance(msg, size, total(msg), promise);
        //队列为空
        if (tailEntry == null) {
            flushedEntry = null;
        } else {
            Entry tail = tailEntry;
            tail.next = entry;
        }
        //添加给尾节点
        tailEntry = entry;
        if (unflushedEntry == null) {
            unflushedEntry = entry;
        }
        
        incrementPendingOutboundBytes(entry.pendingSize, false);
    }

调用addMessage方法会创建一个节点,然后把它加入到Entry队列的尾部,然后跟待外发的字节数量

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);
        }
    }


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) {
                    fireChannelWritabilityChanged(invokeLater);
                }
                break;
            }
        }
    }

private void fireChannelWritabilityChanged(boolean invokeLater) {
        final ChannelPipeline pipeline = channel.pipeline();
	//是否延迟
        if (invokeLater) {
            Runnable task = fireChannelWritabilityChangedTask;
            if (task == null) {
                fireChannelWritabilityChangedTask = task = new Runnable() {
                    @Override
                    public void run() {
                        pipeline.fireChannelWritabilityChanged();
                    }
                };
            }
            channel.eventLoop().execute(task);
        } else {
            pipeline.fireChannelWritabilityChanged();
        }
    }

因为出站缓存是可以无限制添加的,所以可以配置一个HighWaterMark来做一定的限制,当待发量累积超过的时候,会将这个缓存改为不可写状态,并且调用pipeline的fireChannelWritabilityChanged方法来发送一个通知,通知每一个Handler这个事件以便自行处理。

public void addFlush() {
        //暂存数据
        Entry entry = unflushedEntry;
        if (entry != null) {
            if (flushedEntry == null) {
                //还没有flush条目 将flushed指向unflushedEntry
                flushedEntry = entry;
            }
            do {
                flushed ++;
                //锁定当前字段 防止取消
                if (!entry.promise.setUncancellable()) {
                    //锁定失败自动取消
                    int pending = entry.cancel();
                    decrementPendingOutboundBytes(pending, false, true);
                }
                entry = entry.next;
            } while (entry != null);

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

前面已知当系统调用write的时候会调用addMessage添加一个节点到缓存,当系统调用flush的时候会调用addFlush,将unflushedEntry以及以后的节点改为待发送节点,在这过程中会将节点进行锁定,如果锁定过程中已经取消,则将节点回收并且减少出站字节量。

nioBuffers是调用addFlush后调用,将待发送的数据转换为原生的ByteBuffer数组

image.png

ByteBuffer数组存放于FastThreadLocal,经过之前的介绍,通过Map的get方法,保证了不同的线程取出的实例都是不同的。

image.png

步骤如下:

  1. 从flushedEntry节点开始,msg类型必须是ByteBuf,直到遍历到unflushedEntry或者累计超过maxBytes停止
  2. 跳过被关闭的节点
  3. 计算出需要的空间,如果nioBuffers空间不够需要扩容,每次扩容为之前的两倍,直到大于需要的空间。
  4. 将ByteBuf转换为nioBuffers,如果entry的count大于1,则使用循环遍历放入nioBffers

当调用nioBuffers后获取到了待发送信息,当消息发送后应该删除掉已发送的信息,这个时应调用removeBytes方法

image.png

通过current获取flushedEntry节点,然后判断是否可以删除或者增加readerIndex来减少待发送数据大小,中间会调用progress方法来给Promise通知当前的发送情况。

image.png

remove方法会调用removeEntry来删除节点,并且减少总量,最后回收entry对象

image.png

将链表的头往后设置


总结

ChannelOutboundBuffer提供了一个出站缓存队列,当有新消息加入的时候会放在队列尾部,最后将消息批量转换为ByteBuffer数组返回,提高了系统吞吐率。同时ChannelOutboundBuffer没有限制添加容量,而是设置了一个水位线来预防极端情况导致的OOM,利用pipeline的ChannelWritabilityChanged方法,当容量超过水位线的时候会通知,用户可以自定义handler来接受该消息同时自定策略。