从零手写实现 nginx-10-sendfile 零拷贝 zero-copy

2024-06-07 16:04

本文主要是介绍从零手写实现 nginx-10-sendfile 零拷贝 zero-copy,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

大家好,我是老马。很高兴遇到你。

我们为 java 开发者实现了 java 版本的 nginx

https://github.com/houbb/nginx4j

如果你想知道 servlet 如何处理的,可以参考我的另一个项目:

手写从零实现简易版 tomcat minicat

手写 nginx 系列

如果你对 nginx 原理感兴趣,可以阅读:

从零手写实现 nginx-01-为什么不能有 java 版本的 nginx?

从零手写实现 nginx-02-nginx 的核心能力

从零手写实现 nginx-03-nginx 基于 Netty 实现

从零手写实现 nginx-04-基于 netty http 出入参优化处理

从零手写实现 nginx-05-MIME类型(Multipurpose Internet Mail Extensions,多用途互联网邮件扩展类型)

从零手写实现 nginx-06-文件夹自动索引

从零手写实现 nginx-07-大文件下载

从零手写实现 nginx-08-范围查询

从零手写实现 nginx-09-文件压缩

从零手写实现 nginx-10-sendfile 零拷贝

从零手写实现 nginx-11-file+range 合并

从零手写实现 nginx-12-keep-alive 连接复用

从零手写实现 nginx-13-nginx.conf 配置文件介绍

从零手写实现 nginx-14-nginx.conf 和 hocon 格式有关系吗?

从零手写实现 nginx-15-nginx.conf 如何通过 java 解析处理?

从零手写实现 nginx-16-nginx 支持配置多个 server

什么是零拷贝?

零拷贝(Zero Copy)是一种技术,用于在数据传输过程中减少或消除数据在用户空间和内核空间之间的拷贝次数,从而提高传输效率。

它广泛应用于文件传输、网络通信等场景,尤其是在处理大数据量传输时,零拷贝技术能够显著减少CPU的负载,提高系统性能。

零拷贝的基本原理

通常,数据在从磁盘读取到发送到网络的过程中,需要多次在用户空间和内核空间之间进行拷贝。

零拷贝技术通过减少这些拷贝操作,直接在内核空间内完成数据传输,避免了不必要的数据拷贝。

传统的数据传输流程

  1. 从磁盘读取数据到内核空间:操作系统将文件从磁盘读取到内核空间的缓冲区。
  2. 从内核空间拷贝数据到用户空间:应用程序调用read系统调用,将数据从内核缓冲区拷贝到用户空间的缓冲区。
  3. 从用户空间拷贝数据到内核空间:应用程序调用write系统调用,将数据从用户空间的缓冲区拷贝到内核空间的网络缓冲区。
  4. 从内核空间发送数据到网络:操作系统将数据从网络缓冲区发送到网络接口卡(NIC)。

整个过程涉及多次拷贝操作,增加了CPU和内存带宽的消耗。

零拷贝的数据传输流程

零拷贝技术通过减少数据在用户空间和内核空间之间的拷贝次数,提高数据传输效率。以下是几种常见的零拷贝实现方式:

  1. sendfile系统调用

sendfile是Linux内核提供的系统调用,它允许直接将数据从文件描述符传输到网络套接字,而无需将数据拷贝到用户空间。其工作流程如下:

  • 内核将文件数据从磁盘读取到内核缓冲区。
  • 内核直接将数据从内核缓冲区传输到网络缓冲区,并发送到网络接口卡。

这种方式避免了数据在用户空间和内核空间之间的两次拷贝,提高了传输效率。

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
  1. mmapwrite结合使用

mmap系统调用将文件映射到用户空间的内存地址,通过内存映射,可以减少一次数据拷贝,但仍需一次从用户空间到内核空间的拷贝。工作流程如下:

  • 使用mmap将文件映射到用户空间。
  • 使用write将数据从映射的内存区域拷贝到网络缓冲区。
  1. splice系统调用

splice是Linux 2.6.17引入的系统调用,允许将数据在两个文件描述符之间传输,而无需将数据拷贝到用户空间。其工作流程如下:

  • 内核将文件数据从磁盘读取到内核缓冲区。
  • 内核直接将数据从内核缓冲区传输到另一个文件描述符(例如网络套接字)。
ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);

零拷贝的优点

  • 减少CPU负载:由于减少了数据拷贝的次数,CPU的负载显著降低。
  • 提高传输速度:减少数据在内存中的拷贝操作,能够提高传输速度。
  • 降低延迟:减少数据在用户空间和内核空间之间的切换,提高了数据传输的实时性。

零拷贝的应用场景

  • 大文件传输:如视频文件、日志文件等大文件的网络传输。
  • 高性能服务器:如Web服务器、文件服务器等需要处理大量并发请求的服务器。
  • 数据库系统:如数据库备份、恢复等操作中涉及大数据量传输的场景。

核心代码调整

原始分块

    /*** 分块传输-普通方式* @param context 上下文*/protected void dispatchByRandomAccessFile(NginxRequestDispatchContext context) {final ChannelHandlerContext ctx = context.getCtx();final File targetFile = context.getFile();// 分块传输文件内容long totalLength = targetFile.length();long totalRead = 0;try(RandomAccessFile randomAccessFile = new RandomAccessFile(targetFile, "r")) {ByteBuffer buffer = ByteBuffer.allocate(NginxConst.CHUNK_SIZE);while (true) {int bytesRead = randomAccessFile.read(buffer.array());if (bytesRead == -1) { // 文件读取完毕break;}buffer.limit(bytesRead);// 写入分块数据ctx.write(new DefaultHttpContent(Unpooled.wrappedBuffer(buffer)));buffer.clear(); // 清空缓冲区以供下次使用// process 可以考虑加一个 listenertotalRead += bytesRead;logger.info("[Nginx] file process >>>>>>>>>>> {}/{}", totalRead, totalLength);}// 结果响应ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);//如果不支持keep-Alive,服务器端主动关闭请求if (!HttpUtil.isKeepAlive(context.getRequest())) {lastContentFuture.addListener(ChannelFutureListener.CLOSE);}} catch (Exception e) {logger.error("[Nginx] file meet ex", e);throw new Nginx4jException(e);}}

zero-copy

    /*** Netty 之 FileRegion 文件传输: https://www.jianshu.com/p/447c2431ac32** @param context 上下文*/protected void dispatchByZeroCopy(NginxRequestDispatchContext context) {final ChannelHandlerContext ctx = context.getCtx();final File targetFile = context.getFile();// 分块传输文件内容long totalLength = targetFile.length();try {RandomAccessFile randomAccessFile = new RandomAccessFile(targetFile, "r");FileChannel fileChannel = randomAccessFile.getChannel();// 使用DefaultFileRegion进行零拷贝传输DefaultFileRegion fileRegion = new DefaultFileRegion(fileChannel, 0, totalLength);ChannelFuture transferFuture = ctx.writeAndFlush(fileRegion);// 监听传输完成事件transferFuture.addListener(new ChannelFutureListener() {@Overridepublic void operationComplete(ChannelFuture future) {try {if (future.isSuccess()) {// 传输完毕,发送最后一个空内容,标志传输结束ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);// 如果不支持keep-Alive,服务器端主动关闭请求if (!HttpUtil.isKeepAlive(context.getRequest())) {lastContentFuture.addListener(ChannelFutureListener.CLOSE);}} else {// 处理传输失败logger.error("[Nginx] file transfer failed", future.cause());throw new Nginx4jException(future.cause());}} finally {// 确保在所有操作完成之后再关闭文件通道和RandomAccessFiletry {fileChannel.close();randomAccessFile.close();} catch (Exception e) {logger.error("[Nginx] error closing file channel", e);}}}});// 记录传输进度(如果需要,可以通过监听器或其他方式实现)logger.info("[Nginx] file process >>>>>>>>>>> {}", totalLength);} catch (Exception e) {logger.error("[Nginx] file meet ex", e);throw new Nginx4jException(e);}}

这里要注意,文件信息必须在传输完成后关闭。

因为 operationComplete 这个是异步的,直接 TRW 关闭资源会导致失败。很坑...

DefaultFileRegion

DefaultFileRegion是Netty中实现零拷贝文件传输的一个核心类。

它允许你在不将文件内容复制到用户空间的情况下将文件直接传输到网络,极大地提高了大文件传输的效率。

下面是对DefaultFileRegion的详细介绍,包括其工作原理和使用方法。

DefaultFileRegion的基本介绍

DefaultFileRegion类位于Netty的io.netty.channel包中。它实现了FileRegion接口,主要用于将文件的某个部分直接传输到网络套接字上,利用操作系统的零拷贝功能来提高效率。

工作原理

DefaultFileRegion通过调用操作系统的本地I/O方法(如Linux上的sendfile)实现零拷贝传输。它将数据从文件系统直接传输到网络栈,而不需要经过用户空间,这样可以避免不必要的数据拷贝,减少CPU使用,提高传输性能。

构造方法

public DefaultFileRegion(FileChannel file, long position, long count)
  • file: 要传输的文件的FileChannel
  • position: 文件传输的起始位置。
  • count: 要传输的字节数。

主要方法

  1. transferTo
public long transferTo(WritableByteChannel target, long position) throws IOException

将文件的内容从给定的位置传输到目标WritableByteChannel。这个方法会调用操作系统的底层方法来执行零拷贝。

  1. count
public long count()

返回这个文件区域的字节数。

  1. position
public long position()

返回这个文件区域的起始位置。

使用示例

以下是一个使用DefaultFileRegion进行零拷贝文件传输的示例:

import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.DefaultFileRegion;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.LastHttpContent;import java.io.File;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;public void sendFile(ChannelHandlerContext ctx, File file) {try {RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");FileChannel fileChannel = randomAccessFile.getChannel();long fileLength = file.length();// Create a new DefaultFileRegionDefaultFileRegion fileRegion = new DefaultFileRegion(fileChannel, 0, fileLength);// Send the file region over the channelChannelFuture sendFileFuture = ctx.writeAndFlush(fileRegion);// Add a listener to close the file channel after the send is completesendFileFuture.addListener((ChannelFuture future) -> {fileChannel.close();randomAccessFile.close();});// If the request does not support keep-alive, close the connectionif (!HttpUtil.isKeepAlive(request)) {sendFileFuture.addListener(ChannelFutureListener.CLOSE);}} catch (Exception e) {e.printStackTrace();}
}

注意事项

  1. 文件通道的管理:确保在文件传输完成后正确关闭FileChannelRandomAccessFile,避免资源泄漏。
  2. 异常处理:在传输过程中可能会遇到各种异常(如文件被删除或网络中断),需要进行适当的异常处理。
  3. 线程安全:确保FileChannel在传输过程中不会被其他线程关闭或修改。

适用场景

  • 大文件传输DefaultFileRegion非常适合用于传输大文件,如视频流、日志文件等,因为它能显著降低CPU使用率。
  • 高并发场景:在高并发场景下,减少CPU的拷贝操作能提高系统的整体性能和吞吐量。

总之,DefaultFileRegion是Netty中实现高效文件传输的一个强大工具,通过使用操作系统的零拷贝机制,可以显著提高文件传输的效率。

介绍一下零拷贝

小结

本节我们实现了文件的压缩处理,这个对于文件的传输性能提升比较大。

当然,压缩+解压本身也是对性能有损耗的。要结合具体的压缩比等考虑。

下一节,我们考虑实现一下 cors 的支持。

我是老马,期待与你的下次重逢。

开源地址

为了便于大家学习,已经将 nginx 开源

https://github.com/houbb/nginx4j

这篇关于从零手写实现 nginx-10-sendfile 零拷贝 zero-copy的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1039641

相关文章

C++中unordered_set哈希集合的实现

《C++中unordered_set哈希集合的实现》std::unordered_set是C++标准库中的无序关联容器,基于哈希表实现,具有元素唯一性和无序性特点,本文就来详细的介绍一下unorder... 目录一、概述二、头文件与命名空间三、常用方法与示例1. 构造与析构2. 迭代器与遍历3. 容量相关4

C++中悬垂引用(Dangling Reference) 的实现

《C++中悬垂引用(DanglingReference)的实现》C++中的悬垂引用指引用绑定的对象被销毁后引用仍存在的情况,会导致访问无效内存,下面就来详细的介绍一下产生的原因以及如何避免,感兴趣... 目录悬垂引用的产生原因1. 引用绑定到局部变量,变量超出作用域后销毁2. 引用绑定到动态分配的对象,对象

SpringBoot基于注解实现数据库字段回填的完整方案

《SpringBoot基于注解实现数据库字段回填的完整方案》这篇文章主要为大家详细介绍了SpringBoot如何基于注解实现数据库字段回填的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以了解... 目录数据库表pom.XMLRelationFieldRelationFieldMapping基础的一些代

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

Java AOP面向切面编程的概念和实现方式

《JavaAOP面向切面编程的概念和实现方式》AOP是面向切面编程,通过动态代理将横切关注点(如日志、事务)与核心业务逻辑分离,提升代码复用性和可维护性,本文给大家介绍JavaAOP面向切面编程的概... 目录一、AOP 是什么?二、AOP 的核心概念与实现方式核心概念实现方式三、Spring AOP 的关

Nginx分布式部署流程分析

《Nginx分布式部署流程分析》文章介绍Nginx在分布式部署中的反向代理和负载均衡作用,用于分发请求、减轻服务器压力及解决session共享问题,涵盖配置方法、策略及Java项目应用,并提及分布式事... 目录分布式部署NginxJava中的代理代理分为正向代理和反向代理正向代理反向代理Nginx应用场景

Nginx搭建前端本地预览环境的完整步骤教学

《Nginx搭建前端本地预览环境的完整步骤教学》这篇文章主要为大家详细介绍了Nginx搭建前端本地预览环境的完整步骤教学,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录项目目录结构核心配置文件:nginx.conf脚本化操作:nginx.shnpm 脚本集成总结:对前端的意义很多

Python实现字典转字符串的五种方法

《Python实现字典转字符串的五种方法》本文介绍了在Python中如何将字典数据结构转换为字符串格式的多种方法,首先可以通过内置的str()函数进行简单转换;其次利用ison.dumps()函数能够... 目录1、使用json模块的dumps方法:2、使用str方法:3、使用循环和字符串拼接:4、使用字符

Linux下利用select实现串口数据读取过程

《Linux下利用select实现串口数据读取过程》文章介绍Linux中使用select、poll或epoll实现串口数据读取,通过I/O多路复用机制在数据到达时触发读取,避免持续轮询,示例代码展示设... 目录示例代码(使用select实现)代码解释总结在 linux 系统里,我们可以借助 select、

Linux挂载linux/Windows共享目录实现方式

《Linux挂载linux/Windows共享目录实现方式》:本文主要介绍Linux挂载linux/Windows共享目录实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地... 目录文件共享协议linux环境作为服务端(NFS)在服务器端安装 NFS创建要共享的目录修改 NFS 配