零拷贝在Netty中的实现(下)
Posted 罗小为
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了零拷贝在Netty中的实现(下)相关的知识,希望对你有一定的参考价值。
点击上方“罗小为”,选择“置顶公众号”
技术文章第一时间送达
上篇文章())介绍了在Netty中使用CompositeByteBuf和DirectByteBuf实现了零拷贝。本篇则重点分析下ByteBuf中的slice操作和FileRegion包装类的FileChannel.transferTo实现以及Wrap操作是如何实现零拷贝的。
通过slice切片来实现
slice操作的优势在于多个ByteBuf的引用可以指向同一个共享存储区域的ByteBuf。使其表象上看起来多次slice操作生成了多个缓冲实例,实际只是在同一块内存空间上各自对应的索引的不同。
ByteBuf提供了两个slice操作方法:
public ByteBuf slice();
public ByteBuf slice(int index, int length);
不带参数的slice方法等同于buf.slice(buf.readerIndex(),buf.readableBytes())调用,即返回buf中可读部分的切片。而slice(int index,int length)方法相对灵活,依据参数的不同从而获得不同的切片。从拷贝的角度来分析,整个过程是没有数据迁移操作的。结构示意图如下:
(这一点与Go的slice异曲同工,只不过Go把这么优秀的特性直接暴露给了用户)。在Netty中AbstractUnpooledSliceByteBuf用到了这一特性:
// 切片ByteBuf的构造函数,其中字段adjustment为切片ByteBuf相对于被切片ByteBuf的偏移
// 量,两个ByteBuf共用一块内存空间,字段buffer为实际存储数据的ByteBuf
AbstractUnpooledSlicedByteBuf(ByteBuf buffer, int index, int length) {
super(length);
checkSliceOutOfBounds(index, length, buffer);//检查slice是否越界
if (buffer instanceof AbstractUnpooledSlicedByteBuf) {
// 如果被切片ByteBuf也是AbstractUnpooledSlicedByteBuf对象
this.buffer = ((AbstractUnpooledSlicedByteBuf) buffer).buffer;
adjustment = ((AbstractUnpooledSlicedByteBuf) buffer).adjustment + index;
} else if (buffer instanceof DuplicatedByteBuf) {
// 如果被切片ByteBuf为DuplicatedByteBuf对象,则
// 用unwrap得到实际存储数据的ByteBuf赋值buffer
this.buffer = buffer.unwrap();
adjustment = index;
} else {
// 如果被切片ByteBuf为一般ByteBuf对象,则直接赋值buffer
this.buffer = buffer;
adjustment = index;
}
initLength(length);
writerIndex(length);
}
上面为AbstractUnpooledSliceByteBuf的构造函数,下面是其对ByteBuf接口的实现,以getBytes为例:
public ByteBuf getBytes(int index, ByteBuffer dst) {
checkIndex0(index, dst.remaining());//检查是否越界
unwrap().getBytes(idx(index), dst);
return this;
}
public ByteBuf unwrap() {
return buffer;
}
private int idx(int index) {
return index + adjustment;
}
可以看到,是直接在封装的ByteBuf上取的数据,只不过重新计算了索引值,加上了相对偏移量adjustment。
通过FileRegion实现
Netty中使用FileRegion实现文件传输的零拷贝功能,其底层的实现是依据Java NIO的FileChannel.transfer来实现的。
首先从最基础的 Java IO 开始. 假设我们希望实现一个文件拷贝的功能, 那么使用传统的方式, 我们有如下实现:
public static void copyFile(String srcFile, String destFile) throws Exception {
long startTime = System.currentTimeMillis();
byte[] temp = new byte[1024];
FileInputStream in = new FileInputStream(srcFile);
FileOutputStream out = new FileOutputStream(destFile);
int length;
while ((length = in.read(temp)) != -1) {
out.write(temp, 0, length);
}
in.close();
out.close();
long endTime = System.currentTimeMillis();
System.out.println("time:" + (endTime - startTime) + "ms");
}
下面再看下用Java NIO的FileChannel的实现:
public static void copyFileWithFileChannel(String srcFileName, String destFileName) throws Exception {
long startTime = System.currentTimeMillis();
RandomAccessFile srcFile = new RandomAccessFile(srcFileName, "r");
FileChannel srcFileChannel = srcFile.getChannel();
RandomAccessFile destFile = new RandomAccessFile(destFileName, "rw");
FileChannel destFileChannel = destFile.getChannel();
long position = 0;
long count = srcFileChannel.size();
srcFileChannel.transferTo(position, count, destFileChannel);
long endTime = System.currentTimeMillis();
System.out.println("time:" + (endTime - startTime) + "ms");
}
可以看到,FileChannel的方式并没有生成temp数组的一步,直接将源文件的内容拷贝到目标文件,避免了不必要的内存操作。那么Netty中的FileRegion是如何实现这一过程的呢?
void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
RandomAccessFile raf = null;
long length = -1;
try {
// 1. 通过 RandomAccessFile 打开一个文件.
raf = new RandomAccessFile(msg, "r");
length = raf.length();
} catch (Exception e) {
ctx.writeAndFlush("ERR: " + e.getClass().getSimpleName() + ": " + e.getMessage() + '\n');
return;
} finally {
if (length < 0 && raf != null) {
raf.close();
}
}
ctx.write("OK: " + raf.length() + '\n');
if (ctx.pipeline().get(SslHandler.class) == null) {
// SSL not enabled - can use zero-copy file transfer.
// 2. 调用 raf.getChannel() 获取一个 FileChannel.
// 3. 将 FileChannel 封装成一个 DefaultFileRegion
ctx.write(new DefaultFileRegion(raf.getChannel(), 0, length));
} else {
// SSL enabled - cannot use zero-copy file transfer.
ctx.write(new ChunkedFile(raf));
}
ctx.writeAndFlush("\n");
}
可以看到,Netty用RandomAccessFile打开一个文件后,直接用DefaultFileRegion来封装了FileChannel,覆写了其中的transferTo方法,省去了临时buffer的拷贝过程,从而实现零拷贝。
public abstract long transferTo(long position, long count,WritableByteChannel target) throws IOException;
通过wrap操作来实现
如有一个byte数组,我们希望将它转换为一个 ByteBuf 对象, 以便于后续的操作, 那么传统的做法是将此 byte 数组拷贝到 ByteBuf 中, 即:
byte[] bytes = ...
ByteBuf byteBuf = Unpooled.buffer();
byteBuf.writeBytes(bytes);
显然这样的方式也是有一个额外的拷贝操作的,我们可以使用Unpooled的相关方法,包装这个byte数组,生成一个新的ByteBuf实例,而不需要进行拷贝操作。上面的代码可以改为:
byte[] bytes = ...
ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);
可以看到, 我们通过Unpooled.wrappedBuffer方法来将bytes包装成为一个UnpooledHeapByteBuf对象,而在包装的过程中,是不会有拷贝操作的。 即最后我们生成的ByteBuf对象是和bytes数组共用了同一个存储空间,对bytes的修改也会反映到ByteBuf对象中。
以上是关于零拷贝在Netty中的实现(下)的主要内容,如果未能解决你的问题,请参考以下文章