本文为稀土技能社区首发签约文章,30天内制止转载,30天后未获授权制止转载,侵权必究!
零仿制我相信各位小伙伴都听过它的大名,在 Kafka、RocketMQ 等知名的产品中都有运用到它,它首要用于进步 I/O 功能,Netty 是一个把功能作为生命的产品,怎样可能不会去完结它呢?所以这篇文章咱们就来聊聊 Netty 的零仿制。
数据仿制基础
各位小伙伴应该都写过读写文件的应用程序吧?咱们一般都是从磁盘读取文件,然后加工数据,最终写入数据库或发送给其他子系统。
那傍边具体的流程是怎样样的?
- 应用程序建议
read()
调用,由用户态进入内核态。 - CPU 向磁盘建议 I/O 读取恳求。
- 磁盘将数据写入到磁盘缓冲区后,向 CPU 建议 I/O 中止,陈述 CPU 数据已经预备好了。
- CPU 将数据从磁盘缓冲区仿制至内核缓冲区,然后从内核缓冲区将数据仿制至用户缓冲区
- 完结后,
read()
回来,由内核态切换到用户态。
如下:
这个进程有一个比较严重的问题便是 CPU 全程参加数据仿制的进程,并且整个进程 CPU 都不能干其他活,这不是浪费资源,耽误事吗!
怎样解决?引进 DMA 技能
,即直接存储器访问(Direct Memory Access) ,那什么是 DMA
呢?
DMA传输:将数据从一个地址空间仿制到另一个地址空间,供给在外设和存储器之间或许存储器和存储器之间的高速数据传输。
咱们都知道 CPU 是很稀缺的资源,需求力保它时间都在处理重要的工作,一些不重要的工作(比如数据仿制和存储)就不需求 CPU 参加了,让他去处理愈加重要的工作,这样是不是就能够更好地运用 CPU 资源呢?
所以,关于咱们读取文件(尤其是大文件)这种不那么重要且繁琐的工作是能够不需求 CPU 参加了,咱们只需求在两个设备之间建立一种通道,直接由设备 A 经过 DMA
仿制数据到设备 B,如下图:
加入 DMA 后,数据传输进程就变成下图:
CPU 接纳 read()
恳求,将 I/O 恳求发送给 DMA
,这个时分 CPU 就能够去干其他的工作了,等到 DMA
读取足够数据后再向 CPU 发送 IO 中止,CPU 将数据从内核缓冲区仿制到用户缓冲区,这个数据传输进程,CPU 不再与磁盘打交道了,不再参加数据搬运进程了,由 DMA
来处理。
可是,这样就完了吗?仔细再研讨上面的图,就算咱们加入了 DMA
,整个进程也仍然进行了两次内核态&用户态的切换,一次数据仿制的进程,这还只是读取进程,假设再加上写入呢?功能将会进一步下降。
为什么需求零仿制
为什么需求零仿制?由于假设不用它就会慢,功能堪忧啊。体现在哪里呢?咱们来看看一次完整的读写数据交互进程有多杂乱。下面是应用程序完结一次读写操作的进程图:
- 读数据进程如下:
进程 | 剖析 | |
---|---|---|
应用程序调用 read() 函数,读取磁盘数据 |
用户态切换至内核态 | 第 1 次切换 |
DMA 操控器将数据从磁盘仿制到内核缓冲区 | DMA 仿制 | 第 1 次 DMA 仿制 |
CPU 将数据从内核缓冲区仿制到用户缓冲区 | CPU 仿制 | 第 1 次 CPU 仿制 |
CPU 仿制完结后,read() 回来 |
内核态切换至用户态 | 第 2 次切换 |
- 写数据进程
进程 | 剖析 | |
---|---|---|
应用程序调用 write() 向网卡写入数据 |
用户态切换至内核态 | 第 3 次切换 |
CPU 将数据从用户缓冲区仿制到套接字缓冲区 | CPU 仿制 | 第 2 次 DMA 仿制 |
DMA 操控器将数据从内核缓冲区仿制到网卡 | DMA 仿制 | 第 2 次 DMA 仿制 |
完结仿制后,write() 回来 |
内核态切换至用户态 | 第 4 次切换 |
整个进程进行了 4 次切换,2 次 CPU 仿制,2 次 DMA 仿制,功率并不是很高,那怎样进步功能呢?
- 削减用户态和内核态的切换
- 削减仿制进程
所以零仿制就呈现了。
Linux 的零仿制
现在完结零仿制的技能有三种,别离为:
- mmap+write
- sendfile
- sendfile + SG-DMA
下面大明哥顺次介绍这些。
mmap+write
mmap
是一种内存映射文件的机制,它完结了将内核中读缓冲区地址与用户空间缓冲区地址进行映射,然后完结内核缓冲区与用户缓冲区的同享。mmap
能够代替 read()
,然后削减一次 CPU 仿制(内核缓冲区 → 应用程序缓冲区)
进程如下:
进程 | 剖析 | |
---|---|---|
应用程序调用 mmap 读取磁盘数据 | 用户态切换至内核态 | 第 1 次切换 |
DMA 操控器将数据从磁盘仿制到内核缓冲区 | DMA 仿制 | 第 1 次 DMA 仿制 |
CPU 仿制完结后,mmap 回来 | 内核态切换至用户态 | 第 2 次切换 |
- 写数据进程
进程 | 剖析 | |
---|---|---|
应用程序调用 write() 向外设写入数据 |
用户态切换至内核态 | 第 3 次切换 |
CPU 将数据从内核缓冲区仿制到套接字缓冲区 | CPU 仿制 | 第 1 次 CPU 仿制 |
DMA 操控器将数据从内核缓冲区仿制到网卡 | DMA 仿制 | 第 2 次 DMA 仿制 |
完结仿制后,write() 回来 |
内核态切换至用户态 | 第 4 次切换 |
mmap 代替了 read()
,只削减了一次 CPU 仿制,仍然存在 4 次用户状况&内核状况的上下文切换和 3 次仿制,全体来说还不是这么抱负。
sendfile
sendfile
是 Linux2.1 内核版本后引进的一个系统调用函数,专门用来发送文件的函数,它建立了文件的传输通道,数据直接从设备 A 传输到设备 B,不需求经过用户缓冲区。
运用 sendfile
就直接替换了上面的 read()
和 write()
两个函数,这样就只需求需求进行两次切换。如下:
进程 | 剖析 | |
---|---|---|
应用程序调用 sendfile
|
用户态切换至内核态 | 第 1 次切换 |
DMA 把数据从磁盘仿制到内核缓冲区 | DMA 仿制 | 第 1 次 DMA 仿制 |
CPU 把数据从内核缓冲区仿制到套接字缓冲区 | CPU 仿制 | 第 1 次 CPU 仿制 |
DMA 把数据从套接字缓冲区仿制到网卡 | DMA 仿制 | 第 2 次 DMA 仿制 |
完结后,sendfile 回来 | 内核态切换至用户态 | 第 2 次切换 |
这个技能比传统的削减了 2 次用户态&内核态的上下文切换和一次 CPU 仿制。
可是,它有一个缺陷便是由于数据不经过用户缓冲区,所以无法修正数据,只能进行文件传输。
sendfile + SG-DMA
Linux 2.4 内核版本对sendfile
做了进一步优化,假设网卡支持 SG-DMA
(The Scatter-Gather Direct Memory Access)技能,咱们能够不需求将内核缓冲区的数据仿制到套接字缓冲区。
它将内核缓冲区的数据描绘信息(文件描绘符、偏移量等信息)记录到套接字缓冲区,由 DMA
根据这些数据从内核缓冲区仿制到网卡中,然后再一次削减 CPU 仿制。
进程如下:
进程 | 剖析 | |
---|---|---|
应用程序调用 sendfile
|
用户态切换至内核态 | 第 1 次切换 |
DMA 把数据从磁盘仿制到内核缓冲区 | DMA 仿制 | 第 1 次 DMA 仿制 |
SG-DMA 把数据从内核缓冲区仿制到网卡 | DMA 仿制 | 第 2 次 DMA 仿制 |
sendfile 回来 |
内核态切换至用户态 | 第 2 次切换 |
这个进程已经没有了 CPU 仿制了,也只要 2 次上下文件切换,这便是真正的零仿制技能,全程无 CPU 参加,一切数据的仿制都依靠 DMA 来完结。
最终做一个总结:
技能类型 | 上下文切换次数 | CPU 仿制次数 | DMA 仿制次数 |
---|---|---|---|
read() + write()
|
4 | 2 | 2 |
mmap + write()
|
4 | 1 | 2 |
sendfile() |
2 | 1 | 2 |
sendfile() + SG-DMA |
2 | 0 | 2 |
零仿制比传统的 read()
+ write()
办法削减了 2 次上下文切换和 2 次 CPU 仿制,功能至少进步了 1 倍。
Netty 的零仿制
Linux 的零仿制首要是在 OS 层,而 Netty 的零仿制则不同,它完全是在应用层,咱们能够理解为用户态层次的,它的零仿制愈加倾向于优化数据操作这样的概念,首要体现在下面四个方面:
- Netty 供给了
CompositeByteBuf
类,能够将多个 ByteBuf 兼并成一个逻辑上的 ByteBuf,避免了各个 ByteBuf 之间的仿制。 - Netty 供给了
slice
操作,能够将一个 ByteBuf 切分成多个 ByteBuf,这些 ByteBuf 同享同一个存储区域的 ByteBuf,避免了内存的仿制。 - Netty 供给了
wrap
操作,能够将 byte[] 数组、ByteBuf、ByteBuffer 等包装成一个 Netty ByteBuf 目标, 进而避免了仿制操作。 - Netty 供给了 FileRegion,经过 FileRegion 能够将文件缓冲区的数据直接传输给方针 Channel,这样就避免了传统办法经过循环 write 办法导致的内存仿制问题。
下面大明哥就这四种零仿制操作别离简略讲解下(后续出文详细介绍)。
CompositeByteBuf
Composite 的意思是复合、组成,CompositeByteBuf
便是组成的 ByteBuf,它的注释是这样的:
A virtual buffer which shows multiple buffers as a single merged buffer. It is recommended to use ByteBufAllocator.compositeBuffer() or Unpooled.wrappedBuffer(ByteBuf…) instead of calling the constructor explicitly.
翻译便是 CompositeByteBuf
是一个将多个 ByteBuf 兼并成一个 ByteBuf 的虚拟缓冲区,为什么是虚拟缓冲区呢?由于它本身不存储实践数据,而是管理多个实践的缓冲区的引用,形成一个逻辑上接连的 ByteBuf,然后展示给用户一个兼并后的单一缓冲区的视图。图例如下:
下面咱们来演示下。
- 新建 CompositeByteBuf
Netty 为 CompositeByteBuf
供给了一系列的构造函数让咱们新建 CompositeByteBuf
,但一般推荐运用 ByteBufAllocator.compositeBuffer()
来创立一个 CompositeByteBuf 实例。
CompositeByteBuf compositeByteBuf = ByteBufAllocator.DEFAULT.compositeBuffer();
- 组合 CompositeByteBuf
咱们能够运用 addComponent(ByteBuf buffer)
向 CompositeByteBuf
增加实践的 ByteBuf 实例。这些 ByteBuf 将被组组成一个逻辑上的接连缓冲区。
ByteBuf buffer1 = ByteBufAllocator.DEFAULT.buffer(16);
ByteBuf buffer2 = ByteBufAllocator.DEFAULT.buffer(16);
compositeByteBuf.addComponent(buffer1);
compositeByteBuf.addComponent(buffer2);
- 读写 CompositeByteBuf
一旦咱们组合完结 CompositeByteBuf
后,咱们就能够像运用 ByteBuf 相同来运用 CompositeByteBuf
。
compositeByteBuf.readByte();
compositeByteBuf.writeByte('z');
下面经过示例来演示,看看各个 ByteBuf 实践特点值改动情况。
CompositeByteBuf compositeByteBuf = ByteBufAllocator.DEFAULT.compositeBuffer();
ByteBufPrintUtil.printByteBufDetail(compositeByteBuf,"new compositeByteBuf");
//-------
============== new compositeByteBuf================
capacity = 0
maxCapacity = 2147483647
readerIndex = 0
writerIndex = 0
图例如下:
ByteBuf buffer1 = ByteBufAllocator.DEFAULT.buffer(4,8);
buffer1.writeBytes(new byte[]{'a','b'});
ByteBufPrintUtil.printByteBufDetail(buffer1,"buffer1");
// 增加 buffer1
compositeByteBuf.addComponent(buffer1);
ByteBufPrintUtil.printByteBufDetail(compositeByteBuf,"addComponent(buffer1)");
//--------
============== buffer1================
capacity = 4
maxCapacity = 8
readerIndex = 0
writerIndex = 2
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 |ab |
+--------+-------------------------------------------------+----------------+
============== addComponent(buffer1)================
capacity = 2
maxCapacity = 2147483647
readerIndex = 0
writerIndex = 0
从打印的日志能够看到,compositeByteBuf
的 capacity
等于 buffer1
的 writerIndex
,这里有一点不好便是 writerIndex = 0
,实践上它是有值可读的,所以假设咱们期望 writerIndex
也随着一起改动,则能够运用 addComponent(boolean increaseWriterIndex, ByteBuf buffer)
,参数increaseWriterIndex
表明是否需求增加 writerIndex
,假设为 true,则在增加完 ByteBuf 后会将 writerIndex
移动到新增加数据的结尾,假设为 false,则不移动 writerIndex
,咱们能够手动操控。为了后边的演示愈加直观,咱们运用 addComponent(boolean increaseWriterIndex, ByteBuf buffer)
,图例如下:
咱们再加一个 byteBuf2:
ByteBuf buffer2 = ByteBufAllocator.DEFAULT.buffer(5,10);
buffer2.writeBytes(new byte[]{'h','i','j'});
ByteBufPrintUtil.printByteBufDetail(buffer2,"buffer2");
compositeByteBuf.addComponent(true,buffer2);
ByteBufPrintUtil.printByteBufDetail(compositeByteBuf,"addComponent(buffer2)");
//------
============== buffer2================
capacity = 5
maxCapacity = 10
readerIndex = 0
writerIndex = 3
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 69 6a |hij |
+--------+-------------------------------------------------+----------------+
============== addComponent(buffer2)================
capacity = 5
maxCapacity = 2147483647
readerIndex = 0
writerIndex = 5
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 68 69 6a |abhij |
+--------+-------------------------------------------------+----------------+
图例如下:
咱们现在对 byteBuf1 和 byteBuf2 别离读取 1个byte,看看他们的读写索引的改动情况:
buffer1.readByte();
buffer2.readByte();
ByteBufPrintUtil.printByteBufDetail(buffer1,"buffer1");
ByteBufPrintUtil.printByteBufDetail(buffer2,"buffer2");
ByteBufPrintUtil.printByteBufDetail(compositeByteBuf,"compositeByteBuf");
//-----
============== buffer1================
capacity = 4
maxCapacity = 8
readerIndex = 1
writerIndex = 2
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 62 |b |
+--------+-------------------------------------------------+----------------+
============== buffer2================
capacity = 5
maxCapacity = 10
readerIndex = 1
writerIndex = 3
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 69 6a |ij |
+--------+-------------------------------------------------+----------------+
============== compositeByteBuf================
capacity = 5
maxCapacity = 2147483647
readerIndex = 0
writerIndex = 5
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 68 69 6a |abhij |
+--------+-------------------------------------------------+----------------+
readerIndex
是没有影响的,那写呢?
buffer2.writeByte('y');
ByteBufPrintUtil.printByteBufDetail(buffer2,"buffer2");
ByteBufPrintUtil.printByteBufDetail(compositeByteBuf,"compositeByteBuf");
//---
============== buffer2================
capacity = 5
maxCapacity = 10
readerIndex = 1
writerIndex = 4
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 69 6a 79 |ijy |
+--------+-------------------------------------------------+----------------+
============== compositeByteBuf================
capacity = 5
maxCapacity = 2147483647
readerIndex = 0
writerIndex = 5
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 68 69 6a |abhij |
+--------+-------------------------------------------------+----------------+
writerIndex
也没有影响,所以咱们能够判定 CompositeByteBuf
** 与原兼并的 ByteBuf 的读写索引是相互独立的,操作互不影响。**CompositeByteBuf
同享底层数据,假设实践 ByteBuf 底层数据内容产生改动,CompositeByteBuf
会有改动吗?
buffer1.setByte(1,'x');
buffer2.setByte(1,'y');
ByteBufPrintUtil.printByteBufDetail(compositeByteBuf,"compositeByteBuf");
//-----
============== compositeByteBuf================
capacity = 5
maxCapacity = 2147483647
readerIndex = 0
writerIndex = 5
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 78 68 79 6a |axhyj |
+--------+-------------------------------------------------+----------------+
你会发现产生了改动,假设实践 ByteBuf 写入数据呢?
buffer1.writeBytes(new byte[]{'o'});
buffer2.writeBytes(new byte[]{'z'});
ByteBufPrintUtil.printByteBufDetail(buffer1,"buffer1");
ByteBufPrintUtil.printByteBufDetail(buffer2,"buffer2");
// 调整 compositeByteBuf 的指针,要不 compositeByteBuf 读不到
compositeByteBuf.capacity(10);
compositeByteBuf.writerIndex(10);
ByteBufPrintUtil.printByteBufDetail(compositeByteBuf,"compositeByteBuf");
//-----
============== buffer1================
capacity = 4
maxCapacity = 8
readerIndex = 1
writerIndex = 3
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 78 6f |xo |
+--------+-------------------------------------------------+----------------+
============== buffer2================
capacity = 5
maxCapacity = 10
readerIndex = 1
writerIndex = 4
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 79 6a 7a |yjz |
+--------+-------------------------------------------------+----------------+
============== compositeByteBuf================
capacity = 10
maxCapacity = 2147483647
readerIndex = 0
writerIndex = 10
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 78 68 79 6a 00 00 00 00 00 |axhyj..... |
+--------+-------------------------------------------------+----------------+
你会发现一个很恐怖的工作,compositeByteBuf
它并没有打印出来 o
和 z
,这是什么情况,不是底层数据同享么?实践 ByteBuf 写入数据,compositeByteBuf
获取不到,这是哪门子同享?
这其实是跟 compositeByteBuf
机制相关,看 CompositeByteBuf
的源码就理解了,篇幅有限,大明哥就直接告诉你成果:当咱们调用 addComponent()
将一个 ByteBuf 增加到 CompositeByteBuf
时,CompositeByteBuf
会新建一个 Component
目标来存储该 ByteBuf,Component
里边有两个特点很重要:
-
int offset
:ByteBuf 在CompositeByteBuf
的偏移量 -
int endOffset
:ByteBuf 在CompositeByteBuf
的结束偏移量
这两个特点决议了 CompositeByteBuf
是否能够查询到实践 ByteBuf 的数据值。咱们 debug 看下 buffer1 和 buffer2 在 CompositeByteBuf
中的值:
ByteBuf | offset | endOffset |
---|---|---|
buffer1 | 0 | 2 |
buffer2 | 2 | 5 |
现在咱们来读 CompositeByteBuf
中的数据:compositeByteBuf.getByte(2)
,盯梢源码:
public byte getByte(int index) {
Component c = findComponent(index);
return c.buf.getByte(c.idx(index));
}
调用 findComponent()
获取对应的 Component
目标,然后从 Component
目标里边获取实践值。findComponent()
里边有一个很重要的 findIt()
private Component findIt(int offset) {
for (int low = 0, high = componentCount; low <= high;) {
int mid = low + high >>> 1;
Component c = components[mid];
if (c == null) {
throw new IllegalStateException("No component found for offset. " +
"Composite buffer layout might be outdated, e.g. from a discardReadBytes call.");
}
if (offset >= c.endOffset) {
low = mid + 1;
} else if (offset < c.offset) {
high = mid - 1;
} else {
lastAccessed = c;
return c;
}
}
throw new Error("should not reach here");
}
findComponent(2)
得到的是 buffer2 实例目标:
所以 compositeByteBuf.getByte(2)
能够拿到 h
值,可是 compositeByteBuf.getByte(6)
就拿不到 buffer1
和 buffer2
中的值了。
所以咱们能够得出结论:实践 ByteBuf 写入或许修正底层数据后会影响 CompositeByteBuf,可是 CompositeByteBuf 无法获取实践 ByteBuf 写入的值。
那 CompositeByteBuf
写数据呢?我直接告诉你结论,它不会影响实践 ByteBuf 的底层数据,盯梢源码你会发现它会新建一个 Component
目标来存储数据。调用 compositeByteBuf.writeByte(1);
,debug 盯梢下你会发现 CompositeByteBuf
目标后边会多一个 Component
目标:
下面就 CompositeByteBuf
做一个简略的总结:
-
CompositeByteBuf
作为 ByteBuf 四大零仿制技能之一,它供给了一种将多个 ByteBuf 组组成一个逻辑上接连的缓冲区的办法,然后在一些特定的应用场景中供给更好的功能和内存管理。 -
CompositeByteBuf
与实践 ByteBuf 同享底层数据,但他们的读写指针是相互独立的。 - 同享底层数据并不意味着他们的底层数据相互影响,只要经过类似
setBytes()
的办法改写底层数据才会相互影响。 - 实践 ByteBuf 经过类似
writeByte()
的办法来写入数据尽管影响底层数据,可是CompositeByteBuf
读不到,而经过CompositeByteBuf
写入的数据,并不会影响实践 ByteBuf 的底层数据。 -
CompositeByteBuf
适用于需求处理多个小块数据的场景,它能够削减内存开销和数据仿制,然后进步功能。
slice 操作
slice()
办法是 ByteBuf 中用于创立切片的一种零仿制技能。
slice()
产生的切片是一个新的 ByteBuf,它与原始 ByteBuf 同享底层数据,可是具有自己的读写指针。这使得咱们能够在不进行数据仿制的情况下,对原始数据进行子集操作。
slice()
办法有两种:
- 一、
ByteBuf slice()
该办法产生的新的切片,是从原始 ByteBuf 的当时读索引开始,一直到可读字节的结尾。切片的容量和可读字节数与原始 ByteBuf 的可读字节数相同。切片的读写指针与原始 ByteBuf 的读写指针独立,对切片的读不会影响原始 ByteBuf。
ByteBuf originalByteBuf = ByteBufAllocator.DEFAULT.buffer(12,24);
// 写入 9 个字符
originalByteBuf.writeBytes(new byte[]{'a','b','c','d','e','f','g','h','i'});
// 读取 4 个字符
originalByteBuf.readInt();
ByteBufPrintUtil.printByteBuf(originalByteBuf,"originalByteBuf");
//产生一个切片
ByteBuf sliceByteBuf = originalByteBuf.slice();
ByteBufPrintUtil.printByteBuf(sliceByteBuf,"sliceByteBuf");
运转成果:
============== originalByteBuf ================
capacity = 12
maxCapacity = 24
readerIndex = 4
writerIndex = 9
readableBytes = 5
writableBytes = 3
============== sliceByteBuf ================
capacity = 5
maxCapacity = 5
readerIndex = 0
writerIndex = 5
readableBytes = 5
writableBytes = 0
从上的成果咱们能够看出,经过 slice()
产生的切片目标,几个重要特点如下:
-
readerIndex
为 0 -
writerIndex
为源 ByteBuf 的readableBytes()
可读字节数。 -
capacity = maxCapacity
也是源 ByteBuf 的readableBytes()
可读字节数,这样就会导致一个成果,切片 ByteBuf 是不能够写入的,原因是:maxCapacity
和writerIndex
持平。 -
writableBytes
为 0 ,表明切片 ByteBuf 不可写入
切片内容如下:
留意:这里有一部分底层数据[a,b,c,d] ,sliceByteBuf 是 get 不到的。由于原始的 readerIndex = 4
- 二、
ByteBuf slice(int index, int length)
创立一个新的切片,从给定索引方位开始,指定长度的字节。切片的容量和可读字节数等于指定的长度。切片的读写指针与原始 ByteBuf 的读写指针独立,对切片的读不会影响原始 ByteBuf。两个参数意义如下:
-
index
:表明要截取的子序列的开始方位,也便是从那个索引方位开始截取,它的取值规模 0 ≤ index ≤ capacity -
length
:表明要截取的子序列的长度,它的取值规模由源 ByteBuf 的 capacity 和 index 共同决议,应该满意公式:原 capacity ≥ index + length
。
不满意这个条件会抛出类似如下反常:
IndexOutOfBoundsException: PooledUnsafeDirectByteBuf(ridx: 4, widx: 9, cap: 12/24).slice(13, 0)
示例如下:
// 产生一个切片
ByteBuf sliceByteBuf2 = originalByteBuf.slice(4,8);
ByteBufPrintUtil.printByteBuf(sliceByteBuf2,"sliceByteBuf2");
============== sliceByteBuf2 ================
capacity = 8
maxCapacity = 8
readerIndex = 0
writerIndex = 8
readableBytes = 8
writableBytes = 0
重要特点和 slice()
共同,就不多解说了,图例如下:
由于同享底层数据,所以源 ByteBuf 改动底层数据,两个分片 ByteBuf 都会有对应改动:
// 改动前
System.out.println("sliceByteBuf1 :" + sliceByteBuf1.getByte(2));
System.out.println("sliceByteBuf2 :" + sliceByteBuf2.getByte(2));
originalByteBuf.setByte(6,9);
// 改动后
System.out.println("sliceByteBuf1 :" + sliceByteBuf1.getByte(2));
System.out.println("sliceByteBuf2 :" + sliceByteBuf2.getByte(2));
//履行成果-------
sliceByteBuf1 :103
sliceByteBuf2 :103
sliceByteBuf1 :9
sliceByteBuf2 :9
那假设源 ByteBuf 写入数据呢?
// 写入数据
originalByteBuf.writeBytes(new byte[]{'j','k','l','m','n'}});
StringBuilder builder = ByteBufPrintUtil.getPrintBuilder(originalByteBuf,"originalByteBuf");
ByteBufUtil.appendPrettyHexDump(builder,originalByteBuf);
System.out.println(builder.toString());
builder = ByteBufPrintUtil.getPrintBuilder(sliceByteBuf1,"sliceByteBuf1");
ByteBufUtil.appendPrettyHexDump(builder,sliceByteBuf1);
System.out.println(builder.toString());
builder = ByteBufPrintUtil.getPrintBuilder(sliceByteBuf2,"sliceByteBuf2");
ByteBufUtil.appendPrettyHexDump(builder,sliceByteBuf2);
System.out.println(builder.toString());
ByteBufUtil
是 Netty 供给的一个东西类,十分有用,appendPrettyHexDump()
它能够将 ByteBuf 可读部分的数据按照 16 进制格式进行格式化,便于咱们检查。履行成果如下:
============== originalByteBuf================
capacity = 16
maxCapacity = 24
readerIndex = 4
writerIndex = 14
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 65 66 09 68 69 6a 6b 6c 6d 6e |ef.hijklmn |
+--------+-------------------------------------------------+----------------+
============== sliceByteBuf1================
capacity = 5
maxCapacity = 5
readerIndex = 0
writerIndex = 5
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 65 66 09 68 69 |ef.hi |
+--------+-------------------------------------------------+----------------+
============== sliceByteBuf2================
capacity = 8
maxCapacity = 8
readerIndex = 0
writerIndex = 8
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 65 66 09 68 69 6a 6b 6c |ef.hijkl |
+--------+-------------------------------------------------+----------------+
各位小伙伴,对比下这个履行成果,然后对照那两张图再看下就理解了。
对比下 readerIndex、writerIndex 两值的差异,阐明他们直接的读写索引是相互独立的!!
duplicate 操作
duplicate()
创立一个与原始 ByteBuf 具有相同数据内容的新 ByteBuf,它和slice()
相同,也是浅仿制,duplicate()
创立的 ByteBuf 与源 ByteBuf 同享相同的底层数据,可是他们具有自己独立的读写指针。
public class DuplicateTest {
public static void main(String[] args) {
ByteBuf originalByteBuf = ByteBufAllocator.DEFAULT.buffer(12,24);
// 写入 9 个字符
originalByteBuf.writeBytes(new byte[]{'a','b','c','d','e','f','g','h','i'});
// 读取 4 个字符
originalByteBuf.readInt();
ByteBufPrintUtil.printByteBuf(originalByteBuf,"originalByteBuf");
//产生一个切片
ByteBuf duplicateByteBuf = originalByteBuf.duplicate();
ByteBufPrintUtil.printByteBuf(originalByteBuf,"duplicateByteBuf");
}
}
// -----
============== originalByteBuf ================
capacity = 12
maxCapacity = 24
readerIndex = 4
writerIndex = 9
readableBytes = 5
writableBytes = 3
============== duplicateByteBuf ================
capacity = 12
maxCapacity = 24
readerIndex = 4
writerIndex = 9
readableBytes = 5
writableBytes = 3
从履行成果能够看出 duplicateByteBuf
与 originalByteBuf
如出一辙,所以尽管 duplicate()
和 slice()
相同,都是浅仿制,可是 slice()
是切片,它的特点和源 ByteBuf 并不共同,而 duplicate()
是直接仿制整个 ByteBuf,包括 readerIndex
、writerIndex
、capacity
和 maxCapacity
。图例如下:
// originalByteBuf 修正数据
originalByteBuf.setByte(7,'z');
// originalByteBuf 写入数据
originalByteBuf.writeBytes(new byte[]{'j','k','l','m','n','o','p'});
StringBuilder stringBuilder = ByteBufPrintUtil.getPrintBuilder(originalByteBuf,"originalByteBuf");
ByteBufUtil.appendPrettyHexDump(stringBuilder,originalByteBuf);
System.out.println(stringBuilder.toString());
stringBuilder = ByteBufPrintUtil.getPrintBuilder(duplicateByteBuf,"duplicateByteBuf");
ByteBufUtil.appendPrettyHexDump(stringBuilder,duplicateByteBuf);
System.out.println(stringBuilder.toString());
履行成果:
============== originalByteBuf================
capacity = 16
maxCapacity = 24
readerIndex = 4
writerIndex = 16
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 65 66 67 7a 69 6a 6b 6c 6d 6e 6f 70 |efgzijklmnop |
+--------+-------------------------------------------------+----------------+
============== duplicateByteBuf================
capacity = 16
maxCapacity = 24
readerIndex = 4
writerIndex = 9
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 65 66 67 7a 69 |efgzi |
+--------+-------------------------------------------------+----------------+
duplicateByteBuf
底层数据产生了改变(index = 3
方位),图例如下:
经过上面的剖析, slice()
和 duplicate()
是有一些异同点的:
-
slice()
和duplicate()
的相同点在于:它们底层内存都是与源 ByteBuf 同享的,这就意味着经过slice()
和duplicate()
创立的 ByteBuf ,假设源 ByteBuf 对底层数据进行了修正则会影响到他们,可是他们都维持着与源 ByteBuf 不同的读写指针,读写指针互不影响。 -
slice()
和duplicate()
不同点有几个地方:-
slice()
是从源 ByteBuf 截取从readerIndex
到writerIndex
之间的数据,它的最大容量会限制到源 Bytebuf 的readableBytes()
大小,其间writerIndex = capacity = maxCapacity
,所以它无法运用write()
系列办法 -
duplicate()
是将整个源 ByteBuf 的一切特点都仿制过来了,特点值与源 ByteBuf 的特点值相同。
-
运用
slice()
和duplicate()
必定要留意他们是内存同享,读写指针不同享。
注:还有一个很重要的点没有剖析到,那便是引用计数,****slice()
**和 **duplicate()
**派生出来的 ByteBuf 与源 ByteBuf 的引用计数是否同享,ByteBuf 的 API 中还有两个 **retainedSlice()
**和 **retainedDuplicate()
**,这两个办法与 **slice()
**和 **duplicate()
有什么关联,他们派生出来的 ByteBuf 与源 ByteBuf 是否同享呢?问题比较杂乱,这个在源码篇大明哥会详细剖析,咱们暂时先记住,他们引用计数是同享的。
wrap 操作
wrappedBuffer()
用于将不同类型的字节缓冲区包装成一个大的 ByteBuf 目标,这些不同数据源的类型能够是byte[]
、ByteBuffer
、ByteBuf
,并且包装进程中不会产生数据仿制,包装后生成的 ByteBuf 与原始数据源同享底层数据。
假设咱们有一个 byte[]
,咱们期望将其转化为 ByteBuf 目标,然后在 Netty 中运用,传统做法如下:
byte[] bytes = "skjava.com".getBytes(Charset.defaultCharset());
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.directBuffer();
byteBuf.writeBytes(bytes);
这种办法有一次很明显的数据仿制进程,那要怎样根绝这一次的数据仿制进程呢?Netty 供给了 Unpooled.wrappedBuffer()
能够将 byte[]
包装成 ByteBuf 目标,如下:
byte[] bytes = "skjava.com".getBytes(Charset.defaultCharset());
ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);
这种办法就不会产生数据仿制的进程了,当然这个新的 ByteBuf 目标与原始的 byte[] 数组共用底层数据。
下面大明哥演示下,看看实践情况。
byte[] bytes1 = new byte[]{'a','b'};
byte[] bytes2 = new byte[]{'h','i','j'};
byte[] bytes3 = new byte[]{'u','v','w','x'};
ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes1,bytes2,bytes3);
ByteBufPrintUtil.printByteBufDetail(byteBuf,"wrappedBuffer");
//-----
============== wrappedBuffer================
capacity = 9
maxCapacity = 2147483647
readerIndex = 0
writerIndex = 9
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 68 69 6a 75 76 77 78 |abhijuvwx |
+--------+-------------------------------------------------+----------------+
从输出的成果特别像 CompositeByteBuf
,那咱们看看这个 byteBuf 目标到底是一个什么目标:
你看吧,还真的是 CompositeByteBuf
目标,它的 components
如下:
然后和 bytes1
、 bytes2
、bytes3
对照下,你就会发现它和 CompositeByteBuf
运用 addComponent()
增加 ByteBuf 如出一辙,并且经过盯梢 Unpooled.wrappedBuffer()
代码你会发现假设封装的是一个 byte[]
它会将其直接封装为 ByteBuf 目标,假设是多个便是 CompositeByteBuf
:
static <T> ByteBuf wrappedBuffer(int maxNumComponents, ByteWrapper<T> wrapper, T[] array) {
switch (array.length) {
case 0:
break;
case 1:
if (!wrapper.isEmpty(array[0])) {
return wrapper.wrap(array[0]);
}
break;
default:
for (int i = 0, len = array.length; i < len; i++) {
T bytes = array[i];
if (bytes == null) {
return EMPTY_BUFFER;
}
if (!wrapper.isEmpty(bytes)) {
return new CompositeByteBuf(ALLOC, false, maxNumComponents, wrapper, array, i);
}
}
}
return EMPTY_BUFFER;
}
wrappedBuffer()
其本质与 CompositeByteBuf
不同不大,两者都是将多个缓冲字节省封装成一个逻辑上一致的 ByteBuf,读写指针相互独立,底层数据同享,只不过 wrappedBuffer()
能够将多个同步数据的缓冲字节省封装,而 CompositeByteBuf
只能将 ByteBuf 进行封装,wrappedBuffer()
封装回来的目标也是一个 CompositeByteBuf
目标,所以这里就不讲解了。