【浏览器系列】HTTP的演进历程

1,138 阅读21分钟

HTTP是一种允许浏览器向服务器获取资源的协议,是浏览器中最重要且使用最多的协议,是浏览器和服务器之间的通信语言,也是互联网的基石。学习HTTP的最佳途径就是了解其发展史。今天我们就从浏览器发展的视角来聊聊HTTP演进。

一、HTTP/0.9

HTTP/0.9 是1991年提出的,主要用于学术交流,在网络之间传输体积很小的 HTML 文件,所以被称为超文本传输协议。

实现很简单,采用了基于请求响应的模式,从客户端发出请求,服务器返回数据。HTTP/0.9 一个完整的请求流程:

  • 客户端根据IP地址、端口和服务器建立TCP连接(三次握手)
  • 建立连接之后,发送一个GET请求行信息,如GET /index.html 用来获取 index.html
  • 服务器接收请求信息之后,读取对应的HTML文件,并将数据以 ASCII 字符流返回给客户端
  • HTML文档传输完成后,断开连接(四次挥手)

HTTP/0.9请求流程图

有三个特点:

  1. 只有一个请求行,没有HTTP请求头和请求体,因为只需要一个请求行就可以完整表达客户端的需求了
  2. 服务器没有返回头信息,因为不需要告诉客户端太多信息,只需要返回数据就可以了
  3. 返回的文件以ASCII字符流来传输的,因为都是HTML格式的文件,用ASCII字节码传输是最合适的

二、HTTP/1.0

万维网的高速发展带来了很多新的需求,浏览器展示的不单单是HTML文件了,还包括了JavaScript、图片、CSS、音频、视频等不同类型的文件,所以HTTP/0.9已经不能适应网络的发展了,所以就诞生了HTTP/1.0,支持多种类型的文件下载是HTTP/1.0的一个核心诉求

要支持多种类型的文件:

  • 首先浏览器需要知道服务器返回的数据是什么类型的,然后才能根据不同的数据类型做针对性的处理。
  • 其次,由于万维网支持的应用越来越广,所以单个文件的数据量也变得越来越大,为了减轻传输性能,服务器会对数据进行压缩后再传输,所以浏览器需要知道服务器压缩的方法。
  • 再次,由于万维网是支持全球范围的,所以需要提供国际化的支持,服务器需要对不同的地区提供不同的语言版本,这就需要浏览器告诉服务器它想要什么语言版本的页面。
  • 最后,由于增加了各种不同类型的文件,而每种文件的编码形式又可能不一样,为了能够准确的读取文件,浏览器需要知道文件的编码类型。

基于以上问题,HTTP/1.0 的方案是通过请求头和响应头来进行协商,在发起请求时会通过HTTP请求头告诉服务器它期待服务器返回什么类型的文件、采取什么形式的压缩、提供什么语言的文件以及文件的具体编码。

accept: text/html
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh
accept-Charset: ISO-8859-1,utf-8

服务器接收到浏览器发送过来的请求头信息之后,根据请求头的信息来准备响应数据。而浏览器也要根据响应头的信息来处理数据。

content-encoding: br
content-type: text/html; charset=UTF-8

HTTP/1.0请求流程图

HTTP/1.0还引入了状态码,Cache机制以及用户代理的字段。

注意:请求行、请求头、请求体都是同时发送的,图有误解,响应头响应行也同理

三、HTTP/1.1

3.1 对HTTP/1.0 的优化

(1)改进持久连接

HTTP/1.0是短连接,每进行一次HTTP通信就要经历建立TCP连接、传输HTTP数据和断开TCP连接这三个阶段。这样增加了大量无谓的建立连接、断开连接的开销,所以HTTP/1.1增加了持久连接,特点是在一个TCP连接上可以传输多个HTTP请求,只要浏览器或者服务器没有明确断开连接,那么该TCP连接会一直保持

持久连接有效的减少了TCP建立连接和断开连接的次数,减少了服务器的负担,提升了整体HTTP的请求时长。目前浏览器中对于同一个域名,默认允许同时建立6个TCP持久连接

HTTP/1.1持久连接

还使用CDN实现了域名的分片机制。引入了 CDN,并同时为每个域名维护 6 个连接,这样就大大减轻了整个资源的下载时间。

这里我们可以简单计算下:如果使用单个 TCP 的持久连接,下载 100 个资源所花费的时间为 100 * n * RTT;若通过上面的技术,就可以把整个时间缩短为 100 * n * RTT/(6 * CDN 个数)。从这个计算结果来看,我们的页面加载速度变快了不少。

域名分片机制

HTTP/1.1默认开启持久连接,如果不想要,可以在HTTP请求头中加上Connection: close用以关闭。

(2)不成熟的HTTP管线化

持久连接减少了TCP的建立和断开次数,但是它需要等前面的请求返回之后才能进行下一次请求,一个TCP连接单次只能发起一个请求,如果TCP通道中的某个请求因为某些原因没有及时返回,那么就会阻塞后面的所有请求,这就是著名的队头阻塞的问题。

HTTP/1.1的管线化是指将多个HTTP请求整批提交给服务器的技术,虽然可以整批发送请求,但是服务器还是得依据请求顺序来回复浏览器的请求。

由于各种原因,最终放弃了管线化。没能解决队头阻塞的问题。

(3)提供虚拟主机的支持

HTTP/1.0中,每个域名绑定了一个唯一的IP地址,因此一个服务器只能支持一个域名。随着虚拟主机技术的发展,需要实现在一台物理主机上绑定多个虚拟主机,每个虚拟主机都有自己单独的域名,这些单独的域名公用同一个IP地址

因此HTTP/1.1的请求头中增加了Host字段,用来表示当前的域名地址,这样服务器就可以根据不同的Host值做不同的处理。

(4)对动态生成的内容提供了完美支持

HTTP/1.0需要在响应头中设置完整的数据大小,如Content-Length: 901,这样浏览器就可以根据设置的数据大小来接收数据。随着服务器端的技术发展,很多页面的内容都是动态生成的,因此在传输数据之前并不知道最终数据的大小,导致浏览器不知道何时接收完所有的文件数据。

HTTP/1.1引入Chunk Transfer机制解决这个问题:服务器将数据分割成若干个任意大小的数据块,每个数据块发送时附带上上个数据块的长度,最后使用一个零长度的块作为发送完成的标志

(5)客户端Cookie、安全机制

3.2 存在的不足

对带宽的利用率不理想

带宽指的是每秒最大能发送或者接收的字节数,每秒能发送的最大字节数称为上行带宽,每秒能接收的最大字节数称为下行带宽。

比如我们常说的 100M 带宽,实际的下载速度能达到 12.5M/S,而采用 HTTP/1.1 时,也许在加载页面资源时最大只能使用到 2.5M/S,很难将 12.5M 全部用满。

之所以会出现这个问题,主要是由以下三个原因导致的。

a. TCP慢启动

一旦一个 TCP 连接建立之后,就进入了发送数据状态,刚开始 TCP 协议会采用一个非常慢的速度去发送数据,然后慢慢加快发送数据的速度,直到发送数据的速度达到一个理想状态,我们把这个过程称为慢启动。慢启动是 TCP 为了减少网络拥塞的一种策略,我们是没有办法改变的

你可以把每个 TCP 发送数据的过程看成是一辆车的启动过程,当刚进入公路时,会有从 0 到一个稳定速度的提速过程,TCP 的慢启动就类似于该过程。

页面中常用的一些关键资源文件本来就不大,如 HTML 文件、CSS 文件和 JavaScript 文件,通常这些文件在 TCP 连接建立好之后就要发起请求的,但这个过程是慢启动,所以耗费的时间比正常的时间要多很多,这样就推迟了宝贵的首次渲染页面的时长了。

b. 同时开启了多条TCP连接,会竞争固定的带宽

系统同时建立了多条 TCP 连接,当带宽充足时,每条连接发送或者接收速度会慢慢向上增加;而一旦带宽不足时,这些 TCP 连接又会减慢发送或者接收的速度。比如一个页面有 200 个文件,使用了 3 个 CDN,那么加载该网页的时候就需要建立 6 * 3,也就是 18 个 TCP 连接来下载资源;在下载过程中,当发现带宽不足的时候,各个 TCP 连接就需要动态减慢接收数据的速度。

这样就会出现一个问题,因为有的TCP连接下载的是关键资源,如CSS文件、Javascript文件等,而有的TCP连接下载的是图片、视频等普通的资源文件,但是多条TCP连接之前又不能协商让哪些关键资源优先下载,这样就有可能影响关键资源的下载速度了

c. 队头阻塞问题

使用持久连接时,虽然能公用一个 TCP 管道,但是在一个管道中同一时刻只能处理一个请求,在当前的请求没有结束之前,其他的请求只能处于阻塞状态。这意味着我们不能随意在一个管道中发送请求和接收内容。

这是一个很严重的问题,因为阻塞请求的因素有很多,并且都是一些不确定性的因素,假如有的请求被阻塞了 5 秒,那么后续排队的请求都要延迟等待 5 秒,在这个等待的过程中,带宽、CPU 都被白白浪费了。

在浏览器处理生成页面的过程中,是非常希望能提前接收到数据的,这样就可以对这些数据做预处理操作,比如提前接收到了图片,那么就可以提前进行编解码操作,等到需要使用该图片的时候,就可以直接给出处理后的数据了,这样能让用户感受到整体速度的提升。

但队头阻塞使得这些数据不能并行请求,所以队头阻塞是很不利于浏览器优化的。

四、HTTP/2

说到HTTP/2,就不得不提一下SPDY。SPDY 是 Google 开发的一个实验性协议,于 2009 年年中发布,其主要目标是通过解决 HTTP/1.1 中广为人知的一些性能限制来减少网页的加载延迟。

为了达到减少 50% 页面加载时间的目标,SPDY 引入一个新的二进制分帧层,以实现请求和响应复用、优先级和标头压缩,目的是更有效地利用底层 TCP 连接。

到了 2012 年,这个新的实验性协议得到 Chrome、Firefox 和 Opera 的支持,而且越来越多的大型网站(如 Google、Twitter、Facebook)和小型网站开始在其基础设施内部署 SPDY。 事实上,在被行业越来越多的采用之后,SPDY 已经具备了成为一个标准的条件。

观察到这一趋势后,HTTP 工作组 (HTTP-WG) 将这一工作提上议事日程,吸取 SPDY 的经验教训,并在此基础上制定了官方“HTTP/2”标准。 在拟定宣言草案、向社会征集 HTTP/2 建议并经过内部讨论之后,HTTP-WG 决定将 SPDY 规范作为新 HTTP/2 协议的基础。

在接下来几年中,SPDY 和 HTTP/2 继续共同演化,其中 SPDY 作为实验性分支,用于为 HTTP/2 标准测试新功能和建议。 理论不一定适合实践(反之亦然),SPDY 提供一个测试和评估路线,可以对要纳入 HTTP/2 标准中的每条建议进行测试和评估。

4.1 优化

HTTP/1.1存在的主要问题中,慢启动和TCP连接之间相互竞争带宽是由于TCP本身的机制导致的,而队头阻塞是由于HTTP/1.1的机制导致的

虽然 TCP 有问题,但是我们依然没有换掉 TCP 的能力,所以我们就要想办法去规避 TCP 的慢启动和 TCP 连接之间的竞争问题。HTTP/2 的思路就是一个域名只使用一个 TCP 长连接来传输数据,这样整个页面资源的下载过程只需要一次慢启动,同时也避免了多个 TCP 连接竞争带宽所带来的问题

另外,就是队头阻塞的问题,等待请求完成后才能去请求下一个资源,这种方式无疑是最慢的,所以 HTTP/2 需要实现资源的并行请求,也就是任何时候都可以将请求发送给服务器,而并不需要等待其他请求的完成,然后服务器也可以随时返回处理好的请求资源给浏览器。

所以,HTTP/2 的解决方案可以总结为:一个域名只使用一个 TCP 长连接和消除队头阻塞问题。参考下图:

HTTP/2的多路复用

从图中你会发现每个请求都有一个对应的 ID,如 stream1 表示 index.html 的请求,stream2 表示 foo.css 的请求。这样在浏览器端,就可以随时将请求发送给服务器了。之所以可以随意发送,是因为每份数据都有对应的 ID,浏览器接收到之后,会筛选出相同 ID 的内容,将其拼接为完整的 HTTP 响应数据。

HTTP/2 使用了多路复用技术,可以将请求分成一帧一帧的数据去传输,这样带来了一个额外的好处,就是当收到一个优先级高的请求时,比如接收到 JavaScript 或者 CSS 关键资源的请求,服务器可以暂停之前的请求来优先处理关键资源的请求。

为什么不是 HTTP/1.2?为了实现 HTTP 工作组设定的性能目标,HTTP/2 引入了一个新的二进制分帧层,该层无法与之前的 HTTP/1.x 服务器和客户端向后兼容,因此协议的主版本提升到 HTTP/2。

(1)多路复用的实现

通过引入二进制分帧层,实现了 HTTP/2 的多路复用技术,实现资源的并行传输。

HTTP/2协议栈

  • 首先,浏览器准备好请求数据,包括请求行、请求头、请求体
  • 这些数据经过二进制分帧层处理后,被转换为一个个带有请求ID编号的帧,通过协议栈将这些帧发送给服务器
  • 服务器收到所有的帧后,将所有相同ID的帧合成一条完整的请求信息
  • 服务器处理该条请求,将处理的响应行、响应头和响应体分别发送至二进制分帧层
  • 二进制分帧层会将这些响应数据转换为一个个带有请求ID编号的帧,经过协议栈发送给浏览器
  • 浏览器接收到响应帧后,会根据ID编号将帧的数据提交给对应的请求

与HTTP/1.1相比,改变的只是传输方式,其他的什么都没变

(2)可以设置请求的优先级

HTTP/2可以在发送请求时标上该请求的优先级,这样服务器接收到请求之后,会优先处理优先级高的请求

(3)服务器推送

HTTP/2可以直接将数据提前推送到浏览器

可以想象这样一个场景,当用户请求一个HTML页面之后,服务器知道该HTML页面会引用几个重要的JavaScript和CSS文件,那么在接收到HTML请求之后,附带将要使用的CSS文件和JavaScript文件一并发送给浏览器,这样当浏览器解析完HTML文件之后,就能直接拿到需要的CSS文件和JavaScript文件,这对首次打开页面的速度起到了至关重要的作用

(4)头部压缩

无论是 HTTP/1.1 还是 HTTP/2,它们都有请求头和响应头,这是浏览器和服务器的通信语言。HTTP/2 对请求头和响应头进行了压缩。

4.2 存在的不足

(1)TCP的队头阻塞

虽然 HTTP/2 解决了 HTTP/1.1 中的队头阻塞问题,但是 HTTP/2 依然是基于 TCP 协议的,而 TCP 协议依然存在数据包级别的队头阻塞问题。HTTP/1.1协议栈中的TCP传输数据过程如下图:

正常情况下的 TCP 传输数据过程

上图可以看出,从一端发送给另外一端的数据会被拆分为一个个按照顺序排列的数据包,这些数据包通过网络传输到了接收端,接收端再按照顺序将这些数据包组合成原始数据,这样就完成了数据传输。

不过在传输的过程中,有一个数据因为网络故障或者其他原因丢包了,整个TCP的连接会处于暂停状态,需要等待丢失的数据包重新传输过来。可以把TCP连接看出是一个按照顺序传输的管道,管道中的任意一个数据丢失了,之后的数据都需要等待该数据的重新传输。如下图:

TCP 丢包状态

我们把TCP传输过程中,由于单个数据包丢失而造成的阻塞称为TCP上的队头阻塞

正常情况下,HTTP/2是这么传输多路请求的:

HTTP/2 多路复用

HTTP/2中多个请求是跑在一个管道的,如果其中任意一路数据流中出现了丢包的情况,那么就会阻塞该TCP连接中的所有请求,这不同于HTTP/1.1,使用HTTP/1.1时浏览器为每个域名开启了6个TCP连接,如果其中的1个TCP连接发生了队头阻塞,那么其他的5个连接依然可以继续传输数据

所以随着丢包率的增加,HTTP/2的传输效率也会越来越差。有测试数据表明,当系统达到了 2% 的丢包率时,HTTP/1.1 的传输效率反而比 HTTP/2 表现得更好。

(2)TCP建立连接的延迟

除了TCP队头阻塞以外,TCP的握手过程也是影响传输效率的一个重要因素。

网络延迟:从浏览器发送一个数据包到服务器,再从服务器返回数据包到浏览器的整个往返时间,也叫RTT(Round Trip Time),是反映网络性能的一个重要指标。

网络延时

建立TCP连接需要多少个RTT?

HTTP/1和HTTP/2都是使用TCP协议,使用HTTPS的话还需要使用TLS协议,这也是一个握手过程,这就意味着有两个握手延迟过程。

  • 在建立TCP连接的时候,需要进行3次握手来确认连接成功,也就是说需要在消耗完1.5个RTT之后才能进行数据传输
  • 进行TLS连接,TLS有两个版本——TLS1.2和TLS1.3,每个版本建立连接所花的时间不同,大致是需要1~2个RTT

总之,在传输数据之前,大约需要花掉3~4个RTT,如果浏览器和服务器的物理距离较近,那么 1 个 RTT 的时间可能在 10 毫秒以内,也就是说总共要消耗掉 30~40 毫秒。这个时间也许用户还可以接受,但如果服务器相隔较远,那么 1 个 RTT 就可能需要 100 毫秒以上了,这种情况下整个握手过程需要 300~400 毫秒,这时用户就能明显地感受到“慢”了。

(3)TCP协议僵化

TCP协议存在队头阻塞和建立连接延迟等问题,是不是通过改进TCP协议来解决这些问题呢?非常困难。主要有两个原因:

第一个是中间设备的僵化

互联网是由多个网络互联的网状结构,为了能够保障互联网的正常工作,我们需要在互联网的各处搭建各种设备,这些设备就被称为中间设备。这些中间设备有很多种类型,并且每种设备都有自己的目的,这些设备包括了路由器、防火墙、NAT、交换机等。它们通常依赖一些很少升级的软件,这些软件使用了大量的 TCP 特性,这些功能被设置之后就很少更新了。

所以,如果我们在客户端升级了 TCP 协议,但是当新协议的数据包经过这些中间设备时,它们可能不理解包的内容,于是这些数据就会被丢弃掉。这就是中间设备僵化,它是阻碍 TCP 更新的一大障碍。

第二个是操作系统。因为TCP协议是通过操作系统内核来实现的,应用程序只能使用不能修改。通常操作系统的更新都滞后于软件的更新,因此要想自由的更新内核中的TCP协议也是非常困难的。

五、HTTP/3

HTTP/2存在一些比较严重与TCP协议相关的缺陷,但是由于TCP僵化,几乎不可能通过修改TCP协议自身来解决这些问题,所以解决的思路就是绕过TCP协议,发明一个TCP和UDP以外的新的传输协议。

但是这也面临着和修改 TCP 一样的挑战,因为中间设备的僵化,这些设备只认 TCP 和 UDP,如果采用了新的协议,新协议在这些设备同样不被很好地支持。

因此HTTP/3选择了一个折衷的办法——UDP协议,基于UDP协议实现了类似于TCP的多路数据流、传输可靠性等功能,我们把这套功能称为QUIC协议

5.1 QUIC协议

HTTP/2 和 HTTP/3 协议栈

  • 实现了类似TCP的流量控制、传输可靠性的功能。在 UDP 的基础之上增加了一层来保证数据可靠性传输,提供了数据包重传、拥塞控制以及其他一些TCP存在的特性
  • 集成了TLS加密功能。减少了握手所花费的RTT个数。
  • 实现了HTTP/2的多路复用功能。和TCP不同,QUIC实现了在同一物理连接上可以有多个独立的逻辑数据流。实现了数据流的单独传输,解决了TCP中队头阻塞的问题。

QUIC 协议的多路复用

  • 实现了快速握手功能。由于 QUIC 是基于 UDP 的,所以 QUIC 可以实现使用 0-RTT 或者 1-RTT 来建立连接,这意味着 QUIC 可以用最快的速度来发送和接收数据,这样可以大大提升首次打开页面的速度。

5.2 所面临的挑战

  • 从目前的情况来看,服务器和浏览器端都没有对 HTTP/3 提供比较完整的支持。Chrome 虽然在数年前就开始支持 Google 版本的 QUIC,但是这个版本的 QUIC 和官方的 QUIC 存在着非常大的差异。
  • 部署 HTTP/3 也存在着非常大的问题。因为系统内核对 UDP 的优化远远没有达到 TCP 的优化程度,这也是阻碍 QUIC 的一个重要原因。
  • 中间设备僵化的问题。这些设备对 UDP 的优化程度远远低于 TCP,据统计使用 QUIC 协议时,大约有 3%~7% 的丢包率。

六、参考资源

《浏览器工作原理与实践》