目录
SPDY 协议
SPDY 协议是 Google 提出的基于 TCP 的应用层协议,通过压缩、多路复用和优先级来缩短加载时间。该协议是一种更加快速的内容传输协议,于 2009 年发布。
GoogleChrome、MozillaFirefox 以及 Opera 已默认开启 SPDY。Google 曾经称它的测试显示,页面载入提高了一倍。该协议是一种更加快速的内容传输协议。
SPDY 协议设定的目标:
- 页面加载时间(PLT,Page-Load Time)降低 50%;
- 无需网站作者修改任何内容;
- 最小化配置复杂度,无需变更网络基础设施;
SPDY 其与 HTTP/1.1 相比,主要的改变有
- 实现无需先入先出的多路复用
- 为简化客户端和服务器开发的消息—帧机制
- 强制性压缩(包括 HTTP 头部)
- 优先级排序
- 双向通讯
为了达到降低 50% 页面加载时间的目标,SPDY 引入了一个新的二进制分帧数据层,以实现多向请求和响应、优先次序、最小化及消除不必要的网络延迟,目的是更有效地利用底层 TCP 连接;
2015 年 9 月,Google 宣布了计划,移除对 SPDY 的支持,拥抱 HTTP/2,并将在 Chrome 51 中生效。
HTTP/2
HTTP/2 就是 SPDY 的升级版,HTTP-WG(HTTP Working Group)在 2012 年初把 HTTP 2.0 提到了议事日程,吸取 SPDY 的经验教训,并在此基础上制定官方标准。
HTTP/2 的标准化工作由 Chrome、Opera、Firefox、Internet Explorer 11、Safari、Amazon Silk 及 Edge 等浏览器提供支持。HTTP/2 标准于 2015 年 5 月以 RFC 7540 正式发表。
HTTP/2 的主要目标是改进传输性能,更有效地利用网络资源,实现低延迟和高吞吐量。HTTP/2 致力于突破上一代标准众所周知的性能限制,但它也是对之前 1.x 标准的扩展,而非替代。之所以要递增一个大版本到 2.0,主要是因为它改变了客户端与服务器之间交换数据的方式。
从另一方面看,HTTP 的高层协议语义并不会因为这次版本升级而受影响。所有 HTTP 首部、值,以及它们的使用场景都不会变。HTTP/2 支持 HTTP 1.1 里的大部分 Use Case,例如:桌面浏览器、移动浏览器、Web API、Web Server、代理服务器、反向代理服务器、防火墙和 CDN 等。
现有的任何网站和应用,无需做任何修改都可以在 HTTP/2 上跑起来,不用为了利用 HTTP/2 的好处而修改标记。HTTP 服务器必须运行HTTP/2 协议,但大部分用户都不会因此而受到影响。如果你使用 Nginx,只要在配置文件中启动相应的协议就可以了。HTTP/2 完全兼容HTTP1.x 的语义,对于不支持 HTTP/2 的浏览器,Nginx 会自动向下兼容的。
多数主流浏览器已经在 2015 年底支持了该协议。此外,根据 W3Techs 的数据,截至 2019 年 6 月,全球有 36.5% 的网站支持了 HTTP/2。
HTTP/2 的性能
有人专门做过测试(https://www.smashingmagazine.com/2017/04/guide-http2-server-push/#measuring-server-push-performance):
可以看出,启用 HTTP/2 后性能并未大幅度提升,所以在使用 HTTP/2 还是谨慎一些,如果使用不当,反而会使性能下降。
二进制分帧
有别于 HTTP/1.1 在连接中的明文请求,HTTP/2 与 SPDY 一样,将一个 TCP 连接分为若干个流(Stream),每个流中可以传输若干消息(Message),每个消息由若干最小的二进制帧(Frame)组成。这也是 HTTP/1.1 与 HTTP/2 最大的区别所在。 HTTP/2 中,每个用户的操作行为被分配了一个流编号(Stream ID),这意味着用户与服务端之间建立了一个 TCP 通道。
协议将每个请求分割为二进制的控制帧与数据帧部分,以便解析。这个举措在 SPDY 中的实践表明,相比 HTTP/1.1,新页面加载可以加快 11.81% 到 47.7%。
HTTP 2.0 的所有帧都采用二进制编码:
- 帧:客户端与服务器通过交换帧来通信,帧是基于这个新协议通信的最小单位。
- 消息:是指逻辑上的 HTTP 消息,比如:请求、响应等,由一或多个帧组成。
- 流:流是连接中的一个虚拟信道,可以承载双向的消息;每个流都有一个唯一的整数标识符(1、2…N);
Header 压缩
HTTP1.x 的每次通信都会携带一组 Headers,用于描述这次通信的的资源、浏览器属性、Cookie 等,而且每次都要重复发送。
HTTP/2 使用 Encoder 来减少需要传输的 Header 大小,通讯双方各自 Cache 一份 Header fields 表,既避免了重复 Header 的传输,又减小了需要传输的大小。
HTTP/2 实现了 HPACK 算法用于对 HTTP 头部做压缩。其原理在于:
- C/S 都维护一份共同的静态字典(Static Table),其中包含了常见头部名及常见头部名称与值的组合的代码;
- C/S 根据先入先出的原则,维护一份可动态添加内容的共同动态字典(Dynamic Table);
- C/S 支持基于该静态哈夫曼码表的哈夫曼编码(Huffman Coding)。
在 HTTP 头里,有些 key:value 是固定,例如:
:method: GET
:scheme: http
在编码时,它们直接用一个 index 编号代替,例如 :method:GET 的 index 是 2,这些在一个静态表中定义。静态表总共有 61 个 Header Name:
可以在 https://tools.ietf.org/html/rfc7541#appendix-A 查看所有的静态表定义。
使用静态表、动态表、以及 Huffman 编码可以极大地提升压缩效果。对于静态表里的字段,原来需要 N 个字符表示的,现在只需要一个索引即可,对于静态、动态表中不存在的内容,还可以使用哈夫曼编码来减小体积。HTTP/2 标准里也给出了一份详细的静态哈夫曼码表(https://tools.ietf.org/html/rfc7541#appendix-B),它们需要内置在客户端和服务端之中。
例如:下图中的两个请求, 请求一发送了所有的头部字段,第二个请求则只需要发送差异数据,这样可以减少冗余数据,降低开销。
我们来看一个实际的例子,下面是用 WireShark 抓取的访问 Google 首页的包:
上图是是访问 https://www.google.com/ 抓到的第一个请求的头部,可以看到头部的内容,总共占用了 437bytes,我们选中头部的 Cookie,可以看到 Cookie 总共占用了 118bytes。接下来我们看看第二个请求的头部:
从上图可以看到,得益于头部压缩,第二个请求 Cookie 只占用了 1 个字节,我们来看看变化了的 Accept 字段:
由于 Accept 字段与请求一中的内容不同,需要发送给服务器,所以占用了 29bytes。
服务端推送
网站为了使请求数减少,通常采用对页面上的图片、脚本进行极简化处理。但是,这一举措十分不方便,也不高效,依然需要诸多 HTTP 链接来加载页面和页面资源。
在 HTTP 1.1 里,在同一个 TCP 连接里面,上一个 RESP 发送完了,服务器才能发送下一个,但在 HTTP/2 里,可以将多个回应一起发送。
HTTP/2 引入了服务器推送(Server PUSH),当请求一个 HTML 时,如果 HTML 里有 CSS 文件,Server 会一并推给 Client,而不像在 HTTP 1.1,还需要再发一个 CSS 的请求。即服务端向客户端发送比客户端请求更多的数据。这允许服务器直接提供浏览器渲染页面所需资源,而无须浏览器在收到、解析页面后再提起一轮请求,节约了加载时间。
- 服务器可以对一个客户端请求发送多个响应。服务器向客户端推送资源无需客户端明确地请求。
- HTTP 2.0 连接后,客户端与服务器交换 SETTINGS 帧,借此可以限定双向并发的流的最大数量。
- 所有推送的资源都遵守同源策略。换句话说,服务器不能随便将第三方资源推送给客户端,而必须是经过双方确认才行。
- 服务器必须遵循请求-响应的循环,只能借着对请求的响应推送资源。
服务端推送能把客户端所需要的资源伴随着 index.html 一起发送到客户端,省去了客户端重复请求的步骤。正因为没有发起请求,建立连接等操作,所以静态资源通过服务端推送的方式可以极大地提升速度。
从理论上 PUSH 模式下性能会好很多。
- 普通的客户端请求过程:
- 服务端推送的过程:
如果开启了 Server Push 模式,我们很容易意识到一个问题,那就是缓存问题。Server 见到 HTML 页面就把外部资源 Push 给 Client,如果没有缓存,其实很浪费。为了解决这个问题,可以在第一次请求时 Push,后面的请求都不 Push 了。
服务器推送有一个很麻烦的问题。所要推送的资源文件,如果浏览器已经有缓存,推送就是浪费带宽。即使推送的文件版本更新,浏览器也会优先使用本地缓存。下面是 Nginx 官方给出的示例,根据 Cookie 判断是否为第一次访问(https://www.nginx.com/blog/nginx-1-13-9-http2-server-push/)。
server {
listen 443 ssl http2 default_server;
ssl_certificate ssl/certificate.pem;
ssl_certificate_key ssl/key.pem;
root /var/www/html;
http2_push_preload on;
location = /demo.html {
add_header Set-Cookie "session=1";
add_header Link $resources;
}
}
map $http_cookie $resources {
"~*session=1" "";
default "</style.css>; as=style; rel=preload, </image1.jpg>; as=image; rel=preload, </image2.jpg>; as=image; rel=preload";
多路复用(Multiplexing)
多路复用,代替原来的序列和阻塞机制。所有就是请求的都是通过一个 TCP 连接并发完成。 HTTP 1.x 中,如果想并发多个请求,必须使用多个 TCP 链接,且浏览器为了控制资源,还会对单个域名有 6-8 个的 TCP 链接请求限制,如下图,红色圈出来的请求就因域名链接数已超过限制,而被挂起等待了一段时间:
在 HTTP/2 中,多路复用允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息。有了新的分帧机制后,HTTP/2 不再依赖多个TCP 连接去实现多流并行了。每个数据流都拆分成很多互不依赖的帧,而这些帧可以交错(乱序发送),还可以分优先级。最后再在另一端把它们重新组合起来。HTTP 2.0 连接都是持久化的,而且客户端与服务器之间也只需要一个连接(每个域名一个连接)即可。
- 同域名下所有通信都在单个连接上完成。
- 单个连接可以承载任意数量的双向数据流。
- 数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送,因为根据帧首部的流标识可以重新组装。
这一特性,使性能有了极大提升:
- 同个域名只需要占用一个 TCP 连接,消除了因多个 TCP 连接而带来的延时和内存消耗。
- 单个连接上可以并行交错的请求和响应,之间互不干扰。
- 在 HTTP/2 中,每个请求都可以带一个 31bit 的优先值,0 表示最高优先级, 数值越大优先级越低。有了这个优先值,客户端和服务器就可以在处理不同的流时采取不同的策略,以最优的方式发送流、消息和帧。
请求优先级:
- 把 HTTP/2 消息分解为很多独立的帧之后,就可以通过优化这些帧的交错和传输顺序,每个流都可以带有一个 31 比特的优先值:0 表示最高优先级;2^31 - 1 表示最低优先级。
- 服务器可以根据流的优先级,控制资源分配(CPU、内存、带宽),而在响应数据准备好之后,优先将最高优先级的帧发送给客户端。
HTTP/2 一举解决了所有这些低效的问题:浏览器可以在发现资源时立即分派请求,指定每个流的优先级,让服务器决定最优的响应次序。这样请求就不必排队了,既节省了时间,也最大限度地利用了每个连接。
HTTP/2 的多路复用和 HTTP1.1 中的长连接复用有什么区别?
- HTTP/1.0 一次请求-响应,建立一个连接,用完关闭;每一个请求都要建立一个连接;
- HTTP/1.1 Pipeling 解决方式为:若干个请求排队串行化单线程处理,后面的请求等待前面请求的返回才能获得执行机会,一旦有某请求超时等,后续请求只能被阻塞,毫无办法,也就是人们常说的线头阻塞;
- HTTP/2 多个请求可同时在一个连接上并行执行。某个请求任务耗时严重,不会影响到其它连接的正常执行;
ALPN 应用层协议协商
HTTP/2 协议里有个 negotiation(协商)的机制,让客户端和服务器选择使用 HTTP 1.1 还是 2.0,这个是由 ALPN 来实现。
ALPN(Application-Layer Protocol Negotiation,应用层协议协商)使得客户端能够从 HTTP/1.0、HTTP/1.1、HTTP/2 乃至其他非 HTTP 协议中做出选择。
ALPN 是一个 TLS 的扩展,ALPN 使得应用层可以协商在安全连接层之上使用什么协议,避免了额外的往返通讯,并且独立于应用层协议。 ALPN 用于 HTTP/2 连接,和 HTTP/1.x 相比,ALPN 的使用增强了网页的压缩率减少了网络延时。ALPN 和 HTTP/2 协议是伴随着 Google 开发 SPDY 协议出现的。
下面是抓包截图,在 TLS 里的 Client Hello 的包里,我们可以看到 ALPN 里有 H2 和 HTTP/1.1,这就是说客户端支持 HTTP/2 以及 HTTP 1.1。
当 Server 收到后,会识别 Client 发过来的协议列表,如果不认识就忽略掉。如果认识多个,则选择一个最合适的协议发布给 Client。也是在 Server Hello 里的 ALPN 返回,见下图。
在使用 curl 指令时可以使用 --http2-prior-knowledge
选型来标识不进行协商:
curl --http2-prior-knowledge -v -i “XXX”
相关阅读: