原生的ByteBuffer
存在哪些问题呢,Netty
为什么会设计ByteBuf
呢,它的工作原理是什么…..
ByteBuffer存在的问题 ByteBuffer
是JDK1.4
中提供的java.nio.Buffer
, 在内存中预留指定大小的存储空间来存放临时数据,其他Buffer
的子类有:CharBuffer
、DoubleBuffer
、FloatBuffer
、IntBuffer
、LongBuffer
和 ShortBuffer
ByteBuffer
的长度是固定的,一旦分配完成,容量就无法动态扩容收缩,分多了会浪费内存,分少了存放大的数据时会索引越界(当传输数据大于初始化长度
时,会出现BufferOverflowException
索引越界的异常),所以使用ByteBuffer时
,为了解决这个问题,我们一般每次put
操作时,都会对可用空间进行校检,如果剩余空间不足,需要重新创建一个新的ByteBuffer
,然后将旧的ByteBuffer
复制到新的ByteBuffer
中去
ByteBuffer
中只有通过position
获得当前可操作的位置,调用get()
方法,返回ByteBuffer[postion]
处的值,如果是调用put方法,将数据放入ByteBuffer[postion]
的位置
API
功能有限,部分高级功能并不支持,需开发者自己实现,且使用原生ByteBuffer
较为困难(不适合小白专业户)
ByteBuf与ByteBuffer的区别
不同ByteBuf
实现原理各不相同,我们先看最基本的ByteBuf
与原生的ByteBuffer
的区别
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 ByteBuf buf = Unpooled.buffer(10 ); buf.writeBytes("鏖战八方QQ群391619659" .getBytes()); System.out.println("Netty:" + buf); byte [] by = new byte [buf.readableBytes()];buf.readBytes(by); System.out.println("Netty:" + new String(by)); System.out.println("//////////////////////////////////////////无耻的分割线//////////////////////////////////////////" ); ByteBuffer bf1 = ByteBuffer.allocate(100 ); bf1.put("鏖战八方QQ群391619659" .getBytes()); System.out.println("JDK:" +bf1); System.out.println("当前指针:" + bf1.position()); byte [] by1 = new byte [bf1.remaining()];System.out.println(by1.length); bf1.get(by1); System.out.println("未使用flip:" +new String(by1)); System.out.println("//////////////////////////////////////////无耻的分割线//////////////////////////////////////////" ); ByteBuffer bf2 = ByteBuffer.allocate(100 ); bf2.put("鏖战八方QQ群391619659" .getBytes()); System.out.println("JDK:" +bf2); System.out.println("当前指针:" + bf2.position()); bf2.flip(); byte [] by2 = new byte [bf2.remaining()];System.out.println(by2.length); bf2.get(by2); System.out.println("使用flip:" +new String(by2));
指针区别
从日志输出中可以看到,使用JDK自带
的特别的麻烦,远远没有ByteBuf
来的方便,无需关心读写切换指针的问题,在JDK
中,由于只有一个一个position
指针,我们需要通过flip()
进行转换控制,而Netty
却可以很好的帮我们做到扩容,它的内部维护了readerIndex
与writerIndex
两个指针,一开始都是0
,随着数据的写入writerIndex
会增加但不会超过readerIndex
,当我们读取后内部会通过调用discardReadBytes
来释放这部分空间,类似ByteBuffer
的compact
方法,readerIndex
与writerIndex
都是可读取的,等同ByteBuffer
中position -> limit
之间的数据,WriterIndex
和capacity
之间空间是可写的,等同ByteBuffer
中limit -> capacity
前面说到过,JDK
自带的ByteBuffer
无法做到自动扩容,当内容超出的时候会抛出索引越界的异常
,接下来看一段代码
1 2 3 4 5 6 7 ByteBuf buf = Unpooled.buffer(10 ); buf.writeBytes("鏖战八方QQ群391619659" .getBytes()); System.out.println(buf); System.out.println("//////////////////////////////////////////无耻的分割线//////////////////////////////////////////" ); ByteBuffer buffer = ByteBuffer.allocate(10 ); buffer.put("鏖战八方QQ群391619659" .getBytes()); System.out.println(buffer);
问题: 为什么Netty
的ByteBuf
没有报错,capacity
为什么会自动扩容呢?扩容的大小是怎么计算的?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Override public ByteBuf writeBytes (byte [] src) { writeBytes(src, 0 , src.length); return this ; } @Override public ByteBuf writeBytes (byte [] src, int srcIndex, int length) { ensureAccessible(); ensureWritable(length); setBytes(writerIndex, src, srcIndex, length); writerIndex += length; return this ; } @Override public ByteBuf ensureWritable (int minWritableBytes) { if (minWritableBytes < 0 ) { throw new IllegalArgumentException(String.format( "minWritableBytes: %d (expected: >= 0)" , minWritableBytes)); } if (minWritableBytes <= writableBytes()) { return this ; } if (minWritableBytes > maxCapacity - writerIndex) { throw new IndexOutOfBoundsException(String.format( "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s" , writerIndex, minWritableBytes, maxCapacity, this )); } int newCapacity = calculateNewCapacity(writerIndex + minWritableBytes); capacity(newCapacity); return this ; }
扩容分析
调用writeBytes
方法后,ByteBuf
会修改writerIndex
的指针大小,然后判断最小写入字节
是否大于0
,如果都没内容这不是在玩它么(*^▽^*)
接下来是最小写入字节
小于或者等于剩余容量
,那就返回当前的ByteBuf,也就不会扩容了,因为能装得下
接下来继续判断最小写入字节
是否大于当前ByteBuf最大容量 - 已使用容量
,如果最大容量
都装不下说明这已经没法继续玩了,只能装这么多,扩容不了
最后就是扩容处理
了,Netty
的做法是默认64字节
,小于阀值取64
,大于取64 <<= 1
,成倍递增
相比其它的JAVA对象,缓冲区的分配(包括动态扩容)与释放是一个耗时操作,因此需要尽可能的复用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Override public ByteBuf discardReadBytes () { ensureAccessible(); if (readerIndex == 0 ) { return this ; } if (readerIndex != writerIndex) { setBytes(0 , this , readerIndex, writerIndex - readerIndex); writerIndex -= readerIndex; adjustMarkers(readerIndex); readerIndex = 0 ; } else { adjustMarkers(readerIndex); writerIndex = readerIndex = 0 ; } return this ; }
1 2 3 4 5 6 public final Buffer clear () { position = 0 ; limit = capacity; mark = -1 ; return this ; }
回收区别
在Netty
中,ByteBuf
可以通过调用discardReadBytes
进行回收,需要注意 的是,字节数据会发生内存复制,所以频繁调用会导致性能不升反而下降,因此使用之前需要确保是否需要这样处理(性能换运能自我取舍)
,而ByteBuffer
的回收就比较简单,直接将指针初始化,ByteBuffer
的读写都为0
ByteBuf API
查询操作
1 2 3 4 5 6 indexOf(int fromIndex, int toIndex, byte value):从当前ByteBuf中定位首次出现value的位置,起始索引为fromIndex,终点是toIndex,如果未检索到返回-1 否则返回结果的索引位置 bytesBefore(byte value):从当前ByteBuf中定位首次出现的位置,默认readerIndex开始到writerIndex结束 bytesBefore(int length, byte value):默认readerIndex开始到readerIndex+length结束 bytesBefore(int index, int length, byte value):从指定index开始,到readerIndex+length结束 forEachByte(ByteBufProcessor processor):遍历当前ByteBuf可读字节数组,与ByteBufProcessor设置的条件做对比,满足返回索引位置,否则返回-1 forEachByteDesc(ByteBufProcessor processor):倒序迭代,与ByteBufProcessor设置的条件做对比,满足返回索引位置,否则返回-1
孵生ByteBuf
的几种姿势
1 2 3 4 duplicate():返回当前ByteBuf复制的对象,复制后共享缓冲区内容,各自独立读写索引,但是当复制的ByteBuf内容发生改变时,原ByteBuf也会随之改变 copy():复制一个新的ByteBuf对象,完全独立,不存在共享 slice():返回当前ByteBuf的可读子缓冲区,默认readerIndex到writerIndex,内容共享,独立读写索引,但操作并不会影响到原ByteBuf的readerIndex和writerIndex
ByteBuf
转java.nio.ByteBuffer
1 2 nioBuffer():将当前ByteBuf可读的缓冲区转换成ByteBuffer,二者共享一个缓冲区内容引用,但是对ByteBuffer读写操作不会修改原ByteBuf的读写索引,同时ByteBuffer也无法感知ByteBuf的动态扩容 nioBuffer(int index, int length):从ByteBuf指定位置开始长度的缓冲区转换成ByteBuffer
- 说点什么 全文代码:https://gitee.com/battcn/battcn-netty/tree/master/Chapter10-1/battcn-netty-10-1-1
个人QQ:1837307557
battcn开源群(适合新手):391619659
微信公众号:battcn
(欢迎调戏)