万字阐述 Netty 的 I/O 加速神器:零拷贝

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!


零拷贝我相信各位小伙伴都听过它的大名,在 Kafka、RocketMQ 等知名的产品中都有使用到它,它主要用于提升 I/O 性能,Netty 是一个把性能当做生命的产品,怎么可能不会去实现它呢?所以这篇文章我们就来聊聊 Netty 的零拷贝。

数据拷贝基础

各位小伙伴应该都写过读写文件的应用程序吧?我们一般都是从磁盘读取文件,然后加工数据,最后写入数据库或发送给其他子系统。

那当中具体的流程是怎么样的?

  1. 应用程序发起 read()调用,由用户态进入内核态。
  2. CPU 向磁盘发起 I/O 读取请求。
  3. 磁盘将数据写入到磁盘缓冲区后,向 CPU 发起 I/O 中断,报告 CPU 数据已经准备好了。
  4. CPU 将数据从磁盘缓冲区拷贝至内核缓冲区,然后从内核缓冲区将数据拷贝至用户缓冲区
  5. 完成后,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 的零拷贝则不同,它完全是在应用层,我们可以理解为用户态层次的,它的零拷贝更加偏向于优化数据操作这样的概念,主要体现在下面四个方面:

  1. Netty 提供了 CompositeByteBuf类,可以将多个 ByteBuf 合并成一个逻辑上的 ByteBuf,避免了各个 ByteBuf 之间的拷贝。
  2. Netty 提供了 slice 操作,可以将一个 ByteBuf 切分成多个 ByteBuf,这些 ByteBuf 共享同一个存储区域的 ByteBuf,避免了内存的拷贝。
  3. Netty 提供了 wrap 操作,可以将 byte[] 数组、ByteBuf、ByteBuffer 等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作。
  4. 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

从打印的日志可以看到,compositeByteBufcapacity 等于 buffer1writerIndex,这里有一点不好就是 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 它并没有打印出来 oz,这是什么情况,不是底层数据共享么?实际 ByteBuf 写入数据,compositeByteBuf 获取不到,这是哪门子共享?

这其实是跟 compositeByteBuf 机制相关,看 CompositeByteBuf 的源码就明白了,篇幅有限,大明哥就直接告诉你结果:当我们调用 addComponent() 将一个 ByteBuf 添加到 CompositeByteBuf 时,CompositeByteBuf 会新建一个 Component 对象来存储该 ByteBuf,Component 里面有两个属性很重要:

  1. int offset:ByteBuf 在 CompositeByteBuf 的偏移量
  2. 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) 就拿不到 buffer1buffer2 中的值了。

所以我们可以得出结论:实际 ByteBuf 写入或者修改底层数据后会影响 CompositeByteBuf,但是 CompositeByteBuf 无法获取实际 ByteBuf 写入的值

CompositeByteBuf 写数据呢?我直接告诉你结论,它不会影响实际 ByteBuf 的底层数据,跟踪源码你会发现它会新建一个 Component 对象来存储数据。调用 compositeByteBuf.writeByte(1);,debug 跟踪下你会发现 CompositeByteBuf 对象后面会多一个 Component 对象:

下面就 CompositeByteBuf 做一个简单的总结:

  1. CompositeByteBuf 作为 ByteBuf 四大零拷贝技术之一,它提供了一种将多个 ByteBuf 组合成一个逻辑上连续的缓冲区的方式,从而在一些特定的应用场景中提供更好的性能和内存管理。
  2. CompositeByteBuf 与实际 ByteBuf 共享底层数据,但他们的读写指针是互相独立的。
  3. 共享底层数据并不意味着他们的底层数据互相影响,只有通过类似 setBytes() 的方式改写底层数据才会互相影响。
  4. 实际 ByteBuf 通过类似 writeByte() 的方式来写入数据虽然影响底层数据,但是 CompositeByteBuf 读不到,而通过 CompositeByteBuf 写入的数据,并不会影响实际 ByteBuf 的底层数据。
  5. 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 为 0
  • writerIndex 为源 ByteBuf 的 readableBytes() 可读字节数。
  • capacity = maxCapacity 也是源 ByteBuf 的 readableBytes() 可读字节数,这样就会导致一个结果,切片 ByteBuf 是不可以写入的,原因是:maxCapacitywriterIndex 相等。
  • 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

不满足这个条件会抛出类似如下异常:

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

从执行结果可以看出 duplicateByteBuforiginalByteBuf 一模一样,所以虽然 duplicate()slice() 一样,都是浅拷贝,但是 slice() 是切片,它的属性和源 ByteBuf 并不一致,而 duplicate() 是直接拷贝整个 ByteBuf,包括 readerIndexwriterIndexcapacitymaxCapacity。图例如下:

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() 是有一些异同点的:

  1. slice()duplicate() 的相同点在于:它们底层内存都是与源 ByteBuf 共享的,这就意味着通过 slice()duplicate() 创建的 ByteBuf ,如果源 ByteBuf 对底层数据进行了修改则会影响到他们,但是他们都维持着与源 ByteBuf 不同的读写指针,读写指针互不影响。

  2. slice()duplicate() 不同点有几个地方:

    1. slice() 是从源 ByteBuf 截取从 readerIndexwriterIndex 之间的数据,它的最大容量会限制到源 Bytebuf 的 readableBytes() 大小,其中 writerIndex = capacity = maxCapacity,所以它无法使用 write() 系列方法
    2. duplicate() 是将整个源 ByteBuf 的所有属性都复制过来了,属性值与源 ByteBuf 的属性值一样。

使用 slice()duplicate() 一定要注意他们是内存共享,读写指针不共享。

注:还有一个很重要的点没有分析到,那就是引用计数,****slice() **和 **duplicate() **派生出来的 ByteBuf 与源 ByteBuf 的引用计数是否共享,ByteBuf 的 API 中还有两个 **retainedSlice() **和 **retainedDuplicate() **,这两个方法与 **slice() **和 **duplicate() 有什么关联,他们派生出来的 ByteBuf 与源 ByteBuf 是否共享呢?问题比较复杂,这个在源码篇大明哥会详细分析,我们暂时先记住,他们引用计数是共享的。

wrap 操作

wrappedBuffer() 用于将不同类型的字节缓冲区包装成一个大的 ByteBuf 对象,这些不同数据源的类型可以是 byte[]ByteBufferByteBuf,而且包装过程中不会发生数据拷贝,包装后生成的 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 如下:

然后和 bytes1bytes2bytes3 对照下,你就会发现它和 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 对象,所以这里就不讲解了。

相关推荐
qq_17448285756 小时前
springboot基于微信小程序的旧衣回收系统的设计与实现
spring boot·后端·微信小程序
锅包肉的九珍6 小时前
Scala的Array数组
开发语言·后端·scala
心仪悦悦6 小时前
Scala的Array(2)
开发语言·后端·scala
2401_882727577 小时前
BY组态-低代码web可视化组件
前端·后端·物联网·低代码·数学建模·前端框架
心仪悦悦7 小时前
Scala中的集合复习(1)
开发语言·后端·scala
代码小鑫8 小时前
A043-基于Spring Boot的秒杀系统设计与实现
java·开发语言·数据库·spring boot·后端·spring·毕业设计
真心喜欢你吖8 小时前
SpringBoot与MongoDB深度整合及应用案例
java·spring boot·后端·mongodb·spring
激流丶8 小时前
【Kafka 实战】Kafka 如何保证消息的顺序性?
java·后端·kafka
uzong9 小时前
一个 IDEA 老鸟的 DEBUG 私货之多线程调试
java·后端
飞升不如收破烂~9 小时前
Spring boot常用注解和作用
java·spring boot·后端