HTTP/2(2015年发布)已经发布快10年了,云原生社区的RPC框架中,gRPC
是直接基于 HTTP/2 实现。Dubbo
框架的默认协议,也从原先基于 TCP协议
的 dubbo协议
,换成基于 HTTP/1.1、HTTP/2
的 triple协议
。
可能网站、框架还在使用 HTTP/1.1(1997年发布),但随着对系统性能的要求越来越高,HTTP/2 实在应该好好了解一番。
从 HTTP/1.0 到 HTTP/2,每个版本的升级,都存在解决各自最核心的问题,下面从各自问题入手,逐渐了解各版本的功能提升。
1. HTTP/1.0 - 独立连接问题
HTTP/1.1 通过引入持久连接(Keep-Alive)机制,显著改进了 TCP 连接的复用问题。与 HTTP/1.0 的短连接方式相比,HTTP/1.1 的持久连接减少了连接建立和关闭的开销,优化了资源利用,提高了网络性能,从而显著提升了用户体验和 Web 应用的效率。
1.1. HTTP/1.0 的 TCP 连接管理
1. 短连接
默认行为:
- HTTP/1.0 默认使用短连接。每次请求和响应完成后,TCP 连接都会被关闭。这意味着每个资源(如 HTML 文档、图片、CSS 文件等)都需要建立一个新的 TCP 连接。
问题:
- 频繁的连接建立和关闭:每次请求都需要进行 TCP 三次握手和四次挥手,增加了网络延迟。
- 资源消耗:频繁的连接操作消耗了大量的服务器资源,如 CPU 和内存。
- 带宽浪费:每次建立连接都需要传输 TCP 握手数据,浪费了带宽。
2. Keep-Alive 扩展
非标准支持:
- 尽管 HTTP/1.0 不正式支持持久连接,但一些实现引入了
Connection: Keep-Alive
头部,允许在一个连接上发送多个请求。这是一种非标准的扩展,不同实现之间可能存在兼容性问题。
- 尽管 HTTP/1.0 不正式支持持久连接,但一些实现引入了
1.2. HTTP/1.1 的 TCP 连接管理
1. 持久连接(Persistent Connections)
默认行为:
- HTTP/1.1 默认启用持久连接。除非明确指定
Connection: close
,否则连接会保持打开状态,允许在同一连接上发送多个请求和响应。
- HTTP/1.1 默认启用持久连接。除非明确指定
优势:
- 减少连接建立和关闭的开销:减少了 TCP 三次握手和四次挥手的次数,降低了网络延迟。
- 优化资源利用:减少了服务器的 CPU 和内存消耗,提高了资源利用率。
- 节省带宽:避免了频繁的 TCP 握手数据传输,节省了带宽。
2. Keep-Alive 参数
连接保持时间:
- HTTP/1.1 允许客户端和服务器通过
Keep-Alive
头部指定连接保持的时间。例如,服务器可以设置Keep-Alive: timeout=5, max=100
,表示连接最多保持 5 秒,最多处理 100 个请求。
- HTTP/1.1 允许客户端和服务器通过
连接管理:
- 客户端和服务器可以灵活地管理连接的生命周期,根据需要调整连接的保持时间和最大请求数。
1.3. 实际应用和影响
1. 性能提升
减少延迟:
- 持久连接显著减少了每个请求的延迟,特别是对于需要加载多个资源的网页。例如,一个包含多个图片和脚本的网页可以通过一个连接完成所有资源的加载,大大减少了总的加载时间。
提高吞吐量:
- 通过减少连接建立和关闭的开销,持久连接提高了网络的吞吐量,使得服务器能够处理更多的请求。
2. 资源优化
服务器资源:
- 持久连接减少了服务器的 CPU 和内存消耗,使得服务器能够更高效地处理请求,支持更高效的并发连接。
网络资源:
- 持久连接减少了网络带宽的浪费,提高了带宽利用率,特别是在高延迟和低带宽的网络环境中效果显著。
3. 用户体验
更快的页面加载:
- 持久连接显著提升了页面加载速度,改善了用户体验,特别是在加载复杂的网页时。
更好的响应时间:
- 减少的延迟和更高的吞吐量使得 Web 应用能够更快地响应用户请求,提供了更流畅的交互体验。
2. HTTP/1.1 - 队头阻塞问题
HTTP/1.1 的队头阻塞问题主要源于其串行化的请求/响应模型。在一个连接上必须等待前一个请求的响应完成后才能发送下一个请求,这种设计限制了连接的效率,特别是在需要处理多个请求时。通过升级到 HTTP/2 或优化网络和服务器性能,可以有效缓解或解决这一问题。
2.1. 串行请求/响应模型
单一请求处理:
- HTTP/1.1 在一个 TCP 连接上只能同时处理一个请求和一个响应。这意味着必须等待当前请求的响应完成后,才能发送下一个请求。
- 这种设计导致了一个关键问题:如果一个请求的响应需要较长时间(可能是网络传输慢,可能是服务器业务处理慢),后续的请求就会被阻塞,无法在同一个连接上并行处理。
典型场景:
- 在加载网页时,浏览器需要从同一个服务器请求多个资源(如 HTML、CSS、JavaScript、图像等)。如果一个资源请求的响应较慢,其他资源的请求就会被阻塞,导致整个页面加载变慢。
2.2. 浏览器的连接限制
如果基于单个 TCP 连接的 HTTP 请求,存在阻塞问题,那么打开多个 TCP 连接,是解决 HTTP/1.1 阻塞问题最简单方法。不过大多数浏览器最多只可以为每个域名打开6个连接,即6个 TCP 连接,即可以有6个 HTTP 请求同时并行开启。
突破请求数限制做的努力
为了进一步突破6个连接的现在,许多网站从子域名(例如:static.example.com)提供css、js等静态资源,Web浏览器从而可以为每个新域名打开另外的6个连接。这种技术叫 域名分片。
但浏览器既然限制并发的连接上限,肯定有自己的意义。多个 TCP 连接增加了服务器和网络的负担,因为每个连接都需要进行 TCP 握手和维护状态,不仅增加了网络拥塞,维护连接需要消耗更多的内存和CPU等资源。
2.3. 资源请求的依赖性
资源加载顺序:
- 在复杂的网页中,某些资源可能依赖于其他资源的加载完成。例如,JavaScript 文件可能需要在特定的 CSS 文件加载后执行。
- 如果某个关键资源的请求被阻塞,可能会导致整个页面的渲染延迟,从而影响用户体验。
2.4. 网络和服务器因素
服务器处理时间:
- 如果服务器处理某个请求需要较长时间,响应会被延迟,从而导致后续请求被阻塞。
网络延迟:
- 网络条件不佳(如高延迟或丢包)也会加剧队头阻塞问题,因为它会增加请求和响应的时间。
2.5. 请求资源大
1. 文本格式体积大
HTTP/1.1 是一个简单的文本协议,这种简单性也带来的一些问题,尽管消息体可以包含二进制数据(比如图片等),但请求和首部都需要是文本的形式。
文本格式对于人类来说很友好,但对于机器并不友好。向HTTP首部中添加换行符,可以进行一些HTTP攻击。最主要的是消息体会比较大,不如二进制体积小。
2. 请求头冗余
HTTP/1.1 请求头的应用场景很多了,请求头所占的数据量越来越大,但内容大多重复。
就算只有主页需要 cookie,每个发向服务器的 HTTP 请求中都会包含 cookie。通常静态资源,如:图片、css、js 都不需要 cookie的。包括像 Content-Security-Policy
这种关于安全的头部,会导致头部非常大,从而使效率低下的问题越来越突出。通常一个 GET
请求中请求参数不大,但头部数据可能却占了几K。
头部冗余导致带宽浪费,尤其在高延迟和低带宽网络中,对性能影响显著。完全可以利用其较高的重复性好好优化。
3. 合并请求做的努力
另一个常见的优化思路就是发送更少的请求,包括:减少不必要的请求(比如在浏览器中缓存静态资源)。
对于图片来说,有种叫做“精灵图”的打包技术。如果网站上有很多图标,如果每个图标都是一张独立的图片,会导致很多低效的HTTP请求排队。以为图片比较小,所以相对于下载这些图片所需要的时间,发送请求的时间可能会较长。
所以,可以将它们合并到一张大的图片里,然后使用CSS来定位图片位置,让它们看起来像是独立的图片,这样更高效。
如果是 CSS、JS文件,前端框架通常也会将多个文件合并成一个文件,这样需要的请求数就少了,总的代码量并不会变。在合并文件的时候,通常还会去掉代码中不必要的空格、注释等。
这种思路不错,但也会带来一定的复杂性。如果需要修改某个图标,还需要重新维护整个精灵图。
3. HTTP/2 - 解决队头阻塞
HTTP/2 通过一系列技术革新,去解决 HTTP/1.1 的队头阻塞问题。HTTP/1.1 的队头阻塞主要是因为其在一个 TCP 连接上只能同时处理一个请求和响应,这种串行化的处理方式导致了性能瓶颈。HTTP/2 的设计目标之一就是消除这种限制。以下是 HTTP/2 如何解决队头阻塞问题的详细介绍:
3.1. 多路复用
概念:
- HTTP/2 允许在一个单一的 TCP 连接上同时发送和接收多个请求和响应,而不必按照请求的顺序来等待响应。这是通过引入“流”(streams)的概念实现的。
工作机制:
- 在 HTTP/2 中,数据被分成更小的帧,这些帧可以在同一个连接中并行传输。每个流都有一个唯一的标识符,数据帧可以乱序到达和处理。
- 这种机制避免了 HTTP/1.1 中必须等待前一个请求完成后才能发送下一个请求的限制。
优势:
- 通过多路复用,HTTP/2 消除了单个请求阻塞其他请求的情况,提高了数据传输效率和网络利用率。
3.2. 二进制分帧
高效传输:
- HTTP/2 使用二进制格式传输数据,取代了 HTTP/1.1 的文本格式。这种格式更紧凑,解析更快,减少了由于格式解析而导致的延迟。
帧的独立性:
- 数据被分割成二进制帧,帧是独立的,这样即使某个流中的帧被延迟或丢失,也不会影响其他流的数据传输。
3.3. 流优先级和依赖
优先级设置:
- HTTP/2 允许客户端为每个流设置优先级,服务器可以根据这些优先级来决定资源分配策略。
依赖关系:
- 流可以相互依赖,形成一个依赖树结构,服务器可以根据优先级和依赖关系优化资源处理。
效果:
- 通过优先级和依赖关系,关键请求可以被优先处理,进一步减少可能的阻塞和延迟。
3.4. 头部压缩
HTTP/2 的头部压缩机制使用了 HPACK 算法来减少 HTTP 头部信息的冗余传输,从而提高传输效率。这一机制通过静态表和动态表来压缩头部字段及其值,并使用哈夫曼编码进行进一步压缩。以下是对 HPACK 头部压缩的详细介绍,并附带一个简单示例。
3.4.1. HPACK 压缩机制
1. 静态表
- 静态表是一个预定义的头部字段及其常用值的集合。在 HPACK 中,这个表是固定的,包含了常用的 HTTP 头部字段,如
:method
,:path
,:authority
等。 - 使用静态表时,可以通过索引直接引用这些常用的头部字段,减少传输数据量。
2. 动态表
- 动态表是在连接期间动态维护的表,用于存储最近使用的头部字段和值。每当一个新的头部字段被传输时,它可以被添加到动态表中。
- 动态表是可变大小的,服务器和客户端可以根据需要调整其大小。
3. 哈夫曼编码
- 哈夫曼编码是一种可变长度的编码方法,用于对头部字段的名称和值进行进一步压缩。它通过对频繁出现的字符使用更短的编码来减少数据量。
3.4.2. 示例
假设我们有一个 HTTP 请求,其头部如下:
:method: GET
:scheme: https
:authority: www.example.com
:path: /index.html
user-agent: Mozilla/5.0
1. HPACK 压缩过程
使用静态表:
:method: GET
和:scheme: https
可以在静态表中找到对应的索引,因此可以直接使用索引来引用。
使用动态表:
:authority: www.example.com
和user-agent: Mozilla/5.0
可能是首次传输,因此会被添加到动态表中以供后续请求使用。
哈夫曼编码:
:path: /index.html
和其他未在静态表中的值可以使用哈夫曼编码进行压缩。
静态表 VS 动态表
- 静态表:内容固定,不会随连接变化。它包含常用的、标准化的头部字段及其常见值。
- 动态表:内容可变,随着连接的进展动态更新,存储在连接过程中实际传输的头部字段和值。
可以看到静态表内容是固定的。动态表更灵活,但需要动态管理,增加了实现的复杂性,涉及内存和性能的开销,所以性能上不如静态表。
因此,如果内容在静态表中存在,协议优先使用静态表,否则再考虑使用动态表。
压缩后的传输
通过上述步骤,头部信息会被转换为一系列的二进制数据,这些数据比原始的头部信息要小得多。在接收端,这些二进制数据会被解码成原始的头部信息。
3.5. 服务器推送
HTTP/2 的服务器推送(Server Push)是一项强大的功能,它允许服务器在客户端请求某个资源之前,主动将相关资源推送到客户端。这种机制可以显著减少网页加载时间,尤其是在高延迟网络环境中。以下是关于 HTTP/2 服务器推送的详细介绍。
1. 工作原理
客户端请求:
- 客户端发起一个 HTTP 请求,比如请求一个 HTML 页面。
服务器识别依赖:
- 服务器在处理请求时,可以识别出该页面所需的其他资源,比如 CSS、JavaScript 或图片文件。
发送推送承诺(Push Promise):
- 服务器向客户端发送一个
PUSH_PROMISE
帧。这一帧告知客户端将要推送的资源,并包含这些资源的请求头信息。 PUSH_PROMISE
帧的发送顺序是在客户端请求的响应之前,这样客户端可以知道即将接收到哪些资源。
- 服务器向客户端发送一个
主动推送资源:
- 服务器随后将这些资源作为响应推送给客户端,类似于正常的 HTTP 响应。
客户端处理:
- 客户端接收这些推送的资源后,可以将其缓存,以便后续使用,而无需再发起请求。
2. 优势
- 减少延迟:通过提前发送资源,减少了客户端请求这些资源的延迟。
- 优化加载顺序:服务器可以根据页面依赖关系优化资源的推送顺序,提高页面加载效率。
- 减少请求次数:避免了客户端在解析 HTML 后再发起的额外请求,减少了请求次数和服务器负载。
3. 示例
假设一个网页需要加载多个资源:
- 客户端请求
/index.html
。 - 服务器识别出
/index.html
依赖于/style.css
和/script.js
。 - 服务器发送
PUSH_PROMISE
帧给客户端,表示将推送/style.css
和/script.js
。 - 服务器随后主动推送
/style.css
和/script.js
的内容。 - 客户端接收这些资源并缓存,以便在解析 HTML 时直接使用。
4. HTTP、TCP队头阻塞
前面讲解 HTTP/1.1 的队头阻塞问题,其实我们也常接触到 TCP 队头阻塞问题,这两者不是同一个概念。
虽然 HTTP/1.1 也是基于 TCP协议创建连接到,HTTP/1.1 队头阻塞问题,也会因为 TCP 队头阻塞问题受到影响,但二者不能划等号。
HTTP/1.1 的队头阻塞问题主要是由于其串行请求/响应模型导致的,而 TCP 的队头阻塞问题则源于其可靠传输的顺序保证机制。HTTP/1.1 的问题可以通过升级到 HTTP/2 来解决,而 TCP 的问题可以通过使用 QUIC 协议或网络优化来缓解。通过针对不同层次的问题采取适当的解决方案,可以显著提高网络传输效率和用户体验。
4.1. HTTP/1.1 队头阻塞
1. 产生原因
串行请求/响应模型:
- 在 HTTP/1.1 中,一个 TCP 连接上通常只能同时处理一个请求和一个响应。这意味着在一个连接中,必须等待当前请求的响应完成后,才能开始处理下一个请求。
- 这种串行化的处理方式导致了队头阻塞问题:如果一个请求的响应时间较长,后续的请求就会被阻塞,无法及时处理。
有限的并发连接:
- 浏览器为了减轻单连接的限制,通常会对每个域名开启多个并发连接(通常是 6 个左右)。然而,连接数的增加会导致服务器负载增加和网络资源浪费。
- 即便如此,多个连接之间的请求仍然是串行化的,无法完全消除队头阻塞的问题。
2. 影响
- 页面加载速度慢:当一个请求被阻塞时,后续请求的资源无法及时获取,导致页面加载变慢。
- 用户体验下降:由于资源加载缓慢,用户可能会感到网页响应迟缓,影响用户体验。
4.2. TCP 队头阻塞
1. 产生原因
可靠传输的顺序保证:
- TCP 协议旨在提供可靠的、有序的数据传输。它通过序列号来确保数据包按序到达。接收方必须按照序列号的顺序来处理数据包。
- 如果某个数据包丢失或延迟到达,接收方必须等待该数据包被重传或到达,才能继续处理后续的数据包。
网络抖动和丢包:
- 在实际网络环境中,数据包可能会因为各种原因(如网络拥塞、硬件故障等)而丢失、延迟或乱序。
- TCP 的顺序保证机制要求在处理后续数据之前,必须解决这些丢失或延迟的问题,这会导致整个连接的阻塞。
2. 影响
- 传输延迟增加:因为需要等待丢失的数据包被重传,整个数据流的传输延迟会增加。
- 吞吐量降低:由于某个数据包的丢失或延迟,整个连接的吞吐量会降低,因为后续的数据包无法被及时处理。
4.3. 两者之间的关系
层次不同:
- TCP 队头阻塞发生在 传输层,而 HTTP 队头阻塞发生在 应用层。这意味着它们发生在网络协议栈的不同层次。
间接影响:
- 虽然 TCP 的队头阻塞不会直接导致 HTTP 的队头阻塞,但它会间接影响 HTTP 请求的效率。
- 在 HTTP/1.1 中,如果 TCP 连接上的某个数据包丢失,整个连接上的所有 HTTP 请求都会受到影响,因为 TCP 必须等待丢失的数据包重传。
HTTP/2 的改进:
- HTTP/2 通过多路复用技术在应用层解决了 HTTP/1.1 的队头阻塞问题。即使在同一个 TCP 连接上,多个请求和响应也可以同时进行。
- 然而,HTTP/2 仍然依赖于 TCP,因此仍然可能受到 TCP 队头阻塞的影响。如果 TCP 层发生队头阻塞,HTTP/2 的所有流都会受到影响。
4.4. 解决方案概述
4.4.1. HTTP/2 不能真正解决问题
按照前文的方式,从 HTTP/1.1 升级到 HTTP/2,可以“解决”队头阻塞的问题。HTTP/2 引入了多路复用技术,允许在一个 TCP 连接上同时处理多个请求和响应,避免了串行化处理的瓶颈。
虽然 HTTP/2 在应用层有效地缓解了 HTTP 层面的队头阻塞,但由于其依赖于 TCP 作为底层传输协议,仍然会受到 TCP 队头阻塞问题的影响。只有也解决了传输层的队头阻塞问题,才能说HTTP应用层真正解决了。
4.4.2.TCP 队头阻塞的解决
HTTP/2 无法彻底解决队头阻塞问题,解决传输层队头阻塞问题的答案在 HTTP/3,HTTP/3 使用的传输层协议是 QUIC
协议。QUIC 运行在 UDP
之上,而不是 TCP。
QUIC
(Quick UDP Internet Connections)是由 Google 开发的一种传输层网络协议,旨在提高网络应用的性能和安全性。QUIC 的设计初衷是解决 TCP 的一些固有缺陷,特别是在高延迟和高丢包率的网络环境中。它已经被广泛应用于 HTTP/3
中。
QUIC 协议通过多个创新机制解决了传统 TCP 中的队头阻塞问题,以下是详细说明:
1. 多路复用的独立流
流的独立性:
- QUIC 协议允许在单个连接中同时进行多个独立的流传输。每个流都有自己的流 ID,并且在传输过程中是彼此独立的。
- 在 TCP 中,如果一个数据包丢失,接收端必须等待该数据包被重传后才能处理后续的数据包,这就导致了队头阻塞。
- QUIC 通过允许不同流独立传输,使得即便一个流中的数据包丢失,也不会影响其他流的传输和处理。
2. 基于 UDP 的实现
去除 TCP 顺序依赖:
- QUIC 使用 UDP 作为底层协议,而不是 TCP。这意味着 QUIC 不受 TCP 的顺序交付限制,可以自由实现自己的流控制和拥塞控制。
- 因为 QUIC 在用户态实现,可以直接管理数据包的传输顺序和重传策略,而无需依赖内核态的 TCP 栈。
3. 高效的重传机制
快速重传:
- QUIC 协议支持快速重传机制。当检测到数据包丢失时,QUIC 可以立即重传丢失的数据包,而不需要等待传统的 TCP 超时。
- QUIC 的设计允许接收方发送关于丢失数据包的精确反馈,发送方可以根据这些反馈迅速进行重传。
4. 灵活的拥塞控制
自定义拥塞控制算法:
- QUIC 允许实现自定义的拥塞控制算法,这使得它可以在不同网络条件下优化数据传输。
- 由于其在用户态实现,开发者可以根据具体需求调整拥塞控制策略,以减少丢包对传输效率的影响。
5. 前向纠错
纠错机制:
- QUIC 可以在传输数据时添加冗余信息,使得接收方能够在一定程度上自行纠正丢失的数据包,而无需重传。这进一步减少了因丢包带来的延迟。
6. 0-RTT 连接建立
快速连接建立:
- QUIC 支持 0-RTT(零往返时间)连接建立,这意味着在某些情况下,数据可以在连接建立的同时发送。这减少了初始连接建立的延迟。
5. HTTP各版本
HTTP 的每个版本都在解决其前任版本的不足,并适应不断变化的网络需求和技术进步。HTTP/1.1 增强了持久连接和缓存控制,HTTP/2 提升了传输效率和并发能力,而 HTTP/3 则通过 QUIC 协议提供了更快和更可靠的网络传输体验。
1. HTTP/0.9
- 发布年份:1991
特点:
- 最初的 HTTP 版本,设计极为简单。
- 仅支持 GET 方法,没有版本号标识。
- 响应仅包含纯文本数据,没有 HTTP 头部。
- 主要用于传输简单的 HTML 页面。
2. HTTP/1.0
- 发布年份:1996 (RFC 1945)
特点:
- 引入了 HTTP 头部,允许传输更多类型的数据。
- 支持多种方法:GET、POST、HEAD。
- 引入了状态码和响应头,提供更多的响应信息。
- 每个请求/响应对使用一个单独的 TCP 连接。
3. HTTP/1.1
- 发布年份:1997 (RFC 2068),后续更新为 RFC 2616,并最终在 2014 年被 RFC 7230-7235 替代。
特点:
- 默认使用持久连接(Persistent Connection),减少了连接建立的开销。
- 支持请求管道化(Pipelining),允许在同一个连接上发送多个请求而无需等待响应。
- 增强了缓存控制机制。
- 支持分块传输编码(Chunked Transfer Encoding),允许动态生成内容。
- 增加了更多的 HTTP 方法和头字段。
4. HTTP/2
- 发布年份:2015 (RFC 7540)
特点:
- 引入二进制分帧层,提升了传输效率。
- 支持多路复用,允许在一个连接上同时处理多个请求和响应。
- 使用 HPACK 压缩算法对头部进行压缩,减少传输数据量。
- 支持服务器推送(Server Push),允许服务器主动向客户端发送资源。
5. HTTP/3
- 发布年份:2022 (RFC 9114)
特点:
- 基于 QUIC 协议,运行在 UDP 之上,提供更快的连接建立和恢复能力。
- 解决了 TCP 层的队头阻塞问题,通过独立流提供更好的性能。
- 继续支持多路复用和服务器推送。
- 提供更好的安全性和加密性能。
前期的版本都是小版本迭代,有良好的兼容性,如:HTTP/1.0 -> HTTP/1.1。
但 HTTP/1.1 -> HTTP/2,HTTP/2 直接到 HTTP/3,都是大版本迭代,因为变更很大,向前兼容很难。另外市场上各家浏览器、社区生态对新版本的兼容也需要时间,所以主流的使用版本还是 HTTP/1.1。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。