📘 第六章 HTTP首部
📄 一句话概括

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 秒实验

  1. 把 Demo 里的 st.get(u'ar') 改成永远返回 none,看第二组会不会自动退回 200。
  2. 发 Range: bytes=2000-9999,也就是终点超过总长,看服务端该截断还是该报 416。
  3. 想一想:如果资源是实时生成的,Content-Range 里的总长该写什么?

常见误区

  1. 「Accept-Ranges: none 就该回 416」——416 是范围不存在,不支持范围该回 200 加整份。
  2. 「bytes=1000-1099 是九十九个字节」——闭区间,两头都算,是一百个。
  3. 「声明了支持范围就一定能给」——资源会变,客户端必须能退回整份。
自测三问(点开对答案) 1. `Content-Range: bytes 1000-1099/2048` 里的 2048 是什么? —— 资源的总长度,用来算进度条分母;不知道时写 `*`。 2. 服务端声明了 `Accept-Ranges: none`,客户端偏要发 Range,该怎么回? —— 忽略 Range,回 200 加完整正文,不能回 416。 3. 为什么说 `Accept-Ranges` 是给缓存看的? —— 缓存要靠它判断能不能只取一段来补齐自己缺的部分。

能切着给了,接下来要看「这份东西在缓存里放了多久」——那是 Age 的事。

相关笔记