netty系列三 ByteBuf

简介

字节是网络数据的基本单位。 Java NIO 提供了 ByteBuffer 作为字节容器,但是这个类使用起来过于复杂,而且也有些繁琐。Netty使用了即易于使用又具备良好性能的ByteBuf来替代ByteBuffer。

使用模式

  • 堆缓冲区
  • 直接缓冲区
  • 复合缓冲区

堆缓冲区 ——>JVM 的堆空间

最常用的 ByteBuf 模式是将数据存储在 JVM 的堆空间中。这种模式被称为支撑数组(backing array),它能在没有使用池化的情况下提供快速的分配和释放,非常适合于有遗留的数据需要处理的情况。

jvm堆的区域主要是用来存放对象的实例,它的空间大小是JVM内存区域中占比重最大的,也是jvm最大的内存管理模块,这个区域是垃圾收集器主要管理的区域

直接缓冲区 ——>本地内存(比堆缓冲区少一步数据复制)

直接缓冲区是另外一种 ByteBuf 模式. 直接缓冲区的内容并不是驻留在Java的堆上,而是在本地内存中。Java堆上的数据在每次调用本地 I/O 操作之前(或者之后)需要将缓冲区的内容复制到一个中间缓冲区(或者从中间缓冲区把内容复制到缓冲区),而本地内存避开了这个操作。 这也就解释了为何直接缓冲区对于网络数据传输是理想的选择。如果你的数据包含在一个在堆上分配的缓冲区中,那么事实上,在通过套接字发送它之前, JVM将会在内部把你的缓冲区复制到一个直接缓冲区中

直接缓冲区的主要缺点是,相对于基于堆的缓冲区,它们的分配和释放都较为昂贵。如果你正在处理遗留代码,你也可能会遇到另外一个缺点:因为数据不是在堆上, 所以你不得不进行一次复制。显然,与使用堆缓冲区相比,这涉及的工作更多。因此,如果事先知道容器中的数据将会被作为数组来访问,你可能更愿意使用堆内存。

复合缓冲区

复合缓冲区为多个 ByteBuf 提供一个聚合视图,可以根据需要添加或者删除 ByteBuf 实例。实现该功能的类为CompositeByteBuf,它提供了一个将多个缓冲区表示为单个合并缓冲区的虚拟表示.
CompositeByteBuf 中的 ByteBuf实例可能同时包含直接内存分配和非直接内存分配。
如果其中只有一个实例,那么对 CompositeByteBuf 上的 hasArray()方法的调用将返回该组件上的 hasArray()方法的值;否则它将返回 false。
为了举例说明,让我们考虑一下一个由两部分——头部和主体——组成的将通过 HTTP 协议传输的消息。这两部分由应用程序的不同模块产生,将会在消息被发送的时候组装。该应用程序可以选择为多个消息重用相同的消息主体。当这种情况发生时,对于每个消息都将会创建一个新的头部。
因为我们不想为每个消息都重新分配这两个缓冲区,所以使用CompositeByteBuf 是一个完美的选择。 它在消除了没必要的复制的同时,暴露了通用的 ByteBuf API。

/**
 * Netty 通过一个 ByteBuf 子类——CompositeByteBuf——实现了组合模式,它提供了一
 个将多个缓冲区表示为单个合并缓冲区的虚拟表示
 */
public class CompositeBuf {
    public static void main(String args[]){
        CompositeByteBuf messageBuf = Unpooled.compositeBuffer();
        ByteBuf headerBuf = Unpooled.copiedBuffer("head", CharsetUtil.UTF_8); // can be backing or direct
        ByteBuf bodyBuf = Unpooled.copiedBuffer("body", CharsetUtil.UTF_8); // can be backing or direct
        messageBuf.addComponents(headerBuf, bodyBuf);
        System.out.println("Remove Head Before------------------");
        printCompositeBuffer(messageBuf);
        for (ByteBuf buf : messageBuf) {
            System.out.println(buf.toString(CharsetUtil.UTF_8));
        }
        messageBuf.removeComponent(0); // remove the header
        System.out.println("Remove Head After------------------");
        printCompositeBuffer(messageBuf);
        for (ByteBuf buf : messageBuf) {
            System.out.println(buf.toString(CharsetUtil.UTF_8));
        }
    }

    public static void printCompositeBuffer(CompositeByteBuf compBuf){
        int length = compBuf.readableBytes();
        byte[] array = new byte[length];
        compBuf.getBytes(compBuf.readerIndex(), array);
        System.out.println (Arrays.toString(array));
        System.out.println (length);
    }
}

支持的操作

随机访问索引

如同在普通的 Java 字节数组中一样, ByteBuf 的索引是从零开始的:第一个字节的索引是0,最后一个字节的索引总是 capacity() - 1,可以通过index进行访问:

public static void main(String args[]) {
        ByteBuf directBuf = Unpooled.directBuffer(100);
        directBuf.writeBytes("direct buffer".getBytes());
        System.out.println(directBuf.getByte(5));
    }

顺序访问索引

可丢弃的字节

可丢弃字节的分段包含了已经被读过的字节。通过调用 discardReadBytes()方法,可以丢弃它们并回收空间。这个分段的初始大小为 0,存储在 readerIndex 中,会随着 read 操作的执行而增加。

可读字节

ByteBuf 的可读字节分段存储了实际数据。新分配的、包装的或者复制的缓冲区的默认的
readerIndex 值为 0。任何名称以 read 或者 skip 开头的操作都将检索或者跳过位于当前
readerIndex 的数据,并且将它增加已读字节数。

可写字节

可写字节分段是指一个拥有未定义内容的、写入就绪的内存区域。新分配的缓冲区的
writerIndex 的默认值为 0。任何名称以 write 开头的操作都将从当前的 writerIndex 处
开始写数据,并将它增加已经写入的字节数。如果写操作的目标也是 ByteBuf,并且没有指定源索引的值,则源缓冲区的 readerIndex 也同样会被增加相同的大小。

 public static  void  test(){
        ByteBuf directBuf = Unpooled.directBuffer(100);
        directBuf.writeBytes("direct buffer".getBytes());
        System.out.println("总字节数:"+directBuf.capacity());
        System.out.println("可写字节容量:"+directBuf.writableBytes());
        System.out.println("初始化可读字节:"+directBuf.readableBytes());
        System.out.println("初始化可丢弃字节:"+directBuf.readerIndex()+"\n");
        directBuf.readBytes(10);
        System.out.println("读取两个字节"+"\n");
        System.out.println("读取后可写字节容量:"+directBuf.writableBytes());
        System.out.println("读取后可读字节:"+directBuf.readableBytes());
        System.out.println("读取后可丢弃字节:"+directBuf.readerIndex()+"\n");
        directBuf.discardReadBytes();
        System.out.println("执行discardReadBytes后可写字节容量:"+directBuf.writableBytes());
        System.out.println("执行discardReadBytes后可读字节:"+directBuf.readableBytes());
        System.out.println("执行discardReadBytes后可丢弃字节:"+directBuf.readerIndex());
    }

索引管理

索引管理的相关操作如下:

  • 可以通过调用 markReaderIndex()、 markWriterIndex()、 resetWriterIndex()
    和 resetReaderIndex()来标记和重置 ByteBuf 的 readerIndex 和 writerIndex。
  • 可以通过调用 readerIndex(int)或者 writerIndex(int)来将索引移动到指定位置。
 ByteBuf directBuf = Unpooled.directBuffer(100);
        directBuf.writeBytes("direct buffer".getBytes());
        System.out.println("初始化可读字节:"+directBuf.readableBytes());
        directBuf.markReaderIndex();
        //标记读索引
        System.out.println("执行markReaderIndex"+"\n");
        directBuf.readBytes(2);
        System.out.println("读取两个字节"+"\n");
        System.out.println("读取后可读字节:"+directBuf.readableBytes());
        //恢复读索引
        directBuf.resetReaderIndex();
        System.out.println("执行resetReaderIndex后可读字节:"+directBuf.readableBytes());
        directBuf.clear();
        System.out.println("执行clear后可读字节:"+directBuf.readableBytes());
        //可读字节变为0,此时再读取会抛出IndexOutOfBoundsException
        directBuf.readBytes(2);

查找操作

在ByteBuf中有多种可以用来确定指定值的索引的方法,常见方法如下:

  • indexOf(int,int,byte)
  • byteBuf.forEachByte(ByteBufProcessor.FIND_CR):ByteBufProcessor针对一些常见的值定义了许多便利的方法

派生缓冲区

派生缓冲区为 ByteBuf 提供了以专门的方式来呈现其内容的视图。这类视图是通过以下方
法被创建的:

  • duplicate();
  • slice();
  • slice(int, int);
  • Unpooled.unmodifiableBuffer(…);
  • order(ByteOrder);
  • readSlice(int)。

上述这些方法都将返回一个新的 ByteBuf 实例,它具有自己的读索引、写索引和标记
索引。 但是其内部存储和原始对象是共享的。该种方式创建成本很低廉,但是这也意味着,如果你修改了它的内容,也同时修改了其对应的源实例,所以要小心。

读/写操作

正如前文所提到过的,有两种类别的读/写操作:

  • get()和 set()操作,从给定的索引开始,并且保持索引不变;
  • read()和 write()操作, 从给定的索引开始,并且会根据已经访问过的字节数对索
    引进行调整。

ByteBufHolder

ByteBufHolder 只有几种用于访问底层数据和引用计数的方法:

  • content():返回由这个 ByteBufHolder 所持有的 ByteBuf
  • copy():返回这个 ByteBufHolder 的一个深拷贝,包括一个其所包含的 ByteBuf 的非共享拷贝
  • duplicate():返回这个 ByteBufHolder 的一个浅拷贝,包括一个其所包含的 ByteBuf 的共享拷贝.

系统默认自带了一系列的ByteBufHolder,以MemoryFileUpload为例,该类通过封装将filename,contentType,contentTransferEncoding属性与对应的file进行关联。
2
1

ByteBuf分配

按需分配: ByteBufAllocator 接口

为了降低分配和释放内存的开销, Netty 通过 interface ByteBufAllocator 实现了(ByteBuf 的)池化,它可以用来分配我们所描述过的任意类型的 ByteBuf 实例
Netty提供了两种ByteBufAllocator的实现: PooledByteBufAllocator和UnpooledByteBufAllocator。前者池化了ByteBuf的实例以提高性能并最大限度地减少内存碎片。

  • PooledByteBufAllocator:池化了ByteBuf的实例以提高性能并最大限度地减少内存碎片
  • UnpooledByteBufAllocator:不池化ByteBuf实例,并且在每次它被调用时都会返回一个新的实例

使用Unpooled缓冲区分配

Unpooled 是一个工具类,提供静态方法创建未池化的ByteBuf
可能某些情况下,你未能获取一个到 ByteBufAllocator 的引用。对于这种情况, Netty 提供了一个简单的称为 Unpooled 的工具类, 它提供了静态的辅助方法来创建未池化的 ByteBuf实例.提供的方法如下:

  • buffer:返回一个未池化的基于堆内存存储的ByteBuf
  • directBuffer:返回一个未池化的基于直接内存存储的 ByteBuf
  • wrappedBuffer:返回一个包装了给定数据的 ByteBuf
  • copiedBuffer:返回一个复制了给定数据的 ByteBuf

使用ByteBufUtil工具类

ByteBufUtil 提供了用于操作 ByteBuf 的静态的辅助方法。常用方法如hexdump(),equals()等

总结

本文最后来总结下ByteBuf的优点,首先从上文可以看出Netty 的数据处理 主要通过ByteBuf 和ByteBufHolder,ByteBuf优点可总结为:

  • 它可以被用户自定义的缓冲区类型扩展;
  • 通过内置的复合缓冲区类型实现了透明的零拷贝;
  • 容量可以按需增长(类似于 JDK 的 StringBuilder);
  • 在读和写这两种模式之间切换不需要调用 ByteBuffer 的 flip()方法;
  • 读和写使用了不同的索引;
  • 支持方法的链式调用;
  • 支持引用计数;
  • 支持池化。

参考

Netty框架学习之(五):细说数据容器-ByteBuf

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值