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_charfilename=(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设置为合适的偏移量,就可以只发送合并大文件中的某一块内容(原来的小文件),这样就可以大幅降低小文件数量。