有时候从服务器下载的文件会很大,HTTP 协议本身支持一些头部字段能够让你在下载这类文件时进行一些控制。
数据压缩
首先想到的肯定是数据压缩。这个功能在请求方需要用 Accept-Encoding 字段来表明客户端支持什么样的压缩类型,在服务端,是通过 Content-Encoding 这个字段来表示服务器要用什么样的压缩算法来发送数据。
常用的三种压缩算法有:gzip、deflate、br。但是这些算法对于文本来说效率很高,通常能够达到 50%+,而对于图片、音频、视频等文件来说这类文件本身的压缩率很高,就没办法使用 gzip 这类算法来压缩了。
Nginx 的 gzip on 指令可以启用对 “text/html” 的压缩。并且这个指令很智能,只会对文本文件采用压缩,而遇到图片、音频和视频等文件会自动跳过。
分块传输
分块传输是一种 “化整为零” 的思路。这种思路在 HTTP 协议里就是 “chunked” 分块传输编码,在响应报文里用头字段 “Transfer-Encoding: chunked” 来表示,意思是报文里的 body 部分不是一次性发过来的,而是分成了许多的块(chunk)逐个发送。
分块传输也可以用于 “流式数据”,例如由数据库动态生成的表单页面,这种情况下 body 数据的长度是未知的,无法在头字段 “Content-Length” 里给出确切的长度,所以也只能用 chunked 方式分块发送。
“Transfer-Encoding: chunked” 和 “Content-Length” 这两个字段是互斥的,也就是说响应报文里这两个字段不能同时出现,一个响应报文的传输要么是长度已知,要么是长度未知(chunked),这一点你一定要记住。
分块传输的格式:
每个分块包含两个部分,长度头和数据块;
长度头是以 CRLF(回车换行,即 \r\n)结尾的一行明文,用 16 进制数字表示长度;
数据块紧跟在长度头后,最后也用 CRLF 结尾,但数据不包含 CRLF;
最后用一个长度为 0 的块表示结束,即 “0\r\n\r\n”。
范围请求
分块传输很好的解决了大文件传输的部分问题,但还有如下场景需要考虑:
假如在线观看一个电影,这个电影的某些部分你想跳过(比如 “FBI Warning”),直接到达 “激动人心” 的部分,那这个时候就需要一些能够表达这个信息的额外语义了。范围请求能够帮助我们做到这些:
能够允许客户端在请求头里使用专用字段来表示只获取文件的一部分,相当于是客户端的 “化整为零”。
范围请求不是 Web 服务器必备的功能,可以实现也可以不实现,所以服务器必须在响应头里使用字段 “Accept-Ranges: bytes” 明确告知客户端:“我是支持范围请求的”。
如果不支持的话该怎么办呢?服务器可以发送 “Accept-Ranges: none”,或者干脆不发送 “Accept-Ranges” 字段,这样客户端就认为服务器没有实现范围请求功能,只能老老实实地收发整块文件了。
请求头 Range 是 HTTP 范围请求的专用字段,格式是 “bytes=x-y”,其中的 x 和 y 是以字节为单位的数据范围。
要注意 x、y 表示的是 “偏移量”,范围是从 0 计数,例如前 10 个字节表示为 “0-9”,第二个 10 字节表示为 “10-19”,而 “0-10” 实际上是前 11 个字节。
Range 的格式也很灵活,起点 x 和终点 y 可以省略,能够很方便地表示正数或者倒数的范围。假设文件是 100 个字节,那么:
“0-” 表示从文档起点到文档终点,相当于 “0-99”,即整个文件;
“10-” 是从第 10 个字节开始到文档末尾,相当于 “10-99”;
“-1” 是文档的最后一个字节,相当于 “99-99”;
“-10” 是从文档末尾倒数 10 个字节,相当于 “90-99”。
服务器收到 Range 字段后,需要做四件事。
第一,它必须检查范围是否合法,比如文件只有 100 个字节,但请求 “200-300”,这就是范围越界了。服务器就会返回状态码 416,意思是 “你的范围请求有误,我无法处理,请再检查一下”。
第二,如果范围正确,服务器就可以根据 Range 头计算偏移量,读取文件的片段了,返回状态码 “206 Partial Content”,和 200 的意思差不多,但表示 body 只是原数据的一部分。
第三,服务器要添加一个响应头字段 Content-Range,告诉片段的实际偏移量和资源的总大小,格式是 “bytes x-y/length”,与 Range 头区别在没有 “=”,范围后多了总长度。例如,对于 “0-10” 的范围请求,值就是 “bytes 0-10/100”。
最后剩下的就是发送数据了,直接把片段用 TCP 发给客户端,一个范围请求就算是处理完了。
有了范围请求之后,HTTP 处理大文件就更加轻松了,看视频时可以根据时间点计算出文件的 Range,不用下载整个文件,直接精确获取片段所在的数据内容。
不仅看视频的拖拽进度需要范围请求,常用的下载工具里的多段下载、断点续传也是基于它实现的,要点是:
先发个 HEAD,看服务器是否支持范围请求,同时获取文件的大小;
开 N 个线程,每个线程使用 Range 字段划分出各自负责下载的片段,发请求传输数据;
下载意外中断也不怕,不必重头再来一遍,只要根据上次的下载记录,用 Range 请求剩下的那一部分就可以了。
多段数据
刚才说的范围请求一次只获取一个片段,其实它还支持在 Range 头里使用多个 “x-y”,一次性获取多个片段数据。
这种情况需要使用一种特殊的 MIME 类型:“multipart/byteranges”,表示报文的 body 是由多段字节序列组成的,并且还要用一个参数 “boundary=xxx” 给出段之间的分隔标记。
多段数据的格式与分块传输也比较类似,但它需要用分隔标记 boundary 来区分不同的片段,可以通过图来对比一下。
每一个分段必须以 “- -boundary” 开始(前面加两个 “-”),之后要用 “Content-Type” 和 “Content-Range” 标记这段数据的类型和所在范围,然后就像普通的响应头一样以回车换行结束,再加上分段数据,最后用一个 “- -boundary- -”(前后各有两个 “-”)表示所有的分段结束。
示例:
GET /16-2 HTTP/1.1
Host: www.chrono.com
Range: bytes=0-9, 20-29
结果:
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=00000000001
Content-Length: 189
Connection: keep-alive
Accept-Ranges: bytes
–00000000001
Content-Type: text/plain
Content-Range: bytes 0-9/96
this is
–00000000001
Content-Type: text/plain
Content-Range: bytes 20-29/96
ext json d
–00000000001–
压缩 HTML 等文本文件是传输大文件最基本的方法;
分块传输可以流式收发数据,节约内存和带宽,使用 响应头字段 “Transfer-Encoding: chunked” 来表示,分块的格式是 16 进制长度头 + 数据块;
范围请求可以只获取部分数据,即 “分块请求”,实现视频拖拽或者断点续传,使用 请求头字段 “Range” 和响应头字段 “Content-Range”,响应状态码必须是 206;
也可以一次请求多个范围,这时候 响应报文的数据类型是 “multipart/byteranges”,body 里的多个部分会用 boundary 字符串分隔。
关于分块传输(chunked)的需要注意的一点:
http 传输永远是一个请求一个响应的工作模式,只是响应是 chunked 分块,body 数据不是一次性发过来的,而是分批分块发送,但仍然是在一个报文里。
客户端发送请求后等待响应,服务器组织数据,分块发送,最后一个分块是结束标志。客户端依次接收分块,收到结束标志后就把数据拼成完整的报文。