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

If-Range 是续传前的最后一道检查:我手上这份还是那一版吗。 匹配就给片段(206),不匹配就给全量(200)——不是报错。

📝 本节和前面几节的关系

上一节是缓存的条件,这一节是下载的条件。它要解决的竞态很具体:续传时文件被换了。

一、竞态长什么样

下载一个 2 GB 的文件,下到一半断了。 客户端记着「我已经有前 1000 字节」,于是发 Range: bytes=1000- 续传。 可如果这期间服务器上的文件被替换了(发了新版本),客户端拿到的后半段对不上前半段, 拼起来是一份彻底损坏的文件。而且从字节上看不出问题——长度对得上,只是内容是两份不同的文件。 If-Range 就是让客户端先说一句「我记得那版是 v7」。服务端比对:还是 v7,就给片段; 不是 v7,就从头给一份完整的。

二、降级而不是报错

这一点是它和另外四个条件字段最大的不同。 If-Match 不匹配回 412,If-None-Match 不匹配回 200(读场景), 而 If-Range 不匹配直接退回 200 全量。 为什么这么设计?因为对下载来说,能拿到完整文件比报错有用。 报 412 的话客户端还得再来一次,不如这一次就把全量给出去。 Demo 第三组用过期 ETag,结果就是 200 加 2048 字节全量, Content-Range 那一栏是空的——因为它不是片段响应了。

三、它能用两种凭证

If-Range 的值可以是 ETag,也可以是 HTTP 日期。 用日期时它是强比较:必须与 Last-Modified 完全相等, 不是「不晚于」——这一点和 If-Modified-Since 完全不同。 为什么这么严?因为续传要求字节级一致,「不晚于」只说明没改过,不能说明是同一次生成的内容。 顺带一句:这也是为什么 If-Range 不能和弱 ETag 一起用——弱标签只保证语义等价,不保证字节一致, 而续传恰恰要求字节一致。

四、动手 Demo:三种组合:只有 Range、配对 If-Range、过期 If-Range

服务端有一份 2048 字节的资源,当前 ETag 是 v7。 客户端分别只给 Range、给 Range 加正确 If-Range、给 Range 加过期 If-Range。

if rng and ir:
    if ir != ETAG:
        # 副本变了,不能给片段,改成给全量
        return reply(u'HTTP/1.1 200 OK', [(u'ETag', ETAG)], DATA)
    seg = DATA[start:end + 1]
    return reply(u'HTTP/1.1 206 Partial Content',
                 [(u'Content-Range',
                   u'bytes %d-%d/%d' % (start, end, len(DATA)))], seg)

实跑输出:

--------------------------------------------------------------------
If-Range:续传前先确认「还是那一份吗」
--------------------------------------------------------------------
  资源总长                   2048 字节
  当前 ETag                  "v7"

  【只给 Range】
    If-Range                 (没发)
    结果                     HTTP/1.1 206 Partial Content | 正文 100 字节
    Content-Range            bytes 1000-1099/2048
    首尾字节                 232 … 75

  【Range 加正确的 If-Range】
    If-Range                 "v7"
    结果                     HTTP/1.1 206 Partial Content | 正文 100 字节
    Content-Range            bytes 1000-1099/2048
    首尾字节                 232 … 75

  【Range 加过期的 If-Range】
    If-Range                 "v3"
    结果                     HTTP/1.1 200 OK | 正文 2048 字节
    Content-Range            (没有)
    首尾字节                 0 … 255

  这个字段解决的是续传时的一个竞态:下载到一半断了,
  客户端拿着「我要第 1000 到 1099 字节」来续,
  可如果这期间文件被换过了,拼起来就是一份乱掉的文件。
  `If-Range` 就是让客户端先说「我记得那版是 v7」,
  服务端一比对:还是 v7 就给片段(206),不是 v7 就给全量(200)。
  注意它**不是**条件请求那种 412,而是降级成一次完整下载——
  因为对下载来说,能拿到完整文件比报错有用。

只给 Range: bytes=1000-1099:206,正文 100 字节,Content-Range: bytes 1000-1099/2048, 首尾字节是 232 和 75。 加上正确的 If-Range: "v7":还是 206,100 字节,首尾一样。说明凭证匹配,正常续传。 加上过期的 If-Range: "v3":200,2048 字节全量,Content-Range 那一栏是「没有」。 服务端判断副本变了,直接放弃续传,给一份完整的。首尾字节变成 0 和 255。 注意首尾那对数字:bytes(range(256)) 重复八遍,所以第 1000 字节的值是 232(1000 除 256 余 232), 第 1099 字节是 75。这两个数能验证片段确实是从 1000 开始切的。

30 秒实验

  1. 把 If-Range 换成日期,并且填一个比 Last-Modified 晚但不是同一个值,看会不会 206。
  2. 把 If-Range 换成 W/"v7",看服务端该不该接受。
  3. 想一想:如果客户端拿到 200 全量,它该不该覆盖已经下了一半的临时文件?

常见误区

  1. 「If-Range 不匹配回 412」——回 200 全量,是降级不是报错。
  2. 「用日期时是「不晚于」比较」——是强比较,必须完全相等。
  3. 「If-Range 和 If-None-Match 可以一起用」——规范只允许带一个条件,If-Range 单独成立。
自测三问(点开对答案) 1. `If-Range` 解决什么竞态? —— 续传期间资源被替换,导致拼起来的文件前后不是同一份。 2. 条件不满足时回什么? —— 回 200 加全量,而不是 412 之类的错误。 3. 为什么它不能用弱 ETag? —— 续传要求字节级一致,弱标签只保证语义等价。

续传说完了。还有一个条件字段,管的是写场景的另一面。

相关笔记