Netty之Unpooled_Bytebuf

前言

计算机存储基本单位是字节(byte),传输基本单位是bit(位),JAVA NIO提供了ByteBuffer等七种容器来提升传输时的效率,但是在使用时比较复杂,经常要进行读写切换,主要缺点如下:

(1)ByteBuffer长度固定,一旦分配完成,它的容量不能动态扩展和收缩,当需要编码的对象大于ByteBuffer的容量时,会发生索引越界异常;

(2)ByteBuffer只有一个标识位置的指针position,读写的时候需要手工调用flip()和rewind()等,使用者必须小心谨慎地处理这些API,否则很容易导致程序处理失败;

(3)ByteBuffer的API功能有限,一些高级和实用的特性它不支持,需要使用者自己编程实现。

为了弥补这些不足,Netty提供了自己的ByteBuffer实现—— ByteBuf

ByteBuf介绍

ByteBuf是Netty.Buffer中的类,主要特征如下:

  1. 读和写用不同的索引。
  2. 读和写可以随意的切换,不需要调用flip()方法。
  3. 容量能够被动态扩展,和StringBuilder一样。
  4. 用其内置的复合缓冲区可实现透明的零拷贝。
  5. 支持方法链。
  6. 支持引用计数。count == 0,release。
  7. 支持池。

ByteBuf通过两个位置指针来协助缓冲区的读写操作,分别是读操作(使用readerIndex)、写操作(使用writerIndex)。

readerIndex和writerIndex的取值一开始都是0,随着数据的写入writerIndex会增加,读取数据会使readerIndex增加,但是它不会超过writerIndex。在读取之后,0~readerIndex的就被视为discard的,调discardReadBytes方法,可以释放这部分空间,它的作用类似ByteBuffer的compact方法。readerIndex和writerIndex之间的数据是可读取的,等价于ByteBuffer position和limit之间的数据。writerIndex和capacity之间的空间是可写的,等价于ByteBuffer limit和capacity之间的可用空间。

由于写操作不修改readerIndex指针,读操作不修改writerIndex指针,因此读写之间不再需要调整位置指针,这极大地简化了缓冲区的读写操作,避免了由于遗漏或者不熟悉flip()操作导致的功能异常。

传统JAVA NIO ByteBuffer图示:

ByteBuf读写示意图:

ByteBuf零拷贝

netty提供了CompositeByteBuf类实现零拷贝。大多数情况下,在进行网络数据传输时我们会将消息分为消息头head消息体body,甚至还会有其他部分,这里我们简单的分为两部分来进行探讨:

以前的做法

ByteBuffer header = ByteBuffer.allocate(1); header.put("a".getBytes()); header.flip(); ByteBuffer body = ByteBuffer.allocate(1); body.put("b".getBytes()); body.flip(); ByteBuffer message = ByteBuffer.allocate(header.remaining() + body.remaining()); message.put(header); message.put(body); message.flip(); while (message.hasRemaining()){ System.err.println((char)message.get());         }

这样为了得到完整的消息体相当于对内存进行了多余的两次拷贝,造成了很大的资源的浪费。

netty提供的方法

CompositeByteBuf messageBuf = Unpooled.compositeBuffer(); ByteBuf headerBuf = Unpooled.buffer(1); headerBuf.writeByte('a'); ByteBuf bodyBuf = Unpooled.buffer(1); bodyBuf.writeByte('b'); messageBuf.addComponents(headerBuf, bodyBuf); for (ByteBuf buf : messageBuf) { System.out.println((char)buf.readByte()); System.out.println(buf.toString());10 }

这里通过CompositeByteBuf 对象将headerBuf 与bodyBuf组合到了一起,也得到了完整的消息体,但是并未进行内存上的拷贝。可以注意下我在上面代码段中进行的buf.toString()方法的调用,得出来的结果是:指向的还是原来分配的空间地址,也就证明了零拷贝的观点。

ByteBuf引用计数

retain和lock类似,release和unlock类似,内部维护一个计数器,计数器到0的时候就表示已经释放掉了。往一个已经被release掉的buffer中去写数据,会抛出IllegalReferenceCountException: refCnt: 0异常。

ByteBuf buffer = Unpooled.buffer(1); int i = buffer.refCnt();          System.err.println("refCnt : " + i);    //refCnt : 1 buffer.retain(); buffer.retain(); buffer.retain(); buffer.retain(); i = buffer.refCnt(); System.err.println("refCnt : " + i);      //refCnt : 5 boolean release = buffer.release(); i = buffer.refCnt(); System.err.println("refCnt : " + i + " ===== " + release);      //refCnt : 4 ===== false release = buffer.release(4); i = buffer.refCnt(); System.err.println("refCnt : " + i + " ===== " + release);      //refCnt : 0 ===== true

引用计数器实现的原理并不复杂,仅仅只是涉及到一个指定对象的活动引用,对象被初始化后引用计数值为1。只要引用计数大于0,这个对象就不会被释放,当引用计数减到为0时,这个实例就会被释放,被释放的对象不应该再被使用。

ByteBuf支持池

Netty对ByteBuf的分配提供了池支持,具体的类是PooledByteBufAllocator。用这个分配器去分配ByteBuf可以提升性能以及减少内存碎片。Netty中默认用PooledByteBufAllocator当做ByteBuf的分配器。PooledByteBufAllocator对象可以从Channel中或者绑定了Channel的ChannelHandlerContext中去获取到。

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

ByteBuf API介绍

创建ByteBuf

// 创建一个heapBuffer,是在堆内分配的 ByteBuf heapBuf = Unpooled.buffer(5); if (heapBuf.hasArray()) { byte[] array = heapBuf.array(); int offset = heapBuf.arrayOffset() + heapBuf.readerIndex(); int length = heapBuf.readableBytes(); handleArray(array, offset, length); } // 创建一个directBuffer,是分配的堆外内存 ByteBuf directBuf = Unpooled.directBuffer(); if (!directBuf.hasArray()) { int length = directBuf.readableBytes(); byte[] array = new byte[length]; directBuf.getBytes(directBuf.readerIndex(), array); handleArray(array, 0, length); }

这两者的主要区别:

a. 分配的堆外内存空间,在进行网络传输时就不用进行拷贝,直接被网卡使用。但是这些空间想要被jvm所使用,必须拷贝到堆内存中。
b. 分配和释放堆外内存相比堆内存而言,代价是相当昂贵的。
c. 使用这两者buffer中的数据的方式也略有不同,见上面的代码段。

读写数据(readByte writeByte)

ByteBuf heapBuf = Unpooled.buffer(5); heapBuf.writeByte(1); System.err.println("writeIndex : " + heapBuf.writerIndex());//writeIndex : 1 heapBuf.readByte(); System.err.println("readIndex : " + heapBuf.readerIndex());//readIndex : 1 heapBuf.setByte(2, 2); System.err.println("writeIndex : " + heapBuf.writerIndex());//writeIndex : 1 heapBuf.getByte(2); System.err.println("readIndex : " + heapBuf.readerIndex());//readIndex : 1

进行readByte和writeByte方法的调用时会改变readIndex和writeIndex的值,而调用set和get方法时不会改变readIndex和writeIndex的值。上面的测试案例中打印的writeIndex和readIndex均为1,并未在调用set和get方法后被改变。

discardReadBytes方法

先看一张图:

从上面的图中可以观察到,调用discardReadBytes方法后,readIndex置为0,writeIndex也往前移动了Discardable bytes长度的距离,扩大了可写区域。但是这种做法会严重影响效率,它进行了大量的拷贝工作。如果要进行数据的清除操作,建议使用clear方法。调用clear()方法将会将readIndex和writeIndex同时置为0,不会进行内存的拷贝工作,同时要注意,clear方法不会清除内存中的内容,只是改变了索引位置而已。

Derived buffers

这里介绍三个方法(浅拷贝):

duplicate():直接拷贝整个buffer。
slice():拷贝buffer中已经写了的数据。
slice(index,length): 拷贝buffer中从index开始,长度为length的数据。
readSlice(length): 从当前readIndex读取length长度的数据。

上面这几个方法的虽然是拷贝,但是这几个方法并没有实际意义上去复制一个新的buffer出来,它和原buffer是共享数据的。所以说调用这些方法消耗是很低的,并没有开辟新的空间去存储,但是修改后会影响原buffer。这种方法也就是咱们俗称的浅拷贝
要想进行深拷贝,这里可以调用copy()和copy(index,length)方法,使用方法和上面介绍的一致,但是会进行内存复制工作,效率很低。

ByteBuf heapBuf = Unpooled.buffer(5); heapBuf.writeByte(1); heapBuf.writeByte(1); heapBuf.writeByte(1); heapBuf.writeByte(1); // 直接拷贝整个buffer ByteBuf duplicate = heapBuf.duplicate(); duplicate.setByte(0, 2); System.err.println("duplicate: " + duplicate.getByte(0) + "====heapBuf: " + heapBuf.getByte(0));//duplicate: 2====heapBuf: 2 // 拷贝buffer中已经写了的数据 ByteBuf slice = heapBuf.slice(); System.err.println("slice capacity: " + slice.capacity());//slice capacity: 4 slice.setByte(2, 5); ByteBuf slice1 = heapBuf.slice(0, 3); System.err.println("slice1 capacity: "+slice1.capacity());//slice1 capacity: 3 System.err.println("duplicate: " + duplicate.getByte(2) + "====heapBuf: " + heapBuf.getByte(2));//duplicate: 5====heapBuf: 5复制代码

__EOF__

本文作者等不到的口琴
本文链接https://www.cnblogs.com/Courage129/p/14276926.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   等不到的口琴  阅读(3037)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端
点击右上角即可分享
微信分享提示