5. http2的基本概念
http2到底做了些什么呢?而HTTPbis小组究竟又应该把它制定到什么样的程度呢?
事实上,http2有着非常严格的边界,这也给小组成员的创新带来了些许限制。
http2必须维持HTTP的范式。毕竟它只是一个让客户端发送请求到服务器的基于TCP的协议。
不能改变 http:// 和 https:// 这样的URL,也不能对其添加新的结构。使用这类URL的网站太多了,没法指望他们全部改变。
HTTP1的服务器和客户端依然会存在很久,所以我们必须提供HTTP1到http2服务器的代理。
随后,我们也要让这种代理能够将http2的功能一对一的映射到HTTP 1.1的客户端。
删除或者减少协议里面那些可选的部分。虽然这并不算的上是一个需求,但是SPDY和Google的团队都非常喜欢这点。通过让协议里所有的内容都成为了强制性要求,可以防止人们在实现的时候偷懒,从而规避一些将来可能会发生的问题。
不再使用小版本号。服务器和客户端都必须确定自己是否完整兼容http2或者彻底不兼容。如果将来该协议需要被扩充或者变更,那么新的协议将会是http3,而不是http 2.x。
5.1. http2和现有的URI结构
如上所述,现有的URI结构正在被HTTP 1.x使用而不能被更换,所以http2也必须沿用该结构。因此不得不找到一种方式将使用的协议升级至http2,比如可以要求服务器让它作响应时使用http2来替代旧的协议。
HTTP 1.1本身就制定过“升级”的方案:提供一个首部字段,表示允许服务器在收到旧协议请求的同时,可以向客户端发送新协议的响应。但这一方案往往需要花费一次额外的往返通信来作为升级的代价。
而这一代价是SPDY团队不想接受的。因为他们只实现了基于TLS的SPDY,所以他们开发了一个TLS的扩展去简化协议的协商。这个扩展被称作NPN(Next Protocol Negotiation),借助于此,服务器会通知客户端所有它支持的协议,让客户端从中选择一个合适的来进行通讯。
5.2. 为 https:// 所准备的http2
有相当多的人关注到了http2可以在TLS上正常的运作,而SPDY依赖于TLS,所以按理说TLS也应成为http2 必需的组件,不过出乎大家意料的是http2将TLS标记成了可选。然而,全球两大浏览器领导者 —— Firefox和Chrome都明确地表示,他们只会实现基于TLS的http2.
选择TLS的原因的其中之一是希望保护以及尊重用户的隐私,而早期的评估结果也表明,在TLS上建立新的协议更有可能获得成功。而这其中部分原因是人们普遍认为任何来自80端口的流量都是基于HTTP 1.1亦或者是其某个变种的,而不是另外一种全新的协议。
关于是否应该强制使用TLS的主题在邮件组内和会议上引起了不小的争议 —— 这到底是好是坏呢?不管怎么样,对于这种备受争议的话题还是请谨慎讨论,尤其是当你面对一个HTTPbis小组成员的时候。
诸如此类,还有一个激烈而长期的讨论,即:如果选择了使用TLS,那http2是否应该强制规定密码列表,也许应该建立起一个黑名单,又或者它根本就不需要从TLS层得到任何东西。不过这个问题还是留给TLS工作组去解决吧,最后的规范中指定了TLS最低版本为1.2,并且会有加密组的限制。
5.3 基于TLS之上的http2协商
Next Protocol Negotiation (NPN)是一个用来在TLS服务器上协商SPDY的协议。IETF将这个非正式标准进行规范化,从而演变成了ALPN(Application Layer Protocol Negotiation)。ALPN会随着http2的应用被推广,而SPDY的客户端与服务器则会继续使用NPN。
由于NPN先于ALPN诞生,而ALPN又经历了一些标准化过程,所以许多早期的http2客户端和服务器在协商http2时会将这两者同时实现。与此同时,考虑到SPDY会使用NPN,而许多服务器又会同时提供SPDY以及http2,所以在这些服务器上同时支持ALPN以及NPN显然会成为最理所当然的选择。
ALPN和NPN的主要区别在于:谁来决定通信协议。在ALPN的描述中,是让客户端先发送一个协议优先级列表给服务器,由服务器最终选择一个合适的。而NPN则正好相反,客户端有着最终的决定权。
5.4 为 http:// 所准备的http2
正如我们之前所提到的,对于纯文本的HTTP1.1来说,协商http2的方法就是通过给服务器发送一个带升级头部的报文。如果服务器支持http2,它将以“101 Switching”作为回复的状态码,并从此开始在该连接上使用http2。也许你很容易就发现这样一个升级的流程会需要消耗掉一整个的往返时延,但好处是http2连接相比HTTP1可以被更大限度地重用和保持。
虽然有些浏览器厂商的发言人宣称他们不会实现这样的http2会话方式,但IE团队已公开表示他们会实现,与此同时,curl也已经支持了这种方式。
直到今天,没有任何主流浏览器支持非TLS的http2.
6. http2协议
背景介绍就到此为止了,历史的脚步已经将我们推到了今天。现在让我们深入看看该协议的规范,看看那些细节和概念。
6.1. 二进制
http2是一个二进制协议。
仔细想想,如果你是一个曾经跟互联网协议打过交道,那你很可能会本能反对二进制协议,你甚至准备好了一大堆理由来证明基于文本/ascii的协议是多么的有用,正如你曾无数次地通过telnet等应用手工地输入HTTP来发起请求。
基于二进制的http2可以使成帧的使用变得更为便捷。在HTTP1.1和其他基于文本的协议中,对帧的起始和结束识别起来相当复杂。而通过移除掉可选的空白符以及其他冗余后,再来实现这些会变得更容易。
而另一方面,这项决议同样使得我们可以更加便捷的从帧结构中分离出那部分协议本身的内容。而在HTTP1中,各个部分相互交织,犹如一团乱麻。
事实上,由于协议提供了压缩这一特性,而其经常运行在TLS之上的事实又再次降低了基于纯文本实现的价值,反正也没办法直接从数据流上看到文本。因此通常情况下,我们必须习惯使用类似Wireshark这样的工具对http2的协议层一探究竟。
我们可以使用curl这样的工具来调试协议,而如果要进一步地分析网络数据流则需要诸如Wireshark这样的http2解析器。
6.2. 二进制格式

http2会发送有着不同类型的二进制帧,但他们都有如下的公共字段:Type, Length, Flags, Stream Identifier和frame payload
规范中一共定义了10种不同的帧,其中最基础的两种分别对应于HTTP 1.1的DATA和HEADERS。之后我会更详细的介绍它们其中的一部分。
6.3. 多路复用的流
上一节提到的Stream Identifier将http2连接上传输的每个帧都关联到一个“流”。流是一个独立的,双向的帧序列可以通过一个http2的连接在服务端与客户端之间不断的交换数据。
每个单独的http2连接都可以包含多个并发的流,这些流中交错的包含着来自两端的帧。流既可以被客户端/服务器端单方面的建立和使用,也可以被双方共享,或者被任意一边关闭。在流里面,每一帧发送的顺序非常关键。接收方会按照收到帧的顺序来进行处理。
流的多路复用意味着在同一连接中来自各个流的数据包会被混合在一起。就好像两个(或者更多)独立的“数据列车”被拼凑到了一辆列车上,但它们最终会在终点站被分开。下图就是两列“数据火车”的示例
它们就是这样通过多路复用的方式被组装到了同一列火车上。
6.4. 优先级和依赖性
每个流都包含一个优先级(也就是“权重”),它被用来告诉对端哪个流更重要。当资源有限的时候,服务器会根据优先级来选择应该先发送哪些流。
借助于PRIORITY帧,客户端同样可以告知服务器当前的流依赖于其他哪个流。该功能让客户端能建立一个优先级“树”,所有“子流”会依赖于“父流”的传输完成情况。
优先级和依赖关系可以在传输过程中被动态的改变。这样当用户滚动一个全是图片的页面的时候,浏览器就能够指定哪个图片拥有更高的优先级。或者是在你切换标签页的时候,浏览器可以提升新切换到页面所包含流的优先级。
6.5. 头压缩
HTTP是一种无状态的协议。简而言之,这意味着每个请求必须要携带服务器需要的所有细节,而不是让服务器保存住之前请求的元数据。因为http2并没有改变这个范式,所以它也以同样原理工作。
这也保证了HTTP可重复性。当一个客户端从同一服务器请求了大量资源(例如页面的图片)的时候,所有这些请求看起来几乎都是一致的,而这些大量一致的东西则正好值得被压缩。
每个页面请求的资源数量在增多(如前所述),同时 cookies 的使用和请求的大小也在日渐增长。cookies需要被包含在所有请求中,且他们在多个请求中经常是一模一样的。
HTTP 1.1请求的大小正变得越来越大,有时甚至会大于TCP窗口的初始大小,这会严重拖累发送请求的速度。因为它们需要等待带着ACK的响应回来以后,才能继续被发送。这也是另一个需要压缩的理由。
6.5.1. 压缩是非常棘手的课题
HTTPS和SPDY的压缩机制被发现有受BREACH和CRIME攻击的隐患。通过向流中注入一些已知的文本来观察输出的变化,攻击者可以从加密的载荷中推导出原始发送的数据。
为协议的动态内容进行压缩并使其免于被攻击,需要仔细且全面的考虑,而这也正是HTTPbis小组尝试去做的。
HPACK,HTTP/2头部压缩,顾名思义它是一个专为http2头部设计的压缩格式。确切的讲,它甚至被制定写入在另外一个单独的草案里。新的格式同时引入了一些其他对策让破解压缩变得困难,例如采用帧的可选填充和用一个bit作为标记,来让中间人不压缩指定的头部。
用Roberto Peon(HPACK的设计者之一)的话说
“HPACK旨在提供一个一致性的实现使信息量的损失尽可能少,使编解码快速而方便,使接收方能控制压缩文本的大小,允许代理重新建立索引(如,通过代理在前后端共享状态),以及对哈夫曼编码串的更快速比较”
6.6. 重置 - 后悔药
HTTP 1.1的有一个缺点是:当一个含有确切值的Content-Length的HTTP消息被送出之后,你就很难中断它了。当然,通常你可以断开整个TCP链接(但也不总是可以这样),但这样导致的代价就是需要通过三次握手来重新建立一个新的TCP连接。
一个更好的方案是只终止当前传输的消息并重新发送一个新的。在http2里面,我们可以通过发送RST_STREAM帧来实现这种需求,从而避免浪费带宽和中断已有的连接。
6.7. 服务器推送
这个功能通常被称作“缓存推送”。主要的思想是:当一个客户端请求资源X,而服务器知道它很可能也需要资源Z的情况下,服务器可以在客户端发送请求前,主动将资源Z推送给客户端。这个功能帮助客户端将Z放进缓存以备将来之需。
服务器推送需要客户端显式的允许服务器提供该功能。但即使如此,客户端依然能自主选择是否需要中断该推送的流。如果不需要的话,客户端可以通过发送一个RST_STREAM帧来中止。
6.8. 流量控制
每个http2流都拥有自己的公示的流量窗口,它可以限制另一端发送数据。如果你正好知道SSH的工作原理的话,这两者非常相似。
对于每个流来说,两端都必须告诉对方自己还有足够的空间来处理新的数据,而在该窗口被扩大前,另一端只被允许发送这么多数据。
而只有数据帧会受到流量控制。
7. 扩展
http2协议强制规定了接收方必须读取并忽略掉所有未知帧(即未知帧类型的帧)。双方可以在逐跳原则(hop-by-hop basis)基础上协商使用新的帧,但这些帧的状态无法被改变,也不受流控制。
是否应该允许添加扩展的这个话题在制定http2协议的时候被反复讨论了很久,但在draft-12之后,最终尘埃落定确定了允许添加扩展。
但扩展不再是协议本身的一部分,它被记录在核心协议规范之外。现在已经有两种类型的帧被工作组记录在案,它们很可能率先被纳入协议的扩展部分,而这两个曾被当作“原生”的帧非常流行,所以接下来我会详细讨论它们。
7.1. 备选服务(Alternative Services)
随着http2逐渐被接受,我们有理由相信,相对于HTTP 1.x,TCP连接会更长并被保持的更久。对客户端来讲,最好是到每个主机/站点的每一条连接都可以做尽可能多的事情,而这也需要每个连接可以保持更长的时间。
但这会影响到HTTP负载均衡器的正常工作,比如在一个网站会出于性能的考虑,当然也可能是正常的维护或者一些类似的原因,想建议客户端连接到另外一个主机的时候。
服务器将会通过发送Alt-Svc头(或者http2的ALTSVC帧)来告知客户端另一个备选服务。即另外一条指向不同的服务源、主机或端口,但却能获取同样内容的路由。
客户端应该尝试异步的去连接到该服务,如果连接成功的话,即可以使用该备选服务。
7.1.1. 机会型TLS(Opportunistic TLS)
Alt-Svc头部意味着允许服务器基于http://
提供内容,与此同时,这个头部也意味着告知客户端:同样的内容也可以通过TLS连接来获取。
这是个还在讨论中的功能。因为这样的连接会产生一个未认证的、在任何地方也不会被标示为“安全”的TLS连接,也不会在客户端界面上出现任何锁标识,所以没法让用户知道这其实不是常规的HTTP连接。这就是很多人强烈反对机会型TLS的原因。
7.2. 阻塞(Blocked)
这个类型的帧意味着:当服务端存在需要发送的内容,但流控制却禁止发送任何数据时,那么此类型的帧将会被发送且仅发送一次。这种帧设计的目的在于,如果你接收到了此帧,那么连接中必然有错误发生或者是得到了低于期望的传输速度。
在此帧被放到协议扩展部分之前,draft-12中的一段话:
”阻塞帧被包含在草案版本中作为实验性的特性,如果它无法获得良好的反馈,那么该特性最后会被移除。”