• 10.ChannelOutboundBuffer


    ChannelOutboundBuffer 是 Netty 发送缓存, 当 Netty 调用 write 时数据不会真正的去发送而是写入到 ChannelOutboundBuffer 缓存队列, 直到调用 flush 方法 Netty 才会从 ChannelOutboundBuffer 取数据发送.

    Unsafe 实例被创建, 即创建 Channel 时, ChannelOutboundBuffer 被创建, 每个 Unsafe 都会绑定一个 ChannelOutboundBuffer, 也就是说每个客户端连接上服务端都会创建一个 ChannelOutboundBuffer 绑定客户端 Channel. Netty 设计 ChannelOutboundBuffer 是为了减少 TCP 缓存的压力提高系统的吞吐率.

    四个重要字段

    // 待发送(刷新)数据起始节点
    private Entry flushedEntry;
    // 链表中的起始节点
    private Entry unflushedEntry;
    // 尾节点
    private Entry tailEntry;
    // 待发送(刷新)数据个数
    private int flushed;
    

    flushedEntry 到 unflushedEntry 之间的就是待发送数据, unflushedEntry 到 tailEntry 就是暂存数据, 如下图:

    正常情况下待发送数据发送完成后会 flushedEntry 指向 unflushedEntry 位置, 并将 unflushedEntry 指空.

    addMessage 方法源码分析

    并不是每次调用AbstractChannelHandlerContext#write(Object)方法时, 都会调用addMessage方法; 而是一直调用到HeadContext中的write(ChannelHandlerContext, Object, ChannelPromise)方法时, 才会调用该方法.

    所以这里需要注意该方法的 msg 参数, 该参数有可能是经历了好几个ChannelHandler后的结果. 为什么说是好几个呢, 这是因为ChannelHandler可能有 0 个或多个.

    可以认为 msg 就是最后要发送的数据, 通过 ChannelPromise 来决定发送还是不发送.

    // size: msg 未读取的字节数.
    // promise: 该参数要看 write 方法的调用, 如果调用的是:
    //          - write(Object): 那么 promise 就为调用 HeadContext#write(ChannelHandlerContext, Object, ChannelPromise) 所指定的参数.
    //                           因为 write(Object) 方法每次调用都会创建一个新的 ChannelPromise 实例.
    //          - write(Object, ChannelPromise): 那么就为参数指定的.
    public void addMessage(Object msg, int size, ChannelPromise promise) {
        // 创建一个新的 Entry.
        // total() 方法为获取 msg 可读字节数.
        // newInstance 方法中会设置该对象中的 pendingSize 属性, 该属性值为 size + CHANNEL_OUTBOUND_BUFFER_ENTRY_OVERHEAD.
        // CHANNEL_OUTBOUND_BUFFER_ENTRY_OVERHEAD 值由 io.netty.transport.outboundBufferEntrySizeOverhead 指定, 默认为 96.
        Entry entry = Entry.newInstance(msg, size, total(msg), promise);
    
        // 这里说明链表还没有数据, 所以将 flushedEntry 也清空.
        if (tailEntry == null) {
            flushedEntry = null;
        } else {
            // 这里是将新数据添加到上一个数据的后面.
            Entry tail = tailEntry;
            tail.next = entry;
        }
    
        // 将新数据作为尾节点.
        tailEntry = entry;
    
        // 如果 unflushedEntry 为 null, 说明没有未被刷新的元素.
        // 新添加的 Entry 肯定是未被刷新的, 则把当前 Entry 设置为 unflushedEntry.
        if (unflushedEntry == null) {
            unflushedEntry = entry;
        }
    
        // 通过 CAS 操作将 totalPendingSize(待发送的总字节数) 加上 entry 的 pendingSize.
        // 如果 totalPendingSize 的值大于最高水位线, 则会触发 ChannelInboundHandler 的 channelWritabilityChanged 方法.
        incrementPendingOutboundBytes(entry.pendingSize, false);
    }
    

    下面是该方法多次执行的演示图:

    addFlush 刷新操作

    当使用addMessage方法成功将要发送的数据添加进 ChannelOutboundBuffer 后, 就需要 flush 到 Socket 中去.

    但是这个方法并不是做写数据到 Socket 的操作, 而是将unflushedEntry的引用转移到flushedEntry引用中, 表示将从flushedEntry对应的 Entry 开始发送数据; 同时将对应的ChannelPromise设置为不可取消.

    public void addFlush() {
    
        // 当前还没有正在刷新的任务, 就将 unflushedEntry 引用赋值给 flushedEntry.
        // 也就是说这里指定从链表中的那个元素开始发送数据.
        Entry entry = unflushedEntry;
        if (entry != null) {
            if (flushedEntry == null) {
                flushedEntry = entry;
            }
    
            do {
                // 增加发送节点个数
                flushed++;
    
                // 设置当前节点不可取消.
                // AbstractChannelHandlerContext#write 方法会返回一个 DefaultChannelPromise 实例, 该实例可以用于取消任务.
                //
                // 该方法返回 false, 必须异常类型为 CancellationException. 也就是说,
                // 只有这个异常的时候才会关闭当前节点, 否则还是不会关闭当前节点(不发送当前节点的数据).
                if (!entry.promise.setUncancellable()) {
                    // 如果锁定失败, 关闭节点.
                    int pending = entry.cancel();
    
                    // 通过 CAS 操作将 totalPendingSize 减去 entry 的 pendingSize.
                    // 如果 totalPendingSize 的值小于最低水位线, 则会触发 ChannelInboundHandler 的 channelWritabilityChanged 方法.
                    // 当然也可以通过 ctx.channel().isWritable() 方法来判断是否继续向 ChannelOutboundBuffer 中写入数据.
                    //
                    // 注意: 是从 ChannelHandlerContext 链表的头部开始.
                    decrementPendingOutboundBytes(pending, false, true);
                }
    
                entry = entry.next;
            } while (entry != null);
    
            // 该属性设置为 null 就说明要发送数据了.
            unflushedEntry = null;
        }
    }
    

    在调用完ChannelOutboundBuffer#addFlush()方法后, Channel 会调用 AbstractUnsafe 中的 flush0 方法做真正的刷新.

    remove() 删除操作

    由于创建的是 NioServerSocketChannel 所以该方法会在 NioSocketChannel#doWrite(ChannelOutboundBuffer) 方法中调用.

    // writtenBytes 参数表示, 已经写到管道中的字节数.
    public void removeBytes(long writtenBytes) {
        for (;;) {
            // 获取 flushedEntry 的 msg 消息.
            Object msg = current();
            if (!(msg instanceof ByteBuf)) {
                assert writtenBytes == 0;
                break;
            }
            
            final ByteBuf buf = (ByteBuf) msg;
            // 获取已经读到的位置.
            final int readerIndex = buf.readerIndex();
            // 计算出剩余读取的字节.
            final int readableBytes = buf.writerIndex() - readerIndex;
    
            // 下面的这个 if else 请根据序号阅读.
    
            // 1. 假设 writtenBytes 的值为 msg 的字节数 140, 当一次性全部发送完 msg 中的这些数据后 readableBytes 的值肯定为 140.
            //    这是因为 readerIndex 值为 0, 就可以认为该 msg 没有被读取过或没有设置过 readerIndex.
            // 
            // 3. 还有一种情况就是小于, 这种情况是有可能一次性发送了多条数据, 导致 writtenBytes 大于 readableBytes 也就是例子中的 40.
            if (readableBytes <= writtenBytes) {
                if (writtenBytes != 0) {
                    // 如果 ChannelPromise 实现了 ChannelProgressivePromise 接口, 则会调用 tryProgress 方法通知当前进度.
                    progress(readableBytes);
                    // 3.1 当 removeBytes 方法第二次被调用时, 假设 writtenBytes 的值为 150, 那么这 150 中肯定会包含当前 msg 中的剩余字节数 40,
                    //     和下一个 msg 中的字节数 110, 这个时候需要将 writtenBytes 剪掉 readableBytes, 主要是为了判断是否删除下一个 msg.
                    writtenBytes -= readableBytes;
                }
                // 4. 在这里进行移除, 删除后会再一次进行 for 循环.
                remove();
            } else {
                // 2. 这种情况是 readableBytes > writtenBytes, 也就是说有可能没有发送完.
                //    假设 msg 的长度还是 140, writtenBytes 值为 100,
                //    相当于还有 40 个字节没有被发送, 那么这 40 个字节怎么办?
                //    设置读取的下标, 然后重新开始发送.
                if (writtenBytes != 0) {
                    buf.readerIndex(readerIndex + (int) writtenBytes);
                    progress(writtenBytes);
                }
                
                // 注意, 这里是跳出 for 循环, 而不是 remove().
                break;
            }
        }
        
        clearNioBuffers();
    }
    

    void removeBytes(long) 方法的主要功能就是为了判断 flushedEntry 中的 msg 数据有没有发送完, 如果发送完了就删除 flushedEntry, 否则就重新设置 msg 的读取下标.

    而做真正移除工作的是 remove() 方法, 下面是源码.

    public boolean remove() {
        // flushedEntry = null 有两种情况:
        //  - addMessage 方法被调用.
        //  - removeEntry 方法被调用.
        // 该值为 null 可以理解为链表执行完成.
        Entry e = flushedEntry;
        if (e == null) {
            // 如果 nioBufferCount > 0 则调用 Arrays.fill 方法,
            // 将 FastThreadLocal 中的数据清空 并且将 nioBufferCount 值改为 0.
            clearNioBuffers();
            return false;
        }
        
        Object msg = e.msg;
        ChannelPromise promise = e.promise;
        int size = e.pendingSize;
    
        // --flushed == 0 说明,调用 addFlush() 方法后的所有待发送的数据都已经写到 Socket 中,
        // 所以会把 flushedEntry 设置为 null.
        // 如果当前的 Entry 为 tailEntry(最后一个) 则会将 tailEntry 和 unflushedEntry 设置为 null.
        // 出现这种情况请看 讨论1.
        //
        // 否则将 flushedEntry 指向下一个 Entry.
        removeEntry(e);
    
        // 如果当前的 Entry 没有被取消
        if (!e.cancelled) {
            // 如果 msg 实现了 ReferenceCounted 接口,
            // 则会使用 ReferenceCounted#release() 方法来释放资源.
            ReferenceCountUtil.safeRelease(msg);
            
            // 将 promise 标记为成功.
            safeSuccess(promise);
            
            decrementPendingOutboundBytes(size, false, true);
        }
    
        // 回收资源
        e.recycle();
    
        return true;
    }
    

    nioBuffers

    讨论

    1.当前的 Entry 为 tailEntry(最后一个) 则会将 tailEntry 和 unflushedEntry 设置为 null, 那么什么情况下不会是最后一个?

    下面是 removeEntry 方法的源码:

    private void removeEntry(Entry e) {
        if (-- flushed == 0) {
            flushedEntry = null;
            if (e == tailEntry) {
                tailEntry = null;
                unflushedEntry = null;
            }
        } else {
            flushedEntry = e.next;
        }
    }
    

    当前的 Entry 不为 tailEntry, 说明肯定是又执行了一次 addMessage 方法. 那么我个人猜想这可能是因为, 当向管道中写了超过 16 次数据时, 会暂停写入数据, 而是执行别的操作. 这时别的操作中有可能会执行 addMessage 方法, 这个时候就会出现这种情况.

    参考资料

    netty 4源码分析-write
    Netty中的坑(下篇)
    Netty源码分析第七章: 编码器和写数据
    Netty 之 ChannelOutboundBuffer 源码分析
    Netty源码分析之ChannelOutboundBuffer
    ChannelOutboundBuffer

  • 相关阅读:
    STM32关于多线程运行的疑问
    流量校准仪开发日志
    来自一位十余年工作经验的老电子工程师的心里话:该出手时便出手!
    什么是开尔文连接
    node.js
    锂电池充电合集
    UICollectionView 使用 介绍
    UICollectionView 讲解
    sourcetree帮助文档
    使用 Git + Dropbox + SourceTree 做 Source Code Management
  • 原文地址:https://www.cnblogs.com/scikstack/p/13524675.html
Copyright © 2020-2023  润新知