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模块去处理。
2 |
{ ngx_string( "Range" ), offsetof(ngx_http_headers_in_t, range), |
3 |
ngx_http_process_header_line }, |
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变量。
2 |
static ngx_http_output_header_filter_pt ngx_http_next_header_filter; |
5 |
ngx_http_range_header_filter_init(ngx_conf_t *cf) |
7 |
ngx_http_next_header_filter = ngx_http_top_header_filter; |
8 |
ngx_http_top_header_filter = ngx_http_range_header_filter; |
13 |
ngx_http_send_header(ngx_http_request_t *r) |
16 |
r->headers_out.status = r->err_status; |
17 |
r->headers_out.status_line.len = 0; |
20 |
return ngx_http_top_header_filter(r); |
这样,在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的时间,是否相等,如果不相等,表示服务器的文件比客户端已经下载的不一样了,需要重新下载全部文件。
2 |
ngx_http_range_header_filter(ngx_http_request_t *r) |
7 |
ngx_http_range_filter_ctx_t *ctx; |
9 |
if (r->http_version < NGX_HTTP_VERSION_10 |
10 |
|| r->headers_out.status != NGX_HTTP_OK |
12 |
|| r->headers_out.content_length_n == -1 |
15 |
return ngx_http_next_header_filter(r); |
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) |
27 |
if (r->headers_in.if_range && r->headers_out.last_modified_time != -1) { |
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); |
34 |
if (if_range != r->headers_out.last_modified_time) { |
之后便需要解析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完成。
2 |
rc = ngx_http_range_parse(r, ctx); |
5 |
ngx_http_set_ctx(r, ctx, ngx_http_range_body_filter_module); |
7 |
r->headers_out.status = NGX_HTTP_PARTIAL_CONTENT; |
8 |
r->headers_out.status_line.len = 0; |
10 |
if (ctx->ranges.nelts == 1) { |
11 |
return ngx_http_range_singlepart_header(r, ctx); |
14 |
return ngx_http_range_multipart_header(r, ctx); |
17 |
if (rc == NGX_HTTP_RANGE_NOT_SATISFIABLE) { |
18 |
return ngx_http_range_not_satisfiable(r); |
对于单个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 …
2 |
ngx_http_range_singlepart_header(ngx_http_request_t *r, ngx_http_range_filter_ctx_t *ctx) |
4 |
ngx_table_elt_t *content_range; |
5 |
ngx_http_range_t *range; |
7 |
content_range = ngx_list_push(&r->headers_out.headers); |
8 |
if (content_range == NULL) { |
11 |
r->headers_out.content_range = content_range; |
12 |
content_range->hash = 1; |
13 |
ngx_str_set(&content_range->key, "Content-Range" ); |
15 |
content_range->value.data = ngx_pnalloc(r->pool, sizeof ( "bytes -/" ) - 1 + 3 * NGX_OFF_T_LEN); |
16 |
if (content_range->value.data == NULL) { |
20 |
range = ctx->ranges.elts; |
22 |
content_range->value.len = ngx_sprintf(content_range->value.data, |
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; |
27 |
return ngx_http_next_header_filter(r); |
但是对于一次请求多个区块的请求,其处理相对复杂。因为返回的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,也就是上面黄色背景的部分,这部分是通用的,可以在组成给客户端的返回数据的时候重复使用。
2 |
ngx_http_range_multipart_header(ngx_http_request_t *r, |
3 |
ngx_http_range_filter_ctx_t *ctx) |
7 |
ngx_http_range_t *range; |
8 |
ngx_atomic_uint_t boundary; |
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; |
15 |
if (r->headers_out.charset.len) { |
16 |
len += sizeof ( "; charset=" ) - 1 + r->headers_out.charset.len; |
19 |
ctx->boundary_header.data = ngx_pnalloc(r->pool, len); |
20 |
if (ctx->boundary_header.data == NULL) { |
23 |
boundary = ngx_next_temp_number(0); |
32 |
if (r->headers_out.charset.len) { |
33 |
ctx->boundary_header.len = ngx_sprintf(ctx->boundary_header.data, |
35 |
"Content-Type: %V; charset=%V" CRLF |
36 |
"Content-Range: bytes " , |
38 |
&r->headers_out.content_type, |
39 |
&r->headers_out.charset) |
40 |
- ctx->boundary_header.data; |
42 |
r->headers_out.charset.len = 0; |
然后组成”Content-Type: multipart/byteranges; boundary=”这一行头部数据,放到r->headers_out.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); |
6 |
if (r->headers_out.content_type.data == NULL) { |
9 |
r->headers_out.content_type_lowcase = NULL; |
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; |
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去处理。
2 |
ngx_http_range_body_filter(ngx_http_request_t *r, ngx_chain_t *in) |
4 |
ngx_http_range_filter_ctx_t *ctx; |
7 |
return ngx_http_next_body_filter(r, in); |
9 |
ctx = ngx_http_get_module_ctx(r, ngx_http_range_body_filter_module); |
11 |
return ngx_http_next_body_filter(r, in); |
13 |
if (ctx->ranges.nelts == 1) { |
14 |
return ngx_http_range_singlepart_body(r, ctx, in); |
19 |
if (ngx_buf_special(in->buf)) { |
20 |
return ngx_http_next_body_filter(r, in); |
23 |
if (ngx_http_range_test_overlapped(r, ctx, in) != NGX_OK) { |
27 |
return ngx_http_range_multipart_body(r, ctx, in); |
单个range的情况由ngx_http_range_singlepart_body完成,其相对也简单,循环扫描每个in参数的buf链表,如果其在range里面,就将其放到out的链表中,组成一个新的链表,然后调用下一个filter。下面看一下有改动的函数。
1 |
static ngx_int_t ngx_http_range_singlepart_body(ngx_http_request_t *r, ngx_http_range_filter_ctx_t *ctx, ngx_chain_t *in) |
5 |
ngx_chain_t *out, *cl, **ll; |
6 |
ngx_http_range_t *range; |
10 |
range = ctx->ranges.elts; |
13 |
for (cl = in; cl; cl = cl->next) { |
18 |
last = ctx->offset + ngx_buf_size(buf); |
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) { |
23 |
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection-> log , 0, "http range body skip" ); |
25 |
buf->file_pos = buf->file_last; |
31 |
if (range->start > start) { |
33 |
buf->file_pos += range->start - start; |
35 |
if (ngx_buf_in_memory(buf)) { |
36 |
buf->pos += ( size_t ) (range->start - start); |
40 |
if (range->end <= last) { |
42 |
buf->file_last -= last - range->end; |
44 |
if (ngx_buf_in_memory(buf)) { |
45 |
buf->last -= ( size_t ) (last - range->end); |
59 |
return ngx_http_next_body_filter(r, out); |
对于多个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