Netty框架学习之路(七)—— 零拷贝

前言

今天参加了一个面试,面试官问了关于对Netty中零拷贝的理解,答得不是很完美,因此回来后仔细研究了一下。

零拷贝 Zero-Copy

首先,零拷贝不是Netty特有的机制,传统意义上的零拷贝指的是在操作数据时,不需要将数据 buffer 从一个内存区域拷贝到另一个内存区域,因为少了一次内存的拷贝,因此 CPU 的效率就得到的提升。这是一种在 OS 层面上的 Zero-copy,目的是避免在用户态(User-space)与内核态(Kernel-space) 之间来回拷贝数据。

简单来讲,当我们需要从磁盘读取数据并发送至网络上的接受者的时候,传统的方式会经历四次拷贝,四次上下文切换,Java 类库通过 java.nio.channels.FileChannel 中的 transferTo() 方法来在 Linux 和 UNIX 系统上支持零拷贝。具体可参加此文

但是 Netty 中的 Zero-copy 与上述的 Zero-copy 不太一样。Netty 中的零拷贝完全是在用户态的,它的零拷贝的更多的是偏向于优化数据操作 。

直接内存 Direct Buffers

Netty 的接收和发送 ByteBuffer 优先采用 Direct Buffers,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝。

CompositeByteBuf

我们知道在基于流的传输过程(如TCP/IP)中,数据包有可能会被重新封装在不同的数据包中,而单个的数据包对应用而言是没有意义的,只有当这些数据包组成一条完整的消息时才能做出正确的处理。为此我们可能会将这些零碎的数据包拼接成一个新的 ByteBuf 来处理,这其中无形之中会增加两次额外的数据拷贝操作了。

Netty 使用 CompositeByteBuf 类实现了将多个 ByteBuf 合并为一个逻辑上的 ByteBuf。我们看一下 CompositeByteBuf 的内部结构,

    private final ByteBufAllocator alloc;
    private final boolean direct;
    private final List<Component> components;
    private final int maxNumComponents;
    private static final class Component {
        final ByteBuf buf;
        final int length;
        int offset;
        int endOffset;

        Component(ByteBuf buf) {
            this.buf = buf;
            length = buf.readableBytes();
        }

        void freeIfNecessary() {
            buf.release(); 
        }
    }

我们看到在 CompositeByteBuf 内部有一个包含Component 类型对象的list,而 Component 内部包含一个对 ByteBuf 对象的引用。虽然看起来 CompositeByteBuf 是由多个 ByteBuf 组合而成的,不过在 CompositeByteBuf 内部,这些个 ByteBuf 都是单独存在的,CompositeByteBuf 只是逻辑上是一个整体,这样就避免了数据的拷贝,实现了零拷贝。

FileRegion

Netty 文件传输类 DefaultFileRegion 通过 transferTo 方法将文件发送至目标 Channel 中,Netty 官网上有如下代码,

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");
}

Netty 使用了 DefaultFileRegion 来封装一个 FileChannel,然后就可以直接通过它将文件的内容直接写入 Channel 中,而不需要像传统方式拷贝文件内容到临时 buffer,然后再将 buffer 写入 Channel。

总结

本文主要介绍 Netty 中为实现零拷贝所做的处理,跟传统意义上的零拷贝有一定差异,希望读者在实际理解中有所区分。

猜你喜欢

转载自blog.csdn.net/tjreal/article/details/80088139