HTTP/2 相比 1.0 有哪些重大改进?
以及它的的大致内容是什么?

HTTP 2.0 的出现,相比于 HTTP 1.x ,大幅度的提升了 web 性能。在与 HTTP/1.1 完全语义兼容的基础上,进一步减少了网络延迟。而对于前端开发人员来说,无疑减少了在前端方面的优化工作。本文将对 HTTP 2.0 协议 个基本技术点进行总结,联系相关知识,探索 HTTP 2.0 是如何提高性能的。


初露锋芒

HTTP/2: the Future of the Internet 这是 Akamai 公司建立的一个官方的演示,用以说明 HTTP/2 相比于之前的 HTTP/1.1 在性能上的大幅度提升。 同时请求 379 张图片,从Load time 的对比可以看出 HTTP/2 在速度上的优势。

此时如果我们打开 Chrome Developer Tools 查看 Network 一栏可以发现,HTTP/2 在网络请求方面与 HTTP /1.1的明显区别。


HTTP/1:


HTTP/2:


多路复用 (Multiplexing)

多路复用允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息。

众所周知 ,在 HTTP/1.1 协议中 「浏览器客户端在同一时间,针对同一域名下的请求有一定数量限制。超过限制数目的请求会被阻塞」。

Clients that use persistent connections SHOULD limit the number of simultaneous connections that they maintain to a given server. A single-user client SHOULD NOT maintain more than 2 connections with any server or proxy. A proxy SHOULD use up to 2*N connections to another server or proxy, where N is the number of simultaneously active users. These guidelines are intended to improve HTTP response times and avoid congestion.

来源:RFC-2616-8.1.4 Practical Considerations

HTTP/1.1: Connections

该图总结了不同浏览器对该限制的数目。

来源:

Roundup on Parallel Connections

这也是为何一些站点会有多个静态资源 CDN 域名的原因之一,拿 Twitter 为例,

http://twimg.com

,目的就是变相的解决浏览器针对同一域名的请求限制阻塞问题。

而 HTTP/2 的多路复用(Multiplexing) 则允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息。

因此 HTTP/2 可以很容易的去实现多流并行而不用依赖建立多个 TCP 连接,HTTP/2 把 HTTP 协议通信的基本单位缩小为一个一个的帧,这些帧对应着逻辑流中的消息。并行地在同一个 TCP 连接上双向交换消息。


二进制分帧

在不改动 HTTP/1.x 的语义、方法、状态码、URI 以及首部字段….. 的情况下, HTTP/2 是如何做到「突破 HTTP1.1 的性能限制,改进传输性能,实现低延迟和高吞吐量」的 ?

关键之一就是在 应用层(HTTP/2)和传输层(TCP or UDP)之间增加一个二进制分帧层。

在二进制分帧层中, HTTP/2 会将所有传输的信息分割为更小的消息和帧(frame),并对它们采用二进制格式的编码 ,其中 HTTP1.x 的首部信息会被封装到 HEADER frame,而相应的 Request Body 则封装到 DATA frame 里面。

HTTP/2 通信都在一个连接上完成,这个连接可以承载任意数量的双向数据流。

在过去, HTTP 性能优化的关键并不在于高带宽,而是低延迟。TCP 连接会随着时间进行自我「调谐」,起初会限制连接的最大速度,如果数据成功传输,会随着时间的推移提高传输的速度。这种调谐则被称为 TCP 慢启动。由于这种原因,让原本就具有突发性和短时性的 HTTP 连接变的十分低效。

HTTP/2 通过让所有数据流共用同一个连接,可以更有效地使用 TCP 连接,让高带宽也能真正的服务于 HTTP 的性能提升。

总结:

  • 单连接多资源的方式,减少服务端的链接压力,内存占用更少,连接吞吐量更大
  • 由于 TCP 连接的减少而使网络拥塞状况得以改善,同时慢启动时间的减少,使拥塞和丢包恢复速度更快

首部压缩(Header Compression)

HTTP/1.1并不支持 HTTP 首部压缩,为此 SPDY 和 HTTP/2 应运而生, SPDY 使用的是通用的

DEFLATE

算法,而 HTTP/2 则使用了专门为首部压缩而设计的

HPACK

算法。

服务端推送(Server Push)

服务端推送是一种在客户端请求之前发送数据的机制。在 HTTP/2 中,服务器可以对客户端的一个请求发送多个响应。Server Push 让 HTTP1.x 时代使用内嵌资源的优化手段变得没有意义;如果一个请求是由你的主页发起的,服务器很可能会响应主页内容、logo 以及样式表,因为它知道客户端会用到这些东西。这相当于在一个 HTML 文档内集合了所有的资源,不过与之相比,服务器推送还有一个很大的优势:可以缓存!也让在遵循同源的情况下,不同页面之间可以共享缓存资源成为可能。



关于 HTTP/2 的 Server Push 以及 HTTP/2 的缓存策略

典型问题:

「如果客户端早已在缓存中有了一份 copy 怎么办?」还要 Push 吗?

详情参考另一个答案:

HTTP/2 对现在的网页访问,有什么大的优化呢?体现在什么地方

PS:

强烈推荐阅读 Mark Nottingham 在 Velocity Beijing 2015 的 speech

HTTP/2 for Front-End Developers ,关于 HTTP/2 下的前端性能优化相关。

Slide 地址:

HTTP/2 for Front-End Developers

参考资料:

这周正在研究http2.0,网络上各种资料也看了不少,对Leozhang的答案稍作补充。

首先推荐下屈光宇博客里的

HTTP/2 资料汇总

,从协议本身,到部署、优化,再到调试工具和参考书籍都包含了。

然后Leozhang的答案里提到的O'Reilly HTTP2-high-perf-browser-networking这本书里专门有一章详细讨论了HTTP/2

HTTP: HTTP/2

,个人认为是目前看到的资料里介绍的最全面和详细的,HTTP/2的二进制帧,多路复用,请求优先级,流量控制,服务器端推送以及首部压缩等新改进都涉及到了。如果英文版看着比较吃力的话,可以看下国内已经出版的翻译版“

《Web性能权威指南》([加]Ilya Grigorik)【摘要 书评 试读】

” 。

Tester

答案里的"

Introduction · http2 explained

"也还不错,就是翻译的偏生硬。

最后推荐下一篇作者未知的“

HTTP 2.0的那些事

”,一篇文章介绍了HTTP2.0的前世今生,具体的技术细节上不如“high-perf-browser-networking”和“http2 explained”,有知道原作者的知友麻烦告知。

================================传说中的分割线==========================

在我们所处的互联网世界中,HTTP协议算得上是使用最广泛的网络协议。最近http2.0的诞生使得它再次互联网技术圈关注的焦点。任何事物的消退和新生都有其背后推动的力量。对于HTTP来说,这力量复杂来说是各种技术细节的演进,简单来说是用户体验和感知的进化。用户总是希望网络上的信息能尽可能快的抵达眼球,越快越好,正是这种对“快”对追逐催生了今天的http2.0。

1. HTTP2.0的前世

http2.0的前世是http1.0和http1.1这两兄弟。虽然之前仅仅只有两个版本,但这两个版本所包含的协议规范之庞大,足以让任何一个有经验的工程师为之头疼。http1.0诞生于1996年,协议文档足足60页。之后第三年,http1.1也随之出生,协议文档膨胀到了176页。不过和我们手机端app升级不同的是,网络协议新版本并不会马上取代旧版本。实际上,1.0和1.1在之后很长的一段时间内一直并存,这是由于网络基础设施更新缓慢所决定的。今天的http2.0也是一样,新版协议再好也需要业界的产品锤炼,需要基础设施逐年累月的升级换代才能普及。

1.1 HTTP站在TCP之上

理解http协议之前一定要对TCP有一定基础的了解。HTTP是建立在TCP协议之上,TCP协议作为传输层协议其实离应用层并不远。HTTP协议的瓶颈及其优化技巧都是基于TCP协议本身的特性。比如TCP建立连接时三次握手有1.5个RTT(round-trip time)的延迟,为了避免每次请求的都经历握手带来的延迟,应用层会选择不同策略的http长链接方案。又比如TCP在建立连接的初期有慢启动(slow start)的特性,所以连接的重用总是比新建连接性能要好。

1.1 HTTP应用场景

http诞生之初主要是应用于web端内容获取,那时候内容还不像现在这样丰富,排版也没那么精美,用户交互的场景几乎没有。对于这种简单的获取网页内容的场景,http表现得还算不错。但随着互联网的发展和web2.0的诞生,更多的内容开始被展示(更多的图片文件),排版变得更精美(更多的css),更复杂的交互也被引入(更多的js)。用户打开一个网站首页所加载的数据总量和请求的个数也在不断增加。今天绝大部分的门户网站首页大小都会超过2M,请求数量可以多达100个。另一个广泛的应用是在移动互联网的客户端app,不同性质的app对http的使用差异很大。对于电商类app,加载首页的请求也可能多达10多个。对于微信这类IM,http请求可能仅限于语音和图片文件的下载,请求出现的频率并不算高。

1.2 因为延迟,所以慢

影响一个网络请求的因素主要有两个,带宽和延迟。今天的网络基础建设已经使得带宽得到极大的提升,大部分时候都是延迟在影响响应速度。http1.0被抱怨最多的就是连接无法复用,和head of line blocking这两个问题。理解这两个问题有一个十分重要的前提:客户端是依据域名来向服务器建立连接,一般PC端浏览器会针对单个域名的server同时建立6~8个连接,手机端的连接数则一般控制在4~6个。显然连接数并不是越多越好,资源开销和整体延迟都会随之增大。

连接无法复用会导致每次请求都经历三次握手和慢启动。三次握手在高延迟的场景下影响较明显,慢启动则对文件类大请求影响较大。

head of line blocking会导致带宽无法被充分利用,以及后续健康请求被阻塞。假设有5个请求同时发出,如下图:

[图1]

对于http1.0的实现,在第一个请求没有收到回复之前,后续从应用层发出的请求只能排队,请求2,3,4,5只能等请求1的response回来之后才能逐个发出。网络通畅的时候性能影响不大,一旦请求1的request因为什么原因没有抵达服务器,或者response因为网络阻塞没有及时返回,影响的就是所有后续请求,问题就变得比较严重了。

1.3 解决连接无法复用

http1.0协议头里可以设置Connection:Keep-Alive。在header里设置Keep-Alive可以在一定时间内复用连接,具体复用时间的长短可以由服务器控制,一般在15s左右。到http1.1之后Connection的默认值就是Keep-Alive,如果要关闭连接复用需要显式的设置Connection:Close。一段时间内的连接复用对PC端浏览器的体验帮助很大,因为大部分的请求在集中在一小段时间以内。但对移动app来说,成效不大,app端的请求比较分散且时间跨度相对较大。所以移动端app一般会从应用层寻求其它解决方案,长连接方案或者伪长连接方案:

方案一:基于tcp的长链接

现在越来越多的移动端app都会建立一条自己的长链接通道,通道的实现是基于tcp协议。基于tcp的socket编程技术难度相对复杂很多,而且需要自己制定协议,但带来的回报也很大。信息的上报和推送变得更及时,在请求量爆发的时间点还能减轻服务器压力(http短连接模式会频繁的创建和销毁连接)。不止是IM app有这样的通道,像淘宝这类电商类app都有自己的专属长连接通道了。现在业界也有不少成熟的方案可供选择了,google的protobuf就是其中之一。

方案二:http long-polling

long-polling可以用下图表示:


[图2]

客户端在初始状态就会发送一个polling请求到服务器,服务器并不会马上返回业务数据,而是等待有新的业务数据产生的时候再返回。所以连接会一直被保持,一旦结束马上又会发起一个新的polling请求,如此反复,所以一直会有一个连接被保持。服务器有新的内容产生的时候,并不需要等待客户端建立一个新的连接。做法虽然简单,但有些难题需要攻克才能实现稳定可靠的业务框架:

  • 和传统的http短链接相比,长连接会在用户增长的时候极大的增加服务器压力
  • 移动端网络环境复杂,像wifi和4g的网络切换,进电梯导致网络临时断掉等,这些场景都需要考虑怎么重建健康的连接通道。
  • 这种polling的方式稳定性并不好,需要做好数据可靠性的保证,比如重发和ack机制。
  • polling的response有可能会被中间代理cache住,要处理好业务数据的过期机制。

long-polling方式还有一些缺点是无法克服的,比如每次新的请求都会带上重复的header信息,还有数据通道是单向的,主动权掌握在server这边,客户端有新的业务请求的时候无法及时传送。

方案三:http streaming

http streaming流程大致如下:


[图3]

同long-polling不同的是,server并不会结束初始的streaming请求,而是持续的通过这个通道返回最新的业务数据。显然这个数据通道也是单向的。streaming是通过在server response的头部里增加”Transfer Encoding: chunked”来告诉客户端后续还会有新的数据到来。除了和long-polling相同的难点之外,streaming还有几个缺陷:

  • 有些代理服务器会等待服务器的response结束之后才会将结果推送到请求客户端。对于streaming这种永远不会结束的方式来说,客户端就会一直处于等待response的过程中。
  • 业务数据无法按照请求来做分割,所以客户端没收到一块数据都需要自己做协议解析,也就是说要做自己的协议定制。

streaming不会产生重复的header数据。

方案四:web socket

WebSocket和传统的tcp socket连接相似,也是基于tcp协议,提供双向的数据通道。WebSocket优势在于提供了message的概念,比基于字节流的tcp socket使用更简单,同时又提供了传统的http所缺少的长连接功能。不过WebSocket相对较新,2010年才起草,并不是所有的浏览器都提供了支持。各大浏览器厂商最新的版本都提供了支持。

1.4 解决head of line blocking

Head of line blocking(以下简称为holb)是http2.0之前网络体验的最大祸源。正如图1中所示,健康的请求会被不健康的请求影响,而且这种体验的损耗受网络环境影响,出现随机且难以监控。为了解决holb带来的延迟,协议设计者设计了一种新的pipelining机制。

http pipelining

pipelining的流程图可以用下图表示:


[图4]

和图一相比最大的差别是,请求2,3,4,5不用等请求1的response返回之后才发出,而是几乎在同一时间把request发向了服务器。2,3,4,5及所有后续共用该连接的请求节约了等待的时间,极大的降低了整体延迟。下图可以清晰的看出这种新机制对延迟的改变:


[图5]

不过pipelining并不是救世主,它也存在不少缺陷:

  • pipelining只能适用于http1.1,一般来说,支持http1.1的server都要求支持pipelining。
  • 只有幂等的请求(GET,HEAD)能使用pipelining,非幂等请求比如POST不能使用,因为请求之间可能会存在先后依赖关系。
  • head of line blocking并没有完全得到解决,server的response还是要求依次返回,遵循FIFO(first in first out)原则。也就是说如果请求1的response没有回来,2,3,4,5的response也不会被送回来。
  • 绝大部分的http代理服务器不支持pipelining。
  • 和不支持pipelining的老服务器协商有问题。
  • 可能会导致新的Front of queue blocking问题。

正是因为有这么多的问题,各大浏览器厂商要么是根本就不支持pipelining,要么就是默认关掉了pipelining机制,而且启用的条件十分苛刻。可以参考chrome对于pipeling的问题描述

1.5 其它奇技淫巧

为了解决延迟带来的苦恼,永远都会有聪明的探索者找出新的捷径来。互联网的蓬勃兴盛催生出了各种新奇技巧,我们来依次看下这些“捷径”及各自的优缺点。

Spriting(图片合并)

Spriting指的是将多个小图片合并到一张大的图片里,这样多个小的请求就被合并成了一个大的图片请求,然后再利用js或者css文件来取出其中的小张图片使用。好处显而易见,请求数减少,延迟自然低。坏处是文件的粒度变大了,有时候我们可能只需要其中一张小图,却不得不下载整张大图,cache处理也变得麻烦,在只有一张小图过期的情况下,为了获得最新的版本,不得不从服务器下载完整的大图,即使其它的小图都没有过期,显然浪费了流量。

Inlining(内容内嵌)

Inlining的思考角度和spriting类似,是将额外的数据请求通过base64编码之后内嵌到一个总的文件当中。比如一个网页有一张背景图,我们可以通过如下代码嵌入:

background: url(data:image/png;base64,)

data部分是base64编码之后的字节码,这样也避免了一次多余的http请求。但这种做法也有着和spriting相同的问题,资源文件被绑定到了其它文件,粒度变得难以控制。

Concatenation(文件合并)

Concatenation主要是针对js这类文件,现在前端开发交互越来越多,零散的js文件也在变多。将多个js文件合并到一个大的文件里在做一些压缩处理也可以减小延迟和传输的数据量。但同样也面临着粒度变大的问题,一个小的js代码改动会导致整个js文件被下载。

Domain Sharding(域名分片)

前面我提到过很重要的一点,浏览器或者客户端是根据domain(域名)来建立连接的。比如针对Example Domain只允许同时建立2个连接,但mobile.example.com被认为是另一个域名,可以再建立两个新的连接。依次类推,如果我再多建立几个sub domain(子域名),那么同时可以建立的http请求就会更多,这就是Domain Sharding了。连接数变多之后,受限制的请求就不需要等待前面的请求完成才能发出了。这个技巧被大量的使用,一个颇具规模的网页请求数可以超过100,使用domain sharding之后同时建立的连接数可以多到50个甚至更多。

这么做当然增加了系统资源的消耗,但现在硬件资源升级非常之快,和用户宝贵的等待时机相比起来实在微不足道。

domain sharding还有一大好处,对于资源文件来说一般是不需要cookie的,将这些不同的静态资源文件分散在不同的域名服务器上,可以减小请求的size。

不过domain sharding只有在请求数非常之多的场景下才有明显的效果。而且请求数也不是越多越好,资源消耗是一方面,另一点是由于tcp的slow start会导致每个请求在初期都会经历slow start,还有tcp 三次握手,DNS查询的延迟。这一部分带来的时间损耗和请求排队同样重要,到底怎么去平衡这二者就需要取一个可靠的连接数中间值,这个值的最终确定要通过反复的测试。移动端浏览器场景建议不要使用domain sharding,具体细节参考这篇文章

2. 开拓者SPDY

http1.0和1.1虽然存在这么多问题,业界也想出了各种优化的手段,但这些方法手段都是在尝试绕开协议本身的缺陷,都有种隔靴搔痒,治标不治本的感觉。直到2012年google如一声惊雷提出了SPDY的方案,大家才开始从正面看待和解决老版本http协议本身的问题,这也直接加速了http2.0的诞生。实际上,http2.0是以SPDY为原型进行讨论和标准化的。为了给http2.0让路,google已决定在2016年不再继续支持SPDY开发,但在http2.0出生之前,SPDY已经有了相当规模的应用,作为一个过渡方案恐怕在还将一段时间内继续存在。现在不少app客户端和server都已经使用了SPDY来提升体验,http2.0在老的设备和系统上还无法使用(iOS系统只有在iOS9+上才支持),所以可以预见未来几年spdy将和http2.0共同服务的情况。

2.1 SPDY的目标

SPDY的目标在一开始就是瞄准http1.x的痛点,即延迟和安全性。我们上面通篇都在讨论延迟,至于安全性,由于http是明文协议,其安全性也一直被业界诟病,不过这是另一个大的话题。如果以降低延迟为目标,应用层的http和传输层的tcp都是都有调整的空间,不过tcp作为更底层协议存在已达数十年之久,其实现已深植全球的网络基础设施当中,如果要动必然伤经动骨,业界响应度必然不高,所以SPDY的手术刀对准的是http。

  • 降低延迟,客户端的单连接单请求,server的FIFO响应队列都是延迟的大头。
  • http最初设计都是客户端发起请求,然后server响应,server无法主动push内容到客户端。
  • 压缩http header,http1.x的header越来越膨胀,cookie和user agent很容易让header的size增至1kb大小,甚至更多。而且由于http的无状态特性,header必须每次request都重复携带,很浪费流量。

为了增加业界响应的可能性,聪明的google一开始就避开了从传输层动手,而且打算利用开源社区的力量以提高扩散的力度,对于协议使用者来说,也只需要在请求的header里设置user agent,然后在server端做好支持即可,极大的降低了部署的难度。SPDY的设计如下:


[图6]

SPDY位于HTTP之下,TCP和SSL之上,这样可以轻松兼容老版本的HTTP协议(将http1.x的内容封装成一种新的frame格式),同时可以使用已有的SSL功能。SPDY的功能可以分为基础功能和高级功能两部分,基础功能默认启用,高级功能需要手动启用。

SPDY基础功能

  • 多路复用(multiplexing)。多路复用通过多个请求stream共享一个tcp连接的方式,解决了http1.x holb(head of line blocking)的问题,降低了延迟同时提高了带宽的利用率。
  • 请求优先级(request prioritization)。多路复用带来一个新的问题是,在连接共享的基础之上有可能会导致关键请求被阻塞。SPDY允许给每个request设置优先级,这样重要的请求就会优先得到响应。比如浏览器加载首页,首页的html内容应该优先展示,之后才是各种静态资源文件,脚本文件等加载,这样可以保证用户能第一时间看到网页内容。
  • header压缩。前面提到过几次http1.x的header很多时候都是重复多余的。选择合适的压缩算法可以减小包的大小和数量。SPDY对header的压缩率可以达到80%以上,低带宽环境下效果很大。

SPDY高级功能

  • server推送(server push)。http1.x只能由客户端发起请求,然后服务器被动的发送response。开启server push之后,server通过X-Associated-Content header(X-开头的header都属于非标准的,自定义header)告知客户端会有新的内容推送过来。在用户第一次打开网站首页的时候,server将资源主动推送过来可以极大的提升用户体验。
  • server暗示(server hint)。和server push不同的是,server hint并不会主动推送内容,只是告诉有新的内容产生,内容的下载还是需要客户端主动发起请求。server hint通过X-Subresources header来通知,一般应用场景是客户端需要先查询server状态,然后再下载资源,可以节约一次查询请求。

2.2 SPDY的成绩

SPDY的成绩可以用google官方的一个数字来说明:页面加载时间相比于http1.x减少了64%。而且各大浏览器厂商在SPDY诞生之后的1年多里都陆续支持了SPDY,不少大厂app和server端框架也都将SPDY应用到了线上的产品当中。

google的官网也给出了他们自己做的一份测试数据。测试对象是25个访问量排名靠前的网站首页,家用网络%1的丢包率,每个网站测试10次取平均值。结果如下:


[图7]

不开启ssl的时候提升在 27% - 60%,开启之后为39% - 55%。 这份测试结果有两点值得特别注意:

连接数的选择

连接到底是基于域名来建立,还是不做区分所有子域名都共享一个连接,这个策略选择上值得商榷。google的测试结果测试了两种方案,看结果似乎是单一连接性能高于多域名连接方式。之所以出现这种情况是由于网页所有的资源请求并不是同一时间发出,后续发出的子域名请求如果能复用之前的tcp连接当然性能更好。实际应用场景下应该也是单连接共享模式表现好。

带宽的影响

测试基于两种带宽环境,一慢一快。网速快的环境下对减小延迟的提升更大,单连接模式下可以提升至60%。原因也比较简单,带宽越大,复用连接的请求完成越快,由于三次握手和慢启动导致的延迟损耗就变得更明显。

出了连接模式和带宽之外,丢包率和RTT也是需要测试的参数。SPDY对header的压缩有80%以上,整体包大小能减少大概40%,发送的包越少,自然受丢包率影响也就越小,所以丢包率大的恶劣环境下SPDY反而更能提升体验。下图是受丢包率影响的测试结果,丢包率超过2.5%之后就没有提升了:


[图8]

RTT越大,延迟会越大,在高RTT的场景下,由于SPDY的request是并发进行的,所有对包的利用率更高,反而能更明显的减小总体延迟。测试结果如下:


[图9]

SPDY从2012年诞生到2016停止维护,时间跨度对于网络协议来说其实非常之短。如果HTTP2.0没有出来,google或许能收集到更多业界产品的真实反馈和数据,毕竟google自己的测试环境相对简单。但SPDY也完成了自己的使命,作为一贯扮演拓荒者角色的google应该也早就预见了这样的结局。SPDY对产品网络体验的提升到底如何,恐怕只有各大厂产品经理才清楚了。

3. 救世主HTTP2.0

SPDY的诞生和表现说明了两件事情:一是在现有互联网设施基础和http协议广泛使用的前提下,是可以通过修改协议层来优化http1.x的。二是针对http1.x的修改确实效果明显而且业界反馈很好。正是这两点让IETF(Internet Enginerring Task Force)开始正式考虑制定HTTP2.0的计划,最后决定以SPDY/3为蓝图起草HTTP2.0,SPDY的部分设计人员也被邀请参与了HTTP2.0的设计。

3.1 HTTP2.0需要考虑的问题

HTTP2.0与SPDY的起点不同,SPDY可以说是google的“玩具”,最早出现在自家的chrome浏览器和server上,好不好玩以及别人会不会跟着一起玩对google来说无关痛痒。但HTTP2.0作为业界标准还没出生就是众人瞩目的焦点,一开始如果有什么瑕疵或者不兼容的问题影响可能又是数十年之久,所以考虑的问题和角度要非常之广。我们来看下HTTP2.0一些重要的设计前提:

  • 客户端向server发送request这种基本模型不会变。
  • 老的scheme不会变,使用http://和https://的服务和应用不会要做任何更改,不会有http2://。
  • 使用http1.x的客户端和服务器可以无缝的通过代理方式转接到http2.0上。
  • 不识别http2.0的代理服务器可以将请求降级到http1.x。

因为客户端和server之间在确立使用http1.x还是http2.0之前,必须要要确认对方是否支持http2.0,所以这里必须要有个协商的过程。最简单的协商也要有一问一答,客户端问server答,即使这种最简单的方式也多了一个RTT的延迟,我们之所以要修改http1.x就是为了降低延迟,显然这个RTT我们是无法接受的。google制定SPDY的时候也遇到了这个问题,他们的办法是强制SPDY走https,在SSL层完成这个协商过程。ssl层的协商在http协议通信之前,所以是最适合的载体。google为此做了一个tls的拓展,叫NPN(Next Protocol Negotiation),从名字上也可以看出,这个拓展主要目的就是为了协商下一个要使用的协议。HTTP2.0虽然也采用了相同的方式,不过HTTP2.0经过激烈的讨论,最终还是没有强制HTTP2.0要走ssl层,大部分浏览器厂商(除了IE)却只实现了基于https的2.0协议。HTTP2.0没有使用NPN,而是另一个tls的拓展叫ALPN(Application Layer Protocol Negotiation)。SPDY也打算从NPN迁移到ALPN了。

各浏览器(除了IE)之所以只实现了基于SSL的HTTP2.0,另一个原因是走SSL请求的成功率会更高,被SSL封装的request不会被监听和修改,这样网络中间的网络设备就无法基于http1.x的认知去干涉修改request,http2.0的request如果被意外的修改,请求的成功率自然会下降。

HTTP2.0协议没有强制使用SSL是因为听到了很多的反对声音,毕竟https和http相比,在不优化的前提下性能差了不少,要把https优化到几乎不增加延迟的程度又需要花费不少力气。IETF面对这种两难的处境做了妥协,但大部分浏览器厂商(除了IE)并不买帐,他们只认https2.0。对于app开发者来说,他们可以坚持使用没有ssl的http2.0,不过要承担一个多余的RTT延迟和请求可能被破坏的代价。

3.1 HTTP2.0主要改动

HTTP2.0作为新版协议,改动细节必然很多,不过对应用开发者和服务提供商来说,影响较大的就几点。

新的二进制格式(Binary Format)

http1.x诞生的时候是明文协议,其格式由三部分组成:start line(request line或者status line),header,body。要识别这3部分就要做协议解析,http1.x的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合。基于这种考虑http2.0的协议解析决定采用二进制格式,实现方便且健壮。

有人可能会觉得基于文本的http调试方便很多,像firebug,chrome,charles等不少工具都可以即时调试修改请求。实际上现在很多请求都是走https了,要调试https请求必须有私钥才行。http2.0的绝大部分request应该都是走https,所以调试方便无法作为一个有力的考虑因素了。curl,tcpdump,wireshark这些工具会更适合http2.0的调试。

http2.0用binary格式定义了一个一个的frame,和http1.x的格式对比如下图:


[图10]

http2.0的格式定义更接近tcp层的方式,这张二机制的方式十分高效且精简。length定义了整个frame的开始到结束,type定义frame的类型(一共10种),flags用bit位定义一些重要的参数,stream id用作流控制,剩下的payload就是request的正文了。

虽然看上去协议的格式和http1.x完全不同了,实际上http2.0并没有改变http1.x的语义,只是把原来http1.x的header和body部分用frame重新封装了一层而已。调试的时候浏览器甚至会把http2.0的frame自动还原成http1.x的格式。具体的协议关系可以用下图表示:


[图11]

连接共享

http2.0要解决的一大难题就是多路复用(MultiPlexing),即连接共享。上面协议解析中提到的stream id就是用作连接共享机制的。一个request对应一个stream并分配一个id,这样一个连接上可以有多个stream,每个stream的frame可以随机的混杂在一起,接收方可以根据stream id将frame再归属到各自不同的request里面。

前面还提到过连接共享之后,需要优先级和请求依赖的机制配合才能解决关键请求被阻塞的问题。http2.0里的每个stream都可以设置又优先级(Priority)和依赖(Dependency)。优先级高的stream会被server优先处理和返回给客户端,stream还可以依赖其它的sub streams。优先级和依赖都是可以动态调整的。动态调整在有些场景下很有用,假想用户在用你的app浏览商品的时候,快速的滑动到了商品列表的底部,但前面的请求先发出,如果不把后面的请求优先级设高,用户当前浏览的图片要到最后才能下载完成,显然体验没有设置优先级好。同理依赖在有些场景下也有妙用。

header压缩

前面提到过http1.x的header由于cookie和user agent很容易膨胀,而且每次都要重复发送。http2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。高效的压缩算法可以很大的压缩header,减少发送包的数量从而降低延迟。

这里普及一个小知识点。现在大家都知道tcp有slow start的特性,三次握手之后开始发送tcp segment,第一次能发送的没有被ack的segment数量是由initial tcp window大小决定的。这个initial tcp window根据平台的实现会有差异,但一般是2个segment或者是4k的大小(一个segment大概是1500个字节),也就是说当你发送的包大小超过这个值的时候,要等前面的包被ack之后才能发送后续的包,显然这种情况下延迟更高。intial window也并不是越大越好,太大会导致网络节点的阻塞,丢包率就会增加,具体细节可以参考IETF这篇文章。http的header现在膨胀到有可能会超过这个intial window的值了,所以更显得压缩header的重要性。

压缩算法的选择

SPDY/2使用的是gzip压缩算法,但后来出现的两种攻击方式BREACHCRIME使得即使走ssl的SPDY也可以被破解内容,最后综合考虑采用的是一种叫HPACK的压缩算法。这两个漏洞和相关算法可以点击链接查看更多的细节,不过这种漏洞主要存在于浏览器端,因为需要通过javascript来注入内容并观察payload的变化。

重置连接表现更好

很多app客户端都有取消图片下载的功能场景,对于http1.x来说,是通过设置tcp segment里的reset flag来通知对端关闭连接的。这种方式会直接断开连接,下次再发请求就必须重新建立连接。http2.0引入RST_STREAM类型的frame,可以在不断开连接的前提下取消某个request的stream,表现更好。

Server Push

Server Push的功能前面已经提到过,http2.0能通过push的方式将客户端需要的内容预先推送过去,所以也叫“cache push”。另外有一点值得注意的是,客户端如果退出某个业务场景,出于流量或者其它因素需要取消server push,也可以通过发送RST_STREAM类型的frame来做到。

流量控制(Flow Control)

TCP协议通过sliding window的算法来做流量控制。发送方有个sending window,接收方有receive window。http2.0的flow control是类似receive window的做法,数据的接收方通过告知对方自己的flow window大小表明自己还能接收多少数据。只有Data类型的frame才有flow control的功能。对于flow control,如果接收方在flow window为零的情况下依然更多的frame,则会返回block类型的frame,这张场景一般表明http2.0的部署出了问题。

Nagle Algorithm vs TCP Delayed Ack

tcp协议优化的一个经典场景是:Nagle算法Berkeley的delayed ack算法的对立。http2.0并没有对tcp层做任何修改,所以这种对立导致的高延迟问题依然存在。要么通过TCP_NODELAY禁用Nagle算法,要么通过TCP_QUICKACK禁用delayed ack算法。貌似http2.0官方建议是设置TCP_NODELAY。

更安全的SSL

HTTP2.0使用了tls的拓展ALPN来做协议升级,除此之外加密这块还有一个改动,HTTP2.0对tls的安全性做了近一步加强,通过黑名单机制禁用了几百种不再安全的加密算法,一些加密算法可能还在被继续使用。如果在ssl协商过程当中,客户端和server的cipher suite没有交集,直接就会导致协商失败,从而请求失败。在server端部署http2.0的时候要特别注意这一点。

3.2 HTTP2.0里的负能量

SPDY和HTTP2.0之间的暧昧关系,以及google作为SPDY的创造者,这两点很容易让阴谋论者怀疑google是否会成为协议的最终收益方。这其实是废话,google当然会受益,任何新协议使用者都会从中受益,至于谁吃肉,谁喝汤看的是自己的本事。从整个协议的变迁史也可以粗略看出,新协议的诞生完全是针对业界现存问题对症下药,并没有google业务相关的痕迹存在,google至始至终只扮演了一个角色:you can you up。

HTTP2.0不会是万金油,但抹了也不会有副作用。HTTP2.0最大的亮点在于多路复用,而多路复用的好处只有在http请求量大的场景下才明显,所以有人会觉得只适用于浏览器浏览大型站点的时候。这么说其实没错,但http2.0的好处不仅仅是multiplexing,请求压缩,优先级控制,server push等等都是亮点。对于内容型移动端app来说,比如淘宝app,http请求量大,多路复用还是能产生明显的体验提升。多路复用对延迟的改变可以参考下这个测试网址

HTTP2.0对于ssl的依赖使得有些开发者望而生畏。不少开发者对ssl还停留在高延迟,CPU性能损耗,配置麻烦的印象中。其实ssl于http结合对性能的影响已经可以优化到忽略的程度了,网上也有不少文章可以参考。HTTP2.0也可以不走ssl,有些场景确实可能不适合https,比如对代理服务器的cache依赖,对于内容安全性不敏感的get请求可以通过代理服务器缓存来优化体验。

3.3 HTTP2.0的现状

HTTP2.0作为新版本的网络协议肯定需要一段时间去普及,但HTTP本身属于应用层协议,和当年的网络层协议IPV6不同,离底层协议越远,对网络基础硬件设施的影响就越小。HTTP2.0甚至还特意的考虑了与HTTP1.x的兼容问题,只是在HTTP1.x的下面做了一层framing layer,更使得其普及的阻力变小。所以不出意外,HTTP2.0的普及速度可能会远超大部分人的预期。

Firefox 2015年在其浏览器流量中检测到,有13%的http流量已经使用了http2.0,27%的https也是http2.0,而且还处于持续的增长当中。一般用户察觉不到是否使用了http2.0,不过可以装这样一个插件,安装之后如果网站是http2.0的,在地址栏的最右边会有个闪电图标。还可以使用这个网站来测试。对于开发者来说,可以通过Web Developer的Network来查看协议细节,如下图:


[图12]

其中Version:HTTP/2.0已经很明确表明协议类型,Firefox还在header里面插入了X-Firefox-Spdy:“h2”,也可以看出是否使用http2.0。

Chrome在2015年检测到的http2.0流量大概有18%。不过这个数字本来会更高,因为Chrome现在很大一部分流量都在试验QUIC(google正在开辟的另一块疆土)。Chrome上也可以使用类似的插件来判断网站是否是使用http2.0。

4. 移动端HTTP现状4.1 iOS下http现状

iOS系统是从iOS8开始才通过NSURLSession来支持SPDY的,iOS9+开始自动支持http2.0。实际上apple对http2.0非常有信心,推广力度也很大。新版本ATS机制默认使用https来进行网络传输。APN(Apple Push Notifiction)在iOS9上也已经是通过http2.0来实现的了。iOS9 sdk里的NSURLSession默认使用http2.0,而且对开发者来说是完全透明的,甚至没有api来知道到底是用的哪个版本的http协议。

对于开发者来说到底怎么去配置最佳的http使用方案呢?在我看来,因app而异,主要从两方面来考虑:一是app本身http流量是否大而且密集,二是开发团队本身的技术条件。http2.0的部署相对容易很多,客户端开发者甚至不用做什么改动,只需要使用iOS9的SDK编译即可,但缺点是http2.0只能适用于iOS9的设备。SPDY的部署相对麻烦一些,但优点是可以兼顾iOS6+的设备。iOS端的SPDY可以使用twitter开发的CocoaSPDY方案,但有一点需要特别处理:

由于苹果的TLS实现不支持NPN,所以通过NPN协商使用SPDY就无法通过默认443端口来实现。有两种做法,一是客户端和server同时约定好使用另一个端口号来做NPN协商,二是server这边通过request header智能判断客户端是否支持SPDY而越过NPN协商过程。第一种方法会简单一点,不过需要从框架层将所有的http请求都map到另一个port,url mapping可以参考我之前的一篇文章。twitter自己的网站twitter.com使用的是第二种方法。

浏览器端(比如Chrome),server端(比如nginx)都陆续打算放弃支持spdy了,毕竟google官方都宣布要停止维护了。spdy会是一个过渡方案,会随着iOS9的普及会逐步消失,所以这部分的技术投入需要开发团队自己去衡量。

4.2 Android下http现状

android和iOS情况类似,http2.0只能在新系统下支持,spdy作为过渡方案仍然有存在的必要。

对于使用webview的app来说,需要基于chrome内核的webview才能支持spdy和http2.0,而android系统的webview是从android4.4(KitKat)才改成基于chrome内核的。

对于使用native api调用的http请求来说,okhttp是同时支持spdy和http2.0的可行方案。如果使用ALPN,okhttp要求android系统5.0+(实际上,android4.4上就有了ALPN的实现,不过有bug,知道5.0才正式修复),如果使用NPN,可以从android4.0+开始支持,不过NPN也是属于将要被淘汰的协议。

结束语

以上是HTTP从1.x到SPDY,再到HTTP2.0的一些主要变迁技术点。HTTP2.0正处于逐步应用到线上产品和服务的阶段,可以预见未来会有不少新的坑产生和与之对应的优化技巧,HTTP1.x和SPDY也将在一段时间内继续发挥余热。作为工程师,需要了解这些协议背后的技术细节,才能打造高性能的网络框架,从而提升我们的产品体验。

参考链接:

http2 concepts ?· http2 explained

chromium.org/spdy/spdy-

1.单一长连接

在HTTP/2中,客户端向某个域名的服务器请求页面的过程中,只会创建一条TCP连接,即使这页面可能包含上百个资源。 单一的连接应该是HTTP2的主要优势,单一的连接能减少TCP握手带来的时延 。HTTP2中用一条单一的长连接,避免了创建多个TCP连接带来的网络开销,提高了吞吐量。

2.多路复用

HTTP2虽然只有一条TCP连接,但是在逻辑上分成了很多stream。

HTTP2把要传输的信息分割成一个个二进制帧,首部信息会被封装到HEADER Frame,相应的request body就放到DATA Frame,一个帧你可以看成路上的一辆车,只要给这些车编号,让1号车都走1号门出,2号车都走2号门出,就把不同的http请求或者响应区分开来了。但是,这里要求同一个请求或者响应的帧必须是有有序的,要保证FIFO的,但是不同的请求或者响应帧可以互相穿插。这就是HTTP2的多路复用,是不是充分利用了网络带宽,是不是提高了并发度?

http1.x一直都是plain text,对此我只能想到一个优点,便于阅读和debug。但是,现在很多都走https,SSL也把plain text变成了二进制,那这个优点也没了。

于是HTTP2搞了个HPACK压缩来压缩头部,减少报文大小(调试这样的协议将需要curl这样的工具,要进一步地分析网络数据流需要类似Wireshark的http2解析器)。

这个功能通常被称作“缓存推送”。主要的思想是:当一个客户端请求资源X,而服务器知道它很可能也需要资源Z的情况下,服务器可以在客户端发送请求前,主动将资源Z推送给客户端。

这个功能帮助客户端将Z放进缓存以备将来之需。

阅读全文,请点击 HTTP 2 的新特性你 get 了吗?

相关推荐:HTTP/2探索第一篇--概念

HTTP/2 相比于 HTTP 1,大幅提升了 web 性能,减少网络延迟。

HTTP/2: the Future of the Internet 这个链接是一个演示Demo,用来比较 HTTP/2 与 HTTP/1.1 在性能上的差异。

相比 HTTP/1.x,HTTP/2 在底层传输做了很大的改动和优化:

  • 二进制分帧:HTTP/2 采用二进制格式传输数据,而非 HTTP/1.x 的文本格式。 HTTP/2 中,同域名下所有通信都在单个连接上完成,该连接可以承载任意数量的双向数据流。每个数据流都以消息的形式发送,而消息又由一个或多个帧组成。多个帧之间可以乱序发送,根据帧首部的流标识可以重新组装。


  • 头部压缩:HTTP/2 对消息头采用 HPACK 进行压缩传输,能够节省消息头占用的网络的流量。而 HTTP/1.x 每次请求,都会携带大量冗余头信息,浪费了很多带宽资源。头压缩能够很好的解决该问题。
1. HTTP/2在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;

2. 首部表在HTTP/2的连接存续期内始终存在,由客户端和服务器共同渐进地更新;

3. 每个新的首部键-值对要么被追加到当前表的末尾,要么替换表中之前的值。


  • 多路复用:直白的说就是所有的请求都是通过一个 TCP 连接并发完成。HTTP/1.x 虽然通过 pipeline 也能并发请求,但是多个请求之间的响应会被阻塞的,所以 pipeline 至今也没有被普及应用,而 HTTP/2 做到了真正的并发请求。同时,流还支持优先级和流量控制。当流并发时,就会涉及到流的优先级和依赖。优先级高的流会被优先发送。图片请求的优先级要低于 CSS 和 SCRIPT,这个设计可以确保重要的东西可以被优先加载完。

△HTTP1中,请求就因域名链接数已超过限制,而被挂起等待了一段时间

  • Server Push:服务端能够更快的把资源推送给客户端。例如服务端可以主动把 JS 和 CSS 文件推送给客户端,而不需要客户端解析 HTML 再发送这些请求。当客户端需要的时候,它已经在客户端了。

又拍云CDN 在 HTTPS 协议的基础上已实现全平台支持 HTTP/2


推荐阅读:一文读懂 HTTP/2 特性

变化可大了,基本换了个身子!国内已经有很多网站开始使用 HTTP/2 协议了,比如哔哩哔哩、腾讯、淘宝等等,足够证明他的重要性了!

接下来,跟随小林的步伐,小林来跟大家图解 HTTP/2!一起来看看 HTTP/2 牛逼在哪?

我们得先要了解下 HTTP/1.1 协议存在的性能问题,因为 HTTP/2 协议就是把这些性能问题逐个攻破了。

现在的站点相比以前变化太多了,比如:

  • 消息的大小变大了,从几 KB 大小的消息,到几 MB 大小的消息;
  • 页面资源变多了,从每个页面不到 10 个的资源,到每页超 100 多个资源;
  • 内容形式变多样了,从单纯到文本内容,到图片、视频、音频等内容;
  • 实时性要求变高了,对页面的实时性要求的应用越来越多;

这些变化带来的最大性能问题就是 HTTP/1.1 的高延迟,延迟高必然影响的就是用户体验。主要原因如下几个:

  • 延迟难以下降,虽然现在网络的「带宽」相比以前变多了,但是延迟降到一定幅度后,就很难再下降了,说白了就是到达了延迟的下限;
  • 并发连接有限,谷歌浏览器最大并发连接数是 6 个,而且每一个连接都要经过 TCP 和 TLS 握手耗时,以及 TCP 慢启动过程给流量带来的影响;
  • 队头阻塞问题,同一连接只能在完成一个 HTTP 事务(请求和响应)后,才能处理下一个事务;
  • HTTP 头部巨大且重复,由于 HTTP 协议是无状态的,每一个请求都得携带 HTTP 头部,特别是对于有携带 cookie 的头部,而 cookie 的大小通常很大;
  • 不支持服务器推送消息,因此当客户端需要获取通知时,只能通过定时器不断地拉取消息,这无疑浪费大量了带宽和服务器资源。

为了解决 HTTP/1.1 性能问题,具体的优化手段你可以看这篇文章「」,这里我举例几个常见的优化手段:

  • 将多张小图合并成一张大图供浏览器 JavaScript 来切割使用,这样可以将多个请求合并成一个请求,但是带来了新的问题,当某张小图片更新了,那么需要重新请求大图片,浪费了大量的网络带宽;
  • 将图片的二进制数据通过 base64 编码后,把编码数据嵌入到 HTML 或 CSS 文件中,以此来减少网络请求次数;
  • 将多个体积较小的 JavaScript 文件使用 webpack 等工具打包成一个体积更大的 JavaScript 文件,以一个请求替代了很多个请求,但是带来的问题,当某个 js 文件变化了,需要重新请求同一个包里的所有 js 文件;
  • 将同一个页面的资源分散到不同域名,提升并发连接上限,因为浏览器通常对同一域名的 HTTP 连接最大只能是 6 个;

尽管对 HTTP/1.1 协议的优化手段如此之多,但是效果还是不尽人意,因为这些手段都是对 HTTP/1.1 协议的“外部”做优化,而一些关键的地方是没办法优化的,比如请求-响应模型、头部巨大且重复、并发连接耗时、服务器不能主动推送等,要改变这些必须重新设计 HTTP 协议,于是 HTTP/2 就出来了!

HTTP/2 出来的目的是为了改善 HTTP 的性能。协议升级有一个很重要的地方,就是要兼容老版本的协议,否则新协议推广起来就相当困难,所幸 HTTP/2 做到了兼容 HTTP/1.1 。

那么,HTTP/2 是怎么做的呢?

第一点,HTTP/2 没有在 URI 里引入新的协议名,仍然用「http://」表示明文协议,用「https://」表示加密协议,于是只需要浏览器和服务器在背后自动升级协议,这样可以让用户意识不到协议的升级,很好的实现了协议的平滑升级。

第二点,只在应用层做了改变,还是基于 TCP 协议传输,应用层方面为了保持功能上的兼容,HTTP/2 把 HTTP 分解成了「语义」和「语法」两个部分,「语义」层不做改动,与 HTTP/1.1 完全一致,比如请求方法、状态码、头字段等规则保留不变。

但是,HTTP/2 在「语法」层面做了很多改造,基本改变了 HTTP 报文的传输格式。

HTTP 协议的报文是由「Header + Body」构成的,对于 Body 部分,HTTP/1.1 协议可以使用头字段 「Content-Encoding」指定 Body 的压缩方式,比如用 gzip 压缩,这样可以节约带宽,但报文中的另外一部分 Header,是没有针对它的优化手段。

HTTP/1.1 报文中 Header 部分存在的问题:

  • 含很多固定的字段,比如Cookie、User Agent、Accept 等,这些字段加起来也高达几百字节甚至上千字节,所以有必要压缩
  • 大量的请求和响应的报文里有很多字段值都是重复的,这样会使得大量带宽被这些冗余的数据占用了,所以有必须要避免重复性
  • 字段是 ASCII 编码的,虽然易于人类观察,但效率低,所以有必要改成二进制编码

HTTP/2 对 Header 部分做了大改造,把以上的问题都解决了。

HTTP/2 没使用常见的 gzip 压缩方式来压缩头部,而是开发了 HPACK 算法,HPACK 算法主要包含三个组成部分:

  • 静态字典;
  • 动态字典;
  • Huffman 编码(压缩算法);

客户端和服务器两端都会建立和维护「字典」,用长度较小的索引号表示重复的字符串,再用 Huffman 编码压缩数据,可达到 50%~90% 的高压缩率

HTTP/2 为高频出现在头部的字符串和字段建立了一张静态表,它是写入到 HTTP/2 框架里的,不会变化的,静态表里共有 61 组,如下图:

表中的 Index 表示索引(Key),Header Value 表示索引对应的 Value,Header Name 表示字段的名字,比如 Index 为 2 代表 GET,Index 为 8 代表状态码 200。

你可能注意到,表中有的 Index 没有对应的 Header Value,这是因为这些 Value 并不是固定的而是变化的,这些 Value 都会经过 Huffman 编码后,才会发送出去。

这么说有点抽象,我们来看个具体的例子,下面这个 server 头部字段,在 HTTP/1.1 的形式如下:

server: nghttpx\\r\

算上冒号空格和末尾的\\r\ ,共占用了 17 字节,而使用了静态表和 Huffman 编码,可以将它压缩成 8 字节,压缩率大概 47 %

我抓了个 HTTP/2 协议的网络包,你可以从下图看到,高亮部分就是 server 头部字段,只用了 8 个字节来表示 server 头部数据。


根据 RFC7541 规范,如果头部字段属于静态表范围,并且 Value 是变化,那么它的 HTTP/2 头部前 2 位固定为 01,所以整个头部格式如下图:

HTTP/2 头部由于基于二进制编码,就不需要冒号空格和末尾的\\r\ 作为分隔符,于是改用表示字符串长度(Value Length)来分割 Index 和 Value。

接下来,根据这个头部格式来分析上面抓包的 server 头部的二进制数据。

首先,从静态表中能查到 server 头部字段的 Index 为 54,二进制为 110110,再加上固定 01,头部格式第 1 个字节就是 01110110,这正是上面抓包标注的红色部分的二进制数据。

然后,第二个字节的首个比特位表示 Value 是否经过 Huffman 编码,剩余的 7 位表示 Value 的长度,比如这次例子的第二个字节为 10000110,首位比特位为 1 就代表 Value 字符串是经过 Huffman 编码的,经过 Huffman 编码的 Value 长度为 6。

最后,字符串 nghttpx 经过 Huffman 编码后压缩成了 6 个字节,Huffman 编码的原理是将高频出现的信息用「较短」的编码表示,从而缩减字符串长度。

于是,在统计大量的 HTTP 头部后,HTTP/2 根据出现频率将 ASCII 码编码为了 Huffman 编码表,可以在 RFC7541 文档找到这张静态 Huffman 表,我就不把表的全部内容列出来了,我只列出字符串 nghttpx 中每个字符对应的 Huffman 编码,如下图:



通过查表后,字符串 nghttpx 的 Huffman 编码在下图看到,共 6 个字节,每一个字符的 Huffman 编码,我用相同的颜色将他们对应起来了,最后的 7 位是补位的。



最终,server 头部的二进制数据对应的静态头部格式如下:


静态表只包含了 61 种高频出现在头部的字符串,不在静态表范围内的头部字符串就要自行构建动态表,它的 Index 从 62 起步,会在编码解码的时候随时更新。

比如,第一次发送时头部中的「user-agent 」字段数据有上百个字节,经过 Huffman 编码发送出去后,客户端和服务器双方都会更新自己的动态表,添加一个新的 Index 号 62。那么在下一次发送的时候,就不用重复发这个字段的数据了,只用发 1 个字节的 Index 号就好了,因为双方都可以根据自己的动态表获取到字段的数据

所以,使得动态表生效有一个前提:必须同一个连接上,重复传输完全相同的 HTTP 头部。如果消息字段在 1 个连接上只发送了 1 次,或者重复传输时,字段总是略有变化,动态表就无法被充分利用了。

因此,随着在同一 HTTP/2 连接上发送的报文越来越多,客户端和服务器双方的「字典」积累的越来越多,理论上最终每个头部字段都会变成 1 个字节的 Index,这样便避免了大量的冗余数据的传输,大大节约了带宽。

理想很美好,现实很骨感。动态表越大,占用的内存也就越大,如果占用了太多内存,是会影响服务器性能的,因此 Web 服务器都会提供类似 http2_max_requests 的配置,用于限制一个连接上能够传输的请求数量,避免动态表无限增大,请求数量到达上限后,就会关闭 HTTP/2 连接来释放内存。

综上,HTTP/2 头部的编码通过「静态表、动态表、Huffman 编码」共同完成的。


HTTP/2 厉害的地方在于将 HTTP/1 的文本格式改成二进制格式传输数据,极大提高了 HTTP 传输效率,而且二进制数据使用位运算能高效解析。

你可以从下图看到,HTTP/1.1 的响应 和 HTTP/2 的区别:


HTTP/2 把响应报文划分成了两个帧(Frame,图中的 HEADERS(首部)和 DATA(消息负载) 是帧的类型,也就是说一条 HTTP 响应,划分成了两个帧来传输,并且采用二进制来编码。

HTTP/2 二进制帧的结构如下图:

帧头(Fream Header)很小,只有 9 个字节,帧开头的前 3 个字节表示帧数据(Fream Playload)的长度

帧长度后面的一个字节是表示帧的类型,HTTP/2 总共定义了 10 种类型的帧,一般分为数据帧控制帧两类,如下表格:

帧类型后面的一个字节是标志位,可以保存 8 个标志位,用于携带简单的控制信息,比如:

  • END_HEADERS 表示头数据结束标志,相当于 HTTP/1 里头后的空行(“\\r\ ”);
  • END_STREAM 表示单方向数据发送结束,后续不会再有数据帧。
  • PRIORITY 表示流的优先级;

帧头的最后 4 个字节是流标识符(Stream ID),但最高位被保留不用,只有 31 位可以使用,因此流标识符的最大值是 2^31,大约是 21 亿,它的作用是用来标识该 Fream 属于哪个 Stream,接收方可以根据这个信息从乱序的帧里找到相同 Stream ID 的帧,从而有序组装信息。

最后面就是帧数据了,它存放的是通过 HPACK 算法压缩过的 HTTP 头部和包体。

知道了 HTTP/2 的帧结构后,我们再来看看它是如何实现并发传输的。

我们都知道 HTTP/1.1 的实现是基于请求-响应模型的。同一个连接中,HTTP 完成一个事务(请求与响应),才能处理下一个事务,也就是说在发出请求等待响应的过程中,是没办法做其他事情的,如果响应迟迟不来,那么后续的请求是无法发送的,也造成了队头阻塞的问题。

而 HTTP/2 就很牛逼了,通过 Stream 这个设计,多个 Stream 复用一条 TCP 连接,达到并发的效果,解决了 HTTP/1.1 队头阻塞的问题,提高了 HTTP 传输的吞吐量。

为了理解 HTTP/2 的并发是怎样实现的,我们先来理解 HTTP/2 中的 Stream、Message、Frame 这 3 个概念。


你可以从上图中看到:

  • 1 个 TCP 连接包含一个或者多个 Stream,Stream 是 HTTP/2 并发的关键技术;
  • Stream 里可以包含 1 个或多个 Message,Message 对应 HTTP/1 中的请求或响应,由 HTTP 头部和包体构成;
  • Message 里包含一条或者多个 Frame,Frame 是 HTTP/2 最小单位,以二进制压缩格式存放 HTTP/1 中的内容(头部和包体);

因此,我们可以得出 2 个结论:HTTP 消息可以由多个 Frame 构成,以及 1 个 Frame 可以由多个 TCP 报文构成。

在 HTTP/2 连接上,不同 Stream 的帧是可以乱序发送的(因此可以并发不同的 Stream ),因为每个帧的头部会携带 Stream ID 信息,所以接收端可以通过 Stream ID 有序组装成 HTTP 消息,而同一 Stream 内部的帧必须是严格有序的



客户端和服务器双方都可以建立 Stream, Stream ID 也是有区别的,客户端建立的 Stream 必须是奇数号,而服务器建立的 Stream 必须是偶数号。

同一个连接中的 Stream ID 是不能复用的,只能顺序递增,所以当 Stream ID 耗尽时,需要发一个控制帧 GOAWAY,用来关闭 TCP 连接。

在 Nginx 中,可以通过 http2_max_concurrent_streams 配置来设置 Stream 的上限,默认是 128 个。

HTTP/2 通过 Stream 实现的并发,比 HTTP/1.1 通过 TCP 连接实现并发要牛逼的多,因为当 HTTP/2 实现 100 个并发 Stream 时,只需要建立一次 TCP 连接,而 HTTP/1.1 需要建立 100 个 TCP 连接,每个 TCP 连接都要经过TCP 握手、慢启动以及 TLS 握手过程,这些都是很耗时的。

HTTP/2 还可以对每个 Stream 设置不同优先级,帧头中的「标志位」可以设置优先级,比如客户端访问 HTML/CSS 和图片资源时,希望服务器先传递 HTML/CSS,再传图片,那么就可以通过设置 Stream 的优先级来实现,以此提高用户体验。

HTTP/1.1 不支持服务器主动推送资源给客户端,都是由客户端向服务器发起请求后,才能获取到服务器响应的资源。

比如,客户端通过 HTTP/1.1 请求从服务器那获取到了 HTML 文件,而 HTML 可能还需要依赖 CSS 来渲染页面,这时客户端还要再发起获取 CSS 文件的请求,需要两次消息往返,如下图左边部分:



如上图右边部分,在 HTTP/2 中,客户端在访问 HTML 时,服务器可以直接主动推送 CSS 文件,减少了消息传递的次数。

在 Nginx 中,如果你希望客户端访问 /test.html 时,服务器直接推送 /test.css,那么可以这么配置:

location /test.html{ 
  http2_push /test.css; 
}

那 HTTP/2 的推送是怎么实现的?

客户端发起的请求,必须使用的是奇数号 Stream,服务器主动的推送,使用的是偶数号 Stream。服务器在推送资源时,会通过 PUSH_PROMISE 帧传输 HTTP 头部,并通过帧中的 Promised Stream ID 字段告知客户端,接下来会在哪个偶数号 Stream 中发送包体。



如上图,在 Stream 1 中通知客户端 CSS 资源即将到来,然后在 Stream 2 中发送 CSS 资源,注意 Stream 1 和 2 是可以并发的。

HTTP/2 协议其实还有很多内容,比如流控制、流状态、依赖关系等等。

这次主要介绍了关于 HTTP/2 是如何提示性能的几个方向,它相比 HTTP/1 大大提高了传输效率、吞吐能力。

第一点,对于常见的 HTTP 头部通过静态表和 Huffman 编码的方式,将体积压缩了近一半,而且针对后续的请求头部,还可以建立动态表,将体积压缩近 90%,大大提高了编码效率,同时节约了带宽资源。

不过,动态表并非可以无限增大, 因为动态表是会占用内存的,动态表越大,内存也越大,容易影响服务器总体的并发能力,因此服务器需要限制 HTTP/2 连接时长或者请求次数。

第二点,HTTP/2 实现了 Stream 并发,多个 Stream 只需复用 1 个 TCP 连接,节约了 TCP 和 TLS 握手时间,以及减少了 TCP 慢启动阶段对流量的影响。不同的 Stream ID 才可以并发,即时乱序发送帧也没问题,但是同一个 Stream 里的帧必须严格有序。

另外,可以根据资源的渲染顺序来设置 Stream 的优先级,从而提高用户体验。

第三点,服务器支持主动推送资源,大大提升了消息的传输性能,服务器推送资源时,会先发送 PUSH_PROMISE 帧,告诉客户端接下来在哪个 Stream 发送资源,然后用偶数号 Stream 发送资源给客户端。

HTTP/2 通过 Stream 的并发能力,解决了 HTTP/1 队头阻塞的问题,看似很完美了,但是 HTTP/2 还是存在“队头阻塞”的问题,只不过问题不是在 HTTP 这一层面,而是在 TCP 这一层。

HTTP/2 是基于 TCP 协议来传输数据的,TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且连续的,这样内核才会将缓冲区里的数据返回给 HTTP 应用,那么当「前 1 个字节数据」没有到达时,后收到的字节数据只能存放在内核缓冲区里,只有等到这 1 个字节数据到达时,HTTP/2 应用层才能从内核中拿到数据,这就是 HTTP/2 队头阻塞问题。

有没有什么解决方案呢?既然是 TCP 协议自身的问题,那干脆放弃 TCP 协议,转而使用 UDP 协议作为传输层协议,这个大胆的决定, HTTP/3 协议做了!


----------我是分割线------------

另外,程序员要想进大厂先从刷算法做起是个好方法,算法厉害的人进大厂非常容易,这里送一本阿里P8撰写的算法刷题笔记,身边不少朋友通过它加入大厂:

Github 疯传!阿里大佬「LeetCode刷题手册」开放下载了!史上最强悍!

还有,小林自己也写了个「图解网络 PDF」,图解了很多大厂爱问的网络面试题,助力了很多读者冲击大厂:

突击大厂网络面试题!「图解网络」开放下载!

最后祝大家前程似锦,在编码的道路上一马平川。

要是觉得不错的话,那就帮@小林coding

点个赞,点个收藏,评论下更先显温情!

平台注册入口