美文网首页netty程序员Java学习笔记
Netty4(八): 零拷贝 与 CompositeByteBu

Netty4(八): 零拷贝 与 CompositeByteBu

作者: 聪明的奇瑞 | 来源:发表于2018-03-19 17:42 被阅读84次

    什么是零拷贝?

    • 所谓的零拷贝(Zero-copy)就是在操作数据时,不需要将数据 buffer 从一个内存区域拷贝到另一个内存区域,因此少了一次内存的拷贝,CPU 的效率就得到了提升
    • 从 OS 层面上的零拷贝通常指避免 用户态(User-space) 与 内核态(Kernel-space) 之间来回拷贝数据。例如 Linux 提供的 mmap 系统调用,它可以将一段用户空间内存映射到内核空间,当映射成功后,用户对这块内存区域的修改可以直接反应到内核空间,同样,内核空间对这块区域的修改也直接反映到用户空间。正因为有这样的映射关系,我们就不需要再 用户态(User-space) 与 内核态(Kernel-space) 之间拷贝数据,提高数传输的效率
    • 不过,Netty 中的零拷贝与上面所提到的 OS 层面的零拷贝不太一样,Netty 的零拷贝完全是再用户态(Java 层面)的,它的零拷贝更偏向于优化数据操作的概念
    • Netty 中的零拷贝主要体现在如下几方面:
      • Netty 提供了 CompositeByteBuf 类,它可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf,避免各个 ByteBuf 之间的拷贝
      • 通过 wrap 操作,可以将 byte[] 数组、ByteBuf、ByteBuffer 等包装成一个 Netty ByteBuf 对象,进而避免了拷贝操作
      • ByteBuf 支持 slice 操作,因此可以将 ByteBuf 分解为多个共享同一存储区域的 ByteBuf,避免内存拷贝
      • 通过 FileRegion 包装的 FileChannel.tranferTo 实现文件传输,可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统循环 write 方式导致的内存拷贝问题

    通过 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);
    
    • 上面代码中,定义了一个 CompositeByteBuf 对象然后调用 addComponents 方法将 header 与 body 合并为一个逻辑上的 ByteBuf
    public CompositeByteBuf addComponents(boolean increaseWriterIndex, ByteBuf... buffers);
    
    CompositeByteBuf
    • 不过值得注意的是,虽然 CompositeByteBuf 看起来是由两个 ByteBuf 组合而成的,但再其内部,这两个 ByteBuf 是单独存在的,CompositeByteBuf 只是逻辑上是一个整体
    • addComponents(boolean increaseWriterIndex, ByteBuf... buffers) 来添加两个 ByteBuf
      • 其中第一个参数是 true, 表示当添加新的 ByteBuf 时, 自动递增 CompositeByteBuf 的 writeIndex
      • 如果我们调用的是
      compositeByteBuf.addComponents(header, body);
      
      • 那么 compositeByteBuf 的 writeIndex 仍然是0, 因此此时我们就不可能从 compositeByteBuf 中读取到数据, 这一点要特别注意

    通过 wrap 操作实现零拷贝

    • 假设我们有一个 byte 数组,希望把它转换为 ByteBuf 对象以便后续操作,那么传统的做法时将 byte 数组写入到 ByteBuf 中,即:
    byte[] bytes = ...
    ByteBuf byteBuf = Unpooled.buffer();
    byteBuf.writeBytes(bytes);
    
    • 显然这种方式也是有一个额外的拷贝操作,我们可以使用 Unpooled 的相关方法,包装这个 byte 数组来生成一个新的 ByteBuf 实例,而不需要进行拷贝操作
    byte[] bytes = ...
    ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);
    

    通过 slice 操作实现零拷贝

    • slice 操作和 wrap 操作正好相反,Unpooled.wrappedBuffer 可以将多个 ByteBuf 合并成一个,而 slice 操作可以将一个 ByteBuf 切分为多个共享一个内存区域的 ByteBuf 对象,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) 方法可以设置不同的参数来获取到 buf 的不同区域的切片
    ByteBuf byteBuf = ...
    ByteBuf header = byteBuf.slice(0, 5);
    ByteBuf body = byteBuf.slice(5, 10);
    
    • slice 方法产生的 header 和 body 的过程都是没有拷贝操作的,header 和 body 对象再内部其实是共享了 ByteBuf 的存储空间的不同部分而已
    slice

    通过 FileRegion 实现零拷贝

    • 在 Netty 中使用 FileRegion 实现文件传输的零拷贝,不过在底层是依赖于 Java NIO 的 FileChannel.transfer 的零拷贝功能
    • 弱使用最基础的 JAVA IO 实现一个文件的拷贝功能,那么使用传统的方式代码如下
    public static void copyFile(String srcFile, String destFile) throws Exception {
        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();
    }
    
    • 上面是一个典型的读写二进制文件的代码实现,上面的代码中不断从源文件中读取指定长度的数据到 temp 数组中,然后再将 temp 中的内容写入目的文件,这样的拷贝操作对于大文件时,频繁的内存拷贝操作会消耗大量系统资源
    • 下面看一下 JavaNIO 中的 FileChannel 如何实现零拷贝
    public static void copyFileWithFileChannel(String srcFileName, String destFileName) throws Exception {
        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);
    }
    
    • 可以看到使用 FileChannel 后可以直接将源文件的内容拷贝到目的地文件中,不需要再借助一个临时的 buffer,避免了不必要的内存操作
    • 最后看下 Netty 中是怎么使用 FileRegion 来实现零拷贝传输一个文件的
    @Override
    public 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");
    }
    
    • 可以看到第一步是通过 RandomAccessFile 打开了一个文件,然后 Netty 使用了 DefaultFileRegion 来封装一个 FileChannel,即:
    new DefaultFileRegion(raf.getChannel(), 0, length)
    
    • 当有了 FileRegion 后就可以通过它将文件的内容直接写入 Channel 中,而不需要像传统的做法,拷贝文件内容到临时 buffer,再将 buffer 写入 Channel,零拷贝操作对传输大文件有着很大的帮助

    相关文章

      网友评论

        本文标题:Netty4(八): 零拷贝 与 CompositeByteBu

        本文链接:https://www.haomeiwen.com/subject/ipbpqftx.html