【Netty的零拷贝是怎样实现的】

2023-12-13 11:34:29

快速理解

在操作系统中,零copy指的是避免在用户态(User-space) 与内核态(Kernel-space)之间来回copy数据。

前面我有介绍什么是零拷贝: 什么是零拷贝

而Netty的零拷贝模型和操作系统中的零拷贝模型并不完全一样。它主要指的是在操作数据的时间,不需要将数据buffer从一个内存区域拷贝到另一个内存区域。少了一次内存的拷贝,CPU就会得到提升。

Netty的零拷贝主要体现在以下5个方面:

1直接使用堆外内存
避免JVM堆内存到堆外内存的数据拷贝。
2CompositeByteBuffer类
可以组合多个Buffer对象合并成一个逻辑上的对象,避免通过传统内存拷贝的方式将几个Buffer合并成一个大的Buffer。
3Unpooled.wrappedBuffer
通过Unpooled.wrappedBuffer可以将byte数组包装成ByteBuf对象,包装过程中不会产生内存拷贝。
4ByteBuf.slice
ByteBuf.slice操作与Unpooled.wrappedBuffer相反,slice操作可以将一个ByteBuf对象切分为多个ByteBuf对象,切分过程中不会产生内存拷贝,底层共享一个byte数组的存储空间。
5使用FileRegion实现文件传输
FileRegion底层封装了FileChannel#transferTO()方法,可以将文件缓冲区的数据之间传输到目标Channel,避免内核缓冲区到用户缓冲区之间的数据拷贝,这属于操作系统级别的零拷贝。

拓展

堆外内存

我们都知道,Java在将数据发送出去的时候,会现将数据从堆内存拷贝到堆外内存,然后才会将堆外内存再拷贝到内核态,进行消息的收发,代码如下:

static int write(FileDescriptor paramFileDescriptor, ByteBuffer paramByteBuffer, long paramLong, NativeDispatcher paramNativeDispatcher ) throws IOException {

    //如果是直接内存,则直接写入
    if((paramByteBuffer instanceof DirectBuffer)) {
        return writeFormNativeBuffer(paramFileDescriptor , paramByteBuffer , paramLong , paramNativeDispatcher );
    }
    //……否则,先把数据拷贝到直接内存中
    ByteBuffer localByteBuffer = Util.getTemporaryDirectBuffer(k);
    try{
        localByteBuffer.put(paramByteBuffer);
        localByteBuffer.filp();
        paramByteBuffer.position(i);
        int m = writeFormNativeBuffer(paramFileDescriptor , localByteBuffer , paramLong , paramNativeDispatcher );
    }
}

所以,,我们发现,假如我们在收发报文的时间使用直接内存,那么就可以减少一次内存拷贝,Netty就是这么做的。

Netty在通信层进行字节流的接收和发送的时候,如果应用允许Unsafe访问,则会采用DirectByteBuf进行转换,也就是堆外的直接内存,代码如下:

public ByteBuf ioBuffer (int initialCapacity) {
    if (PlatformDependent . hasUnsafe() || isDirectBufferPooled()) {
        return directBuffer(initialCapacity);
    }
    return heapBuffer(initialCapacity);
}

CompositeByteBuffer

考虑一种场景,当一个数据包被拆成了两个字节流通过TCP传输过来以后,那么对于接收者的机器来说,为了方便解析,它需要新建一个ByteeBuf将这两个字节流重组成一个新的数据包,如图:

在这里插入图片描述
那么这种情况下,我们如果直接将两个字节流拷贝到一个新的字节流中,显然会浪费空间和时间,所以Netty推出了CompositeByteBuffer,专门用于拷贝ByteBuf。看图:
在这里插入图片描述
从图中我们可以看到,实际的Buf还是两个,只不过Netty通过CompositeByteBuffer将老的buf通过指针组合映射到新的Buf中,减少了一次拷贝过程。

Unpooled.wrappedBuffer

Unpooled.wrappedBuffer 是创建 CompositeByteBuffer 对象的另一种推荐做法。

Unpooled.wrappedBuffer 方法可以将不同的数据源的一个或者多个数据包装成一个大的Buf对象,其中数据源的类型包括 byte[] 、ByteBuf、ByteBuffer。包装的过程中不会发生数据拷贝操作,包装后生成的ByteBuf对象和原始ByteBuf对象是共享底层的byte数组。

ByteBuf.slice

ByteBuf.slice 和 Unpooled.wrappedBuffer 的逻辑正好相反,ByteBuf.slice 是将一个ByteBuf对象切分成多个共享同一个底层存储的ByteBuf对象。

使用FileRegion文件传输

Netty使用FileRegion实现文件传输的零copy,而FileRegion其实是基于Java底层的。

FileChannel#tranferTo()方法实现的。它可以根据操作系统直接将文件缓冲区的数据发送到目标Channel,底层借助了sendFile能力避免传统通过循环write方式导致的内核拷贝问题。所以FileRegion 是操作系统级别的零拷贝。

JavaDoc的注释如下:

This method is potentially much more efficient than a simple loop that reads from this channel and writes to the target channel .
Many operating systems can transfer bytes directly from thefilesystem cache to the target channel without actually copying them.

sendFile

JDK原生的FileChannel#tranferTo()方法其实是基于Linux的sendFile方法,通过该方法,数据可以直接在内核空间内部进行I/O传输,从而省去了数据在用户空间和内核空间之间的来回拷贝。工作原理如下图:
在这里插入图片描述

文章来源:https://blog.csdn.net/Java_Yangxiaoyuan/article/details/134945034
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。