这是布兰的第 20 篇原创
HTTP/0.9 是于 1991 年提出的,主要用于学术交流,需求很简单——用来在网络之间传递 HTML 超文本的内容,所以被称为超文本传输协议。整体来看,它的实现也很简单,采用了基于请求响应的模式,从客户端发出请求,服务器返回数据。
HTTP/0.9 请求过程
HTTP/0.9 存在许多的问题,比如如下的这些:
所以它已经不能满足当时的需求了,于是乎 HTTP/1.0 来了,它带来了这些:
新增了请求头和请求体,能传输更多的信息给服务器,比如如下请求头字段:Accept 文件类型,Accept-Encoding 压缩格式,Accept-Charset 字符编码格式,Accept-Language 国际化语音:
Accept: text/html
Accept-Encoding: gzip, deflate, br
Accept-Charset: ISO-8859-1,utf-8
Accept-Language: zh-CN,zh
请求头新增 User-Agent 字段,用于服务器统计客户端信息:
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
新增了响应头,能够告诉浏览器更多的信息,比如 Content-Encoding 表示服务器返回文件的压缩类型,Content-Type 告诉浏览器服务器返回的是什么类型的文件以及使用了什么编码格式:
Content-Encoding: gzip
Content-Type: text/html; charset=utf-8
新增响应行状态码,用于告知浏览器当前请求的状态,比如 200 表示请求成功:
HTTP/1.1 200 OK
新增缓存机制,用来缓存已经下载过的资源,减轻了服务端压力。
在构建请求流程上来看,HTTP/1.0 区别于 HTTP/0.9 最大的区别就是在请求和响应的时候新增了不少字段用于在浏览器和服务器之间通信。
HTTP/1.0 虽说已经能够传输不同类型的文件了,但是它还是有缺点的,比如每发出一次 HTTP 请求都需要经历如下阶段:
HTTP/1.0 发送多个同域名请求:
HTTP/1.0 的短链接
可以发现每次请求都需要重新建立 TCP 连接和断开连接的操作,这无疑增加了网络开销,同时也延迟了页面显示。
HTTP/1.1 在请求头中增加了 Connection 字段:用于提供 TCP 的持久连接**:
Connection: keep-alive
它默认是开启持久连接的,即对于同一个域名,浏览器默认支持 6 个 TCP 持久连接。当启用持久连接后,多个同域名下的请求发送会是如下情况:
HTTP/1.1 的持久连接
HTTP/1.1 中新增 Host 字段,用于支持虚拟主机
Host: bubuzou.com
“虚拟主机:一台物理机器上绑定多个虚拟主机,每个虚拟主机有单独的域名,这些域名都公用一个 IP 地址。
HTTP/1.1 通过引入 Chunk transfer 机制来支持动态内容:服务器会将数据分割成若干个任意大小的数据块,每个数据块发送时会附上上个数据块的长度,最后使用一个零长度的块作为发送数据完成的标志。
HTTP/1.1 还引入了客户端 Cookie 机制和安全机制
我们知道 HTTP/1.1 为网络效率做了大量的优化,最核心的有如下三种方式:
虽然 HTTP/1.1 采取了很多优化资源加载速度的策略,也取得了一定的效果,但是 HTTP/1.1对带宽的利用率却并不理想,这也是 HTTP/1.1 的一个核心问题。
带宽是指每秒最大能发送或者接收的字节数。我们把每秒能发送的最大字节数称为上行带宽,每秒能够接收的最大字节数称为下行带宽。
之所以说 HTTP/1.1 对带宽的利用率不理想,是因为 HTTP/1.1 很难将带宽用满。比如我们常说的 100M 带宽,实际的下载速度能达到 12.5M/S,而采用 HTTP/1.1 时,也许在加载页面资源时最大只能使用到 2.5M/S,很难将 12.5M 全部用满。
之所以会出现这个问题,主要是 3 个问题导致的:
第一个原因,TCP 的慢启动
一旦一个 TCP 连接建立之后,就进入了发送数据状态,刚开始 TCP 协议会采用一个非常慢的速度去发送数据,然后慢慢加快发送数据的速度,直到发送数据的速度达到一个理想状态,我们把这个过程称为慢启动。这个过程可以想象是一辆车的启动过程,开始的时候慢,当速度起来后加速就更快了。
而之所以说慢启动会带来性能问题,是因为页面中常用的一些关键资源文件本来就不大,如 HTML 文件、CSS 文件和 JavaScript 文件,通常这些文件在 TCP 连接建立好之后就要发起请求的,但这个过程是慢启动,所以耗费的时间比正常的时间要多很多,这样就推迟了宝贵的首次渲染页面的时长了。
第二个原因,同时开启了多条 TCP 连接,那么这些连接会竞争固定的带宽
你可以想象一下,系统同时建立了多条 TCP 连接,当带宽充足时,每条连接发送或者接收速度会慢慢向上增加;而一旦带宽不足时,这些 TCP 连接又会减慢发送或者接收的速度。
这样就会出现一个问题,因为有的 TCP 连接下载的是一些关键资源,如 CSS 文件、JavaScript 文件等,而有的 TCP 连接下载的是图片、视频等普通的资源文件,但是多条 TCP 连接之间又不能协商让哪些关键资源优先下载,这样就有可能影响那些关键资源的下载速度了。
第三个原因,HTTP/1.1 队头阻塞的问题
我们知道在 HTTP/1.1 中使用持久连接时,虽然能公用一个 TCP 管道,但是在一个管道中同一时刻只能处理一个请求,在当前的请求没有结束之前,其他的请求只能处于阻塞状态。这意味着我们不能随意在一个管道中发送请求和接收内容。
这是一个很严重的问题,因为阻塞请求的因素有很多,并且都是一些不确定性的因素,假如有的请求被阻塞了 5 秒,那么后续排队的请求都要延迟等待 5 秒,在这个等待的过程中,带宽、CPU 都被白白浪费了。
为了解决 HTTP/1.1 中存在的问题,在 HTTP/2 中采用最具颠覆性的方案:多路复用机制。
HTTP/2 多路复用是什么
HTTP/2 的多路复用机制用简单的话来说就是浏览器针对同一域名的资源,只建立一个 TCP 连接通道,所有的针对这个域名的请求全部都在这个通道中完成;
除此之外,数据的传输不再使用文本格式,而是会将它们分割为更小的流和帧,并对他们采用二进制格式的编码。在一个 TCP 连接通道中,支持任意数量的双向数据流,这些数据流是并行、乱序的且它们之间互不干扰。而数据流中传输的数据是二进制帧,它是 HTTP/2 中数据传输的最小单位,一个流中的帧是按照顺序传输的,且是并行的,所以无需按顺序等待。
解决了什么问题
因为只使用一个 TCP 连接,所以减少了由于 TCP 慢启动而消耗的时间,另外也由于只有单条 TCP 连接,所以不存在不同的 TCP 争夺网络带宽的问题。
客户端发送的请求经过二进制分帧层后,不再是一个个完整的 HTTP 请求报文,而是一堆乱序的帧(即不同流的帧是乱的,但是同一条流的帧数顺序传输的),所以就不会按顺序传输,也就不存在等待,从而解决了 HTTP 对头阻塞问题。
是如何实现的
1. 可以设置请求的优先级
在浏览器中,某些数据是非常重要的,比如关键 CSS 或者 JS,这些重要的数据如果比较晚才推送到浏览器,那么对用户来说肯定是一个不好的体验。
所以 HTTP/2 中可以支持设置请求的优先级,这样服务器收到高优先级的请求后,会优先处理。
2. 服务器推送
在 HTTP/2 中服务器解析到一个 HTML 页面后,服务器知道浏览器需要这个页面上引用到的资源,比如 CSS 和 JS,那么服务器就会主动的把这些资源一并推送给浏览器,减少客户端的等待时间。
3. 头部压缩
HTTP/2 使用 HPACK 压缩算法对请求头和响应头进行压缩,虽然单个请求压缩之后效果不是很明显,但是如果一个页面有 100 个请求,那每个请求压缩 20% 之后,那提速效果就很明显了。
而 HPACK 的压缩原理其实就是 2 点:
HTTP/2 依然是基于 TCP 的,所以还存在以下一些问题。
HTTP/2 中多个请求是跑在一个 TCP 连接中的,如果某个数据流中出现了丢包的情况,就会阻塞该 TCP 连接中的所有请求。这个和 HTTP/1.1 中的不同,在 HTTP/1.1 中,由于浏览器为每个域名建立了 6 个 TCP 连接,如果其中一个 TCP 连接发生了队头阻塞,那么其他的 5 个连接依然可以继续传输数据。
在传输数据之前,需要进行 TCP 的 3 次握手,需要花费 1.5 个 RTT;如果是 HTTPS,那还需要进行 TLS 连接,又需要 1 ~ 2 个 RTT。
“网络延迟又叫 RTT(Round Trip Time),是从浏览器发送一个数据包到服务器,再从服务器返回数据包到浏览器的整个往返时间。
总之,在传输数据之前需要花掉 3 ~ 4 个 RTT。如果客户端和服务器距离近的话,那 1 个 RTT 大概是 10ms,但如果远的话,可能是 100ms,所以传输数据之前需要花掉 300ms 左右,这个时候就能感觉到慢了。
我们知道 TCP 协议存在队头阻塞和建立连接延迟的问题,但是又没办法改进 TCP 协议,理由有如下 2 个:
HTTP/3 是基于 UDP 实现的,实现了类似于 TCP 的多路数据流、传输可靠性等功能,我们把这套功能称为 QUIC 协议。