如今 cdn 已成为互联网上的基础设施,会与形形色色的公司打交道。其中既有一些拿着政府资金,结果只让你草草布一个 Nginx 代理就完事的;也有现在那些中国最强势的互联网公司。这里只说互联网公司,为什么它们强势呢?一方面,它们拥有着巨大的流量,中国的 cdn 市场竞争又如此激烈,一个不爽,把量切走,反正排队等着为我服务的 cdn 厂商多的很;另一方面,它们本身的技术积累都很强,所以提起需求来是毫不手软,底气十足。

但这些有着深厚技术积累的公司,有时候提出来的需求只能让人苦笑。

背景

A 是中国目前最大的互联网公司之一,它的视频当然流量也很大。按照业内惯例,它会把内容同时让多家 cdn 厂商服务,然后定期进行打分,排名。参与打分的指标很多,最典型的就是一些 4XX 的错误请求。所以,cdn 厂商的运维及管理人员,会很重视这些指标。

最近,运维发现服务器上有很多 416 请求。416 是 Range 错误的响应码,除去一些正常的 416 外,运维还发现有一类 416 请求的 Range 头是:

Range: byptes=XXXX-0

XXXX 是个非 0 的数。很明显,这是一个错误的 Range 请求,之所以会出现这样的 Range,是因为对方的播放器 bug 导致。但 A 公司要求,把 XXX-0 按照 xxx- 来处理。

我们知道,在 HTTP 协议中规定的是,一个 Range 头可以有如下形式(假设文件大小为1000个字节):

bytes=0-499(前500个字节)
bytes=500-999(第二个500个字节)
bytes=-500(最后500个字节)
bytes=9500-(与上同,最后500个字节)
bytes=0-0,-1(第一个字节和最后一个字节)

那么到底有没有 bytes=500-0 这样的 range 呢?我们从标准和实现两方面来考虑。

标准

参考 RFC2616 14.35.1节:

If the last-byte-pos value is present, it MUST be greater than or equal to the first-byte-pos in that byte-range-spec, or the byte-range-spec is syntactically invalid. The recipient of a byte-range-set that includes one or more syntactically invalid byte-range-spec values MUST ignore the header field that includes that byte-range-set.

所以,理论上,bytes=500-0 这样的 range 段,是错误的。

实现

以 Nginx 为例,我们来看看代码中是如何处理的。

static ngx_int_t  
ngx_http_range_parse(ngx_http_request_t *r, ngx_http_range_filter_ctx_t *ctx,  
    ngx_uint_t ranges)  
{  
    u_char            *p;  
    off_t              start, end, size, content_length;  
    ngx_uint_t         suffix;  
    ngx_http_range_t  *range;  
  
    p = r->headers_in.range->value.data + 6;  
    size = 0;  
    content_length = r->headers_out.content_length_n;  
  
    for ( ;; ) {  
        start = 0;  
        end = 0;  
        suffix = 0;  
  
        while (*p == ' ') { p++; }  
  
        if (*p != '-') {  
            if (*p < '0' || *p > '9') {  
                return NGX_HTTP_RANGE_NOT_SATISFIABLE;  
            }  
  
            while (*p >= '0' && *p <= '9') {  
                start = start * 10 + *p++ - '0';  
            }  
  
            while (*p == ' ') { p++; }  
  
            if (*p++ != '-') {  
                return NGX_HTTP_RANGE_NOT_SATISFIABLE;  
            }  
  
            while (*p == ' ') { p++; }  
  
            if (*p == ',' || *p == '\0') {  
                end = content_length;  
                goto found;  
            }  
  
        } else {  
            suffix = 1;  
            p++;  
        }  
  
        if (*p < '0' || *p > '9') {  
            return NGX_HTTP_RANGE_NOT_SATISFIABLE;  
        }  
  
        while (*p >= '0' && *p <= '9') {  
            end = end * 10 + *p++ - '0';  
        }  
  
        while (*p == ' ') { p++; }  
  
        if (*p != ',' && *p != '\0') {  
            return NGX_HTTP_RANGE_NOT_SATISFIABLE;  
        }  
  
        if (suffix) {  
            start = content_length - end;  
            end = content_length - 1;  
        }  
  
        if (end >= content_length) {  
            end = content_length;  
  
        } else {  
            end++;  
        }  
  
    found:  
  
        if (start < end) {  
            range = ngx_array_push(&ctx->ranges);  
            if (range == NULL) {  
                return NGX_ERROR;  
            }  
  
            range->start = start;  
            range->end = end;  
  
            size += end - start;  
  
            if (ranges-- == 0) {  
                return NGX_DECLINED;  
            }  
        }  
  
        if (*p++ != ',') {  
            break;  
        }  
    }  
  
    if (ctx->ranges.nelts == 0) {  
        return NGX_HTTP_RANGE_NOT_SATISFIABLE;  
    }  
  
    if (size > content_length) {  
        return NGX_DECLINED;  
    }  
  
    return NGX_OK;  
}  

其中,

 found:  
  
        if (start < end) {  
            range = ngx_array_push(&ctx->ranges);  
            if (range == NULL) {  
                return NGX_ERROR;  
            }  
  
            range->start = start;  
            range->end = end;  
  
            size += end - start; 
  
            if (ranges-- == 0) {  
                return NGX_DECLINED;  
            }  
        }  

表明,只有 start<end 的,才会后续进行 range 处理。这里的 start,是指 bytes=a-b 中的 a,end 指的是 b+1。

所以,在 HTTP 服务器的典型实现上,Range: bytes=500-0 也是错误的请求。

现实世界

好了,从理论和典型实现上讨论了 Range 头之后,我们回到第三次元,现实世界。

可以猜想,出现这个问题的原因是 A 公司的播放器开发人员代码的一处 bug 导致。但由于它们是甲方,所以我们应该予以配合。结果是我们在服务器端将 Range: bytes=500-0 这类的请求与 Range: bytes=500- 等同处理。

解决起来很简单,非常简单。但是,好好的标准就这么被破坏了。可以想象,A 是 Top 级的互联网大佬,某家 cdn 帮他们解决这种 Range 错误了,它肯定会对其他 cdn 商家说,这个很简单,那谁都是这么处理的。然后,中国所有的 cdn 商家都这么处理了。再然后,没人记的这东西是否遵循标准了,即使有人拿标准说起,也会被老人们这么教育:“标准只是理想情况,现实中的实现,是复杂多变的…”

更没人会记得,罪魁祸首是某个不合格的播放器开发程序员。