《深入理解Nginx:模块开发与架构解析》一3.8 将磁盘文件作为包体发送

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
日志服务 SLS,月写入数据量 50GB 1个月
简介: 本节书摘来自华章出版社《深入理解Nginx:模块开发与架构解析》一书中的第3章,第3.8节,作者 陶辉,更多章节内容可以访问云栖社区“华章计算机”公众号查看

3.8 将磁盘文件作为包体发送

上文讨论了如何将内存中的数据作为包体发送给客户端,而在发送文件时完全可以先把文件读取到内存中再向用户发送数据,但是这样做会有两个缺点:
为了不阻塞Nginx,每次只能读取并发送磁盘中的少量数据,需要反复持续多次。
Linux上高效的sendfile系统调用不需要先把磁盘中的数据读取到用户态内存再发送到网络中。
当然,Nginx已经封装好了多种接口,以便将磁盘或者缓存中的文件发送给用户。

3.8.1 如何发送磁盘中的文件

发送文件时使用的是3.7节中所介绍的接口。例如:

ngx_chain_t out;
out.buf = b;
out.next = NULL;

return ngx_http_output_filter(r, &out);

两者不同的地方在于如何设置ngx_buf_t缓冲区。在3.2.5节中介绍过,ngx_buf_t有一个标志位in_file,将in_file置为1就表示这次ngx_buf_t缓冲区发送的是文件而不是内存。调用ngx_http_output_filter后,若Nginx检测到in_file为1,将会从ngx_buf_t缓冲区中的file成员处获取实际的文件。file的类型是ngx_file_t,下面看一下ngx_file_t的结构。

typedef struct ngx_file_s ngx_file_t;
struct ngx_file_s {
    //文件句柄描述符
    ngx_fd_t fd;
    //文件名称
    ngx_str_t name;
    //文件大小等资源信息,实际就是Linux系统定义的stat结构
    ngx_file_info_t info;
/*该偏移量告诉Nginx现在处理到文件何处了,一般不用设置它,Nginx框架会根据当前发送状态设置它*/
off_t offset;
   //当前文件系统偏移量,一般不用设置它,同样由Nginx框架设置
   off_t sys_offset;

   //日志对象,相关的日志会输出到log指定的日志文件中
   ngx_log_t *log;
   //目前未使用
   unsigned valid_info:1;
   //与配置文件中的directio配置项相对应,在发送大文件时可以设为1
   unsigned directio:1;
};

fd是打开文件的句柄描述符,打开文件这一步需要用户自己来做。Nginx简单封装了一个宏用来代替open系统的调用,如下所示。

#define ngx_open_file(name, mode, create, access) \
    open((const char *) name, mode|create, access)

实际上,ngx_open_file与open方法的区别不大,ngx_open_file返回的是Linux系统的文件句柄。对于打开文件的标志位,Nginx也定义了以下几个宏来加以封装。

#define NGX_FILE_RDONLY O_RDONLY
#define NGX_FILE_WRONLY O_WRONLY
#define NGX_FILE_RDWR O_RDWR
#define NGX_FILE_CREATE_OR_OPEN O_CREAT
#define NGX_FILE_OPEN 0
#define NGX_FILE_TRUNCATE O_CREAT|O_TRUNC
#define NGX_FILE_APPEND O_WRONLY|O_APPEND
#define NGX_FILE_NONBLOCK O_NONBLOCK

#define NGX_FILE_DEFAULT_ACCESS 0644
#define NGX_FILE_OWNER_ACCESS 0600

因此,在打开文件时只需要把文件路径传递给name参数,并把打开方式传递给mode、create、access参数即可。例如:

ngx_buf_t *b;
b = ngx_palloc(r->pool, sizeof(ngx_buf_t));

u_char* filename = (u_char*)"/tmp/test.txt";
b->in_file = 1;
b->file = ngx_pcalloc(r->pool, sizeof(ngx_file_t));
b->file->fd = ngx_open_file(filename, NGX_FILE_RDONLY|NGX_FILE_NONBLOCK, NGX_FILE_OPEN, 0);
b->file->log = r->connection->log;
b->file->name.data = filename;
b->file->name.len = sizeof(filename)-1;
if (b->file->fd <= 0)
{
    return NGX_HTTP_NOT_FOUND;
}

到这里其实还没有结束,还需要告知Nginx文件的大小,包括设置响应中的Content-Length头部,以及设置ngx_buf_t缓冲区的file_pos和file_last。实际上,通过ngx_file_t结构里ngx_file_info_t类型的info变量就可以获取文件信息:

typedef struct stat ngx_file_info_t;

Nginx不只对stat数据结构做了封装,对于由操作系统中获取文件信息的stat方法,Nginx也使用一个宏进行了简单的封装,如下所示:

#define ngx_file_info(file, sb)  stat((const char *) file, sb)

因此,获取文件信息时可以先这样写:

if (ngx_file_info(filename, &b->file->info) == NGX_FILE_ERROR) {
    return NGX_HTTP_INTERNAL_SERVER_ERROR;
}

之后必须要
设置Content-Length头部:

r->headers_out.content_length_n = b->file->info.st_size;

还需要设置ngx_buf_t缓冲区的file_pos和file_last:

b->file_pos = 0;
b->file_last = b->file->info.st_size;

这里是告诉Nginx从文件的file_pos偏移量开始发送文件,一直到达file_last偏移量处截止。
注意 当磁盘中有大量的小文件时,会占用Linux文件系统中过多的inode结构,这时,成熟的解决方案会把许多小文件合并成一个大文件。在这种情况下,当有需要时,只要把上面的file_pos和file_last设置为合适的偏移量,就可以只发送合并大文件中的某一块内容(原来的小文件),这样就可以大幅降低小文件数量。

3.8.2 清理文件句柄

Nginx会异步地将整个文件高效地发送给用户,但是我们必须要求HTTP框架在响应发送完毕后关闭已经打开的文件句柄,否则将会出现句柄泄露问题。设置清理文件句柄也很简单,只需要定义一个ngx_pool_cleanup_t结构体(这是最简单的方法,HTTP框架还提供了其他方式,在请求结束时回调各个HTTP模块的cleanup方法,将在第11章介绍),将我们刚得到的文件句柄等信息赋给它,并将Nginx提供的ngx_pool_cleanup_file函数设置到它的handler回调方法中即可。首先介绍一下ngx_pool_cleanup_t结构体。

typedef struct ngx_pool_cleanup_s  ngx_pool_cleanup_t;

struct ngx_pool_cleanup_s {
//执行实际清理资源工作的回调方法
    ngx_pool_cleanup_pt   handler;
    //handler回调方法需要的参数
    void *data;
    //下一个ngx_pool_cleanup_t清理对象,如果没有,需置为NULL
    ngx_pool_cleanup_t *next;
};

设置好handler和data成员就有可能要求HTTP框架在请求结束前传入data成员回调handler方法。接着,介绍一下专用于关闭文件句柄的ngx_pool_cleanup_file方法。

void ngx_pool_cleanup_file(void *data)
{
    ngx_pool_cleanup_file_t  *c = data;

    ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, c->log, 0, "file cleanup: fd:%d",c->fd);

    if (ngx_close_file(c->fd) == NGX_FILE_ERROR) {
        ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
                      ngx_close_file_n " \"%s\" failed", c->name);
    }
}

ngx_pool_cleanup_file的作用是把文件句柄关闭。从上面的实现中可以看出,ngx_pool_cleanup_file方法需要一个ngx_pool_cleanup_file_t类型的参数,那么,如何提供这个参数呢?在ngx_pool_cleanup_t结构体的data成员上赋值即可。下面介绍一下ngx_pool_cleanup_file_t的结构。

typedef struct {
    //文件句柄
    ngx_fd_t fd;
    //文件名称
    u_char *name;
    //日志对象
    ngx_log_t *log;
} ngx_pool_cleanup_file_t;

可以看到,ngx_pool_cleanup_file_t中的对象在ngx_buf_t缓冲区的file结构体中都出现过了,意义也是相同的。对于file结构体,我们在内存池中已经为它分配过内存,只有在请求结束时才会释放,因此,这里简单地引用file里的成员即可。清理文件句柄的完整代码如下。

ngx_pool_cleanup_t* cln = ngx_pool_cleanup_add(r->pool, sizeof(ngx_pool_cleanup_file_t));
if (cln == NULL) {
    return NGX_ERROR;
}

cln->handler = ngx_pool_cleanup_file;
ngx_pool_cleanup_file_t  *clnf = cln->data;

clnf->fd = b->file->fd;
clnf->name = b->file->name.data;
clnf->log = r->pool->log;

ngx_pool_cleanup_add用于告诉HTTP框架,在请求结束时调用cln的handler方法清理资源。
至此,HTTP模块已经可以向客户端发送文件了。下面介绍一下如何支持多线程下载与断点续传。

3.8.3 支持用户多线程下载和断点续传

RFC2616规范中定义了range协议,它给出了一种规则使得客户端可以在一次请求中只下载完整文件的某一部分,这样就可支持客户端在开启多个线程的同时下载一份文件,其中每个线程仅下载文件的一部分,最后组成一个完整的文件。range也支持断点续传,只要客户端记录了上次中断时已经下载部分的文件偏移量,就可以要求服务器从断点处发送文件之后的内容。
Nginx对range协议的支持非常好,因为range协议主要增加了一些HTTP头部处理流程,以及发送文件时的偏移量处理。在第1章中曾说过,Nginx设计了HTTP过滤模块,每一个请求可以由许多个HTTP过滤模块处理,而http_range_header_filter模块就是用来处理HTTP请求头部range部分的,它会解析客户端请求中的range头部,最后告知在发送HTTP响应包体时将会调用到的ngx_http_range_body_filter_module模块,该模块会按照range协议修改指向文件的ngx_buf_t缓冲区中的file_pos和file_last成员,以此实现仅发送一个文件的部分内容到客户端。
其实,支持range协议对我们来说很简单,只需要在发送前设置ngx_http_request_t的成员allow_ranges变量为1即可,之后的工作都会由HTTP框架完成。例如:

r->allow_ranges = 1;

这样,我们就支持了多线程下载和断点续传功能。

相关文章
|
4月前
|
运维 持续交付 云计算
深入解析云计算中的微服务架构:原理、优势与实践
深入解析云计算中的微服务架构:原理、优势与实践
153 3
|
4月前
|
运维 监控 负载均衡
探索微服务架构下的服务治理:动态服务管理平台深度解析
探索微服务架构下的服务治理:动态服务管理平台深度解析
|
12天前
|
网络协议 Linux Go
自己动手编写tcp/ip协议栈1:tcp包解析
学习tuntap中的tun的使用方法,并使用tun接口解析了ip包和tcp包,这是实现tcp/ip协议栈的第一步。
44 15
|
19天前
|
存储 人工智能 并行计算
2025年阿里云弹性裸金属服务器架构解析与资源配置方案
🚀 核心特性与技术创新:提供100%物理机性能输出,支持NVIDIA A100/V100 GPU直通,无虚拟化层损耗。网络与存储优化,400万PPS吞吐量,ESSD云盘IOPS达100万,RDMA延迟<5μs。全球部署覆盖华北、华东、华南及海外节点,支持跨地域负载均衡。典型应用场景包括AI训练、科学计算等,支持分布式训练和并行计算框架。弹性裸金属服务器+OSS存储+高速网络综合部署,满足高性能计算需求。
|
2月前
|
XML Java 开发者
Spring底层架构核心概念解析
理解 Spring 框架的核心概念对于开发和维护 Spring 应用程序至关重要。IOC 和 AOP 是其两个关键特性,通过依赖注入和面向切面编程实现了高效的模块化和松耦合设计。Spring 容器管理着 Beans 的生命周期和配置,而核心模块为各种应用场景提供了丰富的功能支持。通过全面掌握这些核心概念,开发者可以更加高效地利用 Spring 框架开发企业级应用。
84 18
|
3月前
|
运维 监控 持续交付
微服务架构解析:跨越传统架构的技术革命
微服务架构(Microservices Architecture)是一种软件架构风格,它将一个大型的单体应用拆分为多个小而独立的服务,每个服务都可以独立开发、部署和扩展。
673 36
微服务架构解析:跨越传统架构的技术革命
|
22天前
|
传感器 监控 安全
智慧工地云平台的技术架构解析:微服务+Spring Cloud如何支撑海量数据?
慧工地解决方案依托AI、物联网和BIM技术,实现对施工现场的全方位、立体化管理。通过规范施工、减少安全隐患、节省人力、降低运营成本,提升工地管理的安全性、效率和精益度。该方案适用于大型建筑、基础设施、房地产开发等场景,具备微服务架构、大数据与AI分析、物联网设备联网、多端协同等创新点,推动建筑行业向数字化、智能化转型。未来将融合5G、区块链等技术,助力智慧城市建设。
|
3月前
|
存储 Linux API
深入探索Android系统架构:从内核到应用层的全面解析
本文旨在为读者提供一份详尽的Android系统架构分析,从底层的Linux内核到顶层的应用程序框架。我们将探讨Android系统的模块化设计、各层之间的交互机制以及它们如何共同协作以支持丰富多样的应用生态。通过本篇文章,开发者和爱好者可以更深入理解Android平台的工作原理,从而优化开发流程和提升应用性能。
|
4月前
|
弹性计算 持续交付 API
构建高效后端服务:微服务架构的深度解析与实践
在当今快速发展的软件行业中,构建高效、可扩展且易于维护的后端服务是每个技术团队的追求。本文将深入探讨微服务架构的核心概念、设计原则及其在实际项目中的应用,通过具体案例分析,展示如何利用微服务架构解决传统单体应用面临的挑战,提升系统的灵活性和响应速度。我们将从微服务的拆分策略、通信机制、服务发现、配置管理、以及持续集成/持续部署(CI/CD)等方面进行全面剖析,旨在为读者提供一套实用的微服务实施指南。
|
4月前
|
存储 监控 API
深入解析微服务架构及其在现代应用中的实践
深入解析微服务架构及其在现代应用中的实践
110 12

推荐镜像

更多