零拷贝在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为实际存储数据的ByteBufAbstractUnpooledSlicedByteBuf(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为例:

@Overridepublic ByteBuf getBytes(int index, ByteBuffer dst) { checkIndex0(index, dst.remaining());//检查是否越界 unwrap().getBytes(idx(index), dst); return this;}
@Overridepublic 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是如何实现这一过程的呢?

@Overridepublic 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中的实现(下)的主要内容,如果未能解决你的问题,请参考以下文章

Netty中的零拷贝是怎么实现的?

零拷贝kafka和netty零拷贝在实现机制上的区别

零拷贝kafka和netty零拷贝在实现机制上的区别

六.Netty入门到超神系列-Java NIO零拷贝实战

百万并发「零拷贝」技术系列之经典案例Netty

RocketMq中零拷贝