概述
网络数据的基本单位总是字节。Java NIO 提供了ByteBuffer 作为它的字节容器,但是这个类使用起来过于复杂,而且也有些繁琐。
Netty 的ByteBuffer 替代品是ByteBuf,一个强大的实现,既解决了JDK API 的局限性,又为网络应用程序的开发者提供了更好的API。
优点
读写指针分离,不需要像 ByteBuffer 一样切换读写模式;
很多地方体现零拷贝理念,例如 slice、duplicate、CompositeByteBuf,减少了数据拷贝过程,提升了性能
支持方法的链式调用;
支持引用计数;
支持池化;
自动扩容;
原理
NIO的ByteBuffer对象只有一个指针,进行读写时,需要切换模式,而ByteBuf设计上就采用了两个索引指针,即读指针与写指针,提高了灵活性和便捷性。
池化/非池化
默认情况下,采用的是池化方式来提升性能。
如需修改该设置,需通过参数来控制
-Dio.netty.allocator.type={unpooled|pooled}
注:非池化,也可以直接使用工具类Unpooled的静态方法来创建缓冲区对象。
创建对象
ByteBuf是一个数据容器,底层实现有两种方式,一是在jvm的堆上分配内存,称之为堆内存;另外一种是通过调用本地方法分配服务器的物理内存,称之为直接内存。
创建对象可以通过ByteBufAllocator类的不同方法来指定以什么样的方式来分配内存。
//以下两句等同,buffer方法内部实际调用了heapBuffer方法,都是分配堆内存
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer()
ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer()
//分配直接内存
ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer();
堆内存模式,底层实际是由数组来实现,接受GC的管理。
当需要进行网络传输时,使用直接内存模式,将会少一次从堆空间拷贝到发送缓冲区的过程,因此读写性能更高。但直接内存的创建和销毁的代价昂贵,因此通常采用池化策略来提升性能。
直接内存不受 JVM 垃圾回收的管理,因此要注意及时主动释放。
ByteBuf对象内置了自动扩容功能,无需人工干预。
读取操作
有两组API,一组是读取数据,同时将读指针后移,方法命名采用 read+类型 模式
另外一组是只读取数据,读指针不变,方法命名采用 get+类型 模式
写入操作
写入操作与读取操作类似,也是两组API,一组写入数据的同时,写指针后移,实际是在末尾追加数据,另外一组只写数据,写指针不变,实际是更新现有的数据。
销毁对象
前面说了,内存分配有堆内内存和直接内存两种模式,并且还使用了池化机制,内存的回收机制相对复杂一些。
Netty 采用了引用计数法来控制回收内存,每个 ByteBuf 都实现了 ReferenceCounted 接口,规则如下:
- 每个 ByteBuf 对象的初始计数为 1
- 调用 release 方法计数减 1
- 调用 retain 方法计数加 1
- 当计数为 0 时,底层内存会被回收
//直接调用对象的方法
buffer.retain();
buffer.release();
//通过工具类来调用
ReferenceCountUtil.retain(buffer);
ReferenceCountUtil.release(buffer);
辅助功能
缓冲区工具类ByteBufUtil
方法prettyHexDump可以友好地显示缓冲区内容,对于开发调试非常有用
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 64 |abcd |
+--------+-------------------------------------------------+----------------+
缓冲区容器接口ByteBufHolder
这是一个缓冲区对象的容器,实际是一个接口,在实际应用中,不仅需要缓冲区来保存对象,还需要一些额外的属性,例如HTTP 响应,除了表示为字节的内容,还包括状态码、cookie 等,这时候就可以自己实现一个类,实现ByteBufHolder接口,扩展属性来使用。
以下是netty自己实现的类,处理http请求的
/**
* Default implementation of {@link FullHttpRequest}.
*/
public class DefaultFullHttpRequest extends DefaultHttpRequest implements FullHttpRequest {
private final ByteBuf content;
private final HttpHeaders trailingHeader;
非池化缓冲区工具Unpooled
提供了静态的辅助方法来创建未池化的ByteBuf实例,使得ByteBuf 同样可用于那些并不需要Netty 的其他组件的非网络项目,使得其能得益于高性能的可扩展的缓冲区API。
不常用操作
清空对象
clear方法,注意,这里的清空,只是重置读写指针的位置,数据并没有擦除,但是对于使用者来说,相当于一个空白的全新缓冲区了。
拷贝操作
主要是两类,浅拷贝duplicate,共用存储,读写指针独立;深度拷贝copy,拷贝数据,完全独立的新对象。
重复读写
某些情况下,希望重复读或者写数据,则可以调用一个mark方法,标记读/写指针位置,然后读/写数据后,再调用reset方法重置读写指针位置。
buffer.markReaderIndex();
buffer.readByte();
buffer.readDouble();
buffer.resetReaderIndex();
切片操作
从一个ByteBuf对象中,截取部分形成一个新对象,新对象的读写指针是独立的,但与原对象共用底层存储。
//无参代表截取读写指针之间的数据
buffer.slice();
//参数分别为起始位置与长度
buffer.slice(0,2);
组合对象
ByteBuf类还有一个子类,名字叫CompositeByteBuf,其目的是实现多个ByteBuf对象的聚合视图,即可以向这个CompositeByteBuf对象中添加多个ByteBuf的对象,既可以是堆内存模式,也可以是直接内存模式,还可以是混合模式。实际的使用场景较少,如发送Http请求时,如多个请求的body部分相同,header不同,则可以采用组合模式,公用body,减少数据创建和拷贝来提升性能。
//创建组合缓冲区
CompositeByteBuf compositeByteBuf=ByteBufAllocator.DEFAULT.compositeBuffer();
//添加对象1
compositeByteBuf.addComponent(buffer1);
//添加对象2
compositeByteBuf.addComponent(buffer2);
//移除索引0处的对象
compositeByteBuf.removeComponent(0);
抛弃已读数据
使用discardReadBytes方法,可以将已读数据抛弃,即将未读数据拷贝到索引0的位置,同时移动读指针到开头,写指针到结尾。
注意,这个过程会发生数据拷贝,而不是零复制。