背景
本文是阅读美国作者Stephen Ludin Javier Garza编写的《HTTP/2基础教程》后,加上本人的理解总结出来的
1. HTTP 进化史
- 超文本被提出:大量的书面材料或图像材料以复杂的方式相互联系,因此不便在纸质上呈现。
1.1 HTTP/0.9 和 HTTP/1.0
HTTP/0.9 是个相当简单的协议。只有一个get方法,没有首部。设计目标仅仅只是获取HTML(只有文本)
HTTP/1.0 添加了大量的内容:
- 首部
- 响应码
- 重定向
- 错误
- 条件请求
- 内容编码(压缩)
- 更多的请求方法
… …
但是HTTP/1.1 人有很多的缺陷
- 不能让多个请求共用一个连接
- 缺少强制的Host首部
- 缓存的选择也相当简陋
1.2 HTTP/1.1
- 因为强制要求客户端提供Host首部,所以虚拟主机托管成为可能,也就是在一个IP上提供多个web服务
- 不需要为每个请求重新发起TCP连接
- 变更如下:
- 缓存相关首部的扩展
- OPTIONS 方法
- Upgrade 首部
- Range (范围)请求
- 压缩和传输编码
- 管道化
1.3 1.1版本之后
- 一直使用到现在,持续了很长的时间
- 最明显的变化是网页的构成、新加入的每种元素都增加了复杂度,也带来了压力。
1.4 SPDY
- Google 工程师提出了HTTP的一种替代方案: SPDY ;它不是第一个希望代替,但是最重要的一个。
- SPDY 为 HTTP/2 奠定了基础
1.5 HTTP/2
提出以下期望:
- 相比于HTTP/1.1 ,最终用户可感知的多数延迟都有能够量化的显著改善;
- 解决HTTP中队头阻塞问题
- 并行的实现机制不依赖与服务器建立多个连接,从而提升TCP 连接的利用率,特别是拥塞控制方面
- 保留HTTP/1.1 的语义,包括(不限于)HTTP 方法、状态码、URI 和首部字段
- 明确定义 HTTP/2 和 HTTP/1.X 交互的方法,特别是通过中介的方法(双向)
- …
2. HTTP/2 快速入门
2.1 启动并运行
现在很多的主流网站都采用了HTTP/2
搭建并运行h2服务器
- 获取并安装一个支持h2的Web服务器;
- 下载并安装一个TLS证书,让浏览器与服务器通过h2连接
。。。。。。
3. Web优化“黑魔法”的动机与方式
3.1 HTTP/1.x的问题
- 队头阻塞
浏览器大多数时候希望从一个域名中获取多个资源。设想这样一个网站,它把所有的图片放在单个域名下。HTTP/1.x并未提供机制来同时请求这些资源。如果只用一个连接,它需要发起请求、等待响应,之后才能发起下一次请求。h1有个特性:管道化,允许依次发送一组请求,但是只能按照发送顺序依次接受请求。而且备受互操作性和部署等问题困扰而无实用价值。
请求问答过程中,如果出现状况,剩下的工作都会阻塞,这就是“队头阻塞”。现代浏览器会针对单个域名开启6个连接,通过各个连接分别发送请求。 - 低效的 TCP 利用
TCP 之所以成功是因为它是最可靠的协议之一,核心概念是拥塞窗口:在接收方确认数据包之前,发送方可以发送的数据包数量。TCP 中还有慢启动的概念,拥塞避免算法,导致传输效率降低。
- 臃肿的消息首部
h1的首部不能压缩,而且消息首部不能忽略
- 受限的优先级设置
如果浏览器对指定域名开启了多个socket,开始请求资源,这时候只能通过:要么发起请求,要么不发起来指定优先级。
- 第三方资源
Web资源有很多完全独立于服务器的控制,这部分影响很大,但至今难有好的解决方案,即使是h2
3.2 Web性能优化技术
- DNS查询优化
- 限制不同域名的数量。(如果采用HTTP/2 ,域名数量对性能的相对影响会只增不减)
- 保证低限度的延迟解析。了解你的DNS服务基础设施的结构,然后从你的最终用户分布的所有区域定期监控解析时间(通过虚拟或真实用户的监控做到)。
- 在主体页面HTML或响应中利用DNS 预取指令。这样,在下载并处理主题页面HTML时,预取指令就能开始解析页面上指定的域名。 例如:
<link rel="dns-prefetch" href="//ajax.googleapis.com">
- 优化TCP 连接
开启一个新连接是一个耗时的操作,如果使用TLS (确实应该)开销会更大,优化方法:-
利用preconnect 指令,连接在使用之前就已经建立好了。例如:
<link rel="preconnect" href="//fonts.example.com" crossorigin>
-
尽早终止并响应
-
实施最新的TLS 最佳实践
-
- 避免重定向
最好不要使用重定向,基本也没有合适理由使用,如果必须使用,使用以下方式:- 利用CDN代替客户端在云端实现重定向
- 如果是同一域名的重定向,使用Web服务器上的rewrite规则
- 客户端缓存
生存时间(TTL)指令告诉浏览器应该缓存某个资源多久。- 所谓的纯静态资源,例如图片或带版本的数据,可以永久缓存
- CSS/JS 和个性化资源,缓存时间大约是会话平均时间的两倍
- 其他类型,具体分析
- 网络边缘缓存
缓存在网站基础服务设施,网络边缘地带加速访问效率- 在多用户可共享,并且能够接受一定的旧数据
- 条件缓存
如果过期时间到了,也有可能返回同样数据,所以提供条件 - 压缩和代码简化
- 避免阻塞 CSS/JS
- 图片优化
最重要的四大特性
二进制分帧
先来理解几个概念:
- 帧:HTTP/2 数据通信的最小单位消息:指 HTTP/2 中逻辑上的 HTTP 消息。例如请求和响应等,消息由一个或多个帧组成。
- 流:存在于连接中的一个虚拟通道。流可以承载双向消息,每个流都有一个唯一的整数ID。
- HTTP/2 采用二进制格式传输数据,而非 HTTP 1.x 的文本格式,二进制协议解析起来更高效。 HTTP / 1 的请求和响应报文,都是由起始行,首部和实体正文(可选)组成,各部分之间以文本换行符分隔。HTTP/2 将请求和响应数据分割为更小的帧,并且它们采用二进制编码。
- HTTP/2 中,同域名下所有通信都在单个连接上完成,该连接可以承载任意数量的双向数据流。每个数据流都以消息的形式发送,而消息又由一个或多个帧组成。多个帧之间可以乱序发送,根据帧首部的流标识可以重新组装。
多路复用
多路复用,代替原来的序列和阻塞机制。所有就是请求的都是通过一个 TCP连接并发完成。 HTTP 1.x 中,如果想并发多个请求,必须使用多个 TCP 链接,且浏览器为了控制资源,还会对单个域名有 6-8个的TCP链接请求限制,如下图,红色圈出来的请求就因域名链接数已超过限制,而被挂起等待了一段时间:
在 HTTP/2 中,有了二进制分帧之后,HTTP /2 不再依赖 TCP 链接去实现多流并行了,在 HTTP/2中:
- 同域名下所有通信都在单个连接上完成。
- 单个连接可以承载任意数量的双向数据流。
- 数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送,因为根据帧首部的流标识可以重新组装。
这一特性,使性能有了极大提升:
- 同个域名只需要占用一个 TCP 连接,消除了因多个 TCP 连接而带来的延时和内存消耗。
- 单个连接上可以并行交错的请求和响应,之间互不干扰。
- 在HTTP/2中,每个请求都可以带一个31bit的优先值,0表示最高优先级, 数值越大优先级越低。有了这个优先值,客户端和服务器就可以在处理不同的流时采取不同的策略,以最优的方式发送流、消息和帧。
服务器推送
HTTP2还在一定程度上改变了传统的“请求-应答”工作模式,服务器不再是完全被动地响应请求,也可以新建“流”主动向客户端发送消息。比如,在浏览器刚请求HTML的时候就提前把可能会用到的JS、CSS文件发给客户端,减少等待的延迟,这被称为"服务器推送"( Server Push,也叫 Cache push)。
例如下图所示,服务端主动把JS和CSS文件推送给客户端,而不需要客户端解析HTML时再发送这些请求。
另外需要补充的是,服务端可以主动推送,客户端也有权利选择是否接收。如果服务端推送的资源已经被浏览器缓存过,浏览器可以通过发送RST_STREAM帧来拒收。主动推送也遵守同源策略,换句话说,服务器不能随便将第三方资源推送给客户端,而必须是经过双方确认才行。
头部压缩
HTTP 1.1请求的大小变得越来越大,有时甚至会大于TCP窗口的初始大小,因为它们需要等待带着ACK的响应回来以后才能继续被发送。HTTP/2对消息头采用HPACK(专为http/2头部设计的压缩格式)进行压缩传输,能够节省消息头占用的网络的流量。而HTTP/1.x每次请求,都会携带大量冗余头信息,浪费了很多带宽资源。
HTTP每一次通信都会携带一组头部,用于描述这次通信的的资源、浏览器属性、cookie等,例如
为了减少这块的资源消耗并提升性能, HTTP/2对这些首部采取了压缩策略:
HTTP/2在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;
首部表在HTTP/2的连接存续期内始终存在,由客户端和服务器共同渐进地更新;
每个新的首部键-值对要么被追加到当前表的末尾,要么替换表中之前的值。
例如:下图中的两个请求, 请求一发送了所有的头部字段,第二个请求则只需要发送差异数据,这样可以减少冗余数据,降低开销。
我们来看一个实际的例子,下面是用WireShark抓取的访问google首页的包:
上图是是访问https://www.google.com/抓到的第一个请求的头部,可以看到头部的内容,总共占用了437 bytes,我们选中头部的cookie,可以看到cookie总共占用了118 bytes。接下来我们看看第二个请求的头部:
从上图可以看到,得益于头部压缩,第二个请求中cookie只占用了1个字节,我们来看看变化了的Accept字段:
由于Accept字段与请求一中的内容不同,需要发送给服务器,所以占用了29 bytes。