Accept-Ranges 是服务端的能力声明:这个资源能不能按字节切着给。
它不是给用户看的,是给客户端和缓存看的——有它才敢发 Range。
上一批 19 个都是请求首部,从这一节换成响应首部。
第一个正好接上 6.4.16 的 Range:一个问,一个答。
一、它只有两个取值
bytes 表示支持按字节范围请求,none 表示不支持。
理论上还能写别的计量单位,但现实中只有字节这一种。
Demo 里同一份 2048 字节的资源,服务端先老老实实报 Accept-Ranges: bytes,
客户端于是发了 Range: bytes=1000-1099,拿回 206 和 100 字节正文,
响应首部里多了一行 Content-Range。
然后服务端改成报 none,客户端照样发 Range,结果拿回 200 和完整 2048 字节。
这一组最关键:客户端发了不该发的请求,服务端退回整份,而不是报错。
二、Content-Range 里的三个数
Content-Range: bytes 1000-1099/2048 读作「给你的是第 1000 到 1099 字节,总共 2048 字节」。
斜杠后面那个数是总长,不是「还剩多少」。
这个总长很有用:客户端靠它算出进度条的分母。
如果服务端也不知道总长(比如边生成边发),可以写 *,那就没法算百分比了。
注意起止都是闭区间,两头都算。bytes=1000-1099 是整整一百个字节,不是九十九个。
这是范围请求最常写错的地方。
三、为什么它是「声明」而不是「承诺」
资源是会变的。 声明了支持范围,不等于每次都能兑现——文件被换掉了、被压缩了、后端换了台机器,都可能让范围请求落空。 所以健壮的客户端必须写两条路:拿到 206 就用片段,拿到 200 就整份重来。 只会处理 206 的下载器,遇上不支持范围的服务器就直接崩。 反过来说,服务端也不能因为「客户端乱发 Range」就回 416。 416 的意思是「你要的范围根本不存在」,不是「我不支持范围」——后者该老老实实回 200 加整份正文。
四、动手 Demo:同一份 2048 字节,三种要法
搭一个能报 Accept-Ranges 的服务端,然后分别发:不带 Range、带 Range 且服务端支持、
带 Range 但服务端声明 none。三组对比看状态码和正文长度怎么变。
def ranged(raw, st):
hd = {}
for ln in X.head_lines(raw)[1:]:
if u':' in ln:
k, v = ln.split(u':', 1)
hd[k.lower()] = v.strip() # 值要去空白
rng = hd.get(u'range', u'')
if not rng.startswith(u'bytes=') or st.get(u'ar') == u'none':
return X.reply(u'HTTP/1.1 200 OK', [(u'Accept-Ranges', st.get(u'ar'))], BODY)
a, _, b = rng[6:].partition(u'-')
a, b = int(a), int(b)
return X.reply(u'HTTP/1.1 206 Partial Content',
[(u'Content-Range', u'bytes %d-%d/%d' % (a, b, len(BODY)))],
BODY[a:b + 1]) # 闭区间,所以要 +1
实跑输出:
--------------------------------------------------------------------
Accept-Ranges:服务器先说「能不能切」,客户端才敢要
--------------------------------------------------------------------
先发一次不带 Range 的普通请求,看服务端怎么自我介绍:
Content-Type: application/octet-stream
Accept-Ranges: bytes
Content-Length: 2048
Connection: close
正文 2048 字节
看到 `Accept-Ranges: bytes`,客户端才知道「这资源支持按字节切」。
于是第二次大胆地只要第 1000 到 1099 字节:
HTTP/1.1 206 Partial Content
Content-Type: application/octet-stream
Accept-Ranges: bytes
Content-Range: bytes 1000-1099/2048
Content-Length: 100
Connection: close
正文 100 字节(要 100 字节就给 100 字节)
反过来,服务端如果声明 `Accept-Ranges: none`:
HTTP/1.1 200 OK
Content-Range:(没有)
正文 2048 字节(退回整份)
三组对比:支持时 206 给 100 字节,声明 none 时 200 给 2048 字节。
所以 `Accept-Ranges` 不是「许愿」,是「报能力」——
客户端看着它决定要不要发 Range,服务端不认的时候要老实退回整份,
而不是回 416 或者回半截。这也是断点续传能工作的前提。
第一组不带 Range,响应 2048 字节正文,首部里就一句 Accept-Ranges: bytes。
第二组要 1000 到 1099,状态码变成 206,Content-Range 写 bytes 1000-1099/2048,
正文正好 100 字节。
第三组服务端改声明 none,客户端仍然发了 Range,结果状态码回到 200,正文回到 2048 字节,
连 Content-Range 都没有。
这三组加起来就是断点续传的全部协议基础:先看能力,再要片段,要不到就整份收下。
30 秒实验
- 把 Demo 里的
st.get(u'ar')改成永远返回none,看第二组会不会自动退回 200。 - 发
Range: bytes=2000-9999,也就是终点超过总长,看服务端该截断还是该报 416。 - 想一想:如果资源是实时生成的,
Content-Range里的总长该写什么?
常见误区
- 「
Accept-Ranges: none就该回 416」——416 是范围不存在,不支持范围该回 200 加整份。 - 「
bytes=1000-1099是九十九个字节」——闭区间,两头都算,是一百个。 - 「声明了支持范围就一定能给」——资源会变,客户端必须能退回整份。
自测三问(点开对答案)
1. `Content-Range: bytes 1000-1099/2048` 里的 2048 是什么? —— 资源的总长度,用来算进度条分母;不知道时写 `*`。 2. 服务端声明了 `Accept-Ranges: none`,客户端偏要发 Range,该怎么回? —— 忽略 Range,回 200 加完整正文,不能回 416。 3. 为什么说 `Accept-Ranges` 是给缓存看的? —— 缓存要靠它判断能不能只取一段来补齐自己缺的部分。能切着给了,接下来要看「这份东西在缓存里放了多久」——那是 Age 的事。
- ☐ (本节完,下一节 6.5.2 Age)