HTTP 大文件传输

1,130 阅读16分钟

早期的 HTTP 传输的主要是一些几 kb 的文件或者图片,但是 HTTP 发展到如今,可以传递各种各样的数据。例如:音频、视频等。但是视频文件往往很大,清晰度高的视频文件往往有几个G的大小,如果一次性传输势必会造成网络拥塞。那么针对大文件的传输,HTTP 有哪些手段来处理呢?本文就来聊聊 HTTP 如何传输大文件。

一、HTTP 数据压缩(compression)

解决大文件传输的问题,那么最简单的方法,就是将大文件变小点。对此,HTTP 中提供一系列用于数据压缩的方法。数据压缩会在三个不同的层面发挥作用:

  • 首先某些格式的文件会采用特定的优化算法进行压缩,
  • 其次在 HTTP 协议层面会进行通用数据加密,即数据资源会以压缩的形式进行端到端传输,
  • 最后数据压缩还会发生在网络连接层面,即发生在 HTTP 连接的两个节点之间。

1.1 文件格式压缩

所有格式的文件都会存在冗余,即浪费的空间。所以如果能在传输前回收这些冗余的部分,那么就能降低传输的压力,是的数据接收得更快。

在计算机中,所有的数据都是以二进制的形式存储的(即:0和1),文件压缩就是通过某些算法简化这些 0 和 1 的排列组合。简单来说,就是用更少的 0 和 1 来表示一个文件。用于文件的压缩算法可以大致分为两类:

  • 无损压缩算法:在压缩与解压缩的过程中,不会对要恢复的数据进行修改。复原后的数据与原始数据是一致的(比特与比特之间一一对应)。 对于图片文件来说,gif 或者 png 格式的文件就是采用了无损压缩算法。.
  • 有损压缩算法:在压缩与解压缩的过程中,会对原始数据进行修改,但是会以用户无法觉察的方式进行。 网络上的视频文件通常采用有损压缩算法,图片是 jpeg

一些特定的文件格式既可以采用无损压缩算法,又可以采用有损压缩算法,例如 webp ,并且有损压缩算法可以对压缩比率进行配置,当然这会导致压缩品质的不同。

通常理想情况是在保持可以接受的品质水准的前提下,压缩比率尽可能得高。所以,在传输之前会采用一些工具对文件进行压缩。

注意:文件压缩是在HTTP传输之前通过一些压缩算法去除文件中的冗余部分,以此来降低需要传输的数据大小。而且重复压缩反而会起反作用,所以对于压缩文件不要使用后续的两种数据压缩方法。

1.2 端到端压缩技术

端到端压缩技术指的是消息体压缩是在服务器端完成的,并且在传输过程中保持不变,直到抵达客户端。不管途中遇到什么样的中间节点,它们都会使消息体保持原样。

但是使用这种压缩方法需要浏览器于服务器进行协商,采用哪种压缩算法(例如:gzip、deflate、br等)进行数据压缩,这以协商过程通常与客户端的Accept-Encoding 请求头部 和 服务器的 Content-Encoding 响应头部相关:

浏览器发送 Accept-Encoding 首部,其中包含有它所支持的压缩算法,以及各自的优先级,服务器则从中选择一种,使用该算法对响应的消息主体进行压缩,并且发送 Content-Encoding 首部来告知浏览器它选择了哪一种算法。由于该内容协商过程是基于编码类型来选择资源的展现形式的,在响应中, Vary首部中至少要包含 Accept-Encoding;这样的话,缓存服务器就可以对资源的不同展现形式进行缓存

image.png

不过这个解决方法也有个缺点,gzip 等压缩算法通常只对文本文件有较好的压缩率,而图片、音频视频等多媒体数据本身就已经是高度压缩的,再用 gzip 处理也不会变小(甚至还有可能会增大一点),所以它就失效了。

1.3 逐跳压缩技术

逐跳压缩技术尽管与端到端压缩技术有些类似,但是它们在一点上有着本质的区别,即这里的压缩指的不是对源头服务器上的资源的压缩,以此来创建一份特定的展现形式然后进行传输,而是对客户端与服务器端之间的任意两个节点之间传递的消息的主体的压缩。在两个相邻的中间节点之间的连接上,可能会应用不同的压缩方式

为了实现这个目的,HTTP 协议中采用了与端到端压缩技术所使用的内容协商机制相类似的机制:节点发送请求,使用 TE首部来宣布它的意愿,另外一个节点则从中选择合适的方法,进行应用,然后在 Transfer-Encoding 首部中指出它所选择的方法。

image.png

在实际应用中,逐跳压缩对于服务器和客户端来说是不可见的,并且很少使用。TE首部和 Transfer-Encoding首部最常用来发送分块响应,允许在获得资源的确切长度之前就可以开始传输。

二、分块传输

第一节中我们谈到,可以将大文件进行压缩,减小大文件的“体积”,以此来提高传输的性能。除了把大文件整体缩小,我们能不能换种思路,把文件分解成多个小块,然后分批将每一小块传输给浏览器,浏览器接收到所有小块后再拼接复原。对此,HTTP/1.1 中开始支持分块传输编码。即,数据分解成一系列数据块,并以一个或多个块发送。

2.1 分块传输编码

HTTP/1.1 中引入了管线化技术,这意味着客户端每次发送请求不需要等待上一个请求回应,所以在一个TCP连接中,服务器可以传送多个回应,这便需要一种机制,区分数据包时属于哪一个回应的。Content-Length 字段就是用于声明本次回应的数据长度。例如:Content-Length: 3945 告诉浏览器,本次回应的长度是3945个字节,后面的字节属于下一个回应。

但是这种方法需要预先知道响应数据的大小,对于一些很耗时的动态操作来说,这意味着,服务器要等到所有操作完成,才能发送数据,显然这样的效率不高。所以,HTTP/1.1 中引入了分块传输编码(chunked transfer encoding),通过在响应报文里用头字段 Transfer-Encoding: chunked 来表示。即数据分解成一系列数据块,并以一个或多个块发送,这样服务器可以发送数据而不需要预先知道发送内容的总大小。

分块传输的编码规则

  1. 每个分块包含两个部分,长度头和数据块;
  2. 长度头是以 CRLF(回车换行,即\r\n)结尾的一行明文,用 16 进制数字表示长度;
  3. 数据块紧跟在长度头后,最后也用 CRLF 结尾,但数据不包含 CRLF;
  4. 最后用一个长度为 0 的块表示结束,即“0\r\n\r\n”

image.png

举个例子:

假设客户端向服务器发送了如下请求:

HTTP/1.1 200 OK 
Content-Type: text/plain 
Transfer-Encoding: chunked 

服务器通过分块传输编码响应如下:

25 
This is the data in the first chunk 

1C 
and this is the second one 

8
sequence 

0

注意Transfer-Encoding: chunkedContent-Length 这两个字段是互斥的,也就是说响应报文里这两个字段不能同时出现,一个响应报文的传输要么是长度已知,要么是长度未知

2.2 Transfer-Encoding 头字段

Transfer-Encoding 消息首部指明了将实体安全传递给用户所采用的编码形式。

Transfer-Encoding 是一个逐跳传输消息首部,即仅应用于两个节点之间的消息传递,而不是所请求的资源本身。一个多节点连接中的每一段都可以应用不同的Transfer-Encoding 值。

当这个消息首部出现在 HEAD 请求的响应中,而这样的响应没有消息体,那么它其实指的是应用在相应的  GET 请求的应答的值。

Transfer-Encoding: chunked
Transfer-Encoding: compress
Transfer-Encoding: deflate
Transfer-Encoding: gzip
Transfer-Encoding: identity
  • chunked 数据以一系列分块的形式进行发送。 Content-Length 首部在这种情况下不被发送。在每一个分块的开头需要添加当前分块的长度,以十六进制的形式表示,后面紧跟着 '\r\n' ,之后是分块本身,后面也是'\r\n' 。终止块是一个常规的分块,不同之处在于其长度为0。终止块后面是一个挂载(trailer),由一系列(或者为空)的实体消息首部构成。

  • compress 采用 Lempel-Ziv-Welch(LZW) 压缩算法。

  • deflate 采用 zlib结构压缩算法

  • gzip 表示采用  Lempel-Ziv coding(LZ77) 压缩算法,以及32位CRC校验的编码方式。这

  • identity 用于指代自身(例如:未经过压缩和修改)。除非特别指明,这个标记始终可以被接受

三、HTTP 范围请求(HTTP range requests)

有了第二节中介绍的分块传输编码技术,我们可以轻松收发大文件了。但是如果我们只需要某些大文件的一部分,那么传递整个文件就会造成不必要的浪费。例如:我们在某些电视剧的时候,希望跳过片头和片尾,或者快进几分钟,这些跳过的部分我们并不需要获取,我们仅需要获取这个视频文件的片段数据,对于分块传输来说并不能实现这个功能。对此 HTTP 协议中提出 范围请求(range request) 的概念。

HTTP 协议范围请求允许服务器只发送 HTTP 消息的一部分到客户端。范围请求在传送大的媒体文件,或者与文件下载的断点续传功能搭配使用时非常有用。

3.1 服务器是否支持范围请求 —— Accept-Ranges

如果响应中存在 Accept-Ranges首部(并且它的值不为 “none”),那么表示该服务器支持范围请求。

Accept-Ranges 响应头字段

服务器使用 HTTP 响应头 Accept-Ranges 标识自身支持范围请求(partial requests)。字段的具体值用于定义范围请求的单位。当浏览器发现 Accept-Ranges 头时,可以尝试继续中断了的下载,而不是重新开始。

Accept-Ranges: bytes
Accept-Ranges: none
  • none:不支持任何范围请求单位,由于其等同于没有返回此头部,因此很少使用。不过一些浏览器,比如IE9,会依据该头部去禁用或者移除下载管理器的暂停按钮。
  • bytes: 范围请求的单位是 bytes (字节)。

3.2 请求范围 - Range

假如服务器支持范围请求的话,你可以使用 Range 首部来生成该类请求。该首部指示服务器应该返回文件的哪一或哪几部分。

Range 请求头字段

Range 是一个请求首部,告知服务器返回文件的哪一部分。在一个  Range 首部中,可以一次性请求多个部分,服务器会以 multipart 文件的形式将其返回。

Range: <unit>=<range-start>-
Range: <unit>=<range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>
  • <unit>: 范围所采用的单位,通常是字节(bytes)。
  • <range-start>: 一个整数,表示在特定单位下,范围的起始值
  • <range-end>: (可选)一个整数,表示在特定单位下,范围的结束值。这个值是可选的,如果不存在,表示此范围一直延伸到文档结束。 例如:
Range: bytes=200-1000, 2000-6576, 19000-

3.3 范围请求的响应

通常服务器收到 Range 字段后,会检查范围是否合法,比如文件只有 100 个字节,但请求“200-300”,这就是范围越界了。服务器就会返回 416 Requested Range Not Satisfiable 如果范围正确,服务器就可以根据 Range 头计算偏移量,读取文件的片段了,返回状态码“206 Partial Content”。服务器会添加一个响应头字段Content-Range,告诉片段的实际偏移量和资源的总大小,然后发送片段给客户端,一个范围请求就算是处理完了。

所以,范围请求会存在以下三种响应状态:

  • 请求成功的情况下,服务器会返回  206 Partial Content 状态码。
  • 请求的范围越界的情况下(范围值超过了资源的大小),服务器会返回 416 Requested Range Not Satisfiable (请求的范围无法满足) 状态码。
  • 不支持范围请求的情况下,服务器会返回 200 OK 状态码。

举个例子,假设客户端发送了如下请求:

GET /z4d4kWk.jpg HTTP/1.1
Host: i.imgur.com
Range: bytes=0-1023

服务器端会返回状态码为 206 Partial Content 的响应:

HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1023/146515
Content-Length: 1024
...
(binary content)

其中:

  • Content-Length首部:用来表示先前请求范围的大小(而不是整张图片的大小)
  • Content-Range响应首部:表示这一部分内容在整个资源中所处的位置

此外,在实际应用中,不仅看视频的拖拽进度需要范围请求,常用的下载工具里的多段下载、断点续传也是基于它实现的,要点是:

  • 先发个 HEAD,看服务器是否支持范围请求,同时获取文件的大小;
  • 开 N 个线程,每个线程使用 Range 字段划分出各自负责下载的片段,发请求传输数据;
  • 下载意外中断也不怕,不必重头再来一遍,只要根据上次的下载记录,用 Range 请求剩下的那一部分就可以了。

3.4 多段数据

Range头部也支持一次请求文档的多个部分。请求范围用一个逗号分隔开。

与单段数据不同的是,服务器会206 Partial Content 状态码和Content-Type:multipart/byteranges; boundary=xxx头部

  • Content-Type:multipart/byteranges表示这个响应有多个byterange。每一部分byterange都有他自己的Content-type头部和Content-Range
  • boundary=xxx:段之间的分隔标记。即对body进行划分。

image.png

假设客户端发送了如下多段数据请求:

curl http://www.example.com -i -H "Range: bytes=0-50, 100-150"

服务器会作出如下响应:

HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=3d6b6a416f9b5
Content-Length: 282

--3d6b6a416f9b5
Content-Type: text/html
Content-Range: bytes 0-50/1270

<!doctype html>
<html>
<head>
    <title>Example Do
--3d6b6a416f9b5
Content-Type: text/html
Content-Range: bytes 100-150/1270

eta http-equiv="Content-type" content="text/html; c
--3d6b6a416f9b5--

3.5 条件范围请求 —— If-Range

当(中断之后)重新开始请求更多资源片段的时候,必须确保自从上一个片段被接收之后该资源没有进行过修改。

If-Range请求首部可以用来生成条件式范围请求,即Range 头字段在一定条件下起作用:

  • 假如条件满足的话,条件请求就会生效,服务器会返回状态码为 206``Partial 的响应,以及相应的消息主体。
  • 假如条件未能得到满足,那么就会返回状态码为 200``OK 的响应,同时返回整个资源。

该首部可以与  Last-Modified 或者 ETag 其中一个一起使用。

If-Range: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
If-Range: <etag>
  • <etag>: 一个资源标签(entity tag)代表着所请求的资源。它是由被双引号包围的ACSII 编码的字符串组成的(例如"675af34563dc-tr34")。当应用弱匹配算法时,E-Tag会有一个 W/ 前缀。
  • <day-name>: "Mon""Tue""Wed""Thu""Fri""Sat"或者"Sun"当中的一个(大小写敏感)。
  • <day>: 两位数字,例如"04"或者"23"
  • <month>: "Jan""Feb","Mar""Apr""May""Jun""Jul""Aug""Sep""Oct""Nov",或者"Dec"中的一个(大小写敏感)。
  • <year>: 四位数字,例如"1990"或者"2016"。
  • <hour>: 两位数字,例如"09"或者"23"
  • <minute>: 两位数字,例如"04"或者"59"
  • <second>: 两位数字,例如"04"或者"59"
  • GMT: 格林威治标准时间。HTTP 协议的日期总是要使用GMT,而不是当地时间。 例如:
If-Range: Wed, 21 Oct 2015 07:28:00 GMT

注意:范围请求是用于指定请求数据的某个部分,而分块传输编码是一种传输方式,两者互相兼容,可以单独或配合使用。

小结:

HTTP 中用于传输大文件的方式主要是数据压缩、分块传输、范围请求三种方法:

  • 数据压缩是从缩小文件本身大小的角度来降低传输的压力,进而缩短传输时间。主要通过文件格式压缩、端到端数据压缩、逐跳数据压缩三种方法
  • 分块传输是从传输方式的角度降低传输压力,文件太大,那就分成多个小块分批次传输。主要用过 Transfer-Encoding: chunked 响应头字段来表示。
  • 范围请求则是考虑到在请求某些大文件时,只需要其中一部分,例如:视频文件快进、断点重传等。这个时候就可以使用范围请求技术,请求指定的部分。

本文到此也就结束了,主要是参考一些大佬或者官方的文章做的一些学习记录,如有错误,请大佬们指正。

参考:
[1] HTTP 协议中的数据压缩
[2] HTTP请求范围
[3] 透视HTTP协议