👈👈👈 欢迎点赞收藏关注哟
首先分享之前的所有文章 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164...
Github : 👉 github.com/black-ant
CASE 备份 : 👉 gitee.com/antblack/ca...
一. 前言
Netty 之所以快有很多原因,但是零拷贝一定是其中必不可少的一环。
对于这一块我一直一知半解,这里来尝试学清楚。
零拷贝的目的很简单,主要是为了减少不必要的数据复制操作
,这里的复制其实主要指的是 用户态
到 内核态
的数据转换。
零拷贝是什么?
零拷贝又叫 Zero-Copy , 其用法是在网络文件处理过程中,不需要将文件拷贝到用户空间,而是直接在内核空间中传输到网络中。
从原理上,零拷贝分为内核空间的处理和文件的处理。在内核层面主要是为了避免用户空间到内核空间的数据传输
。
而在文件层面,主要是为了避免内存缓冲区到文件的传输
。
本文概括 :
- 第二节 : 会梳理零拷贝的相关概念,其中最核心的就是零拷贝的几种实现方式
- 第三节 : 主要来分析 Netty 中如何实现的零拷贝,已经我们日常使用这有什么可以借鉴的
二. 零拷贝涉及哪些知识点
2.1 关于内核态和用户态
这个概念在 Java程序员的用户态内核态笔记 这篇文章里面也做过整理。
简单点说 用户的应用通常是处在用户态中的,当需要调用系统硬件资源的时候,用户态不具有那么高的权限,此时就需要切换到内核态进行资源的控制和管理。
- S1 : 当一个请求来临后 ,首先会通过 Socket 底层组件进行硬件交互
- S2 : 然后传到到 Socket 缓冲区 , 此时处在内核态中
- S3 : 然后再从缓冲区复制到对应的用户态中,此时至少会完成2次拷贝操作
2.2 DMA 技术
DMA 叫直接内存访问,DMA 可以 让外部设备(硬盘,网口)在没有 CPU 干预的情况下,直接访问内存。
数据的传输是需要CPU介入的。 例如先从外部设备读取到 CPU 寄存器,再由寄存器到内存。
2.3 零拷贝的实现方案
- 方案一 : 基于虚拟内存
- 原理 : 多个虚拟内存指向同一个物理地址,这样应用缓冲区和内核缓冲区可以映射到一个地方。
- 效果 : 当两个缓冲区映射为一个后,就可以避免用户态和内核态的相互复制
- 方案二 : mmap/write方式
- 原理 : memory map 可以将文件的内容映射到进程的地址空间 , 可以不通过IO(Read/write)直接读取这个映射区域
- 效果 : mmap 可以用于文件映射,共享内存,匿名内存映射
- 方案三 : sendfile 方式
- 前提 :需要系统支持,例如 Unix 系统
- 原理 :直接在内核中进行数据传输,不需要从内核缓冲区复制到用户缓冲区
- 效果 : 和 mmap 一致,本质上是简化了这个过程
-
方案四 : 带有 scatter/gather 的 sendfile方式
- 原理 : 在方案三的基础上去掉了内存缓冲区和Socket缓冲区的复制 , 通过内存映射的方式将两者关联
- 效果 : 可以减少一次 CPU Copy 过程
-
方案五 : Slice 方式
- 原理 :在两个文件描述符之间进行数据传输,而无需在用户空间和内核空间之间复制数据
- 效果 :主要是分割出逻辑切片,该切片(一个新的缓冲区)会与原始缓冲区共享相同的底层数据
三. Netty 对零拷贝的使用
好了,基础的东西就不深入了,想看得更详细的可以看看参考文档里面的推荐。
下面开始深入理解 Netty 的零拷贝 : Netty 中有以下几种方式实现了零拷贝的方法 :
- 特性一 : ByteBuf 可以使用
直接内存
对 Socket 进行读写,避免了与缓冲区之间的拷贝 - 特性二 : ByteBuf 普遍支持
slice
方法,让一个 ByteBuf分解
为多个共享存储的 ByteBuf ,实现零拷贝 - 特性三 : Netty 通过 CompositeByteBuf 将多个 ByteBuf
合并
成一个逻辑上的 ByteBuf ,减少了不同 ByteBuf 之间的拷贝过程 - 特性四 : Netty 通过 DefaultFileRegion 来实现和文件系统的零拷贝,底层实现为
sendfile
- 特性五 : 为了更高效的使用零拷贝,Netty 中还实现了很多方法,例如
Wrap
: 其目的在于将数组直接包装
成 ByteBuf , 这样就可以避免转换数据的拷贝过程
3.1 通过一个案例来看怎么实现 sendfile
java
public static void main(String[] args) {
String filePath = "path/to/your/file.txt";
String host = "localhost";
int port = 8080;
try {
// S1 : 打开文件通道
FileChannel fileChannel = new FileInputStream(filePath).getChannel();
// S2 : 打开套接字通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(host, port));
// S3 : 使用 transferTo 进行零拷贝的文件到套接字传输
long transferredBytes = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
System.out.println("Transferred bytes: " + transferredBytes);
} catch (Exception e) {
System.out.println("执行异常");
}
}
以上就是一个常见的 sendFile 的处理流程,在 transferTo 方法中会调用 sendFile 进行处理 :
DefaultFileRegion 源码逻辑
可以看到,最终通过 DefaultFileRegion 会发起对 FileChannelImpl 的调用,在调用终点即为 Native 方法 :
java
private native long transferTo0(FileDescriptor var1, long var2, long var4, FileDescriptor var6);
3.2 通过一个案例来看怎么实现 Slice
java
public static void main(String[] args) {
ByteBuf originalBuffer = Unpooled.buffer(10);
originalBuffer.writeBytes(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
// 创建切片,共享原始数据的一部分
ByteBuf slicedBuffer = originalBuffer.slice(2, 4);
// 对切片进行修改会影响原始数据
slicedBuffer.setByte(0, 99);
// 打印原始数据
System.out.println(originalBuffer); // 输出: 01 02 63 04 05 06 07 08 09 0A
}
3.3 深入学习一下 CompositeByteBuf 怎么合并
CompositeByteBuf 类的作用主要是将多个 ByteBuf 合并成一个逻辑层面的 ByteBuf
。 这样的好处就是可以避免在多个 ByteBuf 之间进行数据拷贝。
来看一下其中的几个方法 :
addComponent
: 向一个 CompositeByteBuf 中添加一个缓冲区removeComponent
: 从 CompositeByteBuf 中移除指定索引的子缓冲区component
: 返回指定索引处的子缓冲区
java
// 准备一个数组用来存储所有的 Component 对象,该对象包含了一个 ByteBuf
private Component[] components;
// 首先是写入 :这里我屏蔽了一些代码,只看其中最核心的
private int addComponent0(boolean increaseWriterIndex, int cIndex, ByteBuf buffer) {
// S1 : 检查给定的组件索引是否有效,以及组件容量是否为正数
checkComponentIndex(cIndex);
// S2 : 构建一个新的 Component
Component c = newComponent(ensureAccessible(buffer), 0);
// S3 : 将一个新的 Component 对象插入到 components 列表中
addComp(cIndex, c);
// S4 : 更新组件的偏移量 , 用于读写操作
updateComponentOffsets(cIndex)
}
// 其次是读取 : 通过 readBytes 直接读取
public CompositeByteBuf getBytes(int index, ByteBuffer dst) {
// 获得指定的索引位置
int i = toComponentIndex0(index);
// 开始循环遍历
while (length > 0) {
Component c = components[i];
int localLength = Math.min(length, c.endOffset - index);
dst.limit(dst.position() + localLength);
c.buf.getBytes(c.idx(index), dst);
index += localLength;
length -= localLength;
// 下一个索引位
i ++;
}
}
java
public static void main(String[] args) {
// 创建两个 ByteBuf 作为示例子缓冲区
ByteBuf buffer1 = Unpooled.wrappedBuffer(new byte[]{1, 2, 3});
ByteBuf buffer2 = Unpooled.wrappedBuffer(new byte[]{4, 5, 6});
// 创建 CompositeByteBuf,并添加两个子缓冲区
CompositeByteBuf compositeBuffer = Unpooled.compositeBuffer();
compositeBuffer.addComponent(true, buffer1);
compositeBuffer.addComponent(true, buffer2);
// 创建目标数组
byte[] destination = new byte[6];
// 使用 getBytes 方法将数据复制到目标数组
compositeBuffer.getBytes(0, destination, 0, 6);
// 打印复制后的目标数组
System.out.print("Copied bytes: ");
for (byte b : destination) {
System.out.print(b + " ");
}
}
3.4 Wrap 方法如何使用
java
public static void main(String[] args) {
// 创建一个字节数组
byte[] byteArray = {1, 2, 3, 4, 5};
// 使用 ByteBuf 的 wrap 方法将字节数组包装为 ByteBuf
ByteBuf byteBuf = Unpooled.wrappedBuffer(byteArray);
// 打印 ByteBuf 的内容
System.out.println("Original ByteBuf: " + byteBuf.toString());
// 修改 ByteBuf 的内容
byteBuf.setByte(0, 99);
// 打印修改后的字节数组
System.out.print("Modified byte array: ");
for (byte b : byteArray) {
System.out.print(b + " ");
}
}
总结
勉强学了一遍 , 应用也算了解了,如有错误欢迎指正。
参考文档
👉 推荐看看,受益很多 : 一文彻底弄懂零拷贝原理 - 掘金 (juejin.cn)