HTTP 是如何传输大文件的

​ 在网络的早期,人们发送的文件大小仅为几 KB。到了现如今,我们可以在社交平台上发布动辄几 MB 的图片,可以在线上的流媒体平台在线观看更高清的视频。这些都离不开网络技术与硬件的飞速发展。今天,我们来聊聊HTTP 协议是如何传输大文件的。

​ 目前,HTTP 协议主要采用三种方法来缩短传输大文件所需的时间:

  • 数据压缩 (Data Compression)
  • 分块传输代码 (Chunked Transfer Encoding)
  • 选择性数据请求 (Range Requests)

​ 上面提到的三种方法并不相互矛盾,也就意味着我们通常根据业务场景同时选用多种方式。

数据压缩

数据压缩即通过使用压缩算法减小传输数据的体积。来达到提升传输速度的目的。

how_http_transfer_large_files_1.png

​ 为了压缩数据,我们需要使用一些压缩算法:在发送请求时,浏览器会附带一个 Accept-Encoding 头,其中包含支持的压缩算法列表,一些比较常见的压缩算法有: gzip(GZIP)、compress、deflate 和 br(Brotli)。服务器收到请求后会从列表中选择其中支持的算法,并在响应的 Content-Encoding 头中设置该算法的名称。一旦浏览器接收到响应,它就能够按照相应的算法解析响应体中的数据。

​ 在提到的上述压缩算法中,最为流行的是 GZIP,特别适用于压缩文本数据,比如 HTML、CSS 和 JavaScript。另一个值得一提的算法是 Brotli,Brotli 是 Google 在 2015 年 9 月推出的一种压缩算法,与其他压缩算法相比,Brotli 有着更高的压缩效率。它在压缩 HTML 文件方面的性能甚至超过了 GZIP。

​ 然而,这些高效的算法也有它们的局限性。它们在处理文本数据时效果显著,但在压缩图像或视频等媒体文件时效果有限,因为这些文件本身已经做了优化。比如在计算机上压缩一个视频文件,我们几乎看不到压缩前后的明显区别。将一个大小为 1GB 的视频压缩到几 KB 的程度也是不可能办到的,这样会导致显著的画质损失。因此对于如视频图片这类文件的传输,尽管压缩是一种好的方法,但我们需要更为有效的解决方案——即将文件分块传输,并在客户端组装这些分块数据。

分块传输编码

how_http_transfer_large_files_2.png

​ 通常,HTTP 应答消息中发送的数据是整个发送的,Content-Length 消息头字段表示数据的长度。数据的长度很重要,因为客户端需要知道哪里是应答消息的结束,以及后续应答消息的开始。然而,在 HTTP 1.1 中,引入了分块传输来解决上述的问题。使用分块传输编码,数据分解成一系列数据块,并以一个或多个块发送,这样服务器可以发送数据而不需要预先知道发送内容的总大小。

​ 当服务器发送响应时,会添加一个头部 Transfer-Encoding: chunked,以通知浏览器数据是以分块形式传输的。

how_http_transfer_large_files_3.png

​ 分块传输的每个数据快由下面三部分组成:

  • 一个长度块标记,表示当前分块数据的长度
  • 分块数据块
  • 每个块末尾的 CRLF 分隔符
    (CRLF 指的是 回车符 (Carriage Return,CR)换行符 (Line Feed,LF) 的组合。在 HTTP 的头部信息和分块传输中,CRLF通常用于分隔不同的部分。)

how_http_transfer_large_files_4.png

​ 服务器持续向浏览器传送分块数据,当到达数据流的末尾时,它会附加一个数据快作为结束标记,包括以下部分:

  • 一个以数字 0 结尾的长度块,以及一个 CRLF 分隔符。
  • 一个额外的 CRLF。

​ 在浏览器端,会持续接收传输的数据块,直到接收到结束标记的数据块。接着,浏览器将会移除分块编码,包括 CRLF 和长度信息,将分块数据组合成一个整体。因此,我们在使用 Chrome DevTools 中,只能看到已组装好的数据,而不是分块的数据。

​ 综上,一个 HTTP 消息的 Transfer-Encoding 消息头的值为 chunked,那么,消息体由数量未定的块组成,并以最后一个大小为 0 的块为结束。

how_http_transfer_large_files_5.png

选择性数据请求

​ 分块传输编码对于传输一个大文件来说确实十分有用,但是对于一些网站的视频功能来说,没有必要一次性传输所有的数据块,因此,我们可以采用懒加载的方式,只请求加载部分需要的数据块。这不仅节省了服务端的带宽,同时也减少了本地浏览器存储的压力。

how_http_transfer_large_files_6.png

​ 打开任意一个视频网站(例如 bilibli.com),在播放进度条栏里面可以看到除了已经播放的部分,还有一个灰色的进度条,并且这个灰色的进度条也在随着我们视频的播放逐渐向前。当我们点击灰色区域的时候,视频将立即播放,而不用等待进度条的加载完成。这部分灰色的进度条就是浏览器已经请求成功的视频部分。

how_http_transfer_large_files_7.png

​ 对于范围请求,服务器可以在返回的资源里面添加响应头里面添加 Accept-Ranges: bytes ,以表示服务器接受字节范围请求,并可以根据客户端请求返回部分内容。

​ 范围请求的请求头会带有 Range ,并且它是从 0 开始计数的,表示请求对应字段的资源。

​ 例如请求前 200 字节的数据:

Range: bytes=0-199

​ 继续请求下 200 字节的数据:

Range: bytes=200-399
how_http_transfer_large_files_8.png

​ 服务器收到请求后,如果正确命中了请求段的数据,返回 HTTP 状态码 206 Partal Content 来表示返回了部分内容,请求头里面也会添加 Content-Range 用于标注返回内容的范围与总长,例如图片里所展示的 Content-Range: bytes 200-399/1000 ,如果客户端所请求的区间未被命中或者超出了内容的范围,则返回 HTTP 状态码 416 Range Not Satisfiable 的错误。相应的,服务器也可以无视客户端请求的内容资源区间,返回所有内容并以 HTTP 状态码 200 作为响应。

​ 省略请求的结尾表示从当前字节一直请求到结尾,例如从 408 字节请求到数据段的结尾:

Range: bytes=408-

​ 类似 Array.prototype.slice() ,可以通过传入负值的方法,在不知道数据总长的情况下,请求最后 n 字节的数据,例如请求最后 100 字节的数据可以使用:

Range: bytes=-100

​ 此外,Range 字段也可以用于请求多段数据内容:

Range: bytes=0-99, -100

​ 请求多段数据的时候,服务器响应的 body 又将会与之前所介绍的有些许不同:

how_http_transfer_large_files_9.png
  • 一个以 -- 起始、中间内容行如一个随机的字符串、并以 CRLF 分隔符为结尾的一个用于标注内容边界的 boundary 指示符;
  • Content-TypeContent-Range 组成的两个 Header,展示相应数据块的属性,同样以 CRLF 分隔符结尾;
  • 一个单独的 CRLF,告诉客户端真正的数据即将到来;
  • 最终,其中的一部分数据块以依旧以一个 CRLF 分隔符作为结尾;
how_http_transfer_large_files_10.png

​ 当所有数据传输完毕后,服务器所响应的 body 会使用一个以 -- 起始,以 -- 与 CRLF 分隔符的 boundary 指示符作为结尾。当浏览器接收到结尾的 boundary 指示符时,就知道了请求的多段数据已经全部返回完毕。

​ 综上,当请求多段数据的时候,服务器的响应体看起来会像是这个样子:

how_http_transfer_large_files_11.png

尾声

​ 总结来说,HTTP 1.1 使用了上述的 数据压缩、分块传输代码、选择性数据请求 三种方法,来实现对大段数据传输的加速,这在保持了良好的用户体验的同时,也节省了服务器的带宽与流量。当我们在视频网站观看视频时,亦或是下载文件中网络波动导致需要断点续传时,都潜移默化的享受了这些技术带给我们的便利。

滚动至顶部