Nginx断点续传功能代码浅析-Range模块

HTTP 的Content-Range支持对于一般的网页处理没啥重要的作用,但是对于大文件的下载,CDN回源,点续传功能的作用是非常重要的。

Content-Range允许一次只下载一个文件的一部分,后面再分批次下载文件的其他部分,或者并发下载,提高下载速度,这样如果在下载一个文件的过程中,网络断开了,恢复后不需要重新下载。

nginx 对Content-Range的支持包括header处理和body处理,分别用来解析客户端发送过来的Range header 和裁剪返回给客户端的请求数据Body。其实现分别由2个filter过滤模块完成,分别是ngx_http_range_header_filter_module和ngx_http_range_body_filter_module。下面分别介绍。

零、解析客户端发送的请求Range头

这部分很简单,nginx通过在ngx_http_headers_in请求头处理数组中增加对Range,If-Range的处理函数,从而读取到客户端发送的HEADER的时候,调用对应的函数,将请求数据设置到对应的字段上去。比如对于Range,处理函数为ngx_http_process_header_line ,Range值会放入ngx_http_headers_in_t 的 range字段上面。以便后续filter模块去处理。

1ngx_http_headers_in
2    { ngx_string("Range"), offsetof(ngx_http_headers_in_t, range),
3                 ngx_http_process_header_line },
4 
5    { ngx_string("If-Range"),
6                 offsetof(ngx_http_headers_in_t, if_range),
7                 ngx_http_process_unique_header_line },

一、Header 过滤函数

ngx_http_range_header_filter_module模块回调函数中,只设置了一个ngx_http_range_header_filter_init,用来在nginx filter链表中挂载当前模块的header filter函数。这里说一下nginx对于filter函数链表的组织形式,相当巧妙:利用static 文件局部变量来保存下一个链表节点元素,类似堆栈的方式一个个将后面链接的文件组织起来,达到代码非常简单优美的效果。看下面就知道了。

ngx_http_top_header_filter为全局变量,对于ngx_http_range_filter_module.c文件来说,是static 全局变量,也就是只在本文件有效。多个filter里面各有一份这样的static变量。

1//静态局部变量,用来保存下一个过滤器的函数指针。nginx通常做法,用这种方式来建立一调filter链
2static ngx_http_output_header_filter_pt  ngx_http_next_header_filter;
3 
4static ngx_int_t
5ngx_http_range_header_filter_init(ngx_conf_t *cf)
6{//组成filter链表
7    ngx_http_next_header_filter = ngx_http_top_header_filter;
8    ngx_http_top_header_filter = ngx_http_range_header_filter;
9    return NGX_OK;
10}
11 
12ngx_int_t
13ngx_http_send_header(ngx_http_request_t *r)
14{
15    if (r->err_status) {//如果有错误发生,则发送错误状态。
16        r->headers_out.status = r->err_status;
17        r->headers_out.status_line.len = 0;
18    }
19    //第一个是ngx_http_not_modified_header_filter,后面一个个往后掉用。每个函数独立一个文件,那里面有个静态变量记录它的上一个filter是谁。
20    return ngx_http_top_header_filter(r);//最后一个是ngx_http_header_filter_module模块里面的发送模块
21}

这样,在nginx需要给客户端发送头部header的时候,会调用ngx_http_send_header函数,而其代码很简单,只是调用了ngx_http_top_header_filter,因此,如果header range filter是最后一个挂载的header filter模块的话,ngx_http_top_header_filter链表头节点就等于ngx_http_range_header_filter,也就是最先调用ngx_http_range_header_filter函数进行header过滤处理。

ngx_http_range_header_filter函数用来给客户端返回的header中插入”Accept-Ranges:bytes”头,以及解析客户端发送过来的Range头,其首先检查一下客户端是否支持Range,如果发送了If-Range,则需要检查一下last_modified_time的时间,是否相等,如果不相等,表示服务器的文件比客户端已经下载的不一样了,需要重新下载全部文件。

1static ngx_int_t
2ngx_http_range_header_filter(ngx_http_request_t *r)
3{//ngx_http_top_header_filter链的函数指针,用来过滤发送给客户端的头部数据。
4//ngx_http_send_header函数会调用这里。
5    time_t                        if_range;
6    ngx_int_t                     rc;
7    ngx_http_range_filter_ctx_t  *ctx;
8 
9    if (r->http_version < NGX_HTTP_VERSION_10
10        || r->headers_out.status != NGX_HTTP_OK
11        || r != r->main
12        || r->headers_out.content_length_n == -1
13        || !r->allow_ranges)
14    {//不处理非200的请求,否则直接调用下一个。
15        return ngx_http_next_header_filter(r);
16    }
17 
18    if (r->headers_in.range == NULL
19        || r->headers_in.range->value.len < 7
20        || ngx_strncasecmp(r->headers_in.range->value.data,
21                           (u_char *) "bytes=", 6)
22           != 0)
23    {//如果客户端发送过来的头部数据中没有range字段,则我们也不需要处理range。直接返回即可
24        goto next_filter;
25    }
26 
27    if (r->headers_in.if_range && r->headers_out.last_modified_time != -1) {
28        //语法为If-Range = "If-Range" ":" ( entity-tag | HTTP-date )  后面带的是时间,也就如果修改时间等于XX的话,就给我我没有的,否则给我所有的。
29        if_range = ngx_http_parse_time(r->headers_in.if_range->value.data, r->headers_in.if_range->value.len);
30        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
31                       "http ir:%d lm:%d", if_range, r->headers_out.last_modified_time);
32//If-Range的意思是:“如果entity没有发生变化,那么把我缺失的部分发送给我。
33//如果entity发生了变化,那么把整个entity发送给我”。
34        if (if_range != r->headers_out.last_modified_time) {
35            goto next_filter;//时间跟服务器上的时间不相等,需要返回所有的。所以略过
36        }
37    }

之后便需要解析Range头了。解析后的开始-结束区块会放入ctx->ranges数组中,用来在发送body的时候拼接http multipart 数据。这个工作主要由ngx_http_range_parse函数进行,其会解析字符串的Range格式数据,”Range: bytes=0-1024″请求一块 , “Range:bytes=500-600,601-999”代表客户端请求2块。ngx_http_range_parse不多介绍了,其主要就是做字符串解析。结果会放到ctx->ranges里面。后面拼接返回数据的时候会需要的。

解析请求头完成后,需要准备返回给客户端的头部数据,其实这里还不能拼接返回数据,只是准备了一下相关结构。如果客户端只请求了一块,则由ngx_http_range_singlepart_header完成,否则由ngx_http_range_multipart_header完成。

1//解析range的语法,在本文件开头有介绍。解析出开始,结束等。结果range放入ctx->ranges里面
2    rc = ngx_http_range_parse(r, ctx);
3 
4    if (rc == NGX_OK) {
5        ngx_http_set_ctx(r, ctx, ngx_http_range_body_filter_module);
6        //修改发送的头部字段为206
7        r->headers_out.status = NGX_HTTP_PARTIAL_CONTENT;
8        r->headers_out.status_line.len = 0;
9 
10        if (ctx->ranges.nelts == 1) {//单个RANGE
11            return ngx_http_range_singlepart_header(r, ctx);
12        }
13        //多个range,比如"Range: bytes=500-600,601-999"
14        return ngx_http_range_multipart_header(r, ctx);
15    }
16 
17    if (rc == NGX_HTTP_RANGE_NOT_SATISFIABLE) {
18        return ngx_http_range_not_satisfiable(r);
19    }

对于单个range请求,比较简单,只需要在返回的头部增加“Content-Range: bytes START-END/SIZE”头,数据就放在body里面即可,不需要用multipart的方式组织数据。单个range请求的返回数据形如:

* “HTTP/1.0 206 Partial Content” CRLF
* … header …
* “Content-Type: image/jpeg” CRLF
* “Content-Length: SIZE” CRLF
* “Content-Range: bytes START-END/SIZE” CRLF
* CRLF
* … data …

1static ngx_int_t
2ngx_http_range_singlepart_header(ngx_http_request_t *r, ngx_http_range_filter_ctx_t *ctx)
3{//客户端只请求了一个range,那么我们返回的数据格式为: Content-Range: bytes START-END/SIZE" CRLF
4    ngx_table_elt_t   *content_range;
5    ngx_http_range_t  *range;
6 
7    content_range = ngx_list_push(&r->headers_out.headers);//在输出头里面申请一个header line
8    if (content_range == NULL) {
9        return NGX_ERROR;
10    }
11    r->headers_out.content_range = content_range;
12    content_range->hash = 1;
13    ngx_str_set(&content_range->key, "Content-Range");
14 
15    content_range->value.data = ngx_pnalloc(r->pool, sizeof("bytes -/") - 1 + 3 * NGX_OFF_T_LEN);//申请三个足够长度的数字的字符串
16    if (content_range->value.data == NULL) {
17        return NGX_ERROR;
18    }
19    /* "Content-Range: bytes SSSS-EEEE/TTTT" header */
20    range = ctx->ranges.elts;
21//设置START-END/SIZE格式。
22    content_range->value.len = ngx_sprintf(content_range->value.data,
23                                           "bytes %O-%O/%O",
24                                           range->start, range->end - 1, r->headers_out.content_length_n) - content_range->value.data;
25    r->headers_out.content_length_n = range->end - range->start;//本次返回的数据长度。总长度在SIZE上面
26//·····
27    return ngx_http_next_header_filter(r);
28}

但是对于一次请求多个区块的请求,其处理相对复杂。因为返回的body数据无法直接放在BODY里面发送,需要组织成multipart 的形式才行了。这个由ngx_http_range_multipart_header函数搞定。先看一下多个range请求时返回数据的格式:

“HTTP/1.0 206 Partial Content” CRLF
… header …
Content-Type: multipart/byteranges; boundary=0123456789″ CRLF
CRLF//头部结束
CRLF
“–0123456789″ CRLF
“Content-Type: image/jpeg” CRLF
“Content-Range: bytes START0-END0/SIZE” CRLF
CRLF
… data …
CRLF
“–0123456789″ CRLF
“Content-Type: image/jpeg” CRLF
“Content-Range: bytes START1-END1/SIZE” CRLF
CRLF
… data …
CRLF
“–0123456789–” CRLF

ngx_http_range_multipart_header函数首先调用ngx_next_temp_number获取一个临时数字当做boundary,然后组成boundary_header,也就是上面黄色背景的部分,这部分是通用的,可以在组成给客户端的返回数据的时候重复使用。

1static ngx_int_t
2ngx_http_range_multipart_header(ngx_http_request_t *r,
3    ngx_http_range_filter_ctx_t *ctx)
4{
5    size_t              len;
6    ngx_uint_t          i;
7    ngx_http_range_t   *range;
8    ngx_atomic_uint_t   boundary;
9 
10    len = sizeof(CRLF "--") - 1 + NGX_ATOMIC_T_LEN
11          + sizeof(CRLF "Content-Type: ") - 1
12          + r->headers_out.content_type.len
13          + sizeof(CRLF "Content-Range: bytes ") - 1;
14 
15    if (r->headers_out.charset.len) {
16        len += sizeof("; charset=") - 1 + r->headers_out.charset.len;
17    }
18 
19    ctx->boundary_header.data = ngx_pnalloc(r->pool, len);
20    if (ctx->boundary_header.data == NULL) {
21        return NGX_ERROR;
22    }
23    boundary = ngx_next_temp_number(0);
24    /*
25     * The boundary header of the range:
26     * CRLF
27     * "--0123456789" CRLF
28     * "Content-Type: image/jpeg" CRLF
29     * "Content-Range: bytes "
30     */
31//拼接bondery头,也就是--xxxxx以及后面的那几行头部数据。头部数据之后就是bonder的数据部分了。
32    if (r->headers_out.charset.len) {
33        ctx->boundary_header.len = ngx_sprintf(ctx->boundary_header.data,
34                                           CRLF "--%0muA" CRLF
35                                           "Content-Type: %V; charset=%V" CRLF
36                                           "Content-Range: bytes ",
37                                           boundary,
38                                           &r->headers_out.content_type,
39                                           &r->headers_out.charset)
40                                   - ctx->boundary_header.data;
41 
42        r->headers_out.charset.len = 0;
43 
44    }
45//···

然后组成”Content-Type: multipart/byteranges; boundary=”这一行头部数据,放到r->headers_out.content_type上面。

1//这是最开始那一行声明下面是mutipart 格式数据的行:"Content-Type: multipart/byteranges; boundary=0123456789" CRLF
2//下面的sizeof其实没必要用全部字符串,Content-Type:不需要。
3    r->headers_out.content_type.data =
4        ngx_pnalloc(r->pool, sizeof("Content-Type: multipart/byteranges; boundary=") - 1 + NGX_ATOMIC_T_LEN);
5 
6    if (r->headers_out.content_type.data == NULL) {
7        return NGX_ERROR;
8    }
9    r->headers_out.content_type_lowcase = NULL;
10    /* "Content-Type: multipart/byteranges; boundary=0123456789" */
11    r->headers_out.content_type.len =
12                           ngx_sprintf(r->headers_out.content_type.data,
13                                       "multipart/byteranges; boundary=%0muA", boundary)
14                           - r->headers_out.content_type.data;
15 
16    r->headers_out.content_type_len = r->headers_out.content_type.len;

然后就需要一个个将range的开始,结束部分字符串进行拼接了,也就是”SSSS-EEEE/TTTT” CRLF CRLF”,结果放到range[i].content_range.data上面,用来在后面body过滤函数里面组成返回body。之后就是调用下一个header filter过滤哈数ngx_http_next_header_filter。

到这里头部数据的过滤函数完毕了,总结一下就是:

1.调用ngx_http_range_parse解析客户端请求头:“Range: bytes=0-1024”。
2.如果是单个Range,调用ngx_http_range_singlepart_header拼接”Content-Range: bytes START-END/SIZE”头。
3.如果是多个Range请求,调用ngx_http_range_multipart_header准备multipart数据,以便后面body filter使用。

二、BODY 过滤函数

类似header过滤函数的处理方式,nginx挂载了body过滤函数ngx_http_range_body_filter,用来在ngx_http_output_filter里面需要给客户端发送HTTP BODY的时候调用。

当该函数很简单,就是判断一下ctx->ranges.nelts的数量,如果是单个Range请求,就调用ngx_http_range_singlepart_body完成工作,否则判断一下是否所有的ranges都在第一块buf里面,nginx目前只支持所有的ranges都在一块buf里面的情况,并且也只支持所有的都在buf链表的的一块buf里面。所以支持的比较鸡肋。如果条件满足,就调用ngx_http_range_body_filter去处理。

1static ngx_int_t
2ngx_http_range_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
3{//这是第一个BODY过滤函数
4    ngx_http_range_filter_ctx_t  *ctx;
5 
6    if (in == NULL) {
7        return ngx_http_next_body_filter(r, in);
8    }
9    ctx = ngx_http_get_module_ctx(r, ngx_http_range_body_filter_module);
10    if (ctx == NULL) {
11        return ngx_http_next_body_filter(r, in);
12    }
13    if (ctx->ranges.nelts == 1) {//如果只有一个range
14        return ngx_http_range_singlepart_body(r, ctx, in);
15    }
16    /*下面说明,NGINX只支持整个数据都在一个buffer里面的情况也就是多个body它不好处理
17     * multipart ranges are supported only if whole body is in a single buffer
18     */
19    if (ngx_buf_special(in->buf)) {
20        return ngx_http_next_body_filter(r, in);
21    }
22    //检查是否所有的RANGE都在第一块buf里面。那么在后面的buf里面不行么,不行,nginx不支持
23    if (ngx_http_range_test_overlapped(r, ctx, in) != NGX_OK) {
24        return NGX_ERROR;
25    }
26 
27    return ngx_http_range_multipart_body(r, ctx, in);
28}

单个range的情况由ngx_http_range_singlepart_body完成,其相对也简单,循环扫描每个in参数的buf链表,如果其在range里面,就将其放到out的链表中,组成一个新的链表,然后调用下一个filter。下面看一下有改动的函数。

1static ngx_int_t ngx_http_range_singlepart_body(ngx_http_request_t *r, ngx_http_range_filter_ctx_t *ctx, ngx_chain_t *in)
2{//处理客户端只发送一个range过来的情况
3    off_t              start, last;
4    ngx_buf_t         *buf;
5    ngx_chain_t       *out, *cl, **ll;
6    ngx_http_range_t  *range;
7 
8    out = NULL;
9    ll = &out;
10    range = ctx->ranges.elts;
11//对in参数的要发送出去的缓冲数据链表,一一检查其内容是否在range的start-end之间,
12//不在的就丢弃,只剪裁出区间之内的,发送给客户端。
13    for (cl = in; cl; cl = cl->next) {
14 
15        buf = cl->buf;
16 
17        start = ctx->offset;//在所有数据中的位置。
18        last = ctx->offset + ngx_buf_size(buf);
19 
20        ctx->offset = last;
21        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "http range body buf: %O-%O", start, last);
22        if (range->end <= start || range->start >= last) {//丢弃这个。将其pos指向last从而略过。
23            ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "http range body skip");
24            if (buf->in_file) {
25                buf->file_pos = buf->file_last;
26            }
27            buf->pos = buf->last;
28            buf->sync = 1;
29            continue;
30        }
31        if (range->start > start) {//开头重合,去掉重合部分
32            if (buf->in_file) {
33                buf->file_pos += range->start - start;//向后移动开始部分
34            }
35            if (ngx_buf_in_memory(buf)) {//如果在内存的话就移动指针
36                buf->pos += (size_t) (range->start - start);
37            }
38        }
39 
40        if (range->end <= last) {//尾部重合,去掉尾部
41            if (buf->in_file) {
42                buf->file_last -= last - range->end;
43            }
44            if (ngx_buf_in_memory(buf)) {
45                buf->last -= (size_t) (last - range->end);
46            }
47            buf->last_buf = 1;//标记为最后一块内存,后面的都不需要了。
48            *ll = cl;
49            cl->next = NULL;//直接剪断,后面可能还有输出链的,但是没事,等这个连接关闭后,数据都回收了的。
50            break;
51        }
52        *ll = cl;//后移动。
53        ll = &cl->next;
54    }
55    //到这里后,out变量所指向的链表里面的数据都是range之间的,需要发送给客户端的数据。于是调用下一个filter进行发送。
56    if (out == NULL) {
57        return NGX_OK;
58    }
59    return ngx_http_next_body_filter(r, out);
60}

对于多个range的情况,首先由ngx_http_range_test_overlapped函数判断一下是否in参数的buf是最后一块,如果是最后一块那可以支持,否则,如果所有的ranges都在第一块buf里面,那也支持。其他都不能处理。
ngx_http_range_multipart_body的工作是组成mutipart格式的数据,调用下一个过滤器发送给客户端。具体不啰嗦了。
总结一下,nginx对于多个ranges的请求支持目前还不是让人满意,所以需要使用的同学建议只使用单个Range请求的方式。否则就会出现诡异的500错误而不得善终。不过好消息是:PHP没有cached的请求是不支持range的,image不支持,目前只有static请求是支持的。但是,static请求静态文件的话,一般都是整个文件都在一块buf结构里面,所以多个range也就一定在一块也是第一块buf里面,所以结论是:nginx 对range的支持能够满足大部分需求。


原文出处: http://chenzhenianqing.cn/articles/926.html?utm_source=tuicool&utm_medium=referral

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
nginx是一个高性能的Web服务器和反向代理服务器,它支持多种功能模块,其中之一就是dav模块。dav模块nginx的一个扩展模块,它提供了WebDAV(Web-based Distributed Authoring and Versioning)功能,使得nginx可以作为一个WebDAV服务器来处理文件的上传、下载和管理。 断点续传是指在文件传输过程中,如果传输中断或者出现错误,可以从中断的地方继续传输,而不需要重新开始传输整个文件。nginx的dav模块支持断点续传功能,可以通过以下步骤来实现: 1. 配置nginx的dav模块:在nginx的配置文件中,添加dav模块的配置项,例如: ``` location /dav { dav_methods PUT DELETE MKCOL COPY MOVE; create_full_put_path on; dav_access user:rw group:rw all:r; } ``` 这个配置将启用dav模块,并设置了支持的HTTP方法和访问权限。 2. 启用断点续传:在需要支持断点续传的location中,添加`client_body_temp_path`和`client_max_body_size`配置项,例如: ``` location /dav/upload { dav_methods PUT; create_full_put_path on; dav_access user:rw group:rw all:r; client_body_temp_path /path/to/temp; client_max_body_size 0; } ``` 这个配置将设置临时文件路径和最大上传文件大小。 3. 使用断点续传:客户端可以使用HTTP的PUT方法来上传文件,并在请求头中添加`Content-Range`字段来指定上传的起始位置,例如: ``` PUT /dav/upload/file.txt HTTP/1.1 Host: example.com Content-Length: 1000 Content-Range: bytes 500-1499/2000 <file data> ``` 这个请求将从文件的第500字节开始上传,总共上传1000字节。 通过以上配置和使用方法,nginx的dav模块可以实现断点续传功能,使得文件的上传和下载更加可靠和高效。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值