文章同步于 Github blog

每天都有数以亿计的各种媒体对象经由 HTTP 传送,如图像、文本、影片以及软件程序等。HTTP 要确保它所承载
的“货物”满 足以下条件:

  • 可以被正确地识别(通过Content-Type首部说明媒体格式,Content- Language 首部说明语言),以便浏览器和其他客户端能正确处理内容。
  • 可以被正确地解包(通过Content-Length首部和Content-Encoding首部)。
  • 是最新的(通过实体验证码和缓存过期控制)
  • 符合用户的需要(基于Accept系列的内容协商首部)
  • 在网络上可以快速有效地传输(通过范围请求、差异编码以及其他数据压缩方法)
  • 完整到达、未被篡改(通过传输编码首部和Content-MD5校验和首部)

HTTP 实体首部

HTTP 实体首部描述了 HTTP 报文的内容。HTTP/1.1 版定义了以下 10 个基本字体首部字段。

  • Content-Type:实体中所承载对象的类型。
  • Content-Length: 所传送实体主体的长度或大小。
  • Content-Language: 与所传送对象最相配的人类语言。
  • Content-Encoding: 对象数据所做的任意变换(比如,压缩)。
  • Content-Location: 一个备用位置,请求时可通过它获得对象。
  • Content-Range: 如果这是部分实体,这个首部说明它是整体的哪个部分。
  • Content-MD5: 实体主体内容的校验和。
  • Last-Modified: 所传输内容在服务器上创建或最后修改的日期时间。
  • Expires: 实体数据将要失效的日期时间。
  • Allow: 该资源所允许的各种请求方法,例如,GET 和 HEAD。
  • ETag: 这份文档特定实例的唯一验证码。ETag 首部没有正式定义为实体首部,但它对许多涉及实体的操作来说,都是一个重要的首部。
  • Cache-Control: 指出应该如何缓存该文档。和 ETag 首部类似,Cache-Control 首部也没有正 式定义为实体首部。

实体的大小(Content-Length)

除非使用了分块编码,否则 Content-Length 首部就是带有实体主体的报文必须使用的。使用 Content-Length 首部是为了能够检测出服务器崩溃而导致的报文截尾,并对共享持久连接的多个报文进行正确分段。

Content-Length 首部指示出报文中实体主体的字节大小。这个大小是包含了所有内容编码的。如果主体进行了内容编码,Content-Length 首部说明的就是 编码后(encoded) 的主体的字节长度,而不是未编码的原始主体的长度。

没有 Content-Length 的话,客户端无法区分到底是报文结束时正常的连接关闭,还是报文传输中由于服务器崩溃而导致的连接关闭。客户端需要通过 Content-Length 来检测 报文截尾

Content-Length 首部对于持久连接是必不可少的。如果响应通过持久连接传送, 就可能有另一条 HTTP 响应紧随其后。客户端通过 Content-Length 首部就可以知道报文在何处结束,下一条报文从何处开始。因为连接是持久的,客户端无法依赖 连接关闭来判别报文的结束。

有一种情况下,使用持久连接时可以没有 Content-Length 首部,即采用分块编码(chunked encoding)时。在分块编码的情况下,数据是分为一系列的块来发送的,每块都有大小说明。哪怕服务器在生成首部的时候不知道整个实体的大小(通常是因为实体是动态生成的),仍然可以使用分块编码传输若干已知大小的块。

实体摘要(Content-MD5)

服务器使用 Content-MD5 首部发送对实体主体运行 MD5 算法的结果。只有产生响应的原始服务器可以计算并发送 Content-MD5 首部。如果一份文档使用 gzip 算法进 行压缩,然后用分块编码发送,那么就对整个经 gzip 压缩的主体进行 MD5 计算。

媒体类型(MIME Type)和字符集

Content-Type 首部字段说明了实体主体的 MIME 类型。6MIME 类型是标准化的 名字,用以说明作为货物运载实体的基本媒体类型(比如:HTML 文件、Microsoft Word 文档或是 MPEG 视频等)。客户端应用程序使用 MIME 类型来解释和处理其内容

Content-Type 首部说明的是原始实体主体的媒体类型。例如, 如果实体经过内容编码的话,Content-Type 首部说明的仍是编码之前的实体主体的类型。

Content-Type 首部还支持可选的参数来进一步说明内容的类型。charset(字符集)参数就是个例子,它说明把实体中的比特转换为文本文件中的字符的方法:

Content-Type: text/html; charset=iso-8859-4

MIME 中的 multipart(多部分)电子邮件报文中包含多个报文,它们合在一起作 为单一的复杂报文发送。每一部分都是独立的,有各自的描述其内容的集;不同的 部分之间用分界字符串连接在一起。

HTTP 也支持多部分主体。不过,通常只用在下列两种情形之一:提交填写好的表格,或是作为承载若干文档片段的范围响应。

内容编码(Content-Encoding)

内容编码,是对报文的主体进行的可逆变换。内容编码是和内容的具体格式细节紧密相关的。例如,你可能会用 gzip 压缩文本文件,但不是 JPEG 文 件,因为 JPEG 这类东西用 gzip 压缩的不够好。

HTTP 定义了一些标准的内容编码类型,并允许用扩展编码的形式增添更多的编码。 由 互联网号码分配机构(IANA)对各种编码进行标准化,它给每个内容编码算法分配了唯一的代号。Content-Encoding 首部就用这些标准化的代号来说明编码时使 用的算法。

  • gzip:表明实体采用 GNU zip 编码
  • compress:表明实体采用 Unix 的文件压缩程序
  • deflate:表明实体是用 zlib 的格式压缩的
  • identity:表明没有对实体进行编码。当没有 Content-Encoding 首部时,就默认为这种情况。

Accept-Encoding首部

毫无疑问,我们不希望服务器用客户端无法解码的方式来对内容进行编码。为了避免服务器使用客户端不支持的编码方式,客户端就把自己支持的内容编码方式列表放在请求的 Accept-Encoding 首部里发出去。

如果 HTTP 请求中没有包含 Accept-Encoding 首部,服务器就可以假设客户端能够接受任何编码方式(等价 于发送 Accept-Encoding: *)。

客户端可以给每种编码附带 Q(质量)值参数来说明编码的优先级。Q 值的范围从0.0 到 1.0,0.0 说明客户端不想接受所说明的编码,1.0 则表明最希望使用的编码。 “*”表示“任何其他方法”。决定在响应中回送什么内容给客户端是个更通用的过程,而选择使用何种内容编码则是此过程的一部分。

compress;q=0.5, gzip;q=1.0 gzip;q=1.0, identity; q=0.5, *;q=0

传输编码(Transfer Codings)

传输编码

传输编码也是作用在实体主体上的可逆变换,但使用它们是由于架构方面的原因,同内容的格式无关。使用传输编码是为了改变 报文中的数据在网络上传输的方式。

Transfer-Encoding首部

HTTP 协议中只定义了下面两个首部来描述和控制传输编码。

  • Transfer-Encoding 告知接收方为了可靠地传输报文,已经对其进行了何种编码。
  • TE 用在请求首部中,告知服务器可以使用哪些传输编码扩展。

下面的例子中,请求使用了 TE 首部来告诉服务器它可以接受分块编码(如果是 HTTP/1.1 应用程序的话,这就是必须的)并且愿意接受附在分块编码的报文结尾上 的拖挂:

GET /new_products.html HTTP/1.1
Host: www.joes-hardware.com
User-Agent: Mozilla/4.61 [en] (WinNT; I) TE: trailers, chunked
...

对它的响应中包含 Transfer-Encoding 首部,用于告诉接收方已经用分块编码对
报文进行了传输编码:

HTTP/1.1 200 OK Transfer-Encoding: chunked Server: Apache/3.0
...

在这个起始首部之后,报文的结构就将发生改变。

传输编码的值都是大小写无关的。HTTP/1.1 规定在 TE 首部和 Transfer-Encoding 首部中使用传输编码值。最新的 HTTP 规范只定义了一种传输编码,就是分块编码

分块编码

分块编码把报文分割为若干个大小已知的块。块之间是紧挨着发送的,这样就不需要在发送之前知道整个报文的大小了。

要注意的是,分块编码是一种传输编码,因此是报文的属性,而不是主体的属性

分块与持久连接

若客户端和服务器之间不是持久连接,客户端就不需要知道它正在读取的主体的长度,而只需要读到服务器关闭主体连接为止。

当使用持久连接时,在服务器写主体之前,必须知道它的大小并在 Content- Length 首部中发送。如果服务器动态创建内容,就可能在发送之前无法知道主体的长度。

分块编码为这种困难提供了解决方案,只要允许服务器把主体逐块发送,说明每块的大小就可以了。因为主体是动态创建的,服务器可以缓冲它的一部分,发送其大小和相应的块,然后在主体发送完之前重复这个过程。服务器可以用大小为 0 的块作为主体结束的信号,这样就可以继续保持连接,为下一个响应做准备。

客户端也可以发送分块的数据给服务器。因为客户端事先不知道服务器是否接受分块编码(这是因为服务器不会在给客户端的响应中发送 TE 首部),所以客户端必须做好服务器用411 Length Required(需要Content-Length首部)响应来拒绝分块请求的准备。

分块报文的拖挂

如果客户端的 TE 首部中说明它可以接受拖挂的话,就可以在分块的报文最后加上拖挂。产生原始响应的服务器也可以在分块的报文最后加上拖挂。拖挂的内容是可选的元数据,客户端不一定需要理解和使用(客户端可以忽略并丢弃拖挂中的内容)。

拖挂中可以包含附带的首部字段,它们的值在报文开始的时候可能是无法确定的 (例如,必须要先生成主体的内容)。Content-MD5 首部就是一个可以在拖挂中发送的首部,因为在文档生成之前,很难算出它的 MD5。

除了 Transfer-Encoding、Trailer 以及 Content-Length 首部之外,其他 HTTP 首部都可以作为拖挂发送。

验证码和新鲜度

当文档在客户端“过期”之后(也就是说,客户端不再认为该副本有效),客户端必须从服务器请求一份新的副本。不过,如果该文档在服务器上并未发生改变,客户 端也就不需要再接收一次了——继续使用缓存的副本即可。

这种特殊的请求,称为 条件请求(conditional request),要求客户端使用 验证码 (validator)
来告知服务器它当前拥有的版本号,并仅当它的当前副本不再有效时才要求发送新的副本。

新鲜度

服务器应当告知客户端能够将内容缓存多长时间,在这个时间之内就是新鲜的。 服务器可以用这两个首部之一来提供这种信息: Expires(过期)Cache- Control(缓存控制)

客户端和服务器为了能正确使用 Expires 首部,它们的时钟必须同步。

条件请求(Conditional Requests)

HTTP 为客户端提供了一种方法,仅当资源改变时才请求副本, 这种特殊请求称为有条件的请求。有条件的请求是标准的 HTTP 请求报文,但仅当某个特定条件为真时才执行。

例如,某个缓存服务器可能发送下面的有条件 GET 报文给服务器,仅当文件 /announce.html 从 2002 年 6 月 29 日(这是缓存的文档最后 被作者修改的时间)之后发生改变的情况下才发送它:

GET /announce.html HTTP/1.0
If-Modified-Since: Sat, 29 Jun 2002, 14:30:00 GMT

有条件的请求是通过以“If-”开头的有条件的首部来实现的。

验证码

每个有条件的请求都通过特定的验证码来发挥作用。验证码是文档实例的一个特殊属性,用它来测试条件是否为真。从概念上说,你可以把验证码看作文件的序列号、 版本号,或者最后发生改变的日期时间。

HTTP把验证码分为两类弱验证码(weak validators)强验证码(strong validators)。 弱验证码不一定能唯一标识资源的一个实例,而强验证码必须如此。

最后修改时间(If-Modified-Since)被当作弱验证码,因为尽管它说明了资源最后被修改的时间,但它的描述精度最大就是 1 秒

ETag 首部被当作强验证码,因为每当资源内容改变时,服务器都可以在 ETag 首部放置不同的值。

有时候,客户端和服务器可能需要采用不那么精确的实体标记验证方法。例如,某服务器可能想对一个很大、被广泛缓存的文档进行一些美化修饰,但不想在缓存服务器再验证时产生很大的传输流量。在这种情况下,该服务器可以在标记前面加上“W/”前缀来广播一个“弱”实体标记。

范围请求(Range Requests)

关于客户端如何要求服务器只在资源的客户端副本不再有效的情况下才发送其副本, 我们已经清楚地理解了。HTTP 还进一步锦上添花:它允许客户端实际上只请求文 档的一部分,或者说某个范围

有了范围请求,HTTP 客户端可以通过请求曾获取失败的实体的一个范围(或者说一部分),来恢复下载该实体。当然这有一个前提,那就是从客户端上一次请求该实 体到这次发出范围请求的时段内,该对象没有改变过。例如:

GET /bigfile.html HTTP/1.1
Host: www.joes-hardware.com
Range: bytes=4000-
User-Agent: Mozilla/4.61 [en] (WinNT; I) ...

在本例中,客户端请求的是文档开头 4000 字节之后的部分(不必给出结尾字节数, 因为请求方可能不知道文档的大小)。在客户端收到了开头的 4000 字节之后就失败的情况下,可以使用这种形式的范围请求。

并不是所有服务器都接受范围请求,但很多服务器可以。服务器可以通过在响应中包含 Accept-Ranges 首部的形式向客户端说明可以接受的范围请求。这个首部的值是计算范围的单位,通常是以字节计算的。例如:

HTTP/1.1 200 OK
Date: Fri, 05 Nov 1999 22:35:15 GMT Server: Apache/1.2.4
Accept-Ranges: bytes

涉及范围请求的一系列 HTTP 事务的例子:
image

Range 首部在流行的 点对点(Peer-to-Peer,P2P) 文件共享客户端软件中得到广泛应用,它们从不同的对等实体同时下载多媒体文件的不同部分。

差异编码(Delta encoding)

如果客户端有一个页面的已过期副本,就要请求页面的最新实例。若改变的地方比较少,与其发送完整的新页面给客户端,客户端更愿意服务器只发送页面发生改变的部分,这样就可以更快地得到最新的页面。

差异编码是 HTTP 协议的一个扩展,它通过交换对象改变的部分而不是完整的对象来优化传输性能。差异编码也是一类实例操控,因为它依赖客户端和服务器之间针对特定的对象实例来交换信息。RFC 3229 描述了差异编码。

如果客户端想告诉服务器它愿意接受该页面的差异,只要发送 A-IM 首部 就可以了。A-IM 是 Accept-Instance-Manipulation(接受实例操控) 的缩写。

在 A-IM 首部中,客户端会说明它知道哪些算法可以把差异应用于老版本而得到最新版本。

服务端发送回下面这些内容:一个特殊的响应代码—— 226 IM Used,告知客户端它正在发送的是所请求对象的实例操控,而不是那个完整的对象自身;一个 IM(Instance-Manipulation 的缩写) 首部,说明用于计算差异的算法;新的 ETag 首部Delta-Base 首部,说明用于计算差异的基线文档的 ETag。

image

服务器侧的“差异生成器”根据基线文档和该文档的最新实例,用客户端在 A-IM 首部中指明的算法计算它们之间的差异。客户端侧的“差异应用器” 得到差异,将其应用于基线文档,得到文档的最新实例。例如,如果产生差异的算 法是 Unix 系统的 diff-e 命令,客户端就可以用 Unix 系统中的文本编辑器 ed 提供的 功能来应用差异。

差异编码所用的首部

ETag

文档每个实例的唯一标识符。由服务器在响应中发送;客户端在后继请求的 If-Match 首部和 If-None-Match 首部中可以使用它

If-None-Match

客户端发送的请求首部,当且仅当客户端的文档版本与服务器不同时,才向服务器请求该文档

A-IM

客户端请求首部,说明可以接受的实例操控类型

IM

服务器响应首部,说明作用在响应上的实例操控的类型。当响应代码是226 IM Used 时,会发送这个首部

Delta-Base

服务器响应首部,说明用于计算差异的基线文档的 ETag 值(应当与客户端请求
中的 If-None-Match 首部里的 ETag 相同)

参考


Pines_Cheng
6.5k 声望1.2k 粉丝

不挑食的程序员,关注前端四化建设。