ByteBuf 的工作原理
- 可以自定义缓冲类型
- 通过一个内置的复合缓冲类型实现零拷贝
- 扩展性好,比如 StringBuilder
- 不需要调用 flip() 来切换读/写模式
- 读取和写入索引分开
- 方法链
- 引用计数
- Pooling(池)
ByteBuf中有两个索引:一个用来读,一个用来写
写入数据到 ByteBuf 后,writerIndex(写入索引)增加写入的字节数。读取字节后,readerIndex(读取索引)也增加读取出的字节数。你可以读取字节,直到写入索引和读取索引处在相同的位置。此时ByteBuf不可读,所以下一次读操作将会抛出 IndexOutOfBoundsException,就像读取数组时越位一样。
调用 ByteBuf 的以 “read” 或 “write” 开头的任何方法都将自动增加相应的索引。另一方面,”set” 、 “get”操作字节将不会移动索引位置,它们只会在指定的相对位置上操作字节。
可以给ByteBuf指定一个最大容量值,这个值限制着ByteBuf的容量。任何尝试将写入超过这个值的数据的行为都将导致抛出异常。ByteBuf 的默认最大容量限制是 Integer.MAX_VALUE。
ByteBuf 类似于一个字节数组,最大的区别是读和写的索引可以用来控制对缓冲区数据的访问。下图显示了一个容量为16的空的 ByteBuf 的布局和状态,writerIndex 和 readerIndex 都在索引位置 0 :
ByteBuf 使用模式
HEAP BUFFER(堆缓冲区)
最常用的模式是 ByteBuf 将数据存储在 JVM 的堆空间,这是通过将数据存储在数组的实现。堆缓冲区可以快速分配,当不使用时也可以快速释放。它还提供了直接访问数组的方法,通过 ByteBuf.array() 来获取 byte[]数据。
1 | ByteBuf heapBuf = ...; |
1.检查 ByteBuf 是否有支持数组。
2.如果有的话,得到引用数组。
3.计算第一字节的偏移量。
4.获取可读的字节数。
5.使用数组,偏移量和长度作为调用方法的参数。
访问非堆缓冲区 ByteBuf 的数组会导致UnsupportedOperationException, 可以使用 ByteBuf.hasArray()来检查是否支持访问数组。
DIRECT BUFFER(直接缓冲区)
“直接缓冲区”是另一个 ByteBuf 模式。对象的所有内存分配发生在 堆,对不对?好吧,并非总是如此。在 JDK1.4 中被引入 NIO 的ByteBuffer 类允许 JVM 通过本地方法调用分配内存,其目的是
- 通过免去中间交换的内存拷贝, 提升IO处理速度; 直接缓冲区的内容可以驻留在垃圾回收扫描的堆区以外。
- DirectBuffer 在 -XX:MaxDirectMemorySize=xxM大小限制下, 使用 Heap 之外的内存, GC对此”无能为力”,也就意味着规避了在高负载下频繁的GC过程对应用线程的中断影响.
这就解释了为什么“直接缓冲区”对于那些通过 socket 实现数据传输的应用来说,是一种非常理想的方式。如果你的数据是存放在堆中分配的缓冲区,那么实际上,在通过 socket 发送数据之前,JVM 需要将先数据复制到直接缓冲区。
但是直接缓冲区的缺点是在内存空间的分配和释放上比堆缓冲区更复杂,另外一个缺点是如果要将数据传递给遗留代码处理,因为数据不是在堆上,你可能不得不作出一个副本,如下:
1 | ByteBuf directBuf = ... |
1.检查 ByteBuf 是不是由数组支持。如果不是,这是一个直接缓冲区。
2.获取可读的字节数
3.分配一个新的数组来保存字节
4.字节复制到数组
5.将数组,偏移量和长度作为参数调用某些处理方法
显然,这比使用数组要多做一些工作。因此,如果你事前就知道容器里的数据将作为一个数组被访问,你可能更愿意使用堆内存。
COMPOSITE BUFFER(复合缓冲区)
最后一种模式是复合缓冲区,我们可以创建多个不同的 ByteBuf,然后提供一个这些 ByteBuf 组合的视图。复合缓冲区就像一个列表,我们可以动态的添加和删除其中的 ByteBuf,JDK 的 ByteBuffer 没有这样的功能。
Netty 提供了 ByteBuf 的子类 CompositeByteBuf 类来处理复合缓冲区,CompositeByteBuf 只是一个视图。
警告:
CompositeByteBuf.hasArray() 总是返回 false,因为它可能既包含堆缓冲区,也包含直接缓冲区
- JDK实现 ByteBuffer
1 | // 使用数组保存消息的各个部分 |
这种做法显然是低效的;分配和复制操作不是最优的方法,操纵数组使代码显得很笨拙。
- Netty实现 CompositeByteBuf
1 | CompositeByteBuf messageBuf = ...; |
1.追加 ByteBuf 实例的 CompositeByteBuf
2.删除 索引1的 ByteBuf
3.遍历所有 ByteBuf 实例。
Netty字节级别的操作
索引
ByteBuf 使用zero-based 的 indexing(从0开始的索引),第一个字节的索引是 0,最后一个字节的索引是 ByteBuf 的 capacity - 1,下面代码是遍历 ByteBuf 的所有字节:
1 | ByteBuf buffer = ...; |
注意:
通过索引访问时不会推进 readerIndex (读索引)和 writerIndex(写索引),我们可以通过 ByteBuf 的 readerIndex(index) 或 writerIndex(index) 来分别推进读索引或写索引
ByteBuf 一定符合:0 <= readerIndex <= writerIndex <= capacity。
1.字节,可以被丢弃,因为它们已经被读
2.还没有被读的字节是:“readable bytes(可读字节)”
3.空间可加入多个字节的是:“writeable bytes(写字节)”
读/写操作
读/写操作主要由2类:
- gget()/set() 操作从给定的索引开始,保持不变
- read()/write() 操作从给定的索引开始,与字节访问的数量来适用,递增当前的写索引或读索引
1 | Charset utf8 = Charset.forName("UTF-8"); |
1.创建一个新的 ByteBuf 给指定 String 保存字节
2.打印的第一个字符,N
3.存储当前 readerIndex 和 writerIndex
4.更新索引 0 的字符B
5.打印出的第一个字符,现在B
6.这些断言成功,因为这些操作永远不会改变索引