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


零仿制我相信各位小伙伴都听过它的大名,在 Kafka、RocketMQ 等知名的产品中都有运用到它,它首要用于进步 I/O 功能,Netty 是一个把功能作为生命的产品,怎样可能不会去完结它呢?所以这篇文章咱们就来聊聊 Netty 的零仿制。

数据仿制基础

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

万字论述 Netty 的 I/O 加快神器:零仿制

那傍边具体的流程是怎样样的?

  1. 应用程序建议 read()调用,由用户态进入内核态。
  2. CPU 向磁盘建议 I/O 读取恳求。
  3. 磁盘将数据写入到磁盘缓冲区后,向 CPU 建议 I/O 中止,陈述 CPU 数据已经预备好了。
  4. CPU 将数据从磁盘缓冲区仿制至内核缓冲区,然后从内核缓冲区将数据仿制至用户缓冲区
  5. 完结后,read() 回来,由内核态切换到用户态。

如下:

万字论述 Netty 的 I/O 加快神器:零仿制

这个进程有一个比较严重的问题便是 CPU 全程参加数据仿制的进程,并且整个进程 CPU 都不能干其他活,这不是浪费资源,耽误事吗!

怎样解决?引进 DMA 技能,即直接存储器访问(Direct Memory Access) ,那什么是 DMA 呢?

DMA传输:将数据从一个地址空间仿制到另一个地址空间,供给在外设和存储器之间或许存储器和存储器之间的高速数据传输

咱们都知道 CPU 是很稀缺的资源,需求力保它时间都在处理重要的工作,一些不重要的工作(比如数据仿制和存储)就不需求 CPU 参加了,让他去处理愈加重要的工作,这样是不是就能够更好地运用 CPU 资源呢?

所以,关于咱们读取文件(尤其是大文件)这种不那么重要且繁琐的工作是能够不需求 CPU 参加了,咱们只需求在两个设备之间建立一种通道,直接由设备 A 经过 DMA 仿制数据到设备 B,如下图:

万字论述 Netty 的 I/O 加快神器:零仿制

加入 DMA 后,数据传输进程就变成下图:

万字论述 Netty 的 I/O 加快神器:零仿制

CPU 接纳 read() 恳求,将 I/O 恳求发送给 DMA,这个时分 CPU 就能够去干其他的工作了,等到 DMA 读取足够数据后再向 CPU 发送 IO 中止,CPU 将数据从内核缓冲区仿制到用户缓冲区,这个数据传输进程,CPU 不再与磁盘打交道了,不再参加数据搬运进程了,由 DMA 来处理。

可是,这样就完了吗?仔细再研讨上面的图,就算咱们加入了 DMA,整个进程也仍然进行了两次内核态&用户态的切换,一次数据仿制的进程,这还只是读取进程,假设再加上写入呢?功能将会进一步下降。

为什么需求零仿制

为什么需求零仿制?由于假设不用它就会慢,功能堪忧啊。体现在哪里呢?咱们来看看一次完整的读写数据交互进程有多杂乱。下面是应用程序完结一次读写操作的进程图:

万字论述 Netty 的 I/O 加快神器:零仿制

  • 读数据进程如下:
进程 剖析
应用程序调用 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 仿制(内核缓冲区 → 应用程序缓冲区)

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

进程如下:

进程 剖析
应用程序调用 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,不需求经过用户缓冲区。

万字论述 Netty 的 I/O 加快神器:零仿制

运用 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 仿制。

万字论述 Netty 的 I/O 加快神器:零仿制

进程如下:

进程 剖析
应用程序调用 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,然后展示给用户一个兼并后的单一缓冲区的视图。图例如下:

万字论述 Netty 的 I/O 加快神器:零仿制

下面咱们来演示下。

  • 新建 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

图例如下:

万字论述 Netty 的 I/O 加快神器:零仿制

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),图例如下:

万字论述 Netty 的 I/O 加快神器:零仿制

咱们再加一个 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           |
+--------+-------------------------------------------------+----------------+

图例如下:

万字论述 Netty 的 I/O 加快神器:零仿制

咱们现在对 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 它并没有打印出来 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 中的值:

万字论述 Netty 的 I/O 加快神器:零仿制

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 实例目标:

万字论述 Netty 的 I/O 加快神器:零仿制

所以 compositeByteBuf.getByte(2) 能够拿到 h 值,可是 compositeByteBuf.getByte(6) 就拿不到 buffer1buffer2 中的值了。

所以咱们能够得出结论:实践 ByteBuf 写入或许修正底层数据后会影响 CompositeByteBuf,可是 CompositeByteBuf 无法获取实践 ByteBuf 写入的值

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

万字论述 Netty 的 I/O 加快神器:零仿制

下面就 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。

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 是不能够写入的,原因是:maxCapacitywriterIndex 持平。
  • writableBytes 为 0 ,表明切片 ByteBuf 不可写入

切片内容如下:

万字论述 Netty 的 I/O 加快神器:零仿制

留意:这里有一部分底层数据[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() 共同,就不多解说了,图例如下:

万字论述 Netty 的 I/O 加快神器:零仿制

由于同享底层数据,所以源 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

从履行成果能够看出 duplicateByteBuforiginalByteBuf 如出一辙,所以尽管 duplicate()slice() 相同,都是浅仿制,可是 slice() 是切片,它的特点和源 ByteBuf 并不共同,而 duplicate() 是直接仿制整个 ByteBuf,包括 readerIndexwriterIndexcapacitymaxCapacity。图例如下:

// 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 方位),图例如下:

万字论述 Netty 的 I/O 加快神器:零仿制

经过上面的剖析, 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 与原始数据源同享底层数据。

万字论述 Netty 的 I/O 加快神器:零仿制

假设咱们有一个 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 目标到底是一个什么目标:

万字论述 Netty 的 I/O 加快神器:零仿制

你看吧,还真的是 CompositeByteBuf 目标,它的 components 如下:

万字论述 Netty 的 I/O 加快神器:零仿制

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