本文为稀土掘金技术社区首发签约文章,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 实例。
ini
CompositeByteBuf compositeByteBuf = ByteBufAllocator.DEFAULT.compositeBuffer();
- 组合 CompositeByteBuf
我们可以使用 addComponent(ByteBuf buffer)
向 CompositeByteBuf
添加实际的 ByteBuf 实例。这些 ByteBuf 将被组合成一个逻辑上的连续缓冲区。
ini
ByteBuf buffer1 = ByteBufAllocator.DEFAULT.buffer(16);
ByteBuf buffer2 = ByteBufAllocator.DEFAULT.buffer(16);
compositeByteBuf.addComponent(buffer1);
compositeByteBuf.addComponent(buffer2);
- 读写 CompositeByteBuf
一旦我们组合完成 CompositeByteBuf
后,我们就可以像使用 ByteBuf 一样来使用 CompositeByteBuf
。
ini
compositeByteBuf.readByte();
compositeByteBuf.writeByte('z');
下面通过示例来演示,看看各个 ByteBuf 实际属性值变化情况。
ini
CompositeByteBuf compositeByteBuf = ByteBufAllocator.DEFAULT.compositeBuffer();
ByteBufPrintUtil.printByteBufDetail(compositeByteBuf,"new compositeByteBuf");
//-------
============== new compositeByteBuf================
capacity = 0
maxCapacity = 2147483647
readerIndex = 0
writerIndex = 0
图例如下:
ini
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:
ini
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,看看他们的读写索引的变化情况:
ini
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
是没有影响的,那写呢?
ini
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
会有变化吗?
ini
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 写入数据呢?
ini
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)
,跟踪源码:
java
public byte getByte(int index) {
Component c = findComponent(index);
return c.buf.getByte(c.idx(index));
}
调用 findComponent()
获取对应的 Component
对象,然后从 Component
对象里面获取实际值。findComponent()
里面有一个很重要的 findIt()
ini
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。
arduino
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");
运行结果:
ini
============== 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
为 0writerIndex
为源 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 ≤ capacitylength
:表示要截取的子序列的长度,它的取值范围由源 ByteBuf 的 capacity 和 index 共同决定,应该满足公式:原 capacity ≥ index + length
。
不满足这个条件会抛出类似如下异常:
php
IndexOutOfBoundsException: PooledUnsafeDirectByteBuf(ridx: 4, widx: 9, cap: 12/24).slice(13, 0)
示例如下:
ini
// 产生一个切片
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 都会有对应改变:
csharp
// 改变前
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 写入数据呢?
ini
// 写入数据
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 进制格式进行格式化,便于我们查看。执行结果如下:
ini
============== 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 共享相同的底层数据,但是他们拥有自己独立的读写指针。
ini
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
。图例如下:
csharp
// 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());
执行结果:
ini
============== 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 中使用,传统做法如下:
ini
byte[] bytes = "skjava.com".getBytes(Charset.defaultCharset());
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.directBuffer();
byteBuf.writeBytes(bytes);
这种方式有一次很明显的数据拷贝过程,那要怎么杜绝这一次的数据拷贝过程呢?Netty 提供了 Unpooled.wrappedBuffer()
能够将 byte[]
包装成 ByteBuf 对象,如下:
ini
byte[] bytes = "skjava.com".getBytes(Charset.defaultCharset());
ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);
这种方式就不会发生数据拷贝的过程了,当然这个新的 ByteBuf 对象与原始的 byte[] 数组共用底层数据。
下面大明哥演示下,看看实际情况。
ini
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
:
php
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
对象,所以这里就不讲解了。