有些时候客户端可能只需要请求nginx服务器上的部分数据, 例如: 我们在看电影时, 常常拖动快进条,跳到指定的位置开始观看。 这其实是nginx的断点续传功能, 从指定位置开始观看,相当于向nginx服务器请求某个位置开始的以后的内容。假设nginx服务器上有一个文件,文件的内容为: "0123456789abcdef"一共16个字节。如果客户端只需要2345共4个字节的数据, 则可以在http请求头部加上range字段, 格式为: range: bytes=2-5;       这样nginx服务器就只会把2345返回给客户端,而不会发送整个文件的数据。当然也可以指定多个区间块,例如: range: bytes=2-5,  8-10   则将会把2345以及89a发送给客户端。

        nginx断点续传功能是由ngx_http_range_filter_module实现的。其实这个模块是由两个模块组成的,一个为ngx_http_range_header_filter_module, 用于设置http响应的头部信息,例如: 设置content-range,指定应答的区间块开始结束位置; 设置content-length, 指定断点续传时的应答包体大小; 设置206响应码而不是200响应码等等。 另一个模块为ngx_http_range_body_filter_module, 用于从缓冲区中查找指定区间块内容,并把这个区间块的内容发给客户端。

一、ngx_http_range_header_filter_module模块分析

        断点续传时,这个模块用来设置发给客户端的响应头部信息。 例如:设置content-range,指定应答的区间块开始结束位置; 设置content-length, 指定断点续传时的应答包体大小; 设置206响应码而不是200响应码等等。以此同时,这个模块还会把断点续传的区间块保存起来,这样在发送响应数据给客户端时,就能够根据这些区间块,找到相应的数据并发给客户端。这个模块的入口函数为: ngx_http_range_header_filter, 看下这个函数的实现。

//功能: 1、判断是否需要进行断点续传,如果不需要则交给下一个过滤模块处理
// 2、保存各个区间块,使得发送响应数据时,能够知道要发送哪些区间的数据给客户端
// 3、断点续传时,设置http响应头部,例如设置content-range,content-length
static ngx_int_t ngx_http_range_header_filter(ngx_http_request_t *r)
{
time_t if_range;
ngx_http_core_loc_conf_t *clcf;
ngx_http_range_filter_ctx_t *ctx;
//不支持断点续传则跳到下一个过滤模块处理
if (r->http_version < NGX_HTTP_VERSION_10
|| r->headers_out.status != NGX_HTTP_OK
|| r != r->main
|| r->headers_out.content_length_n == -1
|| !r->allow_ranges)
{
return ngx_http_next_header_filter(r);
}

//创建断点续传模块上下文结构
ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_range_filter_ctx_t));
ngx_array_init(&ctx->ranges, r->pool, 1, sizeof(ngx_http_range_t));

//函数功能,解析http请求头部的range字段,将多个区间保存到ctx->ranges数组中。
switch (ngx_http_range_parse(r, ctx, clcf->max_ranges))
{
case NGX_OK:
//保存这个模块的上下文结构,在发送区间块数据时,才能知道需要发送哪些区间的数据给客户端
ngx_http_set_ctx(r, ctx, ngx_http_range_body_filter_module);
//206响应码
r->headers_out.status = NGX_HTTP_PARTIAL_CONTENT;
r->headers_out.status_line.len = 0;
//处理只有一个区间块情况
if (ctx->ranges.nelts == 1)
{
return ngx_http_range_singlepart_header(r, ctx);
}
//处理多个区间块
return ngx_http_range_multipart_header(r, ctx);
}

return ngx_http_next_header_filter(r);
}

        ngx_http_range_parse函数用于解析http请求头部的range字段,将多个区间保存到ctx->ranges数组中, 例如:range:byte=0-499,500-1000,1000- 则将这三个区间保存到数组中。函数的实现很简单,就是解析range头部, 找到每一个区间块的开始位置、结束位置。 细看下代码就可以看懂,这里就不罗列代码了。

        ngx_http_range_singlepart_header用于设置单个区间块的响应头部信息,看下函数的实现:

//获取一个区间块, 创建一个contnet-range应答头部,并给这个头部设置值。  
//格式为: content-range: bites 30-200/1000 以上为在总大小为1000字节的文件中,
//获取30---200这区间的内容
static ngx_int_t ngx_http_range_singlepart_header(ngx_http_request_t *r,
ngx_http_range_filter_ctx_t *ctx)
{
//创建content-range应答头部空间,并加入到响应头部链表中
content_range = ngx_list_push(&r->headers_out.headers);
r->headers_out.content_range = content_range;
ngx_str_set(&content_range->key, "Content-Range");

content_range->value.data = ngx_pnalloc(r->pool,sizeof("bytes -/") - 1 + 3 * NGX_OFF_T_LEN);

range = ctx->ranges.elts;
//设置content-range的内容
content_range->value.len = ngx_sprintf(content_range->value.data,
"bytes %O-%O/%O",
range->start, range->end - 1,
r->headers_out.content_length_n)
- content_range->value.data;

//重新设置应答包体的长度, 为区间块的长度
r->headers_out.content_length_n = range->end - range->start;
}

         ngx_http_range_multipart_header函数用于在多个区间块中,设置响应包体的公共部分,以及根据各个区间块计算出响应包体的总大小。 看下代码的实现:

//功能: 1、设置所有区间块的数据总大小
// 2、设置每一个区间块的公共响应数据部分,例如: Content-Type:xxx, Content-Range: bytes等
// 3、生成一个随机数
//公共的响应头部数据格式为:
/*****************************************************************
* CRLF
* "--0123456789" CRLF
* "Content-Type: image/jpeg" CRLF
* "Content-Range: bytes "
*****************************************************************/
static ngx_int_t ngx_http_range_multipart_header(ngx_http_request_t *r,
ngx_http_range_filter_ctx_t *ctx)
{
//生成一个随机数
boundary = ngx_next_temp_number(0);

//获取每个块的功能头部
ctx->boundary_header.len = ngx_sprintf(ctx->boundary_header.data,
CRLF "--%0muA" CRLF
"Content-Type: %V" CRLF
"Content-Range: bytes ",
boundary,
&r->headers_out.content_type)
- ctx->boundary_header.data;

//设置类型响应头部字段
r->headers_out.content_type.len =
ngx_sprintf(r->headers_out.content_type.data,
"multipart/byteranges; boundary=%0muA",
boundary)
- r->headers_out.content_type.data;
r->headers_out.content_type_len = r->headers_out.content_type.len;

//根据每一个区块,计算应答包体总大小
range = ctx->ranges.elts;
for (i = 0; i < ctx->ranges.nelts; i++)
{
range[i].content_range.len = ngx_sprintf(range[i].content_range.data,
"%O-%O/%O" CRLF CRLF,
range[i].start, range[i].end - 1,
r->headers_out.content_length_n)
- range[i].content_range.data;

len += ctx->boundary_header.len + range[i].content_range.len
+ (size_t) (range[i].end - range[i].start);
}

//包体长度
r->headers_out.content_length_n = len;

return ngx_http_next_header_filter(r);
}

二、ngx_http_range_body_filter_module模块分析

        这个模块用于根据区间块的位置,在缓冲区中找到数据并发送给客户端。请求一个区间块的数据与请求多个区间块的数据处理方式是不一样的。下面分别看下这两个情况下是如何处理的。

static ngx_int_t ngx_http_range_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
//处理一个区间块
if (ctx->ranges.nelts == 1)
{
return ngx_http_range_singlepart_body(r, ctx, in);
}

//处理多个区间块
return ngx_http_range_multipart_body(r, ctx, in);
}

        (1)请求单个区间块的数据

        考虑下,如果给客户端响应的应答链表由5个数据结点组成,每一个数据结点都存放了要发给客户端的数据, 则刚开始的内存布局如下;

nginx断点续传_数据

        当客户端指定了range, 只请求这个应答链表中的部分数据时,则可能的内存布局如下图:  从图中可以看出第一个数据结点不需要发给客户端,该缓冲区是没有用了,因此将pos与last都指向缓冲区的末尾。最后一个数据结点也是不需要发送给客户端, 但并没有将pos与last指向缓冲区的末尾,因为这个结点会从链表中脱离(可以看下一张图),因此可以不需要把pos与last都指向缓冲区的末尾。

nginx断点续传_客户端_02

        只有中间的三个结点中的数据需要发送给客户端,内存布局如下:  可以看出这三个区间块中的最后一个结点的next指针为null

nginx断点续传_数据_03

        对于没有发送给客户端的数据节点,这些节点没有用了,待请求关闭时,这些数据节点会被回收。看下函数ngx_http_range_singlepart_body是如何从缓冲区中查找单个数据区间,并把这个数据区间的内容发送给客户端的。

//单个块应答包体处理逻辑
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)
{
ngx_chain_t *out, *cl, **ll;
ll = &out;
range = ctx->ranges.elts;

//遍历所有的链表节点,查看数据的开始,结束位置
for (cl = in; cl; cl = cl->next)
{
buf = cl->buf;
start = ctx->offset;
last = ctx->offset + ngx_buf_size(buf);

ctx->offset = last; //ctx->offset当前块之前所有块的总长度(不包括当前块)
//last包括当前块在内的之前所有块的总长度

//当前块不满足要请求的最小区间,则查找下一块。
//以此同时,这一块pos指针指向last,表示这个块不会发送给客户端
if (range->end <= start || range->start >= last)
{
buf->pos = buf->last;
buf->sync = 1;
continue;
}
//当前块满足区间,则查看开始数据位置
if (range->start > start)
{
//改变pos位置,发送响应时从pos位置开始放松,之前的内容不发送
if (ngx_buf_in_memory(buf))
{
buf->pos += (size_t) (range->start - start);
}
}

//当前块位置满足区间,则查看结束数据位置
if (range->end <= last)
{
//改变last位置,发送响应时last之后的内容不在发送
if (ngx_buf_in_memory(buf))
{
buf->last -= (size_t) (last - range->end);
}
//标记为最后一块,即便还有剩余块,这些剩余块也不会发送给客户端
buf->last_buf = 1;
*ll = cl;
cl->next = NULL; //指向清空
break;
}

*ll = cl;
ll = &cl->next;
}

//out链表记录了要发送的分块数据的开始位置
if (out == NULL)
{
return NGX_OK;
}

return ngx_http_next_body_filter(r, out);
}

         (2)请求多个区间块的数据

        nginx服务器目前的实现方式:  请求多个区间块时,应答链表只能由一个结点组成,而不像请求单个区间,应答链表可以由多个节点组成。

假设有一个文件的内容为: "0123456789abcdef", 一共16个字节。如果客户端想要请求两个区间块的数据, 则可以在http请求头部加上range字段, 格式为: range: bytes=0-5, 9-13;       这样nginx服务器就只会把012345以及9abcd返回给客户端,而不会发送整个文件的数据。参考下面的这张图:

nginx断点续传_客户端_04

        看下ngx_http_range_multipart_body函数的实现过程: 把一个缓冲区内容分割成多个链表,http请求中range指定了多少个区间,就分割成多少个链表,并把各个链表链接起来。ngixn使用空间换时间的方法,不在原来的只有一个节点的应答链表上进行处理,而是重新开辟链表节点。需要注意的是,最后面还有一个结点,用来存放随机数。

//多个块包体处理逻辑, 对于多个区间块,则只能针对一个缓冲区进行处理
static ngx_int_t ngx_http_range_multipart_body(ngx_http_request_t *r,
ngx_http_range_filter_ctx_t *ctx,
ngx_chain_t *in)
{
for (i = 0; i < ctx->ranges.nelts; i++)
{
//获取区间的公共头部
b = ngx_calloc_buf(r->pool);
b->memory = 1;
b->pos = ctx->boundary_header.data;
b->last = ctx->boundary_header.data + ctx->boundary_header.len;
hcl = ngx_alloc_chain_link(r->pool);
hcl->buf = b;

//获取区间的开始、结束位置
b = ngx_calloc_buf(r->pool);
b->pos = range[i].content_range.data;
b->last = range[i].content_range.data + range[i].content_range.len;
rcl = ngx_alloc_chain_link(r->pool);
rcl->buf = b;

//数据区间的真正数据
b = ngx_calloc_buf(r->pool);
b->pos = buf->pos + (size_t) range[i].start;
b->last = buf->pos + (size_t) range[i].end;
dcl = ngx_alloc_chain_link(r->pool);
dcl->buf = b;

//将上面的三个结点构成链表
*ll = hcl;
hcl->next = rcl;
rcl->next = dcl;
ll = &dcl->next;
}

//所有块的结束内容,例如:--00000000005--
//也就是最后一个链表节点
b = ngx_calloc_buf(r->pool);
b->last_buf = 1;
b->pos = ngx_pnalloc(r->pool, sizeof(CRLF "--") - 1 + NGX_ATOMIC_T_LEN
+ sizeof("--" CRLF) - 1);
b->last = ngx_cpymem(b->pos, ctx->boundary_header.data,
sizeof(CRLF "--") - 1 + NGX_ATOMIC_T_LEN);
*b->last++ = '-'; *b->last++ = '-';
*b->last++ = CR; *b->last++ = LF;
hcl = ngx_alloc_chain_link(r->pool);
hcl->buf = b;
hcl->next = NULL;
*ll = hcl;

return ngx_http_next_body_filter(r, out);
}