5/15/2015 更新:
HTTP/2 协议标准已经敲定了,是为 RFC7540。
1. HTTP 现状
HTTP 是现在互联网上使用最广泛的网络协议,它的当前版本是 HTTP/1.1,也是使用范围最广的版本(实际上还有不少的代理服务器在使用 HTTP/1.0)。HTTP 的标准由 IETF 和 W3C 共同负责制定,HTTP/1.1 的最初版本于 1997 年 7 月发布,即 RFC 2068。随后在 1999 年 6 月,他们又发布了 RFC 2616,主要针对 RFC 2068 进行一些修订和改善。2014 年 7 月,RFC 2616 更新为 RFC 7230 等六个 RFC 标准。
1.1 传输量和请求数
随着前端技术的发展,包括 JavaScript 层出不穷的框架和类库,以及越来越成熟的构建工具和模块化开发等等,导致网页上需要展现的内容及加载一个页面所需的请求数越来越多,而这一趋势,短期来看,并无扭转的可能。
下图展示了从 2011 年 4 月到 2015 年 4 月网页的平均传输量和请求数,可以看到,现在用户打开一个网页需要下载的资源已经达到将近 2MB,请求数也接近 100。(数据来源:httparchive.org)
1.2 HTTP/1.1 的缺点
越来越多的资源和请求,意味着越来越高的延时。即便带宽和网速节节升高,其对延时的改善也收效甚微:HTTP 的实现在事实上造成的结果是,一个 TCP 连接中只允许有一个主要的请求。这是因为,HTTP/1.1 要求服务器按照它接收到的请求的顺序发送响应。也就是说,这种连接遵循的是先进先出原则。显然,如果有一个请求非常耗时,那么后续请求都将受到影响,所以这就造成了 Head-of-line blocking 问题。
在过去,浏览器曾经使用多个 TCP 连接来分发并行请求,但这种方式实在是弊大于利。如果页面中请求数过多,那么不但有可能触发 TCP 的拥塞从而降低性能,而且过多的请求也意味着传输大量的重复数据。
HTTP/1.1 试图解决性能问题的另一种手段是 HTTP Pipelining。这种技术把多个 HTTP 请求打包在一个 TCP 连接中一一发送,而在发送过程中不需等待服务器对上一个请求的响应。看起来,这像是一个「异步」操作,而且能够降低 TCP 连接数。事实上,对于一些本来就高延时的连接,这种方式确实能够显著降低延时。但对于网络带宽很充裕的连接,效果就不那么明显了。因此这无法从根本上解决 Head-of-line blocking。
而且,HTTP Pipelining 要求服务器和浏览器都支持才可行。更严格的是,只有幂等请求,也就是 HEAD
和 GET
才可以 pipelining,非幂等请求如 POST
和 PUT
是不可以 pipelining 的。因此直到今天,主流浏览器的 HTTP Pipelining 开关还是默认关闭的。
面对上述残酷的事实,机智的 Web 开发者们发明出了不少变通方案,例如 inlining,concatenation,spriting 等技术来减少页面请求从而优化性能。当然这些变通方案有利有弊,个中取舍,端看开发者的业务和需求。
2. HTTP/2 的主要特点
既然当前 Web 应用的性能和体验问题严重受限于 HTTP/1.1,那与其想办法减少请求或者降低负载,还不如从根源上解决问题:修订 HTTP 协议。毕竟,当前互联网上不可或缺的这个协议,其标准的制定距今已经 15 年了。
其实早在 2007 年,IETF 就组织成立了 HTTP Working Group 着手修订 HTTP/1.1。2012 年底,当他们真正开始起草 HTTP 2.0 的时候,发现 Google 已经有一个证实可行的 SPDY 了!于是 HTTP 2.0 就在 SPDY/2 草案的基础上实现了第一个草案:HTTP2 draft-00。之后他们又进行了一些修修补补的工作,并顺便把协议的新版本叫做 HTTP/2,去掉了 “.0”。2015 年 2 月 17 日,IESG 接受 HTTP/2 为 Proposed Standard。
虽然,HTTP/2 的目标是成为 HTTP 的「下一个」版本,但无论是理论上,还是现实中,都无法在 API 或者「语法」层面修改 HTTP 协议。制定 HTTP/2 标准,一些原则和限制还是坚持和遵守的:
- 协议层面上保持不变,请求还是通过 TCP 传送;
- URL scheme,即
http://
和https://
保持不变; - 保持向后兼容,能够与 HTTP/1.x 服务器(包括代理服务器)正常通讯;
- 不再制定子版本,如果协议再有更新,则为 HTTP/3。
2.1 二进制(Binary)
HTTP/2 传输的数据是二进制的。相比 HTTP/1.1 的纯文本数据,二进制数据一个显而易见的好处是:更小的传输体积。这就意味着更低的负载。二进制的帧也更易于解析而且不易出错,纯文本帧在解析的时候还要考虑处理空格、大小写、空行和换行等问题,而二进制帧就不存在这个问题。
在 HTTP/2 的语境下,有三个概念需要厘清,它们就是:流(Stream)、消息(Message) 和帧(Frame)。
Stream
处于一个连接中的双向二进制数据流,可以包含一个或者多个 Message
。
Message
一个完整的请求或者响应,包含多个 Frame
序列。
Frame
HTTP/2 通讯中的最小传输单位,至少含有一个 Frame header
,能够表示它属于哪一个 Stream
。
如下图所示(题图来源:High Performance Browser Networking):
2.2 多路复用(Multiplexing)
之前提到,HTTP/1.1 存在 Head-of-line blocking 问题,多路复用就是用来解决这个问题的。
所谓多路复用,简单来说,就是允许一个 TCP 连接中能够同时发送多个 HTTP 请求,而且请求与响应完全是异步的,耗时长的请求/响应不会阻塞其他请求/响应。具体点讲就是,服务器和客户端都可以把 HTTP 消息分割成多个独立的二进制帧,每一个帧都有一个 Stream ID
标示它所在的流,而这些帧可以插入到流中的任意一个位置(即帧的传送顺序不重要),到达目的地之后,接收端再把收到的帧「组装」起来。
另外,每一个流还有优先级和依赖(Priorities and dependencies),意味着客户端可以指定哪些流是最重要的需要优先传送,哪些流又依赖其他的流从而提供这个依赖参数。
我们知道,建立 TCP 连接的开销非常大,当前存在的一些优化性能的变通方法,归根结底是要减少请求和连接数。多路复用让这些「奇技淫巧」存在的价值大大降低,甚至不再必要。
2.3 头部压缩(Header compression)
HTTP 是无状态的协议,HTTP/2 也保留了这一点。因此 HTTP 请求的头部需要包含用于标识身份的数据比如 cookies
,而这些数据的量也在随着时间增长。每一个请求的头部都包含这些大量的重复数据,无疑是一种很大的负担。对请求头部进行压缩,将会大大减轻这种负担,尤其对移动端来说,性能提高非常明显。
HTTP/2 使用的压缩方式是 HPACK。
2.4 重置(Reset)
在 HTTP/1.1 中,如果客户端决定要中断一个请求,那它非要打开一个新的 TCP 连接告诉服务器不可。这种方式不但浪费带宽,而且可能会带来其他的麻烦。
HTTP/2 引入了一个 RST_STREAM frame 来让客户端在已有的连接中发送重置请求,从而中断或者放弃响应。
2.5 服务器推送(Server push)
HTTP/2 的服务器推送所作的工作就是,服务器在收到客户端对某个资源的请求时,会判断客户端十有八九还要请求其他的什么资源,然后一同把这些资源都发送给客户端,即便客户端还没有明确表示它需要这些资源。
客户端可以选择把额外的资源放入缓存中(所以这个特点也叫 Cache push),也可以选择发送一个 RST_STREAM frame 拒绝任何它不想要的资源。
2.6 加密(Encryption)
HTTP/2 并不强制要求使用 TLS,但 Chrome 和 Firefox 都声明他们只支持基于 TLS 的实现,这有可能会让加密成为事实上强制的规范。
3. 目前的实现
在浏览器端,目前主流浏览器对 HTTP/2 的支持程度如下(数据来源:Can I Use):
如果想在 Firefox 浏览器里体验 HTTP/2,可以参看这里。
服务器端也有不少的实现,比如 Google 和 Twitter 都已经提供了 HTTP/2 的测试服务器。完整的实现列表可以参看这里。