ByteBuffer是Java NIO库中提供的基本类. 对于NIO编程是必不可少的类. 但是, 如果使用ByteBuffer不得不面对以下缺点:
- 空间定长, ByteBuffer通过静态方法ByteBuffer.allocate()或ByteBuffer.allocateDirect()创建, Buffer的尺寸是调用这两个方法的时候就确定了的.
- ByteBuffer只有一个指针. 如果用一个ByteBuffer存储数据, 再传给另一个方法读取, 那么你需要将ByteBuffer的指针调整到要读的数据的起始位置再读. 在这一点上通常容易引起混乱. 加大了编码的复杂度.
Netty的初衷是为Java NIO编程而提供的便捷的解决方案. 但它自然还是要在基础API上进行构建, 这自然还是要使用到ByteBuffer. 在如何使用ByteBuffer这一点上还是有些不错的设计和技巧的.
ByteBuffer示例
1 |
ByteBuffer allocate = ByteBuffer.allcate(4); |
这里可以看到ByteBuffer的单指针的问题, 如果忘记进行flip, 那后面读取整数就会引发越界了. 在边读边写的情况下, 来回调整指针又容易引发一些奇怪的问题.
##ByteBuf API
ByteBuf针对以上一些问题和Netty自身需求, 对ByteBuffer进行了许多改进.
- 支持基本数据类型
同ByteBuffer一样, 实现了基本数据类型的读取, 同时还能支持了读取unsigned数据类型. - 读写操作
read/write系列方法
ByteBuffer采用双指针的方式, 简化了读取. 同readIndex指向读取的起点, writeIndex指向写操作的起点. readIndex不会超过writeIndex. readIndex之前的部分会被变成discard的, 可以用discardReadBytes方法释放这部分空间. 类似于ByteBuffer的compact操作. 这样, ByteBuf的读写操作就不会互相影响了. 极大地简化了使用. -
扩展
通常ByteBuffer的动态扩展是在写入前检查剩余空间, 如果不够, 则重新申请空间, 并复制原内容到新的Buffer, 再进行写入.
ByteBuf的write方法提供了容量检查和扩展, 无需担心溢出问题.
注:扩展操作的具体实现是AbstractByteBuf.ensureWritable0(int).1
2
3
4
5
6
7
8
9// AbstractByteBuf
public ByteBuf (byte[] src, int srcIndex, int length) {
ensureAccessible();
ensureWritable(length);
setBytes(writerIndex, src, srcIndex, length);
writerIndex += length;
return this;
} -
discard操作
ByteBufzhdiscard空间应该尽量利用, 但是不应该经常使用discardReadBytes()方法, 因为discardReadBytes会发生内存复制. - clear操作
不会清空数据, 只是将操作索引进行初始化. - mark&reset
- markReaderIndex: 记录当前读位置
- resetReadIndex: 回到记录的读位置
- markWriteIndex: 记录当前的写位置
- resteWriteIndex: 回到记录的写位置
- 搜索
- indexOf(Int, Int, Byte)
- bytesBefore(Byte)
- bytesBefore(Int, Byte)
bytesBefore(Int, Int ,Byte)
- 遍历
通过forEach系列函数对ByteBuf内容进行遍历.
ByteBufProcessor定义了一些常用的操作- _ByteBufProcessor.FIND_NUL_ (0x00)
- _ByteBufProcessor.FIND_CR_ (‘r’)
- _ByteBufProcessor.FIND_LF_ (‘n’)
- _ByteBufProcessor.FIND_CRLF_ (‘rn’)
- _ByteBufProcessor.FIND_WHITESPACE_ (‘r’)
-
复制
共享内存:- duplicate() 复制对象, 不复制索引
- slice() 当前可用数据部分 (ReadIndex, WriteIndex)
-
slice(Int, Int) 指定的内存段
不共享内存
- copy() 复制数据
- copy(Int, Index) 复制指定区域数据
- 转换为原生Buffer
nioBuffer系列方法. - 指定位置读写
get/set系列方法, 同样会对读写合法性进行检验. 由继承类实现.
ByteBuf功能分析
功能接口分类
- 堆分配
直接分配到Java虚拟机的堆. - 直接内存
非堆内存, 借助Java NIO API分配. 分配回收速度慢一些, 但在NIO操作中可以减少一次读写的复制操作, 速度比堆内存快.
Tips: 建议I/O通信使用DirectByteBuf, 后端业务消息编解码视同HeapByteBuf.《Netty权威指南》
可重用能力分类:
- 池化的ByteBuf // Pooled
- 引用计数的ByteBuf. // Unpooled
AbstractByteBuf
继承并实现了ByteBuf的方法, 但在其实现中定义了更多操作细化的’_’前缀的abstract方法.
主要实现了在读写上的一些通用操作:
- ensureAccessible()
权限检查? - ensureWritable(Int)-> ensureWritable0(Int)
写操作时调用, 确认可以写入. ensureWritable0(int)定义了扩容步骤操作. - checkIndex(Int,Int)
- discardSomeReadBytes()
- adjustMarkers(Int)
AbstractReferenceCountedByteBuf
实现了ReferenceCounted接口的内容. 以此分化出Unpooled系列的ByteBuf.
PooledByteBuf
实现了对象池化, 以此分化出Pooled系列ByteBuf.
内存空间
内存分配
非池化ByteBuf都采用了Allocator分配器对内存进行分配.
1 |
private final ByteBufAllocator alloc; |
对于扩展行为的操作则由Allocator来决定.
申请的内存位置, 可以分为直接内存和堆内存两类.
堆内存
Heap ByteBuf: 通过在堆内申请字节数组获得内存空间
直接内存
Direct ByteBuf: 通过Java NIO库中的DirectByteBuffer申请直接内存空间.
内存操作
Java操作(Heap)
直接通过Java语法层面进行数据操作
Unsafe操作(Heap/Direct)
通过Unsafe提供的API进行底层化的数据操作.
- Heap操作Unsafe直接提供了API.
- Direct操作需要获取到DirectByteBuffer的内存地址才能进行操作.
Unsafe操作实现更底层化, 相较Java语法层面实现效率更高.
Direct操作(Direct)
通过DirectByteBuffer的API对数据进行操作
内存扩展
对于UnpooledByteBuf, 都是通过以下步骤:
- 计算扩展后容量
- 申请相应内存
- 复制数据到新内存
- 重设ByteBuf的内存区
这里和dicard内存区一样发生了内存复制, 会对性能有所损耗. 所以, 扩容也是应该尽量避免的.
近期评论