# HTTP 前世今生
HTTP 超文本传输协议,传输超媒体文档的应用层协议,遵循 C/S 模型,客户端发起请求知道服务器响应。HTTP 为无状态协议(服务器不会在两个请求之间保留任何数据,状态),基于 TCP/IP 层,但可以在任何可靠的传输层上使用,即不会像 UDP 静默丢失数据。
# HTTP 0.9
# 出现时间
1991年
# 出现原因
用来在网络之间传递 HTML 超文本的内容。
# 实现
采用了基于请求响应的模式,从客户端发出请求,服务器返回数据。
# 流程
- 因为 HTTP 都是基于 TCP 协议的,所以客户端先要根据 IP 地址、端口和服务器建立 TCP 连接,而建立连接的过程就是 TCP 协议三次握手的过程。
- 建立好连接之后,会发送一个 GET 请求行的信息,如 GET /index.html 用来获取 index.html。
- 服务器接收请求信息之后,读取对应的 HTML 文件,并将数据以 ASCII 字符流返回给客户端。
- HTML 文档传输完成后,断开连接。
# 图示
# 特点
- 只有一个请求行,并没有 HTTP 请求头和请求体因为只需要一个请求行就可以完整表达客户端的需求了。
- 服务器也没有返回头信息。这是因为服务器端并不需要告诉客户端太多信息,只需要返回数据就可以了。
- 返回的文件内容是以 ASCII 字符流来传输的
# HTTP 1.0
# 出现时间
1994年
# 出现原因
- 随着浏览器的发展在浏览器中展示的不单是 HTML 文件了,还包括了 JavaScript、CSS、图片、音频、视频等不同类型的文件。因此需要支持多种类型的文件下载。
- 文件格式不仅仅局限于 ASCII 编码,还有很多其他类型编码的文件。
# 图示
# 新增特性
- 对多文件提供良好的支持,支持多种不同类型的数据。HTTP/1.0 的方案是通过请求头和响应头来进行协商,在发起请求时候会通过 HTTP 请求头告诉服务器它期待服务器返回什么类型的文件、采取什么形式的压缩、提供什么语言的文件以及文件的具体编码。
- 引入状态码,有的请求服务器可能无法处理,或者处理出错,这时候就需要告诉浏览器服务器最终处理该请求的情况,状态码是通过响应行的方式来通知浏览器的。
- 提供了 Cache 机制,用来缓存已经下载过的数据以减轻服务器的压力
- 加入了用户代理的字段以统计客户端的基础信息,比如 Windows 和 macOS 的用户数量分别是多少。
http 1.0
更好的多类型文件支持,引入状态码,提供缓存机制,加入User-Agent
# HTTP 1.1 里程碑
# 出现时间
1999年
# 出现原因
随着技术的继续发展,需求也在不断迭代更新,很快 HTTP/1.0 也不能满足需求了。
# 新增特性
改进持久连接。
- 由于 http 1.0 是短连接,所以 HTTP/1.0 每进行一次 HTTP 通信,都需要经历建立 TCP 连接、传输 HTTP 数据和断开 TCP 连接三个阶段。这样做会增加大量的开销。为解决这个问题,HTTP/1.1 中增加了 持久连接 的方法,它的特点是在 一个 TCP 连接上可以传输多个 HTTP 请求,只要浏览器或者服务器没有明确断开连接,那么 该 TCP 连接会一直保持。持久连接在 HTTP/1.1 中是默认开启的,如不想采用持久连接,可以在 HTTP 请求头中加上
Connection: close
。 - 目前浏览器中对于同一个域名,默认允许同时建立 6 个 TCP 持久连接。
- 使用 CDN 实现域名分片机制
- 由于 http 1.0 是短连接,所以 HTTP/1.0 每进行一次 HTTP 通信,都需要经历建立 TCP 连接、传输 HTTP 数据和断开 TCP 连接三个阶段。这样做会增加大量的开销。为解决这个问题,HTTP/1.1 中增加了 持久连接 的方法,它的特点是在 一个 TCP 连接上可以传输多个 HTTP 请求,只要浏览器或者服务器没有明确断开连接,那么 该 TCP 连接会一直保持。持久连接在 HTTP/1.1 中是默认开启的,如不想采用持久连接,可以在 HTTP 请求头中加上
不成熟的 HTTP 管线化
HTTP/1.1 中的 管线化 是指将多个 HTTP 请求整批提交给服务器的技术,虽然可以整批发送请求,不过服务器依然需要根据请求顺序来回复浏览器的请求。由于持久连接虽然能减少 TCP 的建立和断开次数,但是它需要等待前面的请求返回之后,才能进行下一次请求。如果 TCP 通道中的某个请求因为某些原因没有及时返回,那么就会阻塞后面的所有请求,这就是著名的队头阻塞的问题。HTTP/1.1 试图用管线化解决队头阻塞问题。
提供虚拟主机的支持
在 HTTP/1.0 中,每个域名绑定了一个唯一的 IP 地址,因此一个服务器只能支持一个域名。但是随着虚拟主机技术的发展,需要实现在一台物理主机上绑定多个虚拟主机,每个虚拟主机都有自己的单独的域名,这些单独的域名都公用同一个 IP 地址。因此,HTTP/1.1 的请求头中增加了 Host 字段,用来表示当前的域名地址,这样服务器就可以根据不同的 Host 值做不同的处理。
对动态生成的内容提供了完美支持
HTTP/1.0 时,需要在响应头中设置完整的数据大小,如 Content-Length: 901,这样浏览器就可以根据设置的数据大小来接收数据。不过随着服务器端的技术发展,很多页面的内容都是动态生成的,因此在传输数据之前并不知道最终的数据大小,这就导致了浏览器不知道何时会接收完所有的文件数据。HTTP/1.1 通过引入 Chunk transfer 机制(分块传输编码机制)来解决这个问题,服务器会将数据分割成若干个任意大小的数据块,每个数据块发送时会附上上个数据块的长度,最后使用一个零长度的块作为发送数据完成的标志。这样就提供了对动态内容的支持。
客户端 Cookie、安全机制
HTTP/1.1 还引入了客户端 Cookie 机制和安全机制
http 1.1
持久性连接,管线化技术解决持久性连接引起的对头阻塞但还未彻底解决,支持虚拟主机(Host),数据分块传输,引入 Cookie 机制和安全机制
# HTTP 2.0 主流
# 出现时间
2015年,大多数主流浏览器也于当年年底支持该标准
# 出现原因
虽然 HTTP/1.1 采取了很多优化资源加载速度的策略,也取得了一定的效果,但是 HTTP/1.1 对带宽的利用率却并不理想。主要是由于以下几个原因:
TCP 的慢启动
一旦一个 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 都被白白浪费了。并且队头阻塞使得数据不能并行请求,所以队头阻塞是很不利于浏览器优化的。
协议开销大
header 里携带的内容过大,且不能压缩,增加了传输的成本。
# 实现思路
HTTP/2 的思路就是一个域名只使用一个 TCP 长连接来传输数据,这样整个页面资源的下载过程只需要一次慢启动,同时也避免了多个 TCP 连接竞争带宽所带来的问题。另外队头阻塞的问题,等待请求完成后才能去请求下一个资源,这种方式无疑是最慢的,所以 HTTP/2 需要实现资源的并行请求,也就是任何时候都可以将请求发送给服务器,而并不需要等待其他请求的完成,然后服务器也可以随时返回处理好的请求资源给浏览器。即一个域名只使用一个 TCP 长连接和消除队头阻塞问题。
# 图示
# 新增特性
多路复用,通过引入二进制分帧层,就实现了 HTTP 的多路复用技术。
首先,浏览器准备好请求数据,包括了请求行、请求头等信息,如果是 POST 方法,那么还要有请求。这些数据经过二进制分帧层处理之后,会被转换为一个个带有请求 ID 编号的帧,通过协议栈将这些帧发送给服务器。服务器接收到所有帧之后,会将所有相同 ID 的帧合并为一条完整的请求信息。然后服务器处理该条请求,并将处理的响应行、响应头和响应体分别发送至二进制分帧层。同样,二进制分帧层会将这些响应数据转换为一个个带有请求 ID 编号的帧。经过协议栈发送给浏览器。浏览器接收到响应帧之后,会根据 ID 编号将帧的数据提交给对应的请求。
设置请求的优先级
我们知道浏览器中有些数据是非常重要的,但是在发送请求时,重要的请求可能会晚于那些不怎么重要的请求,如果服务器按照请求的顺序来回复数据,那么这个重要的数据就有可能推迟很久才能送达浏览器。为了解决这个问题,HTTP/2 提供了请求优先级,可以在发送请求时,标上该请求的优先级,这样服务器接收到请求之后,会优先处理优先级高的请求。数据传输优先级可控,使网站可以实现更灵活和强大的页面控制。
服务器推送
除了设置请求的优先级外,HTTP/2 还可以直接将数据提前推送到浏览器。提前给客户端推送必要的资源,这样就可以相对减少一点延迟时间。
头部压缩
HTTP/2 对请求头和响应头进行了压缩,你可能觉得一个 HTTP 的头文件没有多大,压不压缩可能关系不大,但你这样想一下,在浏览器发送请求的时候,基本上都是发送 HTTP 请求头,很少有请求体的发送,通常情况下页面也有 100 个左右的资源,如果将这 100 个请求头的数据压缩为原来的 20%,那么传输效率肯定能得到大幅提升。
可重置
能在不中断 TCP 连接的情况下停止数据的发送。
http 2.0
引入二进制分帧层,实现 HTTP 多路复用技术,并行处理请求。设置请求优先级,优先处理高优先级请求。 提前向客户端推送数据。压缩头部,提高传输效率。
# HTTP 3.0 未来
TCP 层面依旧存在队头阻塞
在 TCP 传输过程中,由于单个数据包的丢失会造成的阻塞。随着丢包率的增加,HTTP/2 的传输效率也会越来越差。有测试数据表明,当系统达到了 2% 的丢包率时,HTTP/1.1 的传输效率反而比 HTTP/2 表现得更好。
TCP 建立连接的延时
TCP 的握手过程也是影响传输效率的。我们知道 HTTP/1 和 HTTP/2 都是使用 TCP 协议来传输的,而如果使用 HTTPS 的话,还需要使用 TLS 协议进行安全传输,而使用 TLS 也需要一个握手过程,这样就需要有两个握手延迟过程。总之,在传输数据之前,我们需要花掉 3~4 个 RTT,若服务器相隔较远,那么 1 个 RTT 就可能需要 100 毫秒以上了,这种情况下整个握手过程需要 300~400 毫秒,这时用户就能明显地感受到“慢”了。
TCP 协议僵化
中间设备的僵化:如果我们在客户端升级了 TCP 协议,但是当新协议的数据包经过这些中间设备时,它们可能不理解包的内容,于是这些数据就会被丢弃掉。这就是中间设备僵化,它是阻碍 TCP 更新的一大障碍。操作系统也是导致 TCP 协议僵化的另外一个原因, 因为 TCP 协议都是通过操作系统内核来实现的,应用程序只能使用不能修改。通常操作系统的更新都滞后于软件的更新,因此要想自由地更新内核中的 TCP 协议也是非常困难的。
# 实现思路
HTTP/3 选择了一个折衷的方法 —— UDP 协议,基于 UDP 实现了类似于 TCP 的多路数据流、传输可靠性等功能,我们把这套功能称为 QUIC 协议。
QUIC(Quick UDP Internet Connection) 是谷歌推出的一套基于 UDP 的传输协议,它实现了 TCP + HTTPS + HTTP/2 的功能,目的是保证可靠性的同时降低网络延迟。因为 UDP 是一个简单传输协议,基于 UDP 可以摆脱 TCP 传输确认、重传慢启动等因素,建立安全连接只需要一的个往返时间,它还实现了 HTTP/2 多路复用、头部压缩等功能。众所周知 UDP 比 TCP 传输速度快,TCP 是可靠协议,但是代价是双方确认数据而衍生的一系列消耗。其次 TCP 是系统内核实现的,如果升级 TCP 协议,就得让用户升级系统,这个的门槛比较高,而 QUIC 在 UDP 基础上由客户端自由发挥,只要有服务器能对接就可以。
WebTransport 是否与 UDP Socket API 一样?不可以。WebTransport 不是 UDP Socket API。虽然 WebTransport 使用 HTTP/3,而 HTTP/3“在后台”使用 UDP,WebTransport 在加密和拥塞控制方面有一些要求,因此不仅仅是一个基本的 UDP Socket API。
# 连接迁移
一条 TCP 连接是由四元组标识的(源 IP,源端口,目的 IP,目的端口)。什么叫连接迁移呢?就是当其中任何一个元素发生变化时,这条连接依然维持着,能够保持业务逻辑不中断。当然这里面主要关注的是客户端的变化,因为客户端不可控并且网络环境经常发生变化,而服务端的 IP 和端口一般都是固定的。比如大家使用手机在 WIFI 和 4G 移动网络切换时,客户端的 IP 肯定会发生变化,需要重新建立和服务端的 TCP 连接。又比如大家使用公共 NAT 出口时,有些连接竞争时需要重新绑定端口,导致客户端的端口发生变化,同样需要重新建立 TCP 连接。所以从 TCP 连接的角度来讲,这个问题是无解的。
当用户的地址发生变化时,如 WIFI 切换到 4G 场景,基于 TCP 的 HTTP 协议无法保持连接的存活。QUIC 基于连接 ID 唯一识别连接。当源地址发生改变时,QUIC 仍然可以保证连接存活和数据正常收发。那 QUIC 是如何做到连接迁移呢?很简单,QUIC 是基于 UDP 协议的,任何一条 QUIC 连接不再以 IP 及端口四元组标识,而是以一个 64 位的随机数作为 ID 来标识,这样就算 IP 或者端口发生变化时,只要 ID 不变,这条连接依然维持着,上层业务逻辑感知不到变化,不会中断,也就不需要重连。由于这个 ID 是客户端随机产生的,并且长度有 64 位,所以冲突概率非常低。
# 低连接延时
当在浏览器地址栏中输入 https://www.abc.com
时,首先进行 DNS 递归查询 www.abc.com
以获取对应的 IP 地址,接着执行 TCP 三次握手建立连接需 1 个 RTT,随后进行 TLS 握手,按最广泛应用的 TLS 1.2 版本计算,新连接需 2 个 RTT,若为非首次建连则可通过会话重用减少至 1 个 RTT,最后是 HTTP 业务数据交互,假设数据能在一次交互中完成,则需 1 个 RTT;因此,完成一次简短的 HTTPS 业务数据交互总共需要:新连接 4RTT + DNS;会话重用 3RTT + DNS。特别是对于数据量小的请求,握手过程占据大量时间,对用户体验造成较大影响,尤其是在网络条件较差时,RTT 延时增加将显著恶化用户体验。
即使用上了 TLS 1.3,精简了握手过程,最快能做到 0-RTT 握手(首次是 1-RTT);但是对用户感知而言, 还要加上 1RTT 的 TCP 握手开销。Google 有提出 Fastopen 的方案来使得 TCP 非首次握手就能附带用户数据,但是由于 TCP 实现僵化,无法升级应用,相关 RFC 到现今都是 experimental 状态。这种分层设计带来的延时,有没有办法进一步降低呢? QUIC 通过合并加密与连接管理解决了这个问题,实现真正意义上的 0-RTT 的握手, 让与 server 进行第一个数据包的交互就能带上用户数据。
QUIC 由于基于 UDP,无需 TCP 连接,在最好情况下,短连接下 QUIC 可以做到 0RTT 开启数据传输。而基于 TCP 的 HTTPS,即使在最好的 TLS1.3 的 early data 下仍然需要 1RTT 开启数据传输。而对于目前线上常见的 TLS1.2 完全握手的情况,则需要 3RTT 开启数据传输。对于 RTT 敏感的业务,QUIC 可以有效的降低连接建立延迟。究其原因一方面是 TCP 和 TLS 分层设计导致的:分层的设计需要每个逻辑层次分别建立自己的连接状态。另一方面是 TLS 的握手阶段复杂的密钥协商机制导致的。
QUIC 握手过程:
- 预判与初始化:客户端首先检查本地是否已经缓存了服务器的配置参数(如证书配置信息)。如果已经存在,则可以直接进入正式握手阶段(进入第 5 步);如果不存在,则需要先获取这些配置信息。
- 初步握手 (Inchoate Client Hello, CHLO):当客户端没有服务器的配置参数时,会发送一个初步的 Client Hello(CHLO)消息给服务器,请求必要的配置参数。
- 服务器响应 (Rejection, REJ):服务器接收到 CHLO 后,会回复一个 Rejection(REJ)消息,其中包含了部分服务器配置参数,如公钥等信息。
- 处理服务器响应:客户端接收到 REJ 消息后,会提取并存储这些配置参数(回到第 1 步),然后准备进入正式握手阶段。
- 正式握手 (Full Client Hello, CHLO):客户端发送包含更多详细信息的 Full Client Hello 消息,其中包括客户端选择的公开数。此时,客户端利用已知的服务器配置参数和自己的公开数计算出初始密钥 K1。
- 服务器验证与响应 (Server Hello, SHLO):服务器接收到 Full Client Hello 后,会验证客户端的信息。如果不同意连接就回复 REJ(同第 3 步)。如果同意连接,服务器会使用相同的算法计算出初始密钥 K1,并选择一个临时公开数,然后发送 Server Hello(SHLO)消息给客户端。SHLO 消息使用初始密钥 K1 加密,并包含服务器选择的临时公开数。
- 客户端验证与密钥派生:客户端接收到 SHLO 消息后,使用初始密钥 K1 解密消息,提取出服务器的临时公开数。之后,客户端和服务器分别基于 SHA-256 算法,利用临时公开数和初始密钥 K1 派生出会话密钥 K2。
- 会话密钥交换:双方都计算出会话密钥 K2 后,开始使用这个密钥进行加密通信,初始密钥 K1 不再使用。QUIC 握手过程至此完成。未来会话密钥 K2 的更新过程与初次握手类似,但数据包中的某些字段会有所不同。
# 可自定义的拥塞控制
QUIC 的传输控制不再依赖内核的拥塞控制算法,而是实现在应用层上,这意味着我们根据不同的业务场景,实现和配置不同的拥塞控制算法以及参数。GOOGLE 提出的 BBR 拥塞控制算法与 CUBIC 是思路完全不一样的算法,在弱网和一定丢包场景,BBR 比 CUBIC 更不敏感,性能也更好。在 QUIC 下我们可以根据业务随意指定拥塞控制算法和参数,甚至同一个业务的不同连接也可以使用不同的拥塞控制算法。
# 无队头阻塞
虽然 HTTP2 实现了多路复用,但是因为其基于面向字节流的 TCP,因此一旦丢包,将会影响多路复用下的所有请求流。QUIC 基于 UDP,在设计上就解决了队头阻塞问题。TCP 队头阻塞的主要原因是数据包超时确认或丢失阻塞了当前窗口向右滑动,我们最容易想到的解决队头阻塞的方案是不让超时确认或丢失的数据包将当前窗口阻塞在原地。QUIC 也正是采用上述方案来解决 TCP 队头阻塞问题的。TCP 为了保证可靠性,使用了基于字节序号的 Sequence Number 及 Ack 来确认消息的有序到达。
QUIC 使用 Stream ID 来标识当前数据流属于哪个资源请求,这同时也是数据包多路复用传输到接收端后能正常组装的依据。重传的数据包 Packet N+M 和丢失的数据包 Packet N 单靠 Stream ID 的比对一致仍然不能判断两个数据包内容一致,还需要再新增一个字段 Stream Offset,标识当前数据包在当前 Stream ID 中的字节偏移量。有了 Stream Offset 字段信息,属于同一个 Stream ID 的数据包也可以乱序传输了(HTTP/2 中仅靠 Stream ID 标识,要求同属于一个 Stream ID 的数据帧必须有序传输),通过两个数据包的 Stream ID 与 Stream Offset 都一致,就说明这两个数据包的内容一致。
QUIC 协议原理浅解 (opens new window)
# 特性
用户态构建
QUIC 是在用户层构建的,所以不需要每次协议升级时进行内核修改。同时由于用户态可构建,QUIC 的开源项目十分之多。
实现了类似 TCP 的流量控制及传输可靠性,灵活的拥塞控制机制
虽然 UDP 不提供可靠性的传输,但 QUIC 在 UDP 的基础之上增加了一层来保证数据可靠性传输。它提供了数据包重传、拥塞控制以及其他一些 TCP 中存在的特性。TCP 的拥塞控制机制是刚性的。该协议每次检测到拥塞时,都会将拥塞窗口大小减少一半。相比之下,QUIC 的拥塞控制设计得更加灵活,可以更有效地利用可用的网络带宽,从而获得更好的吞吐量。
集成了 TLS 加密功能
目前 QUIC 使用的是 TLS1.3,相较于早期版本 TLS1.3 有更多的优点,其中最重要的一点是减少了握手所花费的 RTT 个数。
实现了 HTTP/2 中的多路复用功能
和 TCP 不同,QUIC 实现了在同一物理连接上可以有多个独立的逻辑数据流。实现了数据流的单独传输,就解决了 TCP 中队头阻塞的问题。
实现了快速握手功能
由于 QUIC 是基于 UDP 的,所以 QUIC 可以实现使用 0-RTT 或者 1-RTT 来建立连接,这意味着 QUIC 可以用最快的速度来发送和接收数据,这样可以大大提升首次打开页面的速度。QUIC 使用相同的 TLS 模块进行安全连接。然而,与TCP 不同的是,QUIC 的握手机制经过优化,避免了每次两个已知的对等者之间建立通信时的冗余协议交换,甚至可以做到 0RTT 握手。
连接迁移
当客户端 IP 或端口发生变化时(这在移动端比较常见),TCP 连接基于两端的 ip:port 四元组标示,因此会重新握手,而 UDP 不面向连接,不用握手。其上层的 QUIC 链路由于使用了 64 位 Connection id 作为唯一标识,四元组变化不影响链路的标示,也不用重新握手。
更好的错误处理能力
QUIC 使用增强的丢失恢复机制和转发纠错功能,以更好地处理错误数据包。该功能对于那些只能通过缓慢的无线网络访问互联网的用户来说是一个福音,因为这些网络用户在传输过程中经常出现高错误率。
# 面对的问题
- 服务器和浏览器端都没有对 HTTP/3 提供比较完整的支持
- 系统内核对 UDP 的优化远远没有达到 TCP 的优化程度,这也是阻碍 QUIC 的一个重要原因。
- 中间设备僵化的问题。这些设备对 UDP 的优化程度远远低于 TCP,据统计使用 QUIC 协议时,大约有 3%~7% 的丢包率
# 未来
从标准制定到实践再到协议优化还需要走很长一段路;并且因为动了底层协议,所以 HTTP/3 的增长会比较缓慢,这和 HTTP/2 有着本质的区别。但是腾讯等公司已经尝试在生产中落地 http3 的使用,例如 QQ 兴趣部落。2020年五月初,微软宣布开源自己的内部 QUIC 库 -- MsQuic,将全面推荐 QUIC 协议替换 TCP/IP 协议。所以总体来说 http3 未来可期。