超文本传输协议 HTTP/0.9
用来在网络之间传递 HTML 超文本的内容,所以被称为超文本传输协议。

- 因为HTTP都是基于TCP协议的,所以客户端先要根据IP地址、端口和服务器建立TCP连接,而建立连接的过程就是TCP协议三次握手的过程。
- 建立好连接之后,会发送一个GET请求行的信息,如GET/index.html用来获取index.html。
- 服务器接收请求信息之后,读取对应的HTML文件,并将数据以ASCII字符流返回给客户端。
- HTML文档传输完成后,断开连接。
HTTP/0.9 的实现特点:
- 第一个是只有一个请求行,并没有HTTP请求头和请求体,因为只需要一个请求行就可以完整表达客户端的需求了。
- 第二个是服务器也没有返回头信息,这是因为服务器端并不需要告诉客户端太多信息,只需要返回数据就可以了。
- 第三个是返回的文件内容是以ASCII字符流来传输的,因为都是HTML格式的文件,所以使用 ASCII 字节码来传输是最合适的。
被浏览器推动的 HTTP/1.0
支持多种类型的文件下载是 HTTP/1.0 的一个核心诉求
为了让客户端和服务器能更深入地交流,HTTP/1.0引入了请求头和响应头,它们都是以为 Key-Value 形式保存的,在HTTP发送请求时,会带上请求头信息,服务器返回数据时,会先返回响应头信息

那 HTTP/1.0 是怎么通过请求头和响应头来支持多种不同类型的数据呢?
- 首先,浏览器需要知道服务器返回的数据是什么类型的,然后浏览器才能根据不同的数据类型做针对性的处理。
- 其次,由于万维网所支持的应用变得越来越广,所以单个文件的数据量也变得越来越大。为了减轻传输性能,服务器会对数据进行压缩后再传输,所以浏览器需要知道服务器压缩的方法。
- 再次,由于万维网是支持全球范围的,所以需要提供国际化的支持,服务器需要对不同的地区提供不同的语言版本,这就需要浏览器告诉服务器它想要什么语言版本的页面。
- 最后,由于增加了各种不同类型的文件,而每种文件的编码形式又可能不一样,为了能够准确地读取文件,浏览器需要知道文件的编码类型。
基于以上问题,HTTP/1.0的方案是通过请求头和响应头来进行协商,在发起请求时候会通过 HTTP 请求头告诉服务器它期待服务器返回什么类型的文件、采取什么形式的压缩、提供什么语言的文件以及文件的具体编码。
accept: text/html
accept-encoding: gzip, deflate, br
accept-Charset: ISO-8859-1,utf-8
accept-language: zh-CN,zh
其中第一行表示期望服务器返回 html 类型的文件,第二行表示期望服务器可以采用 gzip、deflate 或者 br 其中的一种压缩方式,第三行表示期望返回的文件编码是 UTF-8 或者 ISO-8859-1,第四行是表示期望页面的优先语言是中文。
服务器接收到浏览器发送过来的请求头信息之后,会根据请求头的信息来准备响应数据。不过有时候会有一些意外情况发生,比如浏览器请求的压缩类型是gzip,但是服务器不支持gzip,只支持br压缩,那么它会通过响应头中的content-encoding字段告诉浏览器最终的压缩类型,也就是说最终浏览器需要根据响应头的信息来处理数据。下面是一段响应头的数据信息:
content-encoding: br
content-type: text/html; charset=UTF-8
其中第一行表示服务器采用了br的压缩方法,第二行表示服务器返回的是 html 文件,并且该文件的编码类型是 UTF-8。
有了响应头的信息,浏览器就会使用br方法来解压文件,再按照UTF-8的编码格式来处理原始文件,最后按照HTML的方式来解析该文件。这就是HTTP/1.0支持多文件的一个基本的处理流程。HTTP/1.0除了对多文件提供良好的支持外,还依据当时实际的需求引入了很多其他的特性,这些特性都是通过请求头和响应头来实现的。下面我们来看看新增的几个典型的特性:
- 有的请求服务器可能无法处理,或者处理出错,这时候就需要告诉浏览器服务器最终处理该请求的情况,这就引入了状态码。状态码是通过响应行的方式来通知浏览器的。
- 为了减轻服务器的压力,在HTTP/1.0中提供了Cache机制,用来缓存已经下载过的数据。
- 服务器需要统计客户端的基础信息,比如Windows和macOS的用户数量分别是多少,所以HTTP/1.0的请求头中还加入了用户代理的字段。
缝缝补补的 HTTP/1.1
1. 改进持久连接
HTTP/1.0 每进行一次 HTTP 通信,都需要经历建立 TCP 连接、传输 HTTP 数据和断开 TCP 连接三个阶段

为了解决这个问题,HTTP/1.1中增加了持久连接的方法,它的特点是在一个TCP连接上可以传输多个 HTTP 请求,只要浏览器或者服务器没有明确断开连接,那么该TCP连接会一直保持。

2. 不成熟的 HTTP 管线化
持久连接虽然能减少TCP的建立和断开次数,但是它需要等待前面的请求返回之后,才能进行下一次请求。如果TCP通道中的某个请求因为某些原因没有及时返回,那么就会阻塞后面的所有请求,这就是著名的队头阻塞的问题。
HTTP/1.1 中试图通过管线化的技术来解决队头阻塞的问题。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 通过引入Chunktransfer机制来解决这个问题,服务器会将数据分割成若干个任意大小的数据块,每个数据块发送时会附上上个数据块的长度,最后使用一个零长度的块作为发送数据完成的标志。这样就提供了对动态内容的支持。
5. 客户端 Cookie、安全机制
除此之外,HTTP/1.1 还引入了客户端Cookie机制和安全机制。
HTTP/1.1 的主要问题
对带宽的利用率却并不理想
带宽是指每秒最大能发送或者接收的字节数。我们把每秒能发送的最大字节数称为上行带宽,每秒能够接收的最大字节数称为下行带宽。
之所以说 HTTP/1.1 对带宽的利用率不理想,是因为HTTP/1.1很难将带宽用满。比如我们常说的 100M 带宽,实际的下载速度能达到12.5M/S,而采用HTTP/1.1时,也许在加载页面资源时最大只能使用到 2.5M/S,很难将 12.5M 全部用满。原因:
第一个原因,TCP 的慢启动。
一旦一个 TCP 连接建立之后,就进入了发送数据状态,刚开始TCP协议会采用一个非常慢的速度去发送数据,然后慢慢加快发送数据的速度,直到发送数据的速度达到一个理想状态,我们把这个过程称为慢启动。
第二个原因,同时开启了多条 TCP 连接,那么这些连接会竞争固定的带宽。
你可以想象一下,系统同时建立了多条TCP连接,当带宽充足时,每条连接发送或者接收速度会慢慢向上增加;而一旦带宽不足时,这些TCP连接又会减慢发送或者接收的速度。比如一个页面有 200 个文件,使用了 3 个 CDN,那么加载该网页的时候就需要建立 6 * 3,也就是 18个TCP连接来下载资源;在下载过程中,当发现带宽不足的时候,各个 TCP 连接就需要动态减慢接收数据的速度。
这样就会出现一个问题,因为有的TCP连接下载的是一些关键资源,如CSS文件、JavaScript文件等,而有的TCP连接下载的是图片、视频等普通的资源文件,但是多条TCP连接之间又不能协商让哪些关键资源优先下载,这样就有可能影响那些关键资源的下载速度了。
第三个原因,HTTP/1.1 队头阻塞的问题。
我们知道在 HTTP/1.1 中使用持久连接时,虽然能公用一个TCP管道,但是在一个管道中同一时刻只能处理一个请求,在当前的请求没有结束之前,其他的请求只能处于阻塞状态。这意味着我们不能随意在一个管道中发送请求和接收内容。
HTTP/2 的多路复用
虽然TCP有问题,但是我们依然没有换掉TCP的能力,所以我们就要想办法去规避TCP的慢启动和TCP连接之间的竞争问题。
基于此,HTTP/2的思路就是一个域名只使用一个TCP长连接来传输数据,这样整个页面资源的下载过程只需要一次慢启动,同时也避免了多个TCP连接竞争带宽所带来的问题。
另外,队头阻塞的问题,等待请求完成后才能去请求下一个资源,这种方式无疑是最慢的,所以 HTTP/2 需要实现资源的并行请求,也就是任何时候都可以将请求发送给服务器,而并不需要等待其他请求的完成,然后服务器也可以随时返回处理好的请求资源给浏览器。
HTTP/2 的解决方案可以总结为:一个域名只使用一个TCP长连接和消除队头阻塞问题。

服务器端接收到这些请求后,会根据自己的喜好来决定优先返回哪些内容,比如服务器可能早就缓存好了index.html和bar.js的响应头信息,那么当接收到请求的时候就可以立即把index.html和bar.js的响应头信息返回给浏览器,然后再将index.html和bar.js的响应体数据返回给浏览器。之所以可以随意发送,是因为每份数据都有对应的ID,浏览器接收到之后,会筛选出相同ID的内容,将其拼接为完整的HTTP响应数据。
HTTP/2 使用了多路复用技术,可以将请求分成一帧一帧的数据去传输,这样带来了一个额外的好处,就是当收到一个优先级高的请求时,比如接收到 JavaScript 或者 CSS 关键资源的请求,服务器可以暂停之前的请求来优先处理关键资源的请求。
多路复用的实现

- 首先,浏览器准备好请求数据,包括了请求行、请求头等信息,如果是POST方法,那么还要有请求体。
- 这些数据经过二进制分帧层处理之后,会被转换为一个个带有请求ID编号的帧,通过协议栈将这些帧发送给服务器。
- 服务器接收到所有帧之后,会将所有相同ID的帧合并为一条完整的请求信息。
- 然后服务器处理该条请求,并将处理的响应行、响应头和响应体分别发送至二进制分帧层。
- 同样,二进制分帧层会将这些响应数据转换为一个个带有请求ID编号的帧,经过协议栈发送给浏览器。
- 浏览器接收到响应帧之后,会根据ID编号将帧的数据提交给对应的请求。
浏览器接收到响应帧之后,会根据 ID 编号将帧的数据提交给对应的请求。
HTTP/2 其他特性
1. 可以设置请求的优先级
2. 服务器推送
3. 头部压缩
在TCP传输过程中,由于单个数据包的丢失而造成的阻塞称为TCP上的队头阻塞。
HTTP/3
选择了一个折衷的方法——UDP协议,基于UDP实现了类似于TCP的多路数据流、传输可靠性等功能,我们把这套功能称为QUIC协议。

- 实现了类似 TCP 的流量控制、传输可靠性的功能。虽然UDP不提供可靠性的传输,但QUIC在UDP的基础之上增加了一层来保证数据可靠性传输。它提供了数据包重传、拥塞控制以及其他一些 TCP 中存在的特性。
- 集成了TLS加密功能。目前QUIC使用的是TLS1.3,相较于早期版本TLS1.3有更多的优点,其中最重要的一点是减少了握手所花费的RTT个数。
- 实现了 HTTP/2 中的多路复用功能。和TCP不同,QUIC实现了在同一物理连接上可以有多个独立的逻辑数据流(如下图)。实现了数据流的单独传输,就解决了TCP中队头阻塞的问题。
- 实现了快速握手功能。由于QUIC是基于UDP的,所以QUIC可以实现使用0-RTT或者1-RTT来建立连接,这意味着QUIC可以用最快的速度来发送和接收数据,这样可以大大提升首次打开页面的速度。
参考自浏览器工作实践