在Netty中,还有一个比较常见的对象ByteBuf,其实相当于JavaNio中的ByteBuffer,只是ByteBuf对Nio中ByteBuffer的功能做了大量的工作而已非常增强,让我们简单了解一下ByteBuf。以下代码演示了ByteBuf的创建及其内容的打印。这体现了与普通ByteBuffers最大的区别之一,就是ByteBuf可以自动扩容。默认长度为256,如果内容长度超过阈值,会自动触发扩容。publicclassByteBufExample{publicstaticvoidmain(String[]args){ByteBufbuf=ByteBufAllocator.DEFAULT.buffer();//自动扩容log(buf);StringBuildersb=newStringBuilder();for(inti=0;i<32;i++){//演示的时候可以把循环的值展开,看看展开效果sb.append("-"+i);}buf.writeBytes(sb.toString().getBytes());日志(缓冲器);}privatestaticvoidlog(ByteBufbuf){StringBuilderbuilder=newStringBuilder().append("读取索引:").append(buf.readerIndex())//获取读取索引.append("写入索引:").append(buf.writerIndex())//获取写入索引.append("capacity:").append(buf.capacity())//获取capacity.append(StringUtil.NEWLINE);//DumpByteBuf的内容到StringBuilderByteBufUtil.appendPrettyHexDump(builder,buf);System.out.println(builder.toString());}}字节B有两种创建uf的方法。首先是创建一个基于堆内存的ByteBuf。ByteBufbuffer=ByteBufAllocator.DEFAULT.heapBuffer(10);二是创建一个基于直接内存(堆外内存)的ByteBuf。Java中的这种内存分为两部分,一部分是不需要jvm管理的直接内存,也称为堆外内存。堆外内存是在JVM堆外的内存区域分配内存对象。这部分内存不虚拟机由操作系统管理,可以减少垃圾回收对应用程序的影响ByteBufAllocator.DEFAULT.directBuffer(10);直接内存的好处就是读写性能会更高。如果数据存放在堆中,这时候就需要将Java堆空间中的数据发送到远程服务器。首先需要将堆内数据复制到直接内存(堆外内存),然后发送。如果数据直接存放在堆外内存中,那么发送时就少了一个拷贝步骤。但它也有缺点。由于JVM内存管理的缺失,我们需要自己维护堆外内存,防止内存溢出。另外需要注意的是,ByteBuf默认是通过池化技术创建的。pooling技术在之前的课程中已经多次提到。其核心思想是实现对象的复用,从而减少频繁创建和销毁对象带来的性能开销。池化功能是否开启可以通过以下环境变量控制,其中unpooled表示不开启。-Dio.netty.allocator.type={unpooled|pooled}publicclassNettyByteBufExample{publicstaticvoidmain(String[]args){ByteBufbuf=ByteBufAllocator.DEFAULT.buffer();System.out.println(buf);}}ByteBuf的存储结构ByteBuf的存储结构如图3-1所示。从这张图中我们可以看出,ByteBuf其实是一个字节容器,里面包含了三个被丢弃的字节。这部分数据无效。可读字节,这部分数据是ByteBuf的主要数据,从ByteBuf读取的数据都来自这部分;writablebytes,所有写入ByteBuf的数据都会存储在这段可扩展字节中,也就是说ByteBuf最多可以扩展多少容量。
图3-1在ByteBuf中,有两个指针readerIndex:读指针,每读取一个字节,readerIndex加1。ByteBuf中总共有writeIndex-readerIndex个字节可以被读。当readerIndex和writeIndex相等时,无法读取ByteBuf。writeIndex:写指针,每写入一个字节,writeIndex加1,直到达到容量,即可触发扩容,然后继续写入。ByteBuf中还有一个maxCapacity最大容量。默认值为Integer.MAX_VALUE。ByteBuf写入数据时,如果容量不足,会触发扩容,直到容量扩容到maxCapacity。ByteBuf中常用的方法对于ByteBuf,常用的方法是write和readWrite相关方法对于write方法,ByteBuf提供了对各种数据类型的写,比如writeChar,写char类型writeInt,写int类型writeFloat,写float类型writeBytes,writenioByteBufferwriteCharSequence,writestringpublicclassByteBufExample{publicstaticvoidmain(String[]args){ByteBufbuf=ByteBufAllocator.DEFAULT.heapBuffer();//可以自动扩容buf.writeBytes(newbyte[]{1,2,3,4});//写四个字节日志(buf);buf.writeInt(5);//写一个int类型,也是4字节log(buf);}privatestaticvoidlog(ByteBufbuf){System.out.println(buf);StringBuilderbuilder=newStringBuilder().append("读取索引:").append(buf.readerIndex()).append("写入索引:").append(buf.writerIndex()).append("容量:").append(buf.capacity()).append(StringUtil.NEWLINE);//将内容放入ByteBuf,转储到StringBuilderByteBufUtil.appendPrettyHexDump(builder,buf);System.out.println(builder.toStri吴());}}Expansion向ByteBuf写入数据时,如果发现容量不足,就会触发扩容。具体扩容规则是假设ByteBuf的初始容量为10,如果写入后数据大小不超过512字节,则选择下一个16的整数倍作为存储容量。比如写入数据后大小为12,扩容后容量为16,如果写入后数据大小超过512字节,则选择下一个2^n^。比如写入后的大小是512字节,那么扩容后的容量就是2^10^=1024。(因为2^9^=512,长度不够)扩容不能超过maxcapacity,否则会报错。Reader相关方法reader方法也针对不同的数据类型提供了不同的操作方法,readByte,读取一个字节readInt,读取一个int类型readFloat,读取一个float类型publicclassByteBufExample{publicstaticvoidmain(String[]args){ByteBufbuf=ByteBufAllocator.DEFAULT.heapBuffer();//自动扩容buf.writeBytes(newbyte[]{1,2,3,4});日志(缓冲器);System.out.println(buf.readByte());日志(缓冲器);}privatestaticvoidlog(ByteBufbuf){StringBuilderbuilder=newStringBuilder().append("读取索引:").append(buf.readerIndex()).append("写入索引:").append(buf.writerIndex()).append("容量:").append(buf.capacity()).append(StringUtil.NEWLINE);//将ByteBuf的内容转储到StringBuilder中,ByteBufUtil.appendPrettyHexDump(builder,buf);System.out.println(builder.toString());}}从下面的结果可以看出,读取一个字节后,这个字节就变成了丢弃的部分,再次读取时,只能读取数据中未读取的部分。读取索引:0写入索引:7容量:256+-----------------------------------------------+|0123456789abcdef|+--------+------------------------------------------------+-----------------+|00000000|01020304050607|.......|+--------+-----------------------------------------------+----------------+1读索引:1写索引:7容量:256+------------------------------------------------+|0123456789abcdef|+--------+------------------------------------------------+----------------+|00000000|020304050607|......|+--------+-----------------------------------------------+----------------+进程结束,退出代码为0此外,如果想要重复读取已经读取过的数据,这里有两个方法实现标记和重置f.writeBytes(新字节[]{1,2,3,4,5,6,7});日志(缓冲器);buf.markReaderIndex();//标记读取索引位置System.out.println(buf.readInt());日志(缓冲器);buf.resetReaderIndex();//重置为标志位System.out.println(buf.readInt());log(buf);}另外,如果不想改变读取指针位置来获取数据。ByteBuf一开始就提供了get方法。该方法基于索引位置读数并允许重复读数。ByteBuf的零拷贝机制需要说明一下。ByteBuf的零拷贝机制和我们之前的前提是一样的。操作系统层面的零拷贝则不同。操作系统层面的零拷贝是指当我们要向远程服务器发送文件时,需要先从内核空间拷贝到用户空间,再从用户空间拷贝到网卡缓冲区内核空间。发送,导致副本数量增加。ByteBuf中的零拷贝思想也是一样的,减少了数据的拷贝,提高了性能。如图3-2所示,假设有一个原始的ByteBuf,我们要对这个ByteBuf的两部分数据进行操作。按照正常的思路,我们会新建两个ByteBuf,然后将原来ByteBuf中的一些数据复制到两个新的ByteBuf中,但是这样会涉及到数据的复制,在并发量大的情况下,会影响性能。
图3-2ByteBuf提供了一个slice方法,可以在不复制数据的情况下对原始ByteBuf进行拆分。使用方法如下:publicstaticvoidmain(String[]args){ByteBufbuf=ByteBufAllocator.DEFAULT.buffer();//自动扩容buf.writeBytes(newbyte[]{1,2,3,4,5,6,7,8,9,10});日志(缓冲器);ByteBufbb1=buf.slice(0,5);ByteBufbb2=buf.slice(5,5);日志(bb1);日志(bb2);System.out.println("修改原始数据");buf.setByte(2,5);//修改原buf数据log(bb1);//再次打印bb1的结果,发现数据变了}上面代码中,将原buf逐片切片,每个Fragments为5字节。为了证明这个切片没有数据拷贝,我们修改原来buf的index2的值,然后打印第一个切片bb1,可以发现bb1的结果变了。说明两个分片指向的数据和原来的buf是一样的。Unpooled在前面的案例中,我们经常会用到Unpooled工具类,它和非pooledByteBuf的创建、组合、复制是一样的。假设有一个协议数据,它由一个header和一个messagebody组成,这两部分分别放在两个ByteBuf中。ByteBufheader=...ByteBufbody=...我们希望把header和body合并成一个ByteBuf,通常的做法是ByteBufallBuf=Unpooled.buffer(header.readableBytes()+body.readableBytes());allBuf。writeBytes(header);allBuf.writeBytes(body);在这个过程中,我们将header和body复制到新的allBuf中,这个过程无形中增加了两次数据复制操作。有没有更有效的方法来减少副本的数量来达到同样的目的?在Netty中,提供了一个CompositeByteBuf组件,它提供了这个功能。publicclassByteBufExample{publicstaticvoidmain(String[]args){ByteBufheader=ByteBufAllocator.DEFAULT.buffer();//自动扩容header.writeCharSequence("header",CharsetUtil.UTF_8);ByteBufbody=ByteBufAllocator.DEFAULT.buffer();body.writeCharSequence("body",CharsetUtil.UTF_8);CompositeByteBufcompositeByteBuf=Unpooled.compositeBuffer();//第一个参数为true,表示添加新的ByteBuf时,CompositeByteBuf的writeIndex自动递增。//默认为false,即writeIndex=0,所以我们不能从compositeByteBuf中读取数据。compositeByteBuf.addComponents(true,header,body);日志(复合字节缓冲区);}privatestaticvoidlog(ByteBufbuf){StringBuilderbuilder=newStringBuilder().append("读取索引:").append(buf.readerIndex()).append("写入索引:").append(buf.writerIndex()).append("容量:").append(buf.capacity()).append(StringUtil.NEWLINE);//将ByteBuf内容转储到StringBuilderByteBufUtil.appendPrettyHexDump(builder,buf);System.out.println(builder.toString());}}CompositeByteBuf之所以能够做到零拷贝,是因为在结合header和body的时候,并没有拷贝这两个数据,而是通过CompositeByteBuf构建了一个逻辑整体,里面还是包含了两个真实的对象,也就是一个指针指向同一个对象,所以这个类似于浅拷贝的实现。wrappedBuffer在Unpooled工具类中提供了一个wrappedBuffer方法来实现CompositeByteBuf的零拷贝功能。使用方法如下。publicstaticvoidmain(String[]args){ByteBufheader=ByteBufAllocator.DEFAULT.buffer();//自动扩容header.writeCharSequence("header",CharsetUtil.UTF_8);ByteBufbody=ByteBufAllocator.DEFAULT.buffer();body.writeCharSequence("body",CharsetUtil.UTF_8);ByteBufallBb=Unpooled.wrappedBuffer(header,body);日志(allBb);//对于零拷贝机制,修改原始ByteBuf中的值会影响allBb头.setCharSequence(0,"Newer0",CharsetUtil.UTF_8);日志(allBb);}copiedBuffercopiedBuffer和wrappedBuffer最大的区别是这个方法会实现数据的复制。下面的代码演示了copiedBuffer和wrappedbuffer的区别,可以看到在case标记的位置,修改了原来ByteBuf的值,allBb不受影响。publicstaticvoidmain(String[]args){ByteBufheader=ByteBufAllocator.DEFAULT.buffer();//自动扩容header.writeCharSequence("header",CharsetUtil.UTF_8);ByteBufbody=ByteBufAllocator.DEFAULT.buffer();body.writeCharSequence("body",CharsetUtil.UTF_8);ByteBufallBb=Unpooled.copiedBuffer(header,body);日志(allBb);header.setCharSequence(0,"Newer0",CharsetUtil.UTF_8);//caselog(allBb);}内存释放是针对不同的ByteBuf创建的,内存释放的方法不同。UnpooledHeapByteBuf,使用JVM内存,只需要等待GC回收UnpooledDirectByteBuf,使用外存,需要特殊的方法回收内存PooledByteBuf之类使用池化机制,需要更复杂的规则来回收内存如果ByteBuf使用堆External内存创建,然后尝试手动释放内存,如何释放呢?Netty使用引用计数的方式来控制内存回收,每个ByteBuf都实现了ReferenceCounted接口。每个ByteBuf对象的初始计数为1。当调用release方法时,计数器减一。如果计数器为0,当ByteBuf被回收并调用retain方法时,计数器加一,表示其他处理程序在调用者用完之前立即调用释放。不会造成回收。当计数器为0时,底层内存将被回收。此时即使ByteBuf对象还存在,它的方法也不能正常使用。版权声明:本博客所有文章除另有约定外均采用CCBY-NC-SA4.0。转载请注明来自Mic带你学建筑!如果本文对您有帮助,请给个关注和点赞。您的坚持是我不断创作的动力。欢迎关注同名微信公众号获取更多技术干货!