Web性能优化(三):HTTP协议的优化

3,152 阅读14分钟

--本文采自本人公众号【猴哥别瞎说】

HTTP 协议是互联网的基础协议,也是网页开发的必备知识。本文主要介绍 HTTP 协议的历史演变和设计思路。

HTTP 是基于 TCP/IP 协议的应用层协议。关于 TCP 的介绍,可以看看这两篇文章:三次握手改善堵塞

发展脉络

HTTP 的发展脉络,是非常有趣的。我们来看看这个过程:

看它的各个版本发布的时间,和互联网兴起的时间点是一致的。看它的设计原则,像不像现在经常看到的某个软件产品占领市场的方式?

  1. 一开始尽可能地以简单的方式,获取用户;
  2. 当用户规模上来了,拥有了市场话语权之后,开始制定市场的规范;
  3. 及时修正规范中的瑕疵;
  4. 跟上时代的步伐,自我迭代,发展向前;

那我们就尝试以这个角度来看看,它是怎么发展的吧~

HTTP 0.9

这个时候的协议,特点就是:简介。简介到什么程度呢?HTTP 0.9 是请求只有一行的协议。举个栗子:

GET /index.html

上面命令表示,TCP 连接(connection)建立后,客户端向服务器请求(request)网页index.html。

请求只有一行,包括 GET 方法和要请求的文档的路径。响应是一个超文本文档,没有首部,也没有其他元数据,只有 HTML。这实在是简单得不能再简单了!

简单!但有效啊!此时的互联网发展还不是那么发达。这么简单的一个协议,已经能够满足当时人们的需求。

以 1991 年这个低调开端为起点,HTTP 在随后几年中展现了自己的生命力,得到了迅速发展。

HTTP 1.0

背景

1991 年到 1995 年,HTML 规范和一种新型的名叫“Web 浏览器”的软件都获得了快速发展。与此同时,面向消费者的公共互联网基础设施,也日渐兴起并迅速发展起来。

随着人们对新兴 Web 的需求越来越多,以及它们在公共 Web 上的应用迅速爆发, HTTP 0.9 的很多根本性不足便暴露出来:

  • 它只能提供 HTML 格式的应答么?我们想要更加丰富的应答内容...
  • 它可以提供更多关于请求和响应的各种元数据么?
  • ...

因为HTTP 0.9 的简单化,让其拥有了大量的用户。也因为它的简单化,让很多新兴的 Web 开发者社区能够在其基础之上(而不是推翻它,另寻其他协议)推出很多满足市场需求的实验性实现。

详细

就在这些实验性开发的基础之上,HTTP 工作组总结了一套最佳实践,将其写成了规范,变成了 HTTP 1.0。当然也有人说,HTTP 1.0 并不是一个正式的规范或互联网标准。但这不重要。我们来看看经典的 HTTP 1.0 都有什么功能吧:

  1. 首先,任何格式的内容都可以发送。这使得互联网不仅可以传输文字,还能传输图像、视频、二进制文件。这为互联网的大发展奠定了基础。

  2. 其次,除了GET命令,还引入了POST命令和HEAD命令,丰富了浏览器与服务器的互动手段。

  3. 再次,HTTP请求和应答的格式也变了。除了数据部分,每次通信都必须包括头信息(HTTP header),用来描述一些元数据。

  4. 其他的新增功能还包括状态码(status code)、多字符集支持、多部分发送(multi-part type)、权限(authorization)、缓存(cache)、内容编码(content encoding)等。

一个非常经典的栗子:

//请求

GET / HTTP/1.0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)
Accept: */*

//应答

HTTP/1.0 200 OK 
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 05 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 5 August 1996 15:55:28 GMT
Server: Apache 0.84

<html>
  <body>Hello World</body>
</html>

和 HTTP 0.9 相比,复杂多了对吧。

HTTP 1.0 缺点

HTTP/1.0 的主要缺点是,每个TCP连接只能发送一个请求。发送数据完毕,连接就关闭,如果还要请求其他资源,就必须再新建一个连接。

看之前的文章,就会知道TCP连接的新建成本很高:因为需要客户端和服务器三次握手,并且开始时发送速率较慢(slow start)。所以,HTTP 1.0版本的性能比较差。随着网页加载的外部资源越来越多,这个问题就愈发突出了。

为了解决这个问题,有些浏览器在请求时,用了一个非标准的Connection字段:


Connection: keep-alive

这个字段要求服务器不要关闭TCP连接,以便其他请求复用。服务器同样回应这个字段:


Connection: keep-alive

一个可以复用的TCP连接就建立了,直到客户端或服务器主动关闭连接。

但是!!! 这不是标准字段,不同实现的行为可能不一致。因此不是根本的解决办法。

HTTP 1.1

为了解决 HTTP 1.0 的这个突出问题,仅仅过了半年的时间,HTTP 1.1 就推出了。这个版本进一步完善了 HTTP 协议,一直用到了今天,直到现在还是最流行的版本。

它不仅仅解决了 HTTP 1.0 中出现的 TCP 请求连接问题,还增加了更多的功能:持久化连接、管道机制、分块传输编码等。我们来看看:

持久化连接

HTTP 1.1 改变了 HTTP 协议的语义,默认使用持久连接。即TCP连接默认不关闭,可以被多个请求复用,不用声明 Connection: keep-alive。它的优点与解决的问题在 HTTP 1.0 中已经讲过,在此不敷述。

客户端和服务器发现对方一段时间没有活动,就可以主动关闭连接。不过,规范的做法是,客户端在最后一个请求时,发送Connection: close,明确要求服务器关闭TCP连接。

换句话说,除非明确告知(通过 Connection: close 首部),否则服务器默认会保持连接打开。

管道机制

HTTP 1.1 还引入了管道机制(pipelining),即在同一个TCP连接里面,客户端可以同时发送多个请求

举例来说,客户端需要请求两个资源。以前的做法是,在同一个TCP连接里面,先发送A请求,然后等待服务器做出回应,收到后再发出B请求。如下图:

管道机制则是允许浏览器同时发出A请求和B请求。这样就加快了请求的效率。

不过,这里的管道机制,仅仅是针对请求端。

对于服务端,依照按照 FIFO 的顺序,先回应A请求,完成后再回应B请求。这个设定,是因为HTTP 1.x 只能严格串行地返回响应。特别是,HTTP 1.x 不允许一个连接上的多个响应数据交错到达(多路复用),因而一个响应必须完全返回后,下一个响应才会开始传输。

服务端的这种设定,在遇到譬如“队首堵塞”问题的时候,会极大影响性能。这个缺陷,将会在 HTTP 2 中得到修正。

Content-Length 字段

既然管道机制中的一个 TCP 连接可以传送多个应答,那么势必就需要一种机制来区分数据包是属于哪一个回应的。这就是Content-length字段的作用,声明本次回应的数据长度。


Content-Length: 2408

上面代码告诉浏览器,本次回应的长度是 2408 个字节,后面的字节就属于下一个回应了。

分块传输编码

服务端使用 Content-Length 字段的前提条件是:服务端发送应答之前,必须知道应答的数据长度。

对于一些很耗时的动态操作来说,这意味着,服务器要等到所有操作完成,才能发送数据,显然这样的效率不高。更好的处理方法是,产生一块数据,就发送一块,采用"流模式"(stream)取代"缓存模式"(buffer)。

因此,HTTP1.1 规定可以不使用 Content-Length 字段,而使用"分块传输编码"(chunked transfer encoding)。只要请求或回应的头信息有Transfer-Encoding字段,就表明回应将由数量未定的数据块组成。


Transfer-Encoding: chunked

每个非空的数据块之前,会有一个16进制的数值,表示这个块的长度。最后是一个大小为0的块,就表示本次回应的数据发送完了。

HTTP 1.1 的缺点

虽然 HTTP 1.1 允许复用TCP连接,但是同一个TCP连接里面,所有的数据通信是按次序进行的。服务器只有处理完一个回应,才会进行下一个回应。要是前面的回应特别慢,后面就会有许多请求排队等着。这称为"队首堵塞"(Head-of-line blocking)。

另一个潜在的问题是:HTTP首部的不断增多,因为所有HTTP首部都是以纯文本形式发送的(不会经过任何压缩),会导致较高的额外负荷。这在某些应用中可能造成严重的性能问题。

这两个点都会在 HTTP 2 中得到解决。我们继续往下看。

SPDY 协议

2009年,谷歌公开了自行研发的 SPDY 协议,主要解决 HTTP 1.1 效率不高的问题。这个协议在Chrome浏览器上证明可行以后,就被当作 HTTP 2 的基础,主要特性都在 HTTP 2 之中得到继承。

可以理解为:SPDY 协议是 HTTP 2 协议的前身。

SPDY 协议是HTTP 2 协议的催化剂,但 SPDY 并不是 HTTP 2。如果以 Git 的概念来描述:HTTP 2 是 SPDY 的一个分支,随后,两者都各自有了进一步的发展。

HTTP 2

HTTP 2 诞生自 2005 年。

HTTP 2 致力于突破上一代标准众所周知的性能限制,但它也是对之前 1.x 标准的扩展,而非替代。HTTP 的语义不变,提供的功能不变,HTTP 方法、状态码、URI 和首部字段,等等这些核心概念也不变;这些方面的变化都不在考虑之列。

之所以要递增一个大版本到 2,主要是因为它改变了客户端与服务器之间交换数据的方式:它增加了新的二进制分帧数据层。这一层并不兼容之前的 HTTP 1.x 服务端及客户端。

二进制分帧层

HTTP 2 性能增强的核心,全在于新增的二进制分帧层,它定义了如何封装 HTTP 消息并在客户端与服务器之间传输。

看上图,HTTP 2 在已有的经典层级架构中,增加了一个二进制分帧层。再看右边,HTTP 1.x 以换行符作为纯文本的分隔符,而 HTTP 2 是一个彻底的二进制协议,将所有传输的信息分割为更小的消息和帧(图中的头信息帧和数据帧),并对它们采用二进制格式的编码。

这直接导致了 HTTP 1.x 与 HTTP 2 的不兼容。

流、消息和帧

新的二进制分帧机制改变了客户端与服务器之间交互数据的方式。为了说明这个过程,我们需要了解 HTTP 2 的几个新概念。

  • 流: 已建立的连接上的双向字节流。
  • 消息: 与逻辑消息对应的完整的一系列数据帧。
  • 帧: HTTP 2 通信的最小单位,每个帧包含帧首部,至少也会标识出当前帧所属的流。

所有 HTTP 2 通信都在一个连接上完成,这个连接可以承载任意数量的双向数据流。相应地,每个数据流以消息的形式发送,而消息由一或多个帧组成,这些帧可以乱序发送,然后再根据每个帧首部的流标识符重新组装。

这简简单单的几句话里浓缩了大量的信息。

要理解 HTTP 2,就必须理解流、消息和帧这几个基本概念:

  • 所有通信都在一个 TCP 连接上完成。
  • 流是连接中的一个虚拟信道,可以承载双向的消息;每个流都有一个唯一的整数标识符(1、2...N)。
  • 消息是指逻辑上的 HTTP 消息,比如请求、响应等,由一或多个帧组成。
  • 帧是最小的通信单位,承载着特定类型的数据,如 HTTP 首部、负荷,等等。

简言之,HTTP 2.0 把 HTTP 协议通信的基本单位缩小为一个一个的帧,这些帧对应着逻辑流中的消息。相应地,很多流可以并行地在同一个 TCP 连接上交换消息。

多向请求与响应

HTTP 2 中,客户端和服务器可以把 HTTP 消息分解为互不依赖的帧,然后乱序发送,最后再在另一端把它们重新组合起来。不再需要类似 HTTP 1.x 那样按照顺序一一对应,这样就避免了"队首堵塞"。

看下图的栗子:

图中包含了同一个连接上多个传输中的数据流: 客户端正在向服务器传输一个 DATA 帧(stream 5),与此同时,服务器正向客户端乱序发送 stream 1 和 stream 3 的一系列帧。此时,一个连接上有 3 个请求/响应并行交换!

把 HTTP 消息分解为独立的帧,交错发送,然后在另一端重新组装是 HTTP 2 最重要的一项增强。它解决了 HTTP 1.x 中存在的队首阻塞问题,也消除了并行处理和发送请求及响应时对多个连接的依赖。结果,就是应用速度更快、 开发更简单、部署成本更低。

头部信息压缩

HTTP 协议不带有状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如 Cookie 和 User Agent,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。

HTTP 2 对这一点做了优化,引入了头信息压缩机制(header compression)。一方面,头信息使用gzip或compress压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。

服务器推送

HTTP 2 新增的一个强大的新功能,就是服务器可以对一个客户端请求发送多个响应。换句话说,除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。

这个功能,可以为HTTP 1.x 时代的大多数插入或嵌入资源的无奈提供一个完美的解决方案。

HTTP 2 的限制

网络信息传输的过程,需要有很多的中间设备。要成功使用 HTTP 2,需要各端都支持 HTTP 2 协议。如果任何一个中间设备不支持,连接都不会成功。

优化建议

说了这么多,我们来看看具体的对于 HTTP 协议的优化建议吧。

针对 HTTP 1.0

HTTP 1.0的优化策略非常简单,就一句话:升级到 HTTP 1.1。完了!

针对 HTTP 1.1

  1. 减少 DNS 查询
  2. 减少 HTTP 请求:任何请求都不如没有请求快!
  3. 使用CDN:从地理上将数据放到接近客户端的地方。
  4. 添加 Expires 首部并配置 ETag 标签:合理利用浏览器的压缩机制。
  5. Gzip资源:所有文本资源都应该使用 Gzip 压缩。
  6. 避免 HTTP 重定向:HTTP 重定向几位好事。

针对 HTTP 2

这个没啥好说的。希望各个设备能够早日支持 HTTP 2 协议。让我们能够尽快用上 HTTP 2 吧。

号外

最近,看到 Google 开始研究 HTTP 3 系列了,打算将HTTP 协议底层的 TCP 协议转变为 UDP 协议。时代在发展,Google 就应该做这些前研探索~ 点赞!


前端性能优化系列:

(一):从TCP的三次握手讲起

(二):针对TCP传输过程中的堵塞

(三):HTTP协议的优化

(四):图片优化

(五):浏览器缓存策略

(六):浏览器是如何工作的?

(七):webpack性能优化