Netty4实战第五章:Buffers

  • Post author:
  • Post category:其他





本章主要内容





  • ByteBuf






  • ByteBufHolder








  • ByteBufAllocator









  • 使用上述接口






传输数据时一般都会使用一个缓冲区包装数据。Java的NIO有自己的Buffer类,之前我们讨论过,它们实现的功能有限并且没有优化过。使用JDK的ByteBuffer往往是比较麻烦也比较复杂的。缓冲区是网络应用非常重要的一个组件,有必要提供给开发者,并且应该是API的一部分。



幸运的是,Netty提供了功能强大的缓冲区实现,它用来表示一个字节序列,帮助开发者操作原始字节数据或者自定义的Java对象。Netty中的缓冲区叫ByteBuf,实际上等价于JDK的ByteBuffer。ByteBuf的作用是通过Netty的管道传输数据。它从根本上解决了JDK缓冲区的问题,并且Netty应用开发者经常会使用到它,这些因素都让它有很强的生产力。在与JDK的缓冲期相比有很大优势。



请注意,后面在介绍缓冲区的时候,我都会指明是Netty的实现还是JDK的实现,因为经常要比较它们的差异和各自的优缺点。



这一章,我们将要学习Netty的缓冲区API,以及Netty的缓冲区相比与JDK的,优点在什么地方。而且还会学习在Netty应用中如何



访问收发的数据。这一章也是本书后面内容的基础,因为缓冲API几乎在每个Netty应用中都要使用。




因为缓冲区传送数据都是通过Netty的

ChannelPipeline和

ChannelHandler,所以Netty应用都会使用到缓冲区API。在本书第六章会去详细介绍ChannelPipeline和ChannelHandler。









一、Buffer API







Netty的Buffer API主要有两个接口:






  • ByteBuf






  • ByteBufHolder





Netty通过引用计数来确定什么时候安全释放Buf回收系统资源。而且这些操作都是自动的,并不需要开发者做什么。这样Netty就可以使用池技术或其他技术提高性能并将内存利用率保持在合理水平。这些都不需要开发者做什么,不过当你开发一个Netty应用的时候,你应该尽快处理你的数据和释放资源。

Netty缓冲API有以下几个优点:



  • 如果有需要你可以自定义缓冲类型



  • 内置复合缓冲类型实现零拷贝



  • 容量是按需扩展的,类似StringBuffer



  • 不需要通过调用

    flip


    ()方法切换读写模式





  • 读索引和写索引是分开的





  • 方法调用是链式结构的




  • 引用技术功能自动释放资源



  • 池优化技术



上面这些我们后面都会介绍,包括池优化技术。现在我们先从最常用的字节容器开始。






二、字节容器

ByteBuf







当你需要通过网络与对端如数据库交换数据时,都是通过字节来沟通的。因此在网络应用中,需要一种高效的、实用的、易于使用的数据结构。Netty的

ByteBuf实现不仅满足这些要求,而且还有很多其他优点,所以它是一个理想的数据容器,并且优化了与字节数据的交互。







ByteBuf是一个可以让你有效的读写字节数据的数据容器。为了易于操作,它使用了两个索引:一个用来读一个用来写。这样你只需要调整读索引就可以很方便的重复读取容器内的数据了。






2.1、工作方式











当向

ByteBuf写数据时,写索引会根据根据写入的字符数增加同样数值。当你开始读数据时,读索引也会增加,只到读写索引处于同一位置。然后

ByteBuf就不可读了,如果继续读的话就会抛出

IndexOutOfBoundsException异常,类似读数组越界一样。




















当调用任何以read或write开头的方法时,读写索引都会自动增长。也有一些相对操作方法如歌get或set,这些方法不会去移动索引,但会去操作给定的相对索引。















ByteBuf是有容量上限的,如果将写索引移到超过容量上限的位置就会出现异常。默认的容量上限是

Integer.MAX_VALUE。



















下图展示了

ByteBuf的基本结构图。














从上图可以看出,

ByteBuf很类似字节数组,最明显的区别就是ByteBuf有分开了读写索引进行访问数据。








后面的章节我们会学习

ByteBuf上的操作。现在我们先看看不同类型的

ByteBuf。







2.2、不同类型的ByteBuf





常用的ByteBuf一共有三种,其他的也有不少,不过一般开发者很少会用到,都是Netty内部使用。可能你还会实现自己的缓冲区,不过这个不在本章内容之内。首先来看看你可能最感兴趣的缓冲。






堆缓冲






最常用的

ByteBuf是存储数据在JVM堆内存上的缓冲区。这种缓冲区内部实现就是一个数组。这种类型就算你不实用池技术它也能很快分配或释放内存空间。而且它提供了直接访问数组的方法,使用它很容易重构旧代码项目。下面展示一下它的基本用法。










        ByteBuf heapBuf = ...;
        //检测ByteBuf是否有数组
        if (heapBuf.hasArray()) {
            //获取数组引用
            byte[] array = heapBuf.array();
            //计算第一个字节所在位置
            int offset = heapBuf.arrayOffset() + heapBuf.position();
            //获取可读字节总数
            int length = heapBuf.readableBytes();
            //使用自己的业务逻辑处理数据
            YourImpl.method(array, offset, length);
        }



如果访问非堆内存

ByteBuf的数组,就会抛出

UnsupportedOperationException。因此访问ByteBuf数组之前应该使用

hasArray()方法检测此缓冲区是否支持数组。如果你使用过JDK的

ByteBuffer,会发现它们蛮像的。















直接缓冲区



















另一个

ByteBuf的实现是直接缓冲区。直接的意思就是这个缓冲区分配的内存是在JVM堆内存外部。使用直接内存时你不会在JVM堆空间中看见它的使用量。所以你计算你的应用内存使用量时,直接内存区域也要算上,然后限制它的最大使用量,防止系统内存不够用出现错误。通过网络传输数据时使用直接缓冲性能是很高的。因为当你使用其他类型缓冲区,比如堆内存,JVM会将你缓冲区的数据复制到直接内存中,然后再去传输数据。














当然直接缓冲也是有缺点的,相比堆缓冲,直接缓冲分配和释放内存资源开销比较大。这也是为什么Netty提供池技术优化它。直接缓冲的另一个缺点就是不能再通过数组访问缓冲区的数据了,所以如果你需要重构老项目就需要复制数据。







下面的一部分代码展示了如何通过数组的方式访问直接缓冲区的数据。








        ByteBuf directBuf = ...;
        //检查缓冲区是否有数组,返回flase就是直接缓冲
        if (!directBuf.hasArray()) {
            //获取可读字节数量
            int length = directBuf.readableBytes();
            //初始化相同长度的直接数组
            byte[] array = new byte[length];
            //读取数据到数组中
            directBuf.getBytes(array);
            //用户业务逻辑操作字节数组
            YourImpl.method(array, 0, array.length);
        }



可以看出,直接缓冲区需要将数据复制到数组,如果你想直接通过字节数组访问数据,用堆缓冲区更好一些。






组合缓冲区










现在来介绍一个组合缓冲区,它的名字叫

CompositeByteBuf。人如其名,它允许你将不同类型的ByteBuf组合在一起并且提供访问他们的方式。好消息你可以动态添加和删除ByteBuf,就像我们常用的List一样。你如果使用过JDK的

ByteBuffer,就不会有这么好的功能。

CompositeByteBuf也像是各种类型ByteBuf的视图,它的

hasArray()方法返回的false,因为它里面可能包含多个堆缓冲或直接缓冲。













例如,一条消息可能由两个部分组成:消息头和消息体。模块化应用中,这两部分可能是由不同模块的产生并组合在一起然后再发送。很多时候,你发送的消息往往只修改一部分,比如消息体一样,改变消息头。这里就不用每次都重新分配缓存了。









上面这种场景就很适合

CompositeByteBuf,不需要内存拷贝,而且使用的是和非组合缓冲相同的API。下图展示了

CompositeByteBuf组合消息头和消息体的结构。




















如果你使用JDK的

ByteBuffer,是绝对不可能做到这个的。如果要组合JDK的

ByteBuffer,要么创建一个

ByteBuffer数组去包含它们,要么就是创建一个新的

ByteBuffer,把两部分的内容都复制到新的

ByteBuffer中。下面的伪代码展示这两个方式。





























        //第一种使用数组组合
        ByteBuffer[] message = new ByteBuffer[] { header, body };

        //第二种将头尾都复制到新的ByteBuffer
        ByteBuffer message2 = ByteBuffer.allocate(
                header.remaining()+ body.remaining();
        message2.put(header);
        message2.put(body);
        message2.flip();



上面的方式都有很明显的缺点:使用数组处理,代码量及API都比较复杂。使用数据复制代价有很大,浪费时间和资源。下面我们看看Netty的

CompositeByteBuf怎么处理这种情况。



















        CompositeByteBuf compBuf = ...;
        ByteBuf heapBuf = ...;
        ByteBuf directBuf = ...;
        //将数据都放入到CompositeByteBuf
        compBuf.addComponent(heapBuf, directBuf);
        .....
        //移除索引是0的数据,就像List
        compBuf.removeComponent(0);
        //遍历组合缓冲中的缓冲实例
        for (ByteBuf buf: compBuf) {
            System.out.println(buf.toString());
        }



CompositeByteBuf还有很多其他方法,这里就不一一介绍了。它的方法名取得都很简单,稍微去看一下API Doc就知道了。













CompositeBytebuf也是不能通过数组的方式直接访问其里面的数据。下面展示CompositeBytebuf怎么访问数据,你会非常熟悉它们,因为和直接缓冲简直是一毛一样的。




























        CompositeBuf compBuf = ...;
        //检测是否有数组,CompositeBuf会返回false
        if (!compBuf.hasArray()) {
            //获取可读字节总数
            int length = compBuf.readableBytes();
            //初始化数组长度与可读总数相同
            byte[] array = new byte[length];
            //将数据读到字节数组
            compBuf.getBytes(array);
            //处理数据的业务逻辑
            YourImpl.method(array, 0, array.length);
        }




CompositeByteBuf是

ByteBuf的子类型,所以在它上面可以操作一些ByteBuf的方法,当然,也有一些它自己的方法。




CompositeByteBuf对读写操作也有优化。它聚集或分散数据时不会有性能惩罚也不会有内存泄漏的问题。这些都是Netty底层帮我们实现的,开发者使用的时候不用担心底层实现细节。













JDK的实现中就没有类似

CompositeByteBuf这样的类,这里Netty提供的缓冲API比JDK的

java.nio包下的缓冲API具有更丰富的功能。


































三、

ByteBuf的字节操作



















ByteBuf提供了很多读写内容的方法。如果你使用过JDK的

ByteBuffer,那你就能很快学会这些操作,没用过也没关系,这些API都是比较简单的。相比较JDK的API,Netty提供的API用户体验和性能更好一些。








3.1、随机访问



和原始字节数组一样,

ByteBuf的索引也是从0开始的。也就是说它的第一个字节的位置是0,最后一个字节的位置是容量-1。例如下面的代码,可以直接遍历ByteBuf的所有字节,而不用去管它内部如何实现的。


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



前面说过,使用set或get开头的方法不会增长它的读写索引,所以如果你使用上面的方式读取数据,可以写代码更新读写索引。



3.2、顺序访问



ByteBuf提供了两个索引来支持顺序访问的需求,一个是

readerIndex读索引支持读操作的,一个是

writerIndex写索引支持写操作的,各自之间分工很明确。而JDK的

ByteBuffer只有一个索引,所以每次切换读写模式的时候你都需要调用一下

flip()方法。下图展示了ByteBuf被两个索引分成三个区域的结构。






























3.3、可废弃的内容


可废弃的内容区域是已经被读操作读取过的内容,所以可能已经被废弃了。初始化的时候,这部分区域的大小是0,随着读取已经写入的数据,它的大小也会跟着增长。不过只有read开头的方法可以,比如前面用到的get开头的方法就不行。读取的数据可以通过调用

discardReadBytes()方法回收未使用的空间。调用这个方法之后的结构图如下图所示。








可以看到调用

discardReadBytes()之后,废弃区域没了,读索引减到0,写索引也减少了相应的数量,而可写区域增加了,很明显,废弃区域合并到可写区域了,然后读写索引减少了相应的数值。当然这个操作是有性能代价的,一般情况下不用这么做,除非你需要这调整空间。










3.5、可读字节内容(实际内容)



ByteBuf中的实际内容存储在可读字节区域。以read或skip开头的方法,你读了多少数量的字节,相应的读索引就会增加多少。如果读操作的参数也是一个ByteBuf,并且没有指定目的索引,那这个ByteBuf参数的写索引也会有相应的增长。



如果没有可读内容了还继续执行读操作,就会抛出

IndexOutOfBoundException异常。对于新分配的,或者新包含或者新复制的ByteBuf,读索引默认是0。





下面的代码展示了如何读取ByteBuf中的所有可读内容。











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


3.5、可写字节



这块区域是需要填充的未定义区域。以

write开头的方法会从当前的

writerIndex位置写入数据,并且写了多少数据这个

writerIndex也会增加相应的数量。如果写操作方法的参数也是一个

ByteBuf并且没有指定源索引,则这个参数的读索引也会增长相应的数量。














如果没有足够的可写区域了还继续使用写操作就会抛出

IndexOutOfBoundException异常。新分配的ByteBuf的写索引默认值也是0。






下面的代码展示了用Int类型数据填满可写区域。








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


3.6、重置读写索引


调用

clear()方法可以将读写索引都设置成0。这个操作不会清空内容,但是会重置读写索引。因此这个方法和JDK的

ByteBuffer.clear()方法的语义是不一样的。写了数据读了一部分数据的区域划分就是上面的图,共有三个区域。下图展示了一下调用clear()方法之后的区域。















相比于

discardReadBytes()方法,clear()方法就不会有性能损失。因为这个方法只是重置两个索引,并没有内存复制的操作。









3.7、查询操作


各种各样的

indexOf()方法可以让你以统一的标准获取指定位置的内容。也可以使用ByteBufProcessor实现的静态单字节查询方法实现复杂的动态顺序查询功能。





如果你需要解码可变长度数据如NULL结尾字符串,你会发现

bytesBefore(byte)方法很好用。例如,你现在写的网络应用发送的数据都是

NULL结尾的。使用

bytesBefore()方法可以很方便获取数据,不用每次都读一个字节还要检查是不是

NULL。如果没有

ByteBufProcessor,这些功能都需要你自己实现。而且它实现的功能都很高效,因为它在处理的过程中很少需要那些绑定检查。
























3.8、标记和重置


前面说过很多次,每个ByteBuf都有两个标记索引,一个读索引一个写索引。调用重置方法你可以修改这两个索引的位置。除了没有读限制之外它的工作方式和InputStream很类似。


也可以通过

readerIndex(int)或


writerIndex(int)去精确调整这两个索引。不过要注意如果精确调整的时候传的参数是无效的位置那么就会抛出

IndexOutOfBoundException异常。






3.9、派生缓冲区


派生缓冲区,也就是创建一个已经存在的缓冲区的视图,可以调用

duplicate()


,


slice()


,


slice(int, int)


,




readOnly()和


order(ByteOrder)等方法实现。派生缓冲区会产生自己的读写索引和其他标记索引,但是他们共享内部的数据。因为它们共享内部的数据,所以创建派生缓冲是没有什么性能损耗的,而且是比较好的方式,例如你想拥有一个缓冲区的切片。



如果需要复制一个缓冲区,可以使用

copy()或


copy(int, int)方法。下面的代码展示了怎么获取一个ByteBuf的切片。




        Charset utf8 = Charset.forName(“UTF-8“);
        //根据给定的字符串内容创建一个ByteBuf
        ByteBuf buf = Unpooled.copiedBuffer(“Netty in Action rocks!“, utf8);
        //创建ByteBuf的切片,起始位置是0,截止位置是14
        ByteBuf sliced = buf.slice(0, 14);
        //打印切片内容,正常应该是"Netty in Action"
        System.out.println(sliced.toString(utf8);
        //修改索引0内容
        buf.setByte(0, (byte) ’J’);
        //断言会成功,因为他们共享内部的数据,其实修改的就是他们内部指向的共同数据块
        assert buf.get(0) == sliced.get(0);


可以发现,切片和原始ByteBuf里面的内容其实是同一块内存区域。现在我们来看看怎么复制ByteBuf,以及它和切片ByteBuf有什么不同,请看下面的代码。




        Charset utf8 = Charset.forName(“UTF-8“);
        //同样根据给定字符串创建ByteBuf
        ByteBuf buf = Unpooled.copiedBuffer(“Netty in Action rocks!“, utf8);
        //复制ByteBuf 0-14位置的内容
        ByteBuf copy = buf.copy(0, 14);
        //输出复制的ByteBuf内容,正常应该是"Netty in Action"
        System.out.println(copy.toString(utf8);
        //同样修改内容
        buf.setByte(0, (byte) ’J’);
        //断言会成功,因为复制缓冲和原始缓冲并没有共享数据
        assert buf.get(0) != copy.get(0);


API基本上都是一样的,不过不同的方式派生的ByteBuf,内部也会有细微差别。



一般情况下建议使用切片,除非必须不共享数据才使用复制方式。因为复制

ByteBuf需要进行内存复制操作,不仅消耗更多资源,执行方法也会更耗时。




3.10、读写操作


ByteBuf的读写操作共有两大类:


  • 基于索引的,指定索引读写相应位置的数据

  • 从当前索引读写数据,然后增长读写索引


先来看一下这些方法,下面列出来的都是比较常用的方法,如果想看其他方法,请参考API DOC。先看一下读操作的。



名称


描述



getBoolean(int)


获取指定位置的

Boolean类型数据



getByte(int)


获取指定位置的字节内容



getUnsignedByte(int)


获取指定位置的无符号字节内容



getMedium(int)


获取指定位置的24位整数内容



getUnsignedMedium(int)


获取指定位置的无符号24位整数内容



getInt(int)


获取指定位置的Int类型内容



getUnsignedInt(int)


获取指定位置的无符号Int类型内容



getLong(int)


获取指定位置的Long类型内容



getUnsignedLong(int)


获取指定位置的无符号Long类型内容



getShort(int)


获取指定位置的Short类型内容



getUnsignedShort(int)


获取指定位置的无符号Short类型内容


getBytes(int, …)


从给定位置开始将数据转换到目标容器中

它的set操作和get操作都很类似,请看下面的表格。



名称


描述


setBoolean(int, boolean)


设置指定位置的Boolean类似数据


setByte(int, int)


设置指定位置字节数据


setMedium(int, int)


设置指定位置24位整数数据


setInt(int, int)


设置指定位置Int类型数据


setLong(int, long)


设置指定位置Long类型数据


setShort(int, int)


设置指定位置Short类型数据


setBytes(int,…)


从指定位置将源数据容器中转换过来

你可能会发现set方法没有无符号类型的。这是因为在设置数据的时候没有无符号的概念。上面已经介绍了相关的方法,现在我们来看看怎么在实际代码中使用它们。


        Charset utf8 = Charset.forName("UTF-8");
        //根据字符串创建ByteBuf
        ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);
        //打印位置0的字符,正常输出'N'
        System.out.println((char) buf.getByte(0));
        //查询现在的读写索引
        int readerIndex = buf.readerIndex();
        int writerIndex = buf.writerIndex();
        //修改位置0的数据
        buf.setByte(0, (byte)"B");
        //再次打印位置0的数据,这次应该是'B'
        System.out.println((char) buf.getByte(0));
        //set或get方法不会改变读写索引
        assert readerIndex = buf.readerIndex();
        assert writerIndex = buf.writerIndex();

接下来介绍到的读写操作就是read和write开头的方法,前面说过,这些方法也会影响到缓冲的读写索引。这里面用到最多的方法就是把ByteBuf当成一个流来读取数据。同样,write方法相当于在ByteBuf上追加数据。常用的read方法如下表格。



名称


描述



readBoolean()


读取当前读索引

Boolean数据,同时读索引会加1



readByte()


读取当前读索引Byte

数据,

同时

读索引会加1



readUnsignedByte()


读取当前读索引无符号Byte

数据,

同时

读索引会加1



readMedium()


读取当前读索引24位整数

数据,

同时

读索引会加1



readUnsignedMedium()


读取当前读索引无符号24位整数

数据,

同时

读索引会加1



readInt()


读取当前读索引Int类型

数据,

同时

读索引会加1



readUnsignedInt()


读取当前读索引无符号Int类型

数据,

同时

读索引会加1



readLong()


读取当前读索引Long类型

数据,

同时

读索引会加1



readUnsignedLong()


读取当前读索引无符号Long类型

数据,

同时

读索引会加1



readShort()


读取当前读索引Short类型

数据,

同时

读索引会加1



readUnsignedShort()


读取当前读索引无符号Short类型

数据,

同时

读索引会加1



readBytes(int)


从当前读索引读取指定长度的字节内容,读索引也会增加指定长度



它的write方法基本上和read方法也是一一对应的。



名称


描述



writeBoolean(boolean)


在当前写索引位置写入Boolean类型数据,同时写索引加1



writeByte(int)


当前写索引写入Byte类型数据,同时写索引加1



writeMedium(int)


当前写索引写入24位整数数据,同时写索引会加3,24位=3字节



writeInt(int)


在当前写索引写入32位整数数据,同时写索引加4,32位=4字节



writeLong(long)


当前写索引写入64位整数数据,同时写索引加8,64位=8字节


writeShort(int)


当前写索引写入Short类型数据,同时写索引加2


writeBytes(ByteBuf src, int length)


从源数据中转换指定长度数据到当前写索引,同时写索引增加相应数量

现在我们来看看上面这些read和write方法在实际项目中的应用。

        Charset utf8 = Charset.forName("UTF-8");
        //根据字符串创建ByteBuf
        ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);
        //打印首位置字符,正常是'N'
        System.out.println((char) buf.readByte());
        //获取当前的读写索引
        int readerIndex = buf.readerIndex();
        int writerIndex = buf.writerIndex();
        //修改写索引0位置的数据
        buf.writeByte( (byte) '?');
        //写数据读索引不会变化,写索引会加1
        assert readerIndex = buf.readerIndex();
        assert writerIndex != buf.writerIndex();

一直忘记了说一件事,获取当前读写索引的方法是不会修改读写索引。上面已经介绍了很多读写数据的方法,当然还有一些其他很有用的方法,下面会介绍这些方法。


3.11、其他常用方法


还有一些常用的方法我们没有介绍到,下表列出来这些常用方法,并简要介绍它们的用法。



名称


描述


isReadable()


有一个字节可读就会返回true


isWritable()


有一个可写字节空间就会返回true


readableBytes()


返回可读字节内容总数


writablesBytes()


返回可写内容区域总数


capacity()


返回缓冲区目前容量,如果超过这个数值,缓冲区会扩容,

只到达到maxCapacity()的限制


maxCapacity()


返回缓冲区最大容量


hasArray()


如果缓冲区内部以数组存储返回true


array()


返回内部存储的数组,如果没有数组则

抛出

UnsupportedOperationException

前面介绍的都是Java基础类型,但很多时候我们网络应用需要处理普通Java对象,也就是Java Bean。需要存储和检查,而且在容器中按照顺序存储也很重要。因此,Netty提供了另一个数据容器

MessageBuf。







四、


ByteBufHolder












你有一个对象,里面很多属性,但是都是通过字节去传输的。例如,HTTP协议中的相应数据就很符合这种情况。HTTP响应数据有很多属性,如果状态码,Cookie等等,而且它实际的内容都是以字节方式传输的。










因为这种情况很普遍,所以Netty抽象了一个接口

ByteBufHolder。这样做的好处就是Netty可以优化分配ByteBuf,例如使用池技术,也能够自动释放这部分资源。
















ByteBufHolder实际上比较有用的方法就是访问它里面存储的数据以及使用引用计数功能。下表展示了它常用的方法。

























名称


描述


content()


返回它存储的内容的ByteBuf


copy()


返回一个深拷贝的ByteBufHolder,也就是


他们的数据不共享

如果你传输的数据存储的是ByteBuf,那么使用

ByteBufHolder是一个不错的选择。


















4.1、Netty缓冲区帮助类

使用JDK的NIO API的一个困难是完成一个简单的任务有很多重复性的代码。虽然Netty提供的缓冲区API已经很容易使用了,但是Netty还是提供了很多工具类方便开发者创建或使用缓冲区。这一小节我们主要介绍开发中常用的三个工具类。





4.2、

ByteBufAllocator






前面提到过,Netty提供了池技术优化了各种ByteBuf。为了实现这个功能Netty抽象了一个

ByteBufAllocator接口。它主要是用来分配ByteBuf实例的。是否使用池化技术要看使用的是哪个实现类,但是因为它们实现了同一个接口,所以API都是一样的,对于开发者来说没什么区别。






我们先来看一下

ByteBufAllocator提供了哪些操作。







名称


描述


buffer()


创建ByteBuf,类似可能是堆或直接的,依赖具体的实现


buffer(int)




buffer(int, int)




heapBuffer()


创建堆类型ByteBuf


heapBuffer(int)




heapBuffer(int, int)




directBuffer()


创建直接类型ByteBuf


directBuffer(int)




directBuffer(int, int)




compositeBuffer()


创建复合类型缓冲区


compositeBuffer(int)




heapCompositeBuffer()


内部为堆类型ByteBuf的复合缓冲区


heapCompositeBuffer(int)




directCompositeBuffer()


内部为直接类型ByteBuf的


复合缓冲区


directCompositeBuffer(int)




ioBuffer()


创建用于IO操作的ByteBuf


有些方法后面带了一个或两个Int类型参数,这两个参数就是指定ByteBuf的初始容量和最大容量的。你应该记得ByteBuf是可以扩容的。ByteBuf可以不断扩容只到达到了最大容量。


获取

ByteBufAllocator的引用也是很容易的事情。通过Channel或者你实现的ChannelHandler的方法中的

ChannelHandlerContext。更多关于

ChannelHandlerContext和ChannelHandler的知识将会在第六章介绍。









下面的代码展示了获取

ByteBufAllocator的方式。













Channel channel = ...;
ByteBufAllocator allocator = channel.alloc();
....
ChannelHandlerContext ctx = ...;
ByteBufAllocator allocator2 = ctx.alloc();

Netty提供了两种不同类型的

ByteBufAllocator实现。一种是基于池技术实现的,这种方式可以减少分配和回收的成本,并且维持内存使用量在一个比较小的范围。这里面的实现方式也是比较复杂,已经超过了本书的内容范围,如果对这部分内容感兴趣可参考很多操作系统的内存分配实现逻辑,大体和Netty的算法一样。






另外一种

ByteBufAllocator的实现就是没有使用池技术优化的,这种每次都会返回新的

ByteBuf实例。Netty默认使用

PooledByteBufAllocator,这个就是池技术实现的

ByteBufAllocator。不过如果不想用默认的,也很容易修改,可以在

ChannelConfig里设置,或者在应用的启动器中指定。很多关于应用启动器方面的内容在第九章会讲到。

















4.3、



Unpooled






有时候你没办法获取

ByteBufAllocator的实例,也就是没办法使用上一节介绍的方式创建ByteBuf实例。为了应对这种情况,Netty提供了一个工具类

Unpooled。这个工具类提供了很多静态方法帮助我们创建ByteBuf实例,不过没有使用池技术。







先来看看它提供了哪些常用方法给我们使用。











名称


描述



buffer()


创建未使用池技术的堆缓冲

ByteBuf



buffer(int)





buffer(int, int)





directBuffer()


创建未使用池技术的直接缓冲

ByteBuf



directBuffer(int)





directBuffer(int, int)




wrappedBuffer()


创建包含给定数据的ByteBuf


copiedBuffer()


复制给定数据然后创建ByteBuf

你也可以在非Netty项目中使用

Unpooled,因为它的API很简单,性能也很高,项目中如果需要这种高性能缓冲区的地方可以尝试一下。











4.4、




ByteBufUtil






另一个比较有用的类是

ByteBufUtil。这个类提供了很多静态方法直接操作

ByteBuf。相比较上面的

Unpooled,这个类提供的方法是比较通用的并且不用关心你的ByteBuf是使用了池技术的还是没有使用。













例如它最常用的方法

hexdump(),这个方法返回ByteBuf内容的十六进制格式。这个方法可以使用在很多场景,例如日志场景。十六进制格式的内容也很容易转成字节表示。你也许奇怪为什么不直接显示字节内容。主要是因为字节内容人类难以阅读,而16进制就比较友好了。













这个类提供了很多ByteBuf相关的静态方法,例如复制、编码、解码等等,这个大家在以后开发中会慢慢用到,甚至当你需要实现自己的

ByteBuf的时候也会用到它们。



















五、总结





这一章主要学习了Netty提供的数据容器,以及它和JDK提供的有什么区别。




这一章我们还学习了Netty的数据容器有哪些类型的实现,已经各自的使用场景和性能高低,还有分配或释放需要的资源代价。





下一章我们主要学习

ChannelHandler,前面说过,这个玩意关系到我们自己的业务逻辑,所以也是必不可少的一部分。而且

ChannelHandler很多地方都会使用这章介绍的数据容器,学习下一章也能帮助我们将这一章的知识用于实际的开发中。