1. 概述
零拷贝即Zero-copy,就是在操作数据时,不需要将数据buffer从一个内存区域拷贝到另一个内存区域,因为少了内存的拷贝,因此CPU的效率就得到提升。
在OS层面上的零拷贝,通常指避免在用户态与内核态之间来回拷贝数据。例如Linux提供的mmap系统调用,它可以将一段用户空间内存映射到内核空间,当映射成功后,用户对这段内存区域的修改可以直接反映到内核空间;同样的,内核空间对这段区域的修改也直接反映到用户空间。正因为有这样的映射关系,我们就不需要在用户态与内核态之间拷贝数据,提高了数据传输的效率。
需要注意的是,Netty中的Zero-copy与我们上面所提到的OS层面Zero-copy不太一样,Netty的Zero-copy完全是在用户态(Java层面)的,它的Zero-copy的更多是偏向于优化数据操作这样的概念。
Netty的零拷贝体现在如下几个方面:
- Netty提供了CompositeByteBuf类,它可以将多个ByteBUf合并为一个逻辑上的ByteBuf,避免了各个ByteBuf之间的拷贝
- 通过wrap操作,我们可以将byte[]数组、ByteBuf、ByteBuffer等包装成一个Netty ByteBuf对象进而避免了拷贝操作
- ByteBuf支持slice操作,因此可以将ByteBuf分解为多个共享同一存储区域的ByteBuf,避免了内存的拷贝
- 通过FileRegion包装的FileChannel.tranferTo实现文件传输,可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。
2. 通过 CompositeByteBuf 实现零拷贝
假设我们有一份协议数据, 它由头部和消息体组成, 而头部和消息体是分别存放在两个 ByteBuf 中的, 即:
ByteBuf header = ...
ByteBuf body = ...
我们在代码处理中, 通常希望将 header 和 body 合并为一个 ByteBuf, 方便处理, 那么通常的做法是:
ByteBuf allBuf = Unpooled.buffer(header.readableBytes() + body.readableBytes());
allBuf.writeBytes(header);
allBuf.writeBytes(body);
可以看到, 我们将 header 和 body 都拷贝到了新的 allBuf 中了, 这无形中增加了两次额外的数据拷贝操作了.
那么有没有更加高效优雅的方式实现相同的目的呢? 我们来看一下 CompositeByteBuf 是如何实现这样的需求的吧:
ByteBuf header = ...
ByteBuf body = ...
CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer();
compositeByteBuf.addComponents(true, header, body);
addComponents方法将 header 与 body 合并为一个逻辑上的 ByteBuf, 即:
不过需要注意的是, 虽然看起来 CompositeByteBuf 是由两个 ByteBuf 组合而成的, 不过在 CompositeByteBuf 内部, 这两个 ByteBuf 都是单独存在的, CompositeByteBuf 只是逻辑上是一个整体。
上面 CompositeByteBuf 代码还以一个地方值得注意的是, 我们调用addComponents(boolean increaseWriterIndex, ByteBuf… buffers) 来添加两个 ByteBuf, 其中第一个参数是 true, 表示当添加新的 ByteBuf 时, 自动递增 CompositeByteBuf 的 writeIndex,如果我们调用的是:
compositeByteBuf.addComponents(header, body);
那么其实 compositeByteBuf 的 writeIndex 仍然是0, 因此此时我们就不可能从 compositeByteBuf 中读取到数据, 这一点希望大家要特别注意.
除了上面直接使用 CompositeByteBuf 类外, 我们还可以使用 Unpooled.wrappedBuffer 方法, 它底层封装了 CompositeByteBuf 操作, 因此使用起来更加方便:
ByteBuf header = ...
ByteBuf body = ...
ByteBuf allByteBuf = Unpooled.wrappedBuffer