HTTP/2.x

警告
本文最后更新于 2024-01-16,文中内容可能已过时。

HTTP/2.x

官方文档:HTTP/2.0规范文档

官网 PPT:HTTP/2 is here, let's optimize! - Velocity SC 2015 - Google 幻灯片,好多图都是从这里截取的

官方出的书:HTTP: HTTP/2 - High Performance Browser Networking (O’Reilly)

个人博客:深入理解 http2.0 协议,看这篇就够了!

关于 HTTP 协议2.x版本的基本概念,以及其解决的问题,请看《HTTP 协议基础》的HTTP/2.0小节,这里就不重复了。

HTTP/1.x给人的感觉像是一个一开始就没怎么好好设计的玩具,HTTP/2.0才开始像是一个经过设计的面向工业界的解决方案,在兼容HTTP/1.x的同时做到了性能的提升,而且从HTTP/1.x切换到HTTP/2.x的成本也很低,非常建议升级。

HTTP/2.x目前就只有HTTP/2.0这一个版本,而且已经开始大规模应用了,后文的讲解主要也是集中于HTTP/2.0

多路复用

更详细的解释,请看《HTTP 协议基础》的HTTP/2.0小节,这里只是留个引子。

HTTP/1.x 中,每次请求都会建立一次 HTTP 连接,也就是我们常说的 3 次握手 4 次挥手,这个过程在一次请求过程中占用了相当长的时间,即使开启了 Keep-Alive,解决了多次连接的问题,但是依然有两个效率上的问题:

  • 第一个:串行的文件传输。当请求 a 文件时,b 文件只能等待,等待 a 连接到服务器、服务器处理文件、服务器返回文件,这三个步骤。我们假设这三步用时都是 1 秒,那么 a 文件用时为 3 秒,b 文件传输完成用时为 6 秒,依此类推。(注:此项计算有一个前提条件,就是浏览器和服务器是单通道传输)

  • 第二个:连接数过多。我们假设 Apache 设置了最大并发数为 300,因为浏览器限制,浏览器发起的最大请求数为 6,也就是服务器能承载的最高并发为 50,当第 51 个人访问时,就需要等待前面某个请求处理完成。

HTTP/2.x的多路复用就是为了解决上述的两个性能问题。

HTTP/2.x 中,有两个非常重要的概念,分别是帧(frame)和流(stream)。帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流。

多路复用,就是在一个 TCP 连接中可以存在多条流。换句话说,也就是可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能。

HTTP/2.x性能提升的本质,就是从一开始通过多个 tcp 连接实现并发请求。到通过在单个 tcp 连接中的多个流来实现并发请求。提高了 tcp 的利用率。

启用 HTTP/2.0

启用HTTP/2.0的方式也很简单,以 Nginx 为例,首先 Nginx 的版本需要满足以下条件

  • nginx 版本高于 1.9.5;

  • --with-http_ssl_module--with-http_v2_module

首先需要支持配置SSL/TSL,然后就是加上一点配置即可

1
listen       443 ssl http2 ;

然后重启

1
2
nginx -s stop
nginx

真的非常简单。

帧的格式

HTTP/2帧格式简介_http帧格式-CSDN博客

帧格式 - HTTP2 学习笔记

1
2
3
4
5
6
7
8
9
+-----------------------------------------------+
|                 Length (24)                   |
+---------------+---------------+---------------+
|   Type (8)    |   Flags (8)   |
+-+-------------+---------------+-------------------------------+
|R|                 Stream Identifier (31)                      |
+=+=============================================================+
|                   Frame Payload (0...)                      ...
+---------------------------------------------------------------+

帧首部的字段定义如下:

  • 长度 (Length):帧载荷的长度,以一个无符号 24 位整数表示。必须不能 (MUST NOT) 发送大于 $2^{14}$ (16,384) 的值,除非接收者已经为 SETTINGS_MAX_FRAME_SIZE 设置了更大的值。注意:9 字节的帧首部不包含在这个值之内。也就是整个帧的大小是 9+length 字节。

  • 类型 (Type):8 位的帧类型。帧类型决定了帧的格式和语义。HTTP/2 实现 必须 (MUST) 忽略并丢弃未知类型的帧。

  • 标记 (Flags):一个特定于帧类型的 8 位 boolean 标记保留字段。标记的语义特定于指示的帧类型。一个特定帧类型中没有定义语义的标记 必须 (MUST) 被忽略,且 必须 (MUST) 在发送时被复位 (0x0)。

  • R:一个保留的 1 位字段。这个位的语义还没有定义,而在发送时这个位 必须 (MUST) 保持复位 (0x0) 状态,在接收时 必须 (MUST) 被忽略。

  • 流标识符 (Stream Identifier):流标识符被表示为一个 31 位无符号整型值。保留0x0值,用于那些与整个连接关联的帧,而不是一个独立的流。

具体类型的帧,具体的标记,以及具体的帧负载,我们有需要再去学。

一个简单的 HTTP/2.0 的请求过程

参考博客:HTTP/2 HEADERS and DATA Frames

消息(message)这个概念,是一个或两个 HEADER 帧 (携带 HTTP 消息头)、零个或多个 DATA 帧和一个可选的终结 HEADER 帧的组合。一条消息可以代表一个 HTTP 请求或者一个 HTTP 响应,一条消息是一个HTTP/2.0流的一半。

一个HTTP/2.0流是请求消息和响应消息的组合。注意,不要将消息与flag_end_stream混淆,后者表示任何一端为特定流发送的最后一帧。

(假设 stream ID 为 13)一个典型的浏览器发起的 GET 请求将有:

  • 一个HEADERS帧,其 stream ID 为 13,其标记中flag_end_headers=true,而且flag_end_stream=true,因为 GET 请求没有消息体,所以没有DATA

一个典型的 GET 请求的响应将有:

  • 一个HEADERS帧,其 stream ID 为 13,而且其标记中flag_end_headers=true

  • 一个或多个DATA帧,其 stream ID 为 13,最后一个DATA帧的标记中flag_end_stream=true

注意,由于多路复用,帧可以交错在一起(interleave);这意味着,如果您有两个并发响应 (例如流 13 和流 15),则可以使用此序列

HEADERS(13) HEADERS(15) DATA(15) DATA(13) DATA(13] DATA(15) DATA(15]

其中 ] 表示这个帧是这个流中的最后一个帧。

我们需要注意,请求和其对应的响应的帧使用的是同一个 streamID,一个请求用一个流 id,多个请求并行,就用多个流 id 进行区分即可。所以说二进制分帧和流是多路复用的基础。

HTTP/2.0 是否必须基于 HTTPS

参考博客:谈谈 HTTP/2 的协议协商机制 | JerryQu 的小站

h2c,代表 HTTP/2 ClearText,在 HTTP Upgrade 机制中会用到,而h2,则代表基于 TLS 的 HTTP/2

HTTP/2 协议本身并没有要求它必须基于 HTTPS(TLS)部署,但是当前主流浏览器,都只支持基于 HTTPS 部署的 HTTP/2;如果你的 HTTP/2 服务要支持浏览器访问,那就必须基于 HTTPS 部署;如果只给自己客户端用,可以不部署 HTTPS(这个页面列举了很多支持 h2c 的 HTTP/2 服务端、客户端实现)。

服务端推送

相比于HTTP/1.1的性能优化,请看HTTP/2.0引入服务端推送

简单实践

这个值得研究,可以提升页面的加载速度,TODO

既然 http/2.0 支持服务端推送,那么 Websocket 和 SSE 技术还有必要存在吗?

参考博客:有了 HTTP/2,Websocket 还有市场吗?

关于 Websocket,请看《WebSocket 理论知识》等相关博客

关于 SSE,请看《SSE 简单实践》

HTTP/2.0服务器推送让服务器能够主动推送响应到客户端缓存中。在典型的HTTP/1.x工作流中,浏览器请求一个页面,服务器在响应中返回一个 HTML,然后就是等待浏览器解析响应并发送额外请求来获取额外的内嵌资源(JavaScript、CSS 等)。服务器推送使服务器能够试探性地向客户端发送资源。此时,浏览器不必解析 HTML 页面并找到需要加载的其它资源;而是服务器能够立即开始发送它们。

有了这些改进和类似的能力,很自然地就会问:HTTP/2.0是 Websocket 或者 SSE 这类推送技术的替代品吗?

答案显然是否定的,理由很简单:正如我们上面所看到的,HTTP/2.0引入了服务器推送,让服务器能够主动地推送资源到客户端缓存。然而它并没有允许推送数据到客户端应用本身。服务器的推送只是由浏览器来处理,并不会让应用代码介入,这也就意味着应用程序无法使用 API 来获取这些事件的通知

由于 SSE 是基于 HTTP 的,其天然适配于HTTP/2.0,这样 SSE 就可以集两者之长:HTTP/2.0可以基于多路复用流形成一个高效传输层,同时 SSE 给应用提供了 API 使之能够进行推送。

帧优先级

因为数据流是并行的,而且相互之间不影响,因此,数据流可以有优先级,也就是说我们可以优先处理某一个数据流的请求并返回

  • 每一个数据流都可以有一个权重(weight),一个 1-256 之间的数字,权重较大的优先进行传输。

  • 每一个流都可以有一个父流(parent stream),父流传完了,子流才能传。

消息头压缩 - HPACK 算法

有需要再去研究

从 HTTP/1.1 到 HTTP/2.0 性能优化方式的变化

参考内容:HTTP/2 is here, let’s optimize! - Velocity SC 2015 - Google 幻灯片 第 14 张开始的内容

个人博客:从 view source 说说 http 性能优化 - 掘金

网路优化的方向

上面这张图,表示是从两个方向来降低网页的加载时间:

  • 增加网络带宽:随着带宽的增加,在超过 5Mbps 之后,性能提高了个位数的百分比

  • 降低网络的延时:随着网络延时的减小,网页加载时间线性提升

结论很清晰:为了加快整个互联网的速度,我们应该更多的在降低 RTT 这个方向上寻找方法。如果我们能把跨大西洋的 RTT 时间从 150 毫秒减少到 100 毫秒,这将比将用户的带宽从 3.9 Mbps 增加到 10 Mbps 甚至 1 Gbps 产生更大的影响

HTTP/2是为了在互联网上低延迟地内容传输而设计的协议。

经典的优化 Web 应用传输速度的方式

  • 减少 DNS 查找

  • 复用 TCP 连接

  • 使用内容交付网络 - 这个没懂

  • 最小化 HTTP 重定向的次数,为了减少网页延时,我们应该减少重定向,尤其是跨域的重定向,因为跨域的话得从 dns 开始重走一遍,太耗时。

  • 减少不必要的请求

  • 传输资源之前进行压缩

  • 在客户端缓存资源

  • 减少不必要的资源

一些在 HTTP/1.0 时代的优化措施,在 HTTP/2.0 时代会降低性能

域名分片

HTTP/1.x 以纯文本方式传输,虽然一个 TCP 连接用于多个 HTTP 请求,但是收到请求的服务器必须按照请求收到的顺序发送响应,也就是说一个 TCP 连接在同一时刻只能处理一个 http 请求,如果一个 TCP 连接种处理的多个请求种,排在前面的请求处理的很慢,那么后面的请求就得等着,这就是队头阻塞(Head-of-line blocking)

那我创建多个 TCP 连接是不是就可以了,是的,但是浏览器都有单域名的 TCP 并发量限制(Chrome 限制 6 个 TCP 并发)。

那既然是单域名限制并发量,那把资源分散在不同的域名不就行了,所以 HTTP/1.x 时代经常采用域名分片(domain shard)做性能优化。

然而不合理的域名分片,会导致一系列问题,比如:

1,每一个 TCP 连接在传输中会有头部字节开销,越多的 TCP 连接会产生大量重复数据,实际吞吐量(goodput)比率就会减少;

2,如果 TCP 连接数量超过网络负载,会造网络拥塞和 TCP 重连(重新握手增加延时)

所以说域名分片的关键在数量上的限制,否则会严重影响性能。

HTTP/2.0时代,通过引入帧还有流的概念实现了 TCP 的多路复用,在单个 TCP 连接中就可以实现多个请求的并发,因此也就不再需要通过域名分片来绕过浏览器的 TCP 数量限制,而且,在使用HTTP/2.x的时候进行域名分片还会对 HTTP 的性能造成负面印象:

  • 无法进行帧的优先级的控制,分成了多个 TCP 连接了嘛。无法控制不同的 TCP 上的帧的返回顺序

  • 降低 HPACK 算法的压缩效果,因为你分成了多个 TCP 了嘛,HPACK 算法只能压缩单个 TCP 连接上的请求消息头

因此,在使用HTTP/2.x的时候不要使用域名分片

对需要请求多个小资源的场景的优化

小资源整合成大资源

HTTP/1.x 时代为了减少网络请求,我们会将多个对小资源的请求合并成一个对一个大的资源的请求,最典型的就是图片,通过类似雪碧图这种方式压缩合并多个小资源为一个大资源,

这样做的好处是:

  • 一次性下载一个大资源相当于并行了多个对小资源的请求,会快一点

  • 一次性下载一个大资源也方便压缩

然而同时也产生了新的问题,

  • 就算我们只想用一个小资源,也得等到整个大资源下载好之后才能进行

  • 我们要更新大资源中的小资源,哪怕一个字节的改动,都需要全量更新这个大资源,浏览器缓存也必须全量更新。

资源内联

资源内联就是将外部资源合并在网页文件中来减少 http 请求,<style> 或是 <script> 这样文本资源可以直接内联在网页中,但非文本资源必须使用 base64 编码,base64 与原始资源相比,有字节上的增加(字节用 base64 转码后字节数增加33%),这样就增加了额外的传输数据。

比如原本是通过 http 从服务器获取一张图片,现在可以把它转化为 base64 格式内嵌在网页中,这样一来就减少了 http 请求次数

例如:

内联前:

1
<img src="http://www.example.com/img/xxx.gif" alt="1x1 transparent (GIF) pixel" />

内联后

1
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAAAAACH5BAAAAAAALAAAAAABAAEAAAICTAEAOw==" alt="1x1 transparent (GIF) pixel" />

缺点同样明显:

  • 资源内联和上节的资源合并有同样问题,网页中内联资源不能被独立缓存,如果有一个小资源需要更新,则需要重新拉取整个页面来全量更新缓存;有时多个页面引入了同一个资源,也会造成资源的重复加载。所以说如果一个资源本身比较小且不需要经常更新,那么可以考虑内联在页面中,如果资源本身需要频繁更新,那么还是算了吧!

  • 内联资源无法被多路传输,也无法设置优先级,而且也无法被客户端拒绝

HTTP/2.0引入服务端推送

HTTP/2.0中通过服务端推送解决了小资源的请求问题,当客户端请求一个网页的时候,服务端知道这个网页还依赖其他的静态资源,比如xxx.jsxxx.cssxxx.png,此时,服务端会主动将这些网页依赖的资源主动推送给客户端,

  • 这些资源可以通过多路传输传递给客户端,我们也可以对这些资源设置传输的优先级

  • 客户端收到这些静态资源之后可以将其放到浏览器缓存中

  • 服务端推送是完全由flow control来控制,客户端可以控制只接收固定大小的资源,比如 5kb,避免服务端给客户端主动推送过多的资源,客户端也可以直接拒绝接收这些资源。客户端甚至可以让服务端先传一个静态资源的一部分,然后暂停,等一段时间之后再传剩下的部分。

    HTTP/2.0 的 flow-control 是 receiver 控制 sender 发送速度的一种机制,参考博客:RFC7540 笔记(五)——Flow Control - laike9m’s blog

服务端推送具有同源限制。

0%