Netty中的零拷贝与我们传统理解的零拷贝不太一样。
传统的零拷贝指的是数据传输过程中,不需要CPU进行数据的拷贝。主要是数据在用户空间与内核中间之间的拷贝。
发送数据时的实现方法啊
File.read(bytes)
Socket.send(bytes)
这种方式需要四次数据拷贝和四次上下文切换:
- 数据从磁盘读取到内核的read buffer
- 数据从内核缓冲区拷贝到用户缓冲区
- 数据从用户缓冲区拷贝到内核的socket buffer
- 数据从内核的socket buffer拷贝到网卡接口的缓冲区
可以看得到,数据从内核缓冲区拷贝到用户缓冲区,从内核态切换成用户态,然后又拷贝数据到内核(用户态->内核态),这么一来回,其实2,3两步没有必要。
通过调用java的FileChannel.transferTo方法,可以避免上面两次多余的拷贝
- 调用transferTo,数据从文件由DMA引擎拷贝到内核read buffer
- 接着DMA从内核read buffer将数据拷贝到网卡接口buffer
上面的两次操作都不需要CPU参与,所以就达到了零拷贝。
Netty中的零拷贝
Netty中也用到了FileChannel.transferTo方法,所以Netty的零拷贝也包括上面将的操作系统级别的零拷贝。除此之外,在ByteBuf的实现上,Netty也提供了零拷贝的一些实现。
关于ByteBuffer,Netty提供了两个接口:
- ByteBuf
- ByteBufHolder
对于ByteBuf,Netty提供了多种实现:
- Heap ByteBuf:直接在堆内存分配
- Direct ByteBuf:直接在内存区域分配而不是堆内存
- CompositeByteBuf:组合Buffer
Netty的零拷贝体现在三个方面:
-
Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
-
Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。
-
Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。