HTTP请求范围
HTTP协议范围请求允许服务器只发送HTTP消息的一部分到客户端。范围请求在传送大的媒体文件,或者与文件下载的断点续传功能搭配使用时非常有用。
检测服务器端是否支持范围请求
假如在响应中存在 Accept-Ranges
首部(并且它的值不为 “none”),那么表示该服务器支持范围请求。例如,你可以使用 cURL 发送一个 HEAD 请求来进行检测。
curl -I http://i.imgur.com/z4d4kWk.jpg
HTTP/1.1 200 OK
...
Accept-Ranges: bytes
Content-Length: 146515
在上面的响应中, Accept-Ranges: bytes 表示界定范围的单位是 bytes 。这里 Content-Length 也是有效信息,因为它提供了要检索的图片的完整大小。
如果站点未发送 Accept-Ranges 首部,那么它们有可能不支持范围请求。一些站点会明确将其值设置为 “none”,以此来表明不支持。在这种情况下,某些应用的下载管理器会将暂停按钮禁用。
curl -I https://www.youtube.com/watch?v=EwTZ2xpQwpA
HTTP/1.1 200 OK
...
Accept-Ranges: none
从服务器端请求特定的范围
假如服务器支持范围请求的话,你可以使用Range
首部来生成该类请求。该首部指示服务器应该返回文件的哪一或哪几部分。
单一范围
我们可以请求资源的某一部分。这次我们依然用curl来进行测试。“-H” 选项可以在请求中追加一个首部行,在这个例子中,是用 Range 首部来请求图片文件的前 1024 个字节。
curl http://i.imgur.com/z4d4kWk.jpg -i -H "Range: bytes=0-1023"
这样生成的请求如下:
GET /z4d4kWk.jpg HTTP/1.1
Host: i.imgur.com
Range: bytes=0-1023
服务器端会返回状态码为 206 Partial Content 的响应:
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1023/146515
Content-Length: 1024
...
(binary content)
在这里,Content-Length 首部现在用来表示先前请求范围的大小(而不是整张图片的大小)。Content-Range 响应首部则表示这一部分内容在整个资源中所处的位置。
多重范围
Range头部也支持一次请求文档的多个部分。请求范围用一个逗号分隔开。
curl http://www.example.com -i -H "Range: bytes=0-50, 100-150"
服务器返回206 Partial Content状态码和Content-Type:multipart/byteranges; boundary=3d6b6a416f9b5头部,Content-Type:multipart/byteranges表示这个响应有多个byterange。每一部分byterange都有他自己的Centen-type头部和Content-Range,并且使用boundary参数对body进行划分。
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=3d6b6a416f9b5
Content-Length: 282
--3d6b6a416f9b5
Content-Type: text/html
Content-Range: bytes 0-50/1270
<!doctype html>
<html>
<head>
<title>Example Do
--3d6b6a416f9b5
Content-Type: text/html
Content-Range: bytes 100-150/1270
eta http-equiv="Content-type" content="text/html; c
--3d6b6a416f9b5--
条件式范围请求
当(中断之后)重新开始请求更多资源片段的时候,必须确保自从上一个片段被接收之后该资源没有进行过修改。
If-Range
请求首部可以用来生成条件式范围请求:
- 假如条件满足的话,条件请求就会生效,服务器会返回状态码为206 Partial 的响应,以及响应的消息主体。
- 假如条件未能得到满足,那么就会返回状态码为 200 OK 的响应,同时返回整个资源。
该首部可以与 Last-Modified 验证器或者 ETag 一起使用,但是二者不能同时使用。
If-Range: Wed, 21 Oct 2015 07:28:00 GMT
范围请求的响应
与范围请求相关的有三种状态:
- 在请求成功的情况下,服务器会返回 206 Partial Content 状态码。
- 在请求的范围越界的情况下(范围值超过了资源的大小),服务器会返回 416 Requested Range Not Satisfiable (请求的范围无法满足) 状态码。
- 在不支持范围请求的情况下,服务器会返回 200 OK 状态码。
与分块传输编码的对比
Transfer-Encoding
首部允许分块编码,这在数据量很大,并且在请求未能完全处理之前无法知晓响应的体积大小的情况下非常有用。服务器会直接把数据发送给客户端而无需进行缓存或者确定响应的精确大小——后者会增加延迟。范围请求与分块传输是兼容的,可以单独或搭配使用。
Accept-Ranges
服务器使用HTTP响应头Accept-Ranges
标识自身支持范围请求(partial requests)。字段的具体值用于定义范围请求的单位。
当浏览器发现Accept-Ranges
头时,可以尝试继续中断了的下载,而不是重新开始。
语法
Accept-Ranges: bytes
Accept-Ranges: none
指令
- none
- 不支持任何范围请求单位,由于其等同于没有返回此头部,因此很少使用。
- 不过一些浏览器,比如IE9,会依据该头部去禁用或者移除下载管理器的暂停按钮。
- bytes
范围请求的单位是 bytes (字节)。
Range
The Range 是一个请求首部,告知服务器返回文件的哪一部分。
- 在一个 Range 首部中,可以一次性请求多个部分,服务器会以 multipart 文件的形式将其返回。
- 如果服务器返回的是范围响应,需要使用 206 Partial Content 状态码。
- 假如所请求的范围不合法,那么服务器会返回 416 Range Not Satisfiable 状态码,表示客户端错误。
- 服务器允许忽略 Range 首部,从而返回整个文件,状态码用 200 。
语法
Range: <unit>=<range-start>-
Range: <unit>=<range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>, <range-start>-<range-end>
指令
<unit>
- 范围所采用的单位,通常是字节(bytes)。
<range-start>
- 一个整数,表示在特定单位下,范围的起始值。
<range-end>
- 一个整数,表示在特定单位下,范围的结束值。
- 这个值是可选的,如果不存在,表示此范围一直延伸到文档结束。
示例
Range: bytes=200-1000, 2000-6576, 19000-
服务器如何解析:
/**
* 分析HTTP请求头中的 Range 字段
* @param range_from {http_off_t*} 存储偏移起始位置
* @param range_to {http_off_t*} 存储偏移结束位置
* 注: * {range_from}, {range_to} 下标从0开始
* 请求的 Range 格式:
* Range: bytes={range_from}-, bytes={range_from}-{range_to}
*/
int http_hdr_req_range( http_off_t *range_from, http_off_t *range_to){
const char *myname = "http_hdr_req_range";
if (range_from == NULL)
logger_fatal("%s(%d): range_from null", myname, __LINE__);
if (range_to == NULL)
logger_fatal("%s(%d): range_to null", myname, __LINE__);
char buf[256];
const char *ptr;
char *ptr1;
/* 数据格式: Range: bytes={range_from}-{range_to}
* 或: Range: bytes={range_from}-
*/
ptr = hdr_.http_hdr_entry_value("Range");
if (ptr == NULL)
return -1;
ptr = strstr(ptr, "bytes=");
if (ptr == NULL)
return -1;
ptr += strlen("bytes=");
strncpy(buf, ptr, sizeof(buf));
ptr1 = buf;
while (*ptr1) {
if (*ptr1 == '-' || *ptr1 == ' ') {
*ptr1 = 0;
*range_from = atoi(buf);
if (*range_from < 0)
return (-1);
if (*(++ptr1) == 0)
*range_to = -1;
else
*range_to = atoi(ptr1);
if (*range_to <= 0)
*range_to = -1;
return 0;
}
ptr1++;
}
return -1;
}
char *HTTP_HDR::http_hdr_entry_value( const char *name){
HTTP_HDR_ENTRY *hdr_entry;
hdr_entry = http_hdr_entry(name);
if (hdr_entry == NULL)
return (NULL);
return (char *)hdr_entry->get_value();
}
HTTP_HDR_ENTRY *HTTP_HDR::http_hdr_entry(const char *name){
return get_hdr_entry(name);
}
HTTP_HDR_ENTRY *HTTP_HDR::get_hdr_entry(const char *name)
{
const char *myname = "__get_hdr_entry";
if (entry_lnk_.empty()){
printf("[fatal] %s, %s(%d): entry_lnk null",
__FILE__, myname, __LINE__);
exit(1);
}
for(const auto & iter : entry_lnk_){
if (strcasecmp(name, iter->get_name()) == 0){
return iter;
}
}
return NULL;
}
Content-Range
在HTTP协议中,响应首部Content-Range
显示的是一个数据片段在整个文件中的位置
语法
Content-Range: <unit> <range-start>-<range-end>/<size>
Content-Range: <unit> <range-start>-<range-end>/*
Content-Range: <unit> */<size>
指令
<unit>
- 数据区间所采用的单位。通常是字节(byte)。
<range-start>
- 一个整数,表示在给定单位下,区间的起始值。
<range-end>
- 一个整数,表示在给定单位下,区间的结束值。
<size>
- 整个文件的大小(如果大小未知则用"*"表示)。
示例
Content-Range: bytes 200-1000/67589
客户端如何解析:
/**
* 分析HTTP响应头中的 Range 字段
* @param range_from {http_off_t*} 存储偏移起始位置, 不能为空
* @param range_to {http_off_t*} 存储偏移结束位置, 不能为空
* @param total_length {http_off_t*} 整个数据文件的总长度, 可为空
* @return {int} 返回 0 表示成功,-1 表示失败
* 注: * {range_from}, {range_to} 下标从0开始
* 响应的 Range 格式:
* Content-Range: bytes {range_from}-{range_to}/{total_length}
*/
int HTTP_HDR_RES::http_hdr_res_range(http_off_t *range_from,http_off_t *range_to, http_off_t *total_length){
const char* myname = "http_hdr_res_range";
if (range_from == NULL)
logger_fatal("%s(%d): range_from null", myname, __LINE__);
if (range_to == NULL)
logger_fatal("%s(%d): range_to null", myname, __LINE__);
const char* value;
value = hdr_.http_hdr_entry_value("Content-Range");
if (value == NULL)
return (-1);
char buf[256];
strncpy(buf, value, sizeof(buf));
char *ptr, *pfrom, *pto;
/* 响应的 Range 数据格式:Content-Range: bytes {range_from}-{range_to}/{total_length}
* 其中: {range_from}, {range_to} 的下标是从0开始的
*/
/* Content-Range: bytes 2250000-11665200/11665201 */
/* value: bytes 2250000-11665200/11665201 */
if (strncasecmp(buf, "bytes", sizeof("bytes") - 1) != 0)
return (-1);
ptr = buf + sizeof("bytes") -1;
while (*ptr == ' ' || *ptr == '\t') {
ptr++;
}
if (*ptr == 0)
return (-1);
/* ptr: 2250000-11665200/11665201 */
pfrom = ptr;
pto = strchr(pfrom, '-');
if (pto == NULL || pto == pfrom)
return (-1);
*pto++ = 0;
/* pto: 11665200/11665201 */
ptr = strchr(pto, '/');
if (ptr == NULL || ptr == pto)
return (-1);
*ptr++ = 0;
/* pto: 11665200; ptr: 11665201 */
*range_from = atoi(pfrom);
if (*range_from < 0)
return (-1);
*range_to = atoi(pto);
if (*range_to < 0)
return (-1);
/* 可选项 */
if (total_length) {
*total_length = atoi(ptr);
if (*total_length < 0)
return (-1);
}
return (0);
}