Netty详解之九:使用ByteBuf

上一章介绍了几种典型ByteBuf的原理,这一章介绍它的使用方法,包括Netty是如何使用ByteBuf的。

引用计数

上一章已经提及“引用计数”的概念;引用及计数是一种历史悠久的对象生命周期管理手段。它的核心理念是在对象上增加一个int字段来维护对象“拥有者的数量”,每当对象增加一个拥有者,引用计数加一,反之减一;对象创建之初引用计数等于1,引用计数变成0的那一刻立刻释放。

ByteBuf从ReferenceCounted接口继承,后者定义了引用计数的概念抽象。

public interface ReferenceCounted {
	 //引用计数值
    int refCnt();
    
    //引用计数增加1
    ReferenceCounted retain();
    
    //引用计数增加increment
    ReferenceCounted retain(int increment);
    
    //记录当前上下文信息,当内存泄漏检测到该对象有泄露时,可提供给用户
    ReferenceCounted touch();
    
    //同上,增加一个信息对象
    ReferenceCounted touch(Object hint);
    
    //引用计数减一
    boolean release();
    
    //引用计数减少decrement
    boolean release(int decrement);
}

Netty使用引用计数来管理ByteBuf应该是为了提高性能,因为网络通信往往需要非常频繁地大量创建buf对象,如果依赖GC来释放内存,可能导致内存释放不及时。

但引用计数其实与对象拥有者这个概念紧密相连,在java的世界里,由于gc的存在,导致”对象拥有者“这个概念被开发者所(安全地)忽视。”对象拥有者“可能是另一个对象,或一个运行时函数栈,抑或是一个线程,owner要对目标对象的生命周期负责,最终要么销毁它,要么将所有权转移给另外一个拥有者。

引用计数实际上建立了一种”共享所有权”模型,执行retain表示获得共享的所有权,执行release放弃所有权;而转移所有权却没有对应的操作来表达,只能依靠接口文档说明或某种命名约定。

下面这段代码简单的展示了引用计数的工作方式:

public static void main(String[] args) {
    ByteBuf buf = Unpooled.buffer();
    //这里打印初始值1
    System.out.println(buf.refCnt());
    //正常使用buf
    buf.setByte(0, 1);
    buf.release();
    //这里打印0
    System.out.println(buf.refCnt());
    //这句会抛出IllegalReferenceCountException
    buf.setByte(0, 1);
}

AbstractReferenceCountedByteBuf

抽象类AbstractReferenceCountedByteBuf实现了引用计数逻辑。

public abstract class AbstractReferenceCountedByteBuf extends AbstractByteBuf {

	//updater是一个封装refCnt修改细节的接口,并在refCnt归零是调用deallocate
	private static final ReferenceCountUpdater<AbstractReferenceCountedByteBuf> updater;

    private volatile int refCnt = updater.initialValue();
    
   @Override
    public ByteBuf retain() {
        return updater.retain(this);
    }

    @Override
    public ByteBuf retain(int increment) {
        return updater.retain(this, increment);
    }
    
    protected abstract void deallocate();
}

这里建立一个规约:当refCnt=0时,buf的deallocate方法被调用来释放内存。

注意:引用计数机制释放的是ByteBuf持有的数据区内存,而不是ByteBuf对象本身,后者还是依靠GC。

我们可以看一下四大ByteBuf的deallocate实现:

  • UnpooledHeapByteBuf.deallocate

内存区是byte数组,deallocate只是将数组引用释放而已,还是要等待GC释放。

@Override
protected void deallocate() {
    freeArray(array);
    array = EmptyArrays.EMPTY_BYTES;
}
  • UnpooledDirectByteBuf.deallocate

direct buf是通过JDK的ByteBuffer实现的,默认ByteBuffer也是依靠GC的,但是Netty hack了它的实现细节,可提前释放内存,实现细节与JDK版本、OS都有关,所以是PlatformDependent操作。

@Override
protected void deallocate() {
    ByteBuffer buffer = this.buffer;
    if (buffer == null) {
        return;
    }

    this.buffer = null;

    if (!doNotFree) {
        freeDirect(buffer);
    }
}
protected void freeDirect(ByteBuffer buffer) {
    PlatformDependent.freeDirectBuffer(buffer);
}
  • PooledByteBuf.deallocate

两种PooledByteBuf的内存释放代码是相同的,基本原理是将内存区域归还给分配的chunk,同时对buf对象本身也进行了回收。

@Override
protected final void deallocate() {
    if (handle >= 0) {
        final long handle = this.handle;
        this.handle = -1;
        memory = null;
        chunk.arena.free(chunk, tmpNioBuf, handle, maxLength, cache);
        tmpNioBuf = null;
        chunk = null;
        recycle();
    }
}

private void recycle() {
    recyclerHandle.recycle(this);
}

所以如果Unpooled Heap ByteBuf忘记调用release,最终内存还是会随GC释放的,但可能对性能造成损害;而其他类型的buf忘记调用release,那OOM就是迟早的事。

引用计数使用规则

正确使用引用计数的规则说起来也很简单,就以下几点:

  1. 一个新分配的ByteBuf,它的引用计数是1,分配buf的代码块是它的第一个拥有者
  2. 当传递一个ByteBuf时,是否转移所有权是一种约定,需要文档来说明,也需要开发者严格遵守这个约定
  3. 代码块可调用ByteBuf.retain来主动获得所有权,防止其他地方释放该buf
  4. 如果代码块拥有一个buf(主动retain或从其他地方转移而来),那么有责任释放它,要么调用release,要么转移所有权给其他代码

所有权的转移规则,对于java程序原来说,天然就不那么容易适应,这里也是最容易出问题的地方。

Derived&Wrapped ByteBuf

前面已经讲过,ByteBuf的实现有4种核心类型;但要把我们所能找到的所有ByteBuf具体类型都算起来,Netty可能有超过20种,它们大都是出于适应某种使用场景而建立起来的装饰类型或组合类型,以提高效率。

Derived ByteBuf是从当前buf衍生出来的buf对象,DerivedByteBuf和原buf共享内存和生命周期,在DerivedByteBuf上执行引用计数操作直接影响原buf。

AbstractDerivedByteBuf与引用计数相关的代码片段如下:

public abstract class AbstractDerivedByteBuf extends AbstractByteBuf {

    @Override
    public final int refCnt() {
        return refCnt0();
    }

    int refCnt0() {
        return unwrap().refCnt();
    }

    @Override
    public final ByteBuf retain() {
        return retain0();
    }

    ByteBuf retain0() {
        unwrap().retain();
        return this;
    }
}

slice&duplicate

Derived ByteBuf有两种实现模式,一种是slice(切片,类似String.subString),ByteBuf.slice(…)创建此类buf,ByteBuf.retainedSlice(…)在创建slice的同时执行一次retain。slice的实际类型是UnpooledSlicedByteBuf或PooledSlicedByteBuf。

另一种实现模式是duplicate(副本),ByteBuf.duplicate(…)创建这种模式的buf,ByteBuf.retainedDupliate功能类似。duplicate的实际类型是UnpooledDuplicatedByteBuf或PooledDuplicatedByteBuf。

slice和duplicate本质都是源buf的一个副本,只不过前者被限制在源buf的某一段内存区域上。

所有权问题

slice和duplicate有两种使用使用场景:

  1. 在一个调用栈上,制作buf的一个临时副本,用完即丢弃,完全不涉及所有权的变更;
  2. 这个副本需要被存储起来,那么要对副本调用retain,实现共享所有权利,否则可能发生IllegalReferenceCountException。

Wrapped ByteBuf

Wrapped ByteBuf是指对一个或多个内存对象进行包装,形成一个新的ByteBuf;被包装的内存对象可能是一个byte数组、ByteBuf、或JDK ByteBuffer。

与Derived不同,Wrapped关键字并没有一个统一的抽象类型与之对应,在Unpool工厂类下面有不少创建Wrapped ByteBuf方法,大体可分为以下几种:

  • 包装byte数组
    将bye[]包装为一个ByteBuf,方法:Unpool.wrappedBuffer(byte[] array,...)
  • 包装ByteBuffer
    将JDK NIO ByteBuffer包装为一个ByteBuf,方法:Unpool.wrappedBuffer(ByteBuffer buffer);
  • 包装ByteBuf
    将现有ByteBuf的可读部分,包装为一个ByteBuf,相当于slice,方法:Unpool.wrappedBuffer(ByteBuffer buffer)
  • 组合Buf
    将多个buf组合为一个ByteBuf,在不需要重新分配内存,也不需要内存拷贝的前提下,让一组buf表现出单个buf的外观行为。方法有:Unpool.wrappedBuffer(byte[]... arrays),Unpool.wrappedBuffer(ByteBuf[]... arrays),Unpool.wrappedBuffer(ByteBuffer[]... arrays)

Wrapped ByteBuf显然不是池化的,所以放在Unpool下倒也合理。

CompositeByteBuf

CompositeByteBuf是一种一种特殊Wrapped ByteBuf,是对组合模式的运用,将多个ByteBuf组合为一个ByteBuf。

CompositeByteBuf compositeBuffer = Unpooled.compositeBuffer();
compositeBuffer.addComponent(buf1);
compositeBuffer.addComponent(buf2);

所有权问题

包装行为相当于将原内存对象的所有权转移给了新的Wrapped ByteBuf,原对象不应该再被使用。如果原对象还需要有独立的所有权,应当执行一次retain,此时原对象的内存数据变更会影响到Wrapped ByteBuf,情况会比较微妙,最好不要这么做。

Channel写缓冲区

在通过Channel向外发送数据的过程中,实现编码功能的Channelhandler可能需要分配ByteBuf来存放编码后的数据,它调用ChannelConfig里配置的ByteBufAllocator来分配ByteBuf。ChannelConfig默认的allocator配置是ByteBufAllocator.DEFAULT(全局默认的Allocator),相关代码在DefaultChannelConfig。

我们知道,Channel的写入操作经过pipleline,最终会调用Channel.Unsafe.write方法,它的实现代码在AbstractUnsafe内:

//以下是经过简化的代码片段,抽取最核心的逻辑
protected abstract class AbstractUnsafe implements Unsafe {

    private volatile ChannelOutboundBuffer outboundBuffer = new ChannelOutboundBuffer(AbstractChannel.this);
    
    @Override
    public final void write(Object msg, ChannelPromise promise) {
    	  ...
        int size = pipeline.estimatorHandle().size(msg);
        //Channel.write将数据放入outboundBuffer
        outboundBuffer.addMessage(msg, size, promise);
    }
    
    @Override
    public final void flush() {
		  ...
		  //addFlush准备好写入的数据
        outboundBuffer.addFlush();
        flush0();
    }

	 //flush的时候才回调Channel.doWrite写入socket,请参考NioSocketChannel.doWrite
    @SuppressWarnings("deprecation")
    protected void flush0() {
    	...
    	doWrite(outboundBuffer);
    	...
    }
}
//以下是经过简化的代码片段,抽取最核心的逻辑
public class NioSocketChannel {
 	 @Override
    protected void doWrite(ChannelOutboundBuffer outboundBuffer) throws Exception {
        SocketChannel ch = javaChannel();
        int writeSpinCount = config().getWriteSpinCount();
        do {
            int maxBytesPerGatheringWrite = ((NioSocketChannelConfig) config).getMaxBytesPerGatheringWrite();
            
            //以ByteBuffer数组的形式将数取出来
            ByteBuffer[] nioBuffers = outboundBuffer.nioBuffers(1024, maxBytesPerGatheringWrite);
    		  int nioBufferCnt = outboundBuffer.nioBufferCount();
	        long attemptedBytes = outboundBuffer.nioBufferSize();
	        
	        //Java的NIO SocketChannel支持一次写入多个ByteBuffer
	        final long localWrittenBytes = ch.write(nioBuffers, 0, nioBufferCnt);
	        
	        //按成功写入的字节数移除outboundBuffer的数据
	        outboundBuffer.removeBytes(localWrittenBytes);
	        --writeSpinCount; 
        } while (writeSpinCount > 0);
    }

可以看出,write和flush逻辑都围绕着一个叫做ChannelOutboundBuffer的输出缓冲区展开,Channel.write将数据暂存在ChannelOutboundBuffer内,Channel.flush从该缓冲区取出数据写入Socket。我们现在就来剖析这个ChannelOutboundBuffer。

ChannelOutboundBuffer

先看ChannelOutboundBuffer的成员字段:

public final class ChannelOutboundBuffer {

	 //Channel.doWrite需要ByteBuffer[]形式的数据,为了避免每次分配,这里将ByteBuffer[]缓存在ThreadLocal
	 //Channel.doWrite发生在对应的EventLoop内,肯定是单线程工作的,这个优化是OK的
    private static final FastThreadLocal<ByteBuffer[]> NIO_BUFFERS = new FastThreadLocal<ByteBuffer[]>() {
        @Override
        protected ByteBuffer[] initialValue() throws Exception {
            return new ByteBuffer[1024];
        }
    };

    private final Channel channel;

	 //Channel.write传递过来的一个一个ByteBuf,被组织成单链表的形式,每个对象是一个Entry,节点flushedEntry,tailEntry,unflushedEntry将链表分成了两个区段;
	 //tailEntry是固定的尾节点,tailEntry=null说明链表为空;
	 //如果flushedEntry!=null,那么flushedEntry是头节点,且[flushedEntry,unflushedEntry)区段是Channel.flush即将要写入的数据,可称之为flushed区段,如果此时unflushedEntry=null,那么整个链表都是flushed区段
	 //如果unflushedEntry!=null,那么[unflushedEntry, tailEntry)是尚未flushed区段;
	 //[flushedEntry, unflushedEntry)区间
    private Entry flushedEntry;
    private Entry unflushedEntry;
    private Entry tailEntry;

	 //[flushedEntry, unflushedEntry)区间的entry数量
    private int flushed;
    
    //[flushedEntry, unflushedEntry)转换成nioBuffer的数量,字节数
    private int nioBufferCount;
    private long nioBufferSize;

    private boolean inFail;

	 //尚未flush的字节数
    private volatile long totalPendingSize;

	 //不可写状态标记
    private volatile int unwritable;

	 //触发(不可写->可写)状态事件的task
    private volatile Runnable fireChannelWritabilityChangedTask;
}

Channel.write最终调用ChannelOutboundBuffer.addMessage方法,它的实现如下:

//addMessage将ByteBuf对象添加到内部链表上
public void addMessage(Object msg, int size, ChannelPromise promise) {
    //创建一个包裹msg的Entry
    Entry entry = Entry.newInstance(msg, size, total(msg), promise);
    //如果tailEntry==null,链表是空的
    if (tailEntry == null) { 
        flushedEntry = null;
    } 
    //否则将entry拼接到尾部
    else {
        Entry tail = tailEntry;
        tail.next = entry;
    }
    tailEntry = entry;
    //新加入的entry,显然属于unflushed区间
    if (unflushedEntry == null) {
        unflushedEntry = entry;
    }
    //增加totalPendingSize
    incrementPendingOutboundBytes(entry.pendingSize, false);
}

Channel.flush会先调用ChannelOutboundBuffer.addFlush准备好flush数据区:

public void addFlush() {
    Entry entry = unflushedEntry;
    //如果unflushedEntry==null,说明在前一次addFlush之后,没有新的消息add进来,所以不需要重新计算
    if (entry != null) {
        if (flushedEntry == null) {
            flushedEntry = entry;
        }
        do {
        	  //遍历链表只是为了计算flushed和totalPendingSize
            flushed ++;
            if (!entry.promise.setUncancellable()) {
                decrementPendingOutboundBytes(pending, false, true);
            }
            entry = entry.next;
        } while (entry != null);
        
        //这一步实际将当前整个链表作为flushed区段
        unflushedEntry = null;
    }
}

然后Channel.doWrite调用ChannelOutboundBuffer.nioBuffers拉取整个flushed区域的数据:

//限制ByteBuffer的数量,和总字节数
public ByteBuffer[] nioBuffers(int maxCount, long maxBytes) {
	 //累加字节数
    long nioBufferSize = 0;
    //累计ByteBuffer数量
    int nioBufferCount = 0;

    //从ThreadLocal取出ByteBuffer数组备用
    final InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
    ByteBuffer[] nioBuffers = NIO_BUFFERS.get(threadLocalMap);
    
    //遍历flushed区域
    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) {
            		//加上这个entry,总字节数就超了,提前结束遍历
                if (maxBytes - readableBytes < nioBufferSize && nioBufferCount != 0) {
                    break;
                }
                nioBufferSize += readableBytes;
                
             
                //buf.nioBufferCount()可能需要将ByteBuf转换成ByteBuffer,是有一定性能消耗的
                //如果entry包含多个ByteBuffer,只需计算一次存储在entry.count字段即可
                //另外ByteBuf有可能是CompsiteByteBuf,可包含多个ByteBuffer
                int count = entry.count;
                if (count == -1) {
                		//计算entry.count
                    entry.count = count = buf.nioBufferCount();
                }
                
                //ByteBuffer[]空间可能不够,需要扩展
                int neededSpace = min(maxCount, nioBufferCount + count);
                if (neededSpace > nioBuffers.length) {
                    nioBuffers = expandNioBufferArray(nioBuffers, neededSpace, nioBufferCount);
                    NIO_BUFFERS.set(threadLocalMap, nioBuffers);
                }
               
                //将entry有一个或多个ByteBuffer的情况分开,提升一些性能,因为绝大多数情况执行的是count==1分支
                if (count == 1) {
                		//entry.buf缓存转换而来的ByteBuffer,只需要执行一次
                    ByteBuffer nioBuf = entry.buf;
                    if (nioBuf == null) {
                        entry.buf = nioBuf = buf.internalNioBuffer(readerIndex, readableBytes);
                    }
                    nioBuffers[nioBufferCount++] = nioBuf;
                } else {
                		//nioBuffers处理entry包含多个ByteBuffer的情景
                    nioBufferCount = nioBuffers(entry, buf, nioBuffers, nioBufferCount, maxCount);
                }
                
                //ByteBuffer总数量到上限了
                //注意:不会发生nioBufferCount>maxCount的情况,因为nioBuffers方法也保证了这一点
                if (nioBufferCount == maxCount) {
                    break;
                }
            }
        }
        entry = entry.next;
    }
    this.nioBufferCount = nioBufferCount;
    this.nioBufferSize = nioBufferSize;
    return nioBuffers;
}

上面将每个entry.msg的计算结果(字节数、nioBuffer数)缓存在entry上,这是因为nioBuffers方法返回的数据不一定能一次性全部写入socket(一次写入多少由操作系统的运行时状态决定的)。所以多次调用nioBuffers方法可能会处理同一个entry,这样能提高性能。

Channel.doWrite写入数据成功后(不一定全部写入成功,可能只写入了一部分),调用ChannelOutboundBuffer.removeBytes来删除缓存数据:

//删除已写入数据的代码片段
public void removeBytes(long writtenBytes) {
    for (;;) {
    	  //current是flushedEntry包裹的msg
        Object msg = current();
        final ByteBuf buf = (ByteBuf) msg;
        final int readerIndex = buf.readerIndex();
        final int readableBytes = buf.writerIndex() - readerIndex;
		
		  //如果buf的可读字节全部被写入了,那么删除该msg
        if (readableBytes <= writtenBytes) {
            if (writtenBytes != 0) {
            	   //progress是通过ChannelProgressivePromise向外同步写进度
                progress(readableBytes);
                writtenBytes -= readableBytes;
            }
            //remove删除flushedEntry,并将flushedEntry指向下一个
            remove();
        } 
        //否则,说明buf写入了部分数据,那么调整buf的可读字节数
        else { 
            if (writtenBytes != 0) {
                buf.readerIndex(readerIndex + (int) writtenBytes);
                progress(writtenBytes);
            }
            break;
        }
    }
    //清除NIO_BUFFERS缓存的ByteBuffer引用,以不阻碍GC
    clearNioBuffers();
}

//删除flushedEntry
public boolean remove() {
    Entry e = flushedEntry;
    Object msg = e.msg;
    ChannelPromise promise = e.promise;
    int size = e.pendingSize;
    
    //删除entry,并让flushedEntry指向下一个
    removeEntry(e);

    if (!e.cancelled) {
        //注意:这里对msg(大多数情况是ByteBuf)执行了release
        ReferenceCountUtil.safeRelease(msg);
        safeSuccess(promise);
        decrementPendingOutboundBytes(size, false, true);
    }

    return true;
}

通过上面的代码可知,ChannelOutboundBuffer与Channel(Unsafe)之间是深度耦合的关系,互相配合完成写入数据(或者叫Outbound数据)的管理。

  • Channel.write写入的数据经过pipeline之后会暂存在ChannelOutboundBuffer内,后者用链表的形式组织数据对象;
  • Channel.flush尝试将ChannelOutboundBuffer内的数据写入Socket,但可能需要多个eventLoop循环才能成功,期间可能会有新的数据加入;
  • Channel.flush将数据写入Socket之后,会释放数据对象(执行引用计数release);

最后一点尤其需要使用者注意,当我们将一个ByteBuf传递给Channel.write方法,或在ChannelOutboundHandler中创建ByteBuf传递给ChannelHandlerContext.write;实际上发生了ByteBuf对象的所有权转移,最终会转移给ChannelOutboundBuffer。除非通过ChannelFuture成功地取消了write操作,否则ChannelOutboundBuffer会最终释放ByteBuf对象。下面的代码展示了这种罕见的需求:

public class NettyServerHandler extends SimpleChannelInboundHandler<ByteBuf> {
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(100).writeBytes("This is Netty Server\r\n".getBytes());
        ChannelFuture future = ctx.writeAndFlush(byteBuf);
        //如果取消成功,需要手动释放byteBuf,否则ChannelOutboundBuffer会释放它
        if (future.cancel(false)) {
            byteBuf.release();
        }
    }
}

Channel.isWritable

Channel.isWritable方法返回一个所谓”可写状态”标记,它的在ChannelOutboundBuffer内维护:

public class ChannelOutboundBuffer {

	 //数据进入时,增加PendingOutboundBytes
    private void incrementPendingOutboundBytes(long size, boolean invokeLater) {
        if (size == 0) {
            return;
        }

        long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, size);
        
		  //如果PendingOutboundBytes大于ChannelConfig配置的高水位标记(HighWaterMark),进入Unwritable状态
        if (newWriteBufferSize > channel.config().getWriteBufferHighWaterMark()) {
            setUnwritable(invokeLater);
        }
    }    

	//数据写入socket成功后被移除,同时减小PendingOutboundBytes
	private void decrementPendingOutboundBytes(long size, boolean invokeLater, boolean notifyWritability) {
	    if (size == 0) {
	        return;
	    }
	
	    long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, -size);
	    
	    //如果PendingOutboundBytes小于ChannelConfig配置的低水位标记(LowWaterMark),进入writable状态
	    if (notifyWritability && newWriteBufferSize < channel.config().getWriteBufferLowWaterMark()) {
	        setWritable(invokeLater);
	    }
	}
}

//高低水位标记的默认值
public final class WriteBufferWaterMark {
    private static final int DEFAULT_LOW_WATER_MARK = 32 * 1024;
    private static final int DEFAULT_HIGH_WATER_MARK = 64 * 1024;
}

Channel的isWritable状态控制机制很简单,ChannelOutboundBuffer缓存的数据字节数超过HighWaterMark,isWritable变为false;当缓存接字节数降低到LowWaterMark,isWritale恢复。但是无论isWritable状态如何,Netty不会阻止用户继续向Channel写入数据,只不过ChannelOutboundBuffer占用的内存有不断增大的风险。

Channel.isWritable是一个供用户参考的标记,用户可以参考这个标记来调整发送数据的速度;对于文件传输这样的业务,业务层完全可以依据该标记动态调节将文件块读入内存的速度;而对于游戏这样的业务,可以用这个标记来监测带宽是否满足业务需求(偶尔unWritable是正常的,要持续监测)。

Channel读缓冲区

Channel在读数据时也需要分配ByteBuf来承载数据,此时使用的allocator就是上一节介绍的写数据时使用的allocator。不过与“write"不同的是,”read“并不知道该分配多大的ByteBuf,太大了浪费空间,太小了效率低下(导致多次system call)。所以Channel的读缓冲区机制要解决的不是ByteBuf分配问题,而是分配尺寸的问题,解决该问题的抽象接口是RecvByteBufAllocator。

为了让下面的文字清晰简洁,先抽像两个概念:Channel read和Socket read,前者指EventLoop直接驱动的读操作,EventLoop通过selector发现某个channel可读时,回调Channel.unsafe.read;Channel read的执行过程中,可能需要多次从底层socket读取数据;Channel read包含一次或多次Socket read。

另外下面谈到“message”的概念,我们可认为就是ByteBuf(在非TCP场景下可能是别的东西,暂不涉及)。

public interface RecvByteBufAllocator {

	 //每个Channel分配一个Handle来处理内存分配,这个Handle实现极有可能是有状态的,因为它要依据Channel的历史统计数据来计分配空间
    Handle newHandle();

    /**
     * @deprecated Use {@link ExtendedHandle}.
     */
    interface Handle {
 
 		 //使用传入的allocator分配一个buf
        ByteBuf allocate(ByteBufAllocator alloc);

		 //返回下一次Socket read分配buf的尺寸
        int guess();

		 //Channel read前重置
        void reset(ChannelConfig config);
		
		 //接受Channel反馈的上一次Socket read创建的ByteBuf的数量,正常是1
        void incMessagesRead(int numMessages);

  		 //接受Channel反馈的:上一次Socket read读到的字节数
        void lastBytesRead(int bytes);
        int lastBytesRead();

    	 //接受Channel反馈的:上一次Socket read尝试读的字节数
        void attemptedBytesRead(int bytes);
        int attemptedBytesRead();

		 //指示Channel read,是否要继续执行socket read
        boolean continueReading();

		 //接受Channel反馈:本次Channel read执行结束了
        void readComplete();
    }

    @SuppressWarnings("deprecation")
    @UnstableApi
    interface ExtendedHandle extends Handle {
    	  //continueReading的新版本,有一个接口参数(返回是否还有可读数据的推测)
        boolean continueReading(UncheckedBooleanSupplier maybeMoreDataSupplier);
    }
}

//RecvByteBufAllocator扩展接口
public interface MaxMessagesRecvByteBufAllocator extends RecvByteBufAllocator {
  
    //增加maxMessagesPerRead配置,意思是每次eventLoop的read操作,创建ByteBuf的总数量
    int maxMessagesPerRead();
    MaxMessagesRecvByteBufAllocator maxMessagesPerRead(int maxMessagesPerRead);
}

//另一个RecvByteBufAllocator扩展接口
public interface MaxBytesRecvByteBufAllocator extends RecvByteBufAllocator {

    //maxBytesPerRead配置,约束每次eventLoop的read操作,读取字节总数的限制
    int maxBytesPerRead();
    MaxBytesRecvByteBufAllocator maxBytesPerRead(int maxBytesPerRead);


    //maxBytesPerIndividualRead配置,约束每此socket read读取的字节数,一次Channel read可包含多次socket read
    int maxBytesPerIndividualRead();
    MaxBytesRecvByteBufAllocator maxBytesPerIndividualRead(int maxBytesPerIndividualRead);
}

从上面的抽象我们可以得到一些重要信息:RecvByteBufAllocator应该只承载一些静态的配置,它为每个Channel分配一个Handle来处理读缓冲区的管理事务;该Handle和Channel的read操作密切配合,前者不断从后者获得反馈信息,同时为后者分配内存空间,并决定是否提前结束读操作。

MaxMessagesRecvByteBufAllocator和MaxBytesRecvByteBufAllocator从两个不同的角度来约束channel read读取的数据量,前者是从消息数(其实就是分配的ByteBuf数),后者是从字节数。这种限制是有意义的,因为EventLoop是被多个Channel共享的,单次操作占用太长的时间造成整体性能不平滑。

AdaptiveRecvByteBufAllocator

如果底层使用Nio,那么Netty默认使用的是AdaptiveRecvByteBufAllocator,这个配置在DefaultChannelConfig内。AdaptiveRecvByteBufAllocator继承自MaxMessagesRecvByteBufAllocator,所以我们集中分析MaxMessagesRecvByteBufAllocator这个分支。

我们首先来看AdaptiveRecvByteBufAllocator的基类DefaultMaxMessagesRecvByteBufAllocator,每一层RecvByteBufAllocator实现都有对应的RecvByteBufAllocator.Handle实现,类似Channel和Channel.Unsafe的关系。

DefaultMaxMessagesRecvByteBufAllocator

public abstract class DefaultMaxMessagesRecvByteBufAllocator implements MaxMessagesRecvByteBufAllocator {
	
	//每次Channel read操作,分配的ByteBuf数量限制,默认值是1
    private volatile int maxMessagesPerRead;
    
    //在决策是否要继续执行socket read时,是否参考“是否还有数据的推测“
    private volatile boolean respectMaybeMoreData = true;
}

respectMaybeMoreData概念过于细节,暂时不明白也无妨。DefaultMaxMessagesRecvByteBufAllocator的其他方法就是简单的get&set,代码忽略。

DefaultMaxMessagesRecvByteBufAllocator.handle

关键在DefaultMaxMessagesRecvByteBufAllocator.handle的实现MaxMessageHandle:

//简化的代码片段,一些简单的get&set型方法被忽略
public abstract class DefaultMaxMessagesRecvByteBufAllocator implements MaxMessagesRecvByteBufAllocator {

	...
	
	public abstract class MaxMessageHandle implements ExtendedHandle {
	    private ChannelConfig config;
	    
	    //两个字段来自外部类
	    private final boolean respectMaybeMoreData = DefaultMaxMessagesRecvByteBufAllocator.this.respectMaybeMoreData;
	    private int maxMessagePerRead;
	    
	    //Channel read读取的累计ByteBuf数
	    private int totalMessages;
	    
	    //Channel read读取的累计字节数
	    private int totalBytesRead;
	    
	    //上次socket read尝试读取字节数
	    private int attemptedBytesRead;
	    
	    //上次socket read真实读取的字节数
	    private int lastBytesRead;
	    
	    //这是推测socket是否还有可读数据的实现,”如果上次期望读取读取字节数,等于真实读到的字节数,推测还有数据可读"
	    private final UncheckedBooleanSupplier defaultMaybeMoreSupplier = new UncheckedBooleanSupplier() {
	        @Override
	        public boolean get() {
	            return attemptedBytesRead == lastBytesRead;
	        }
	    };
	    
	    //重置为初始值
	    public void reset(ChannelConfig config) {
	        this.config = config;
	        maxMessagePerRead = maxMessagesPerRead();
	        totalMessages = totalBytesRead = 0;
	    }
	
	   //分配ByteBuf,guess方法决定分配的尺寸
		@Override
	    public ByteBuf allocate(ByteBufAllocator alloc) {
	        return alloc.ioBuffer(guess());
	    }
		
	    @Override
	    public boolean continueReading() {
	        return continueReading(defaultMaybeMoreSupplier);
	    }
	
		 //本次Channel read是否继续执行soceket read
		 //参考因素:autoRead配置,socket是否还有数据的推测,读取数量的限制,前面是否读取到数据
	    @Override
	    public boolean continueReading(UncheckedBooleanSupplier maybeMoreDataSupplier) {
	        return config.isAutoRead() &&
	               (!respectMaybeMoreData || maybeMoreDataSupplier.get()) &&
	               totalMessages < maxMessagePerRead &&
	               totalBytesRead > 0;
	    }
}

上面MaxMessageHandle的代码也挺简单的,为Handle接口定义了相应的实际字段;不过单独理解起来可能有些抽象,待会和Channel Read的逻辑结合起来就清晰了。

AdaptiveRecvByteBufAllocator

从命名上,我们就可以看出AdaptiveRecvByteBufAllocator是按自适应的策略来实现的,它会依据Channel读的情况不断调整下一次分配的ByteBuf尺寸。

它的字段定义如下:

public class AdaptiveRecvByteBufAllocator extends DefaultMaxMessagesRecvByteBufAllocator {

    //默认的buf最小尺寸64 byte
    static final int DEFAULT_MINIMUM = 64;
    //默认的buf初始尺寸1024
    static final int DEFAULT_INITIAL = 1024;
    //默认的buf最大尺寸65536
    static final int DEFAULT_MAXIMUM = 65536;

    //当决定增大buf尺寸时,每次SIZE_TABLE索引的变化量
    private static final int INDEX_INCREMENT = 4;
    
    //当决定减小buf尺寸时,每次SIZE_TABLE索引的变化量
    private static final int INDEX_DECREMENT = 1;

    //尺寸查找表
    private static final int[] SIZE_TABLE;

    //初始化SIZE_TABLE,规则是512字节以内,以16字节为基准,按倍数线性增长;
    //512字节以上,按2的指数增长
    //DEFAULT_MINIMUM,DEFAULT_INITIAL,DEFAULT_MAXIMUM都落在SIZE_TABLE某个slot上
    static {
        List<Integer> sizeTable = new ArrayList<Integer>();
        for (int i = 16; i < 512; i += 16) {
            sizeTable.add(i);
        }

        for (int i = 512; i > 0; i <<= 1) {
            sizeTable.add(i);
        }

        SIZE_TABLE = new int[sizeTable.size()];
        for (int i = 0; i < SIZE_TABLE.length; i ++) {
            SIZE_TABLE[i] = sizeTable.get(i);
        }
    }
    
    //buf最小尺寸,最大尺寸在SIZE_TABLE中索引
    private final int minIndex;
    private final int maxIndex;
    
    //buf初始尺寸
    private final int initial;
    
    public AdaptiveRecvByteBufAllocator() {
        this(DEFAULT_MINIMUM, DEFAULT_INITIAL, DEFAULT_MAXIMUM);
    }
    
   public AdaptiveRecvByteBufAllocator(int minimum, int initial, int maximum) {
        //getSizeTableIndex查找尺寸在SIZE_TABLE中的(最近)位置
        this.minIndex = getSizeTableIndex(minimum);
        this.maxIndex  = getSizeTableIndex(maximum);
        this.initial = initial;
    }
}

SIZE_TABLE的作用是约束分配的字节数,Allocator按所需尺寸在SIZE_TABLE查找一个相近的尺寸。

AdaptiveRecvByteBufAllocator.handle

public class AdaptiveRecvByteBufAllocator {

	 ...
	 
	 public Handle newHandle() {
        return new HandleImpl(minIndex, maxIndex, initial);
    }
    
	private final class HandleImpl extends MaxMessageHandle {
	    private final int minIndex;
	    private final int maxIndex;
	    
	    //下次分配buf尺寸在SIZE_TABLE的index
	    private int index;
	    //下次分配buf的尺寸
	    private int nextReceiveBufferSize;
	    
	    //分配趋势:下坡,或爬坡
	    private boolean decreaseNow;
	
	    HandleImpl(int minIndex, int maxIndex, int initial) {
	        this.minIndex = minIndex;
	        this.maxIndex = maxIndex;
	        index = getSizeTableIndex(initial);
	        nextReceiveBufferSize = SIZE_TABLE[index];
	    }
	
	    @Override
	    public void lastBytesRead(int bytes) {
	        //只有当socket read读取的字节数和期望的字节数一致的时候,才调整下次分配的尺寸
	        if (bytes == attemptedBytesRead()) {
	            record(bytes);
	        }
	        super.lastBytesRead(bytes);
	    }
	
	    @Override
	    public int guess() {
	        return nextReceiveBufferSize;
	    }
	 
	    //这个方法是调整分配尺寸的地方,record这个名字真是...
	    private void record(int actualReadBytes) {
	    
	         //实际读到的字节数,比当前更小的一档还要小
	        if (actualReadBytes <= SIZE_TABLE[max(0, index - INDEX_DECREMENT)]) {
	            //如果现在已经是下坡模式,减小分配尺寸,并停止下坡;否则进入下坡模式
	            if (decreaseNow) {
	                index = max(index - INDEX_DECREMENT, minIndex);
	                nextReceiveBufferSize = SIZE_TABLE[index];
	                decreaseNow = false;
	            } else {
	                decreaseNow = true;
	            }
	        }
	        //实际读到的字节数,不比当前分配的小,立即爬坡
	        else if (actualReadBytes >= nextReceiveBufferSize) {
	            index = min(index + INDEX_INCREMENT, maxIndex);
	            nextReceiveBufferSize = SIZE_TABLE[index];
	            decreaseNow = false;
	        }
	    }
	
	    //当Channel read完成以后,执行一次调整
	    @Override
	    public void readComplete() {
	        record(totalBytesRead());
	    }
	}
}

上面代码的逻辑很容易理解,但是要明白这些逻辑的意图,要站在一个更高的角度来看问题。Netty肯定期望分配一个大小合适的buf,一次性就将socket的可读数据全部读完,以达到最好的性能;因此readComplete之后按照本次总读取字节数来调整下次分配buf尺寸。如果Channel Read过程中发生多次Socket read(这其实是Netty要尽量避免的),不会因为后续读取的字节数较少就立即减少buf尺寸,因为这可能对下一次Channel Read造成不利影响。另外无论是增大还是减小buf尺寸,都采用一种渐进的方式,而且增加的速度相对更快一些。

Channel Read

现在看看Chanel read是如何与RecvByteBufAllocator互操作的,以NioSocketChannel为例:

//简化的Channel read代码片段
public class AbstractNioByteChannel {

	 protected class NioByteUnsafe {
	 
	 	  @Override
        public final void read() {
        
            final ChannelPipeline pipeline = pipeline();
            //使用的allocator和write是一样的
            final ByteBufAllocator allocator = config.getAllocator();
            
            //这个handle默认就是AdaptiveRecvByteBufAllocator.HandleImpl
            final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
            
            //重置allocHandle,注意:AdaptiveRecvByteBufAllocator.HandleImpl.nextReceiveBufferSize不会被重置
            allocHandle.reset(config);

            ByteBuf byteBuf = null;
            boolean close = false;

            do {
                //按nextReceiveBufferSize分配一个buf
                byteBuf = allocHandle.allocate(allocator);
                
                //执行socket读,并将读取的字节数量反馈给allocHandle
                allocHandle.lastBytesRead(doReadBytes(byteBuf));
                
                if (allocHandle.lastBytesRead() <= 0) {
                	 //啥都没读到,直接释放了buf
                    byteBuf.release();
                    byteBuf = null;
                    //<0是EOF标记,socket断开了
                    close = allocHandle.lastBytesRead() < 0;
                    if (close) {
                        readPending = false;
                    }
                    break;
                }
					//读取的消息数+1
                allocHandle.incMessagesRead(1);
                readPending = false;
                pipeline.fireChannelRead(byteBuf);
                byteBuf = null;
            } while (allocHandle.continueReading());

            allocHandle.readComplete();
            pipeline.fireChannelReadComplete();

            if (close) {
                closeOnRead(pipeline);
            }
        }
	 }
}

public class NioSocketChannel extends AbstractNioByteChannel {

    @Override
    protected int doReadBytes(ByteBuf byteBuf) throws Exception {
        final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
        allocHandle.attemptedBytesRead(byteBuf.writableBytes());
        
        //这里执行nio socketchannel的读操作(byteBuf.writeBytes是将目标channel的数据往buf写,相当于目标channel的读
        return byteBuf.writeBytes(javaChannel(), allocHandle.attemptedBytesRead());
    }

读ByteBuf的所有权

Channel read调用pipeline.fireChannelRead触发读事件,同时也将ByteBuf所有权转移给了pipeline,而Pipeline又转交给了Channelhandler,所以ChannelHandler可以持有收到的ByteBuf,并要负责释放它。DefaultChannelPipeline的tail节点会负责兜底,防止没有任何业务ChannelHandler接受该ByteBuf而导致内存泄露。

public DefaultChannelPipeline {

	final class TailContext {
	
	      @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) {
            onUnhandledInboundMessage(ctx, msg);
        }
        protected void onUnhandledInboundMessage(Object msg) {
	        try {
	            logger.debug(
	                    "Discarded inbound message {} that reached at the tail of the pipeline. " +
	                            "Please check your pipeline configuration.", msg);
	        } finally {
	            //释放未处理的msg
	            ReferenceCountUtil.release(msg);
	        }
    	}	
	}
}

总结

ByteBuf的使用确实不是一件容易的事,对于java程序员来说更是如此。所以,在简单的场景下,可以完全由Netty来管理ByteBuf生命周期,我们将ByteBuf当做一个已经打开的IO流,只执行读写操作,参考“编解码器”那一章节里面的示例代码。

另外需要掌握两个底层知识:

  • 向Channel写入的数据,经过编解码器转换成ByteBuf,暂存在ChannelOutboundBuffer里面,netty会在适当的时候再写入socket,换句话说多个数据可能会一次写入socket,也可能分多次写入socket;
  • channel在收数据时,netty会依据最近收到数据的情况,来创建合适大小的ByteBuf来充当接收缓冲区。
  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值