【Netty】ByteBuf相关(一):ByteBuf的结构、分类、核心api简介

【Netty】ByteBuf相关(一):ByteBuf的结构、分类、核心api简介

Scroll Down

目录

前言

在开篇之前,下面先讲一下我学习ByteBuf相关知识的感受。

本以为有了前段时间学习channel、pipeline、事件传播等源码经历,如今学习ByteBuf应该是会越来越轻松才对,结果我发现自己还是太天真了。因为ByteBuf分配内存的源码中,充斥着各种各样的位运算,虽然能从里面学到不少技巧,但由于我日常开发很少用到位运算(可能是我水平不够吧),所以第一次看ByteBuf计算相关源码时是一脸懵逼的。

那么这节就来看看ByteBuf的基础结构以及常见分类,在之后的章节如果忘记就回来看。

Netty Version:4.1.6


ByteBuf结构、核心api

数据结构

首先通过官方给出的结构图直观感受下ByteBuf的内存结构:

      +-------------------+------------------+------------------+
      | discardable bytes |  readable bytes  |  writable bytes  |
      |                   |     (CONTENT)    |                  |
      +-------------------+------------------+------------------+
      |                   |                  |                  |
      0      <=      readerIndex   <=   writerIndex    <=    capacity

接下来解释一下这些名词:

  • discardable bytes:已经被读取过的数据,一般情况下可以理解为无效区域。
  • readable bytes:未读取数据,readable bytes数据区的数据是满的,都是等待读取的数据。
  • writable bytes:空闲区域,可以往这块区域写数据。
  • capacity:表示当前内存容量。
  • readerIndex:读数据起点指针,当需要读数据时,就以当前指针为起点往后读取数据。
  • writerIndex:写数据起点指针,当需要写数据时,就以当前指针为起点往后写数据。

下面通过一些核心api直观感受下代码操作是什么样的:
随机读取数据

 ByteBuf buffer = ...;
 for (int i = 0; i < buffer.capacity(); i ++) {
     byte b = buffer.getByte(i);
     System.out.println((char) b);
 }

顺序读取1字节数据

 ByteBuf buffer = ...;
 while (buffer.isReadable()) {
     System.out.println(buffer.readByte());
 }

顺序读取4字节数据

 ByteBuf buffer = ...;
 while (buffer.isReadable()) {
     System.out.println(buffer.readInt());
 }
  • readBoolean()就是读2字节,其它以此类推。

顺序写数据

 ByteBuf buffer = ...;
 while (buffer.maxWritableBytes() >= 4) {
     buffer.writeInt(random.nextInt());
 }

discardReadBytes()废弃数据
废弃数据后

      +------------------+--------------------------------------+
      |  readable bytes  |    writable bytes (got more space)   |
      +------------------+--------------------------------------+
      |                  |                                      |
 readerIndex (0) <= writerIndex (decreased)        <=        capacity

clear()清除缓冲区索引
清除后

      +---------------------------------------------------------+
      |             writable bytes (got more space)             |
      +---------------------------------------------------------+
      |                                                         |
      0 = readerIndex = writerIndex            <=            capacity

mark和reset方法

读和写操作都有对应的mark方法和reset方法都,其作用就类似于“指针回滚”,mark方法用于标记读/写之前的指针位置,而reset方法则是将当前指针回滚到mark标记处。

下面看看这两接口长啥样:
mark方法

reset方法



ByteBuf分类

先通过类图看看关系:

ByteBuf分类关系

于是可以分为以下6类:

  • Unpooled:非池化,每次申请内存都是新的一次申请。
  • Pooled:池化,每次申请内存都是从jdk已经分配好的内存池中取。
  • direct:堆外内存,即系统直接内存,不受jvm管控。
  • heap:堆内存,就是指jvm的堆内存。
  • 非unsafe:通过jdk的api间接操作底层内存。
  • unsafe:指调用native方法底层直接操作内存(一般不会由用户调用)。

- 除此之外,还有组合ByteBuf等类型,由于不是特别重要就不仔细介绍了,有兴趣的自己去补充一下吧~

关于直接内存,我在网上找到比较简单&全面的概括:

直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区
域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错出现。

JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通
道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。

本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。

更详细的就得要自己补充了。


safe和unsafe区别

下面就以读取数据为例,简单看下safe和unsafea的api的区别。

首先找到io.netty.buffer.AbstractByteBuf#readByte方法:

    @Override
    public byte readByte() {
        checkReadableBytes0(1);
        int i = readerIndex;
        byte b = _getByte(i);
        readerIndex = i + 1;
        return b;
    }

跟进_getByte方法,这是一个抽象方法:

    protected abstract byte _getByte(int index);

先选择一个unsafe实现:
io.netty.buffer.UnpooledUnsafeHeapByteBuf#_getByte

    @Override
    protected byte _getByte(int index) {
        return UnsafeByteBufUtil.getByte(array, index);
    }

跟进getByte方法:
io.netty.buffer.UnsafeByteBufUtil#getByte(byte[], int)

    static byte getByte(byte[] array, int index) {
        return PlatformDependent.getByte(array, index);
    }

持续跟进getByte方法(中间过程略),来到如下代码:
io.netty.util.internal.PlatformDependent0#getByte(byte[], int)

    static byte getByte(byte[] data, int index) {
        return UNSAFE.getByte(data, BYTE_ARRAY_BASE_OFFSET + index);
    }
  • 这里就是调用jdk底层的unsafe实例,根据基础偏移量+index算出总偏移量,从而拿到连续的内存。

之后再选择一个safe的_getByte实现:
io.netty.buffer.PooledHeapByteBuf#_getByte

    @Override
    protected byte _getByte(int index) {
        return HeapByteBufUtil.getByte(memory, idx(index));
    }

跟进这个getByte方法:
io.netty.buffer.HeapByteBufUtil#getByte

    static byte getByte(byte[] memory, int index) {
        return memory[index];
    }
  • 可见,底层就是通过java数组获取。

说白了,unsafe可以理解为通过native法操作内存,而非unsafe就是普通的java方法。


由什么决定ByteBuf类型

看到这个标题,有人可能就会觉得:当然都是由用户决定啊。

的确,无论是否Pooled,是head还是direct都是由用户选定的类、选定的方法决定的,但是是否是unsafe,这个就应该由Netty来控制了,就如jdk底层的Unsafe类一样。

下面就随便挑一个创建ByteBuf的方法看看。


我这里想创建一个Pooled、direct的ByteBuf,那么官方推荐使用PooledByteBufAllocator的接口,我这里就遵循官方找到如下方法:
io.netty.buffer.PooledByteBufAllocator#newDirectBuffer

    @Override
    protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
        PoolThreadCache cache = threadCache.get();
        PoolArena<ByteBuffer> directArena = cache.directArena;

        ByteBuf buf;
        if (directArena != null) {
            buf = directArena.allocate(cache, initialCapacity, maxCapacity);
        } else {
            if (PlatformDependent.hasUnsafe()) {
                buf = UnsafeByteBufUtil.newUnsafeDirectByteBuf(this, initialCapacity, maxCapacity);
            } else {
                buf = new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
            }
        }

        return toLeakAwareBuffer(buf);
    }
  • 可见,是否选用unsafe是由Netty判断能不能拿到底层的unsafe决定的。
  • 当然这只是官方推荐的,要是水平够高不怕翻车,自己操作Netty的unsafe甚至是jdk底层的Unsafe也是没问题的~

如果你想创建Unpooled+direct类型,那么就调用io.netty.buffer.UnpooledByteBufAllocator#newDirectBuffer,创建head类型同理,客户都可以通过Netty提供的开箱即用的接口创建。unsafe是个例外,最好交给Netty控制。



小结

  • ByteBuf类型主要分为:Pooled,Unpooled,Direct,Heap,unsafe,非unsafe。
  • Netty提供给用户的接口中,是没有直接创建unsafe类型的接口的,因为unsafe是直接操作内存的,普通人使用unsafe可能会对开发环境造成直接伤害。
  • unsafe和非unsafe类型的区别主要在于它们的读写等方法,如上面的_getByte方法。