Netty的数据处理API通过两个组件暴露ByteBuf、ByteBufHolder。ByteBuf有如下优点:
- 它可以被用户自定义的缓冲区类型扩展
- 通过内置的复合缓冲区类型实现了透明的零拷贝(不是很理解)
- 容量可以按需增长
- 在读和写这两种模式之间切换不需要调用flip()方法
- 读和写使用了不同的索引
- 支持方法的链式调用
- 支持引用计数(不是很理解)
- 支持池化(不是很理解)
使用模式
堆缓冲区
最常用的ByteBuf模式是将数据存储在JVM的堆空间中。这种模式被称为支撑数据,它能在没有使用池化的情况下提供快速的分配和释放。这种方式如下所示,非常适合有遗留的数据需要处理的情况(有遗留数据需要处理,是怎样的一个场景):
|
|
(对ByteBuf的这种处理方式才是正确的,我在学习尚硅谷的课程时,课程中直接拿到array,然后调用toString方法,最后得到的数据会存在乱码)
当hasArray()方法返回false时,尝试访问支撑数组将触发一个UnsupporterOperationException。
直接缓冲区
直接缓冲区的内容驻留在常规的会被垃圾回收的堆之外,这解释了为什么直接缓冲区对于网络数据传输是理想的选择。如果数据包含在一个在堆上分配的缓冲区中,那么事实上,在通过套接字发送数据之前,JVM将会在内部把缓冲区复制到一个直接缓冲区中。
直接缓冲区的主要缺点是,相对于基于堆的缓冲区,它们的分配和释放都较为昂贵。如果在处理遗留代码,可能因为数据不是在堆上,所以不得不再进行一次复制。
和支撑数组相比,这涉及的工作更多。因此,如果事先知道容器中的数据将会被作为数组来访问,可能更愿意使用堆内存(不是很理解这个说法)。
|
|
符合缓冲区
复合缓冲区为多个ByteBuf提供一个聚合视图,Netty通过CompositeByteBuf实现这个模式,这个类提供了一个将多个缓冲区表示为单个合并缓冲区的虚拟表示。
CompositeByteBuf中的ByteBuf实例可能同时包含直接内存和非直接内存,如果其中只有一个实例,那么对CompositeByteBuf上的hasArray()方法调用将返回该组件上的hasArray()方法的值,否则它将返回false。
这个技术的使用场景如下:一个HTTP协议消息由头部和主体组成,这两部分由应用程序的不同模块产生,将会在消息被发送时组装。应用程序可以选择为多个消息重用相同的消息主体。当这种情况发生时,对于每个消息都将会创建一个新的头部。
|
|
CompositeByteBuf可能不支撑访问其支撑数据,因此访问CompositeByteBuf中的数据类似于访问直接缓冲区的模式:
|
|
Netty使用了CompositeByteBuf来优化套接字的IO操作,尽可能地消除了由JDK的缓冲区实现所导致的性能及内存使用率的惩罚。这种优化发生在Netty的核心代码中,因此不会被暴露出来。(不是很理解在讲什么)
查找操作
最简单的是使用indexOf()进行查找,较为复杂是通过ByteProcessor作为参数的方法搜索,这个接口只定义了一个方法:
|
|
使用ByteProcessor的简单案例:
|
|
派生缓冲区
派生缓冲区为ByteBuf提供了以专门的方式来呈现内容的视图,这类视图是通过如下方法创建的:
- duplicate()
- slice()
- slice(int, int)
- Unpooled.unmodifiableBuffer()
- order(ByteOrder)
- readSlice()
这些方法都将返回一个新的ByteBuf实例,它们具有自己的读、写、标记索引。其内部存储是共享的,这使得派生缓冲区的创建成本很低廉,同时也意味着,如果你修改了它的内容,也同时修改了其对应的源实例。
copy()和copy(int, int)方法可以实现缓冲区的复制。这两个调用返回的ByteBuf拥有独立的数据副本。
order是用于指定使用哪种字节序读取数据。
duplicate和slice的区别
如下代码,最终输出结果为2,0:
|
|
slice和duplicate的区别就在于此,duplicate会将readerIndex和writeIndex都duplicate下来,而slice不会,slice会新建一条readerIndex和writeIndex,具体是如何实现的,我暂时还不想花费精力去研究。
ByteBufHolder
书中简单的提了一下ByteBufHolder,我暂时不研究,等未来有需要再研究。
ByteBuf分配
为了降低分配和释放内存的开销,Netty通过ByteBufAllocator实现了ByteBuf的池化,它可以用来分配任意类型的ByteBuf实例。使用池化是特定于应用程序的决定,并不会以任何方式改变ByteBuf API的含义。
|
|
可以通过Channel(每个都可以用一个不同的ByteBufAllocator实例)或者绑定到ChannelHandler的ChannelHandlerContext获取一个到ByteBufAllocator的引用。
|
|
Netty提供了两种ByteBufAllocator的实现:PooledByteBufAllocator和UnpooledByteBufAllocator。前者池化了ByteBuf的实例以提高性能并最大限度地减少内存碎片。此实现使用了一种成为jemalloc的已被大量现代操作系统所采用的高效方法来分配内存。后者的实现不池化ByteBuf实例,并且每次它被调用时都会返回一个新的实例。
Netty默认使用了PooledByteBufAllocator,但是可以很容易地通过ChannelConfig或者引导程序时指定一个不同的分配器来更改。
Unpooled
这个好东西我已经在使用了。
引用技术
(我觉得Java可能不需要这个技术,但是池化需要这个技术)(好尴尬,刚理解到这一层就发现书中也是怎么说的)
Netty为ByteBuf和ByteBufHolder引入了引用技术技术,它们都实现了ReferenceCounted。
|
|
其他知识点
-
可以调用readerIndex(index)、writeIndex(index)来手动设置readerIndex和writeIndex。
-
可以调用discardReadBytes()丢弃已经读取过的字节,从而回收空间。
-
markReaderIndex()、markWriterIndex()、resetWriterIndex()、resetReaderIndex()可以用来标记和重置ByteBuf的readerIndex和writerIndex(我实际上不知道到这些技术的应用场景)。
-
可以调用clear()方法来将readerIndex和writerIndex都设置为0。
-
往ByteBuf中写入数据时,首先确保目标ByteBuf具有足够的可写入空间来容纳当前要写入的数据,如果没有,则将检查当前的写索引以及最大容量是否可以在扩展后容纳该数据,可以则分配并调整容量,否则就会抛出异常(如何体现在代码中)。
-
ByteBufUtils中的hexdump()以十六进制的表达形式打印ByteBuf的内容;equals(ByteBuf, ByteBuf)用于判断两个ByteBuf实例的相等性。
验证的问题
如果尝试在缓冲区的可读字数已经耗尽时从中读取数据,那么将引发一个IndexOutOfBoundException。那么source.readBytes(ByteBuf dest);
会报错么?
最后验证的结果为会报错,readBytes会尽量去将dest的空间塞满,如果此时source的数据不够,则直接抛出异常。