Nginx开发HTTP模块(七):将磁盘文件作为包体发送



前言

之前在《Nginx开发HTTP模块(六):发送响应》一文中讨论过如何将内存中的数据作为包体发送给客户端,而在发送文件时完全可以先把文件读取到内存中再向用户发送数据,但是这样做会有两个缺点:

  • 为了不阻塞Nginx,每次只能读取并发送磁盘中的少量数据,需要反复持续多次。
  • Linux上高效的sendfile系统调用不需要先把磁盘中的数据读到用户态内存再发送到网络中。
    Nginx封装好了多种接口,以便将磁盘或者缓存中的文件发送给用户。

一、如何发送磁盘中的文件

发送文件时使用的是《Nginx发送HTTP响应》中所介绍的接口:

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

return ngx_http_output_filter(r, &out);

两者不同的地方在于如何设置ngx_buf_t缓冲区。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。

1. 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;

    ngx_int_t                (*thread_handler)(ngx_thread_task_t *task,
                                               ngx_file_t *file);
    void                      *thread_ctx;
    ngx_thread_task_t         *thread_task;

    ngx_event_aio_t           *aio;
	
	// 目前未使用
    unsigned                   valid_info:1;
    // 与配置文件中的directio配置相对应,在发送大文件时可以设为1
    unsigned                   directio:1;
};

2. 发送文件需要做的操作

  • fd是打开文件的句柄描述符,打开文件这一步需要用户自己来做。Nginx简单封装了一个宏来代替open系统调用
    #define ngx_open_file(name, mode, create, access)                            \
    	open((const char *) name, mode|create|O_BINARY, 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 = strlen(filename);
    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方法使用一个宏进行了简单的封装:
    #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偏移量处截止。

二、清理文件句柄

Nginx会异步地将整个文件高效地发送给用户,但是必须要求HTTP框架在响应发送完毕后关闭已经打开的文件句柄,否则将会出现句柄泄漏问题。设置清理文件句柄也很简单,只要定义一个ngx_pool_cleanup_t结构体,将刚得到的文件句柄等信息赋给它,并将Nginx提供的ngx_pool_cleanup_file函数设置到它的handler回调方法中即可。

1. 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方法

2. 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_t类型的参数,只需在ngx_pool_cleanup_t结构体的data成员上赋值即可。

3. ngx_pool_cleanup_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结构体,在内存池中已经为它分配过内存,只有在请求结束时才会释放,因此这里简单地引用fie里的成员即可。清理文件句柄的完整代码如下:
    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_clean_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方法清理资源。

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

  • RFC2616规范中定义了range协议,它可以使得客户端在一次请求中只下载完整文件的某一部分,这样就可支持客户端在开启多个线程的同时下载一份文件,每个线程仅下载完整文件的一部分,最后组成一个完整的文件。
  • range也支持断点续传,只要客户端记录了上次中断时已经下载部分的文件偏移量,就可以要求服务器从断点处发送文件之后的内容。
  • Nginx对range协议的支持非常好,range协议主要增加了一些HTTP头部处理流程,以及发送文件时的偏移量处理。http_range_header_filter模块就是用来处理HTTP请求头部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;
    

总结

本文中主要介绍了Nginx如何将磁盘上的文件作为包体发送,并说明了如何释放文件句柄及如何实现多线程下载和断点续传功能。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值