对于几百 M 甚至上 G 的大文件来说,如果要一口气全部传输过来显然是不现实的,会有大量的等待时间,严重影响用户体验。因此,HTTP 针对这一场景,采取了范围请求的解决方案,允许客户端仅仅请求一个资源的一部分。
对于相对较小的大文件传输,解决办法还有:数据压缩、分块传输
1.数据压缩
浏览器在发送请求时都会带着 Accept-Encoding 头字段,里面是浏览器支持的压缩格式列表,例如 gzip、deflate、br 等,这样服务器就可以从中选择一种压缩算法,放进 Content-Encoding 响应头里,再把原数据压缩后发给浏览器。
2.分块传输
除了压缩文件之外,另一种办法就是分块传输。它们的原理差不多,都是把大文件变小传输。分块传输会把一个大文件切成很多小块,把这些小块依次发给浏览器,浏览器收到之后再组装复原。这样浏览器和服务器都不用在内存中保存全部文件,每次只收发一小部分,网络也不会被大文件长时间占用,内存、带宽等资源也就节省下来了。
具体实现是在 response 响应报文里用头字段 Transfer-Encoding: chunked 来表示,表示报文里的 body 部分不是一次性发过来的,而是分成了许多的块(chunk)逐个发送。当 chunk 为 0 时说明是最后一个,传输结束。
Transfer-Encoding 和 Content-Length 两个字段是互斥的,不能同时出现。一个响应报文的长度要么是已知的,要么是未知的。
3.范围请求
你看电影时,想跳过开头直接看正片,这实际上是想获取一个大文件其中的片段数据,而分块传输没有这个能力。或者请求更大文件。
HTTP 协议为了满足这种需求,提出了范围请求的概念,允许客户端在请求头里使用专用字段来表示只获取文件的一部分。
如何支持:
范围请求不是 Web 服务器必须实现的功能,所以服务器必须在响应头里使用字段
Accept-Ranges: bytes
明确告知客户端自己支持范围请求。如果不支持的话,服务器就会发送
Accept-Ranges:none
Range 字段拆解
而对于客户端而言,它需要指定请求哪一部分,通过Range这个请求头字段确定,格式为bytes=x-y。接下来就来讨论一下这个 Range 的书写格式:
- 0-499表示从开始到第 499 个字节。
- 500- 表示从第 500 字节到文件终点。
- -100表示文件的最后100个字节。
服务器收到 Range 字段后,需要做四件事。
第一,它必须检查范围是否合法,比如文件只有 100 个字节,但请求“200-300”,这就是范围越界了。服务器就会返回状态码 416 Requested Range Not Satisfiable,表示范围请求有误,无法处理。
第二,如果范围正确,服务器就可以根据 Range 头计算偏移量,读取文件的片段了,返回状态码 206 Partial Content,表示 body 只是原数据的一部分。
第三,服务器要添加一个响应头字段 Content-Range,告诉片段的实际偏移量和资源的总大小,格式是 「bytes x-y/length」,与 Range 头区别在没有“=”,范围后多了总长度。例如,对于“0-10”的范围请求,值就是“bytes 0-10/100”。
最后剩下的就是发送数据了,直接把片段用 TCP 发给客户端,一个范围请求就算是处理完了。
具体来说,请求单段数据和请求多段数据,响应头是不一样的。
// 单段数据
Range: bytes=0-9
// 多段数据
Range: bytes=0-9, 30-39
接下来我们就分别来讨论着两种情况。
单段数据
HTTP/1.1 206 Partial Content
Content-Length: 10
Accept-Ranges: bytes
Content-Range: bytes 0-9/100
i am xxxxx
多段数据
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=00000010101
Content-Length: 189
Connection: keep-alive
Accept-Ranges: bytes
--00000010101
Content-Type: text/plain
Content-Range: bytes 0-9/96
i am xxxxx
--00000010101
Content-Type: text/plain
Content-Range: bytes 20-29/96
eex jspy e
--00000010101--
这个时候出现了一个非常关键的字段Content-Type: multipart/byteranges;boundary=00000010101,它代表了信息量是这样的:
- 请求一定是多段数据请求
- 响应体中的分隔符是 00000010101
因此,在响应体中各段数据之间会由这里指定的分隔符分开,而且在最后的分隔末尾添上- -表示结束。