面向前端的HTTP/2介绍
正式版HTTP/2发布于2015年5月,距今已经快5年了,相比老一辈HTTP 1.1有了许多改进。在具体讨论这些改进前,我们先简单回顾下HTTP这一路以来的历程。
历史回顾
HTTP在1991年发布了最初的HTTP0.9版本,主要用于学术交流,目的也只是用来在网络之间传递HTML超文本的内容。HTTP0.9基于TCP,只有一个GET请求类型,请求和文档响应都是ASCII字符流,响应数据类型只有HTML类型,在响应结束后立即断开连接。
随着互联网发展,1994年底出现了拨号上网,网景推出浏览器后,WWW已经不仅局限于学术交流,浏览器中除了承载HTML以外,还包括了JavaScript、CSS、图片、音视频等资源。HTTP1.0在这个背景下于1996年推出(RFC1945),它支持了状态码、方法、头部的概念,响应内容不局限于超文本文件,编码类型也不仅限于ASCII。但是TCP连接在响应返回后依旧会断开连接。
在浏览器等技术发展,HTTP请求更多也更复杂,HTTP1.0的已有问题暴露得越来越明显。TCP频繁建立连接的时延,缓存协商机制的不完整、大文件下载的支持等问题需要解决。于是HTTP1.1在1999年推出(RFC2616),这是个很庞大的协议,全文长达176页,在后续IETF对该规范进行更新时,则被拆分成了总页数更多的六个文档(即RFC7230协议族)。HTTP1.1包含了太多细节和可选的部分,包含不常用的功能和不合适的优化,因此几乎没有任何实现包含完整的协议功能。总的来看,HTTP1.1做了一些尝试:
- TCP持久连接(keep-alive),即在一个TCP连接上发起HTTP请求
- 支持范围请求(Accept-Ranges)
- 更强大的缓存机制(协商缓存和强缓存,以及相关的头部)
- 提出了HTTP pipeline,尝试改善串行HTTP请求引起的线头阻塞(Head-of-line blocking)问题
- 更多的错误相关状态码
- Host头处理
之后HTTP1.1便被一直使用至今,随着互联网页面请求资源的数量和体积增大,HTTP1.1中没能妥善解决的问题越来越明显。HTTPbis工作组在2007年夏天便着手于HTTP1.1标准的更新,并最终在2014年初形成上面提到的RFC7230系列协议族。
SPDY(SPeeDY)是由Google牵头开发的开源协议,意图在TLS和HTTP中间插入中间层,解决HTTP协议的问题。大约在2012年提出,也被大多数主流浏览器支持。最终在2015年HTTP/2协议发布后,逐步放弃支持。实际上HTTP/2也是在SPDY/3草案的基础上形成的协议初稿。
历史问题
传输资源与延迟
当今的互联网环境和20世纪末那会儿已经有了很大的不同,现在的Web页面更像一个应用的概念,而非一个简单的页面(SPA)。从HTTPArchive一个页面下请求的资源数已经上百,请求的资源体积也接近2M。
随着这些年网络硬件条件的迅猛发展,带宽已经不再是影响人们体验的因素,而网络延迟仍然没有太好的改善。高延迟的网络上(比如移动设备),即使拥有高连接速率,也很难获得优质快速的网络体验。页面从访问到打开的近70%时间都发生在网络上。
请求-响应模型
HTTP1.1是无状态协议,需要以客户端请求开始,然后才能响应。一个TCP上同时只能有一个请求/响应。TCP协议的能力并没有被充分利用。在HTTP1.1启用keep-alive后,TCP连接重复建立的问题被解决。但是请求还是需要排队一个一个发送,TCP的RTT(round-trip time)还是比较可观。后续的HTTP1.1提出了HTTP管线化(pipeline),即将多个HTTP请求合并成一个,一起发送,这样的确提高了服务器的资源利用率,但是也会带来线头阻塞(head-of-line blocking)问题,即一个比较耗时的请求会影响后续的所有请求。另外,它会给重试带来麻烦,需要网络中间节点的支持。所以这个特性并没有得到浏览器和服务器认可,实现也并不普及。目前大多数桌面浏览器也是默认关闭这个特性的。
那些年,我们一起克服延迟的办法
针对上面的困难,智慧的开发者们自然也是有了许多应对办法。
- 雪碧图:将小图片整合成一张大图。
- 内联:将高优先级资源或小资源通过script标签或style标签或dataUrl的形式直接内嵌在页面里
- 分片(sharding)与域名散列:将图片或者其他资源分发到不同主机。最初的HTTP1.1规范(RFC2616)提到一个客户端最多只能对同一主机建立两个TCP连接。后来,两个连接的限制被取消了(RFC7230),现在的浏览器一般允许每个域名主机建立6-8个连接。根据httparchive.org的记录显示,在Top30万个URL中平均使用40个TCP连接来显示页面
除此之外,为了减少请求数,前端会将代码合并并打包,这也是webpack这样的工具诞生的背景。
冗长的头部
HTTP1.1中1000+字节的头部都是常见的且体积较大的,如Cookie
。头部信息有许多多余信息。这也让许多大请求建立连接的过程变得很慢。
总结来看,HTTP1.1遗留了下面几个问题:
- 对TCP利用较差,同时只能有一个请求/响应
- 目前应对方法:开多个TCP连接(分片),减少请求数(合并资源);这些方法多少会遇到TCP慢启动、DNS开销等问题
- HTTP头部没有压缩,占用较大空间
- 目前应对方法:减少请求数、使用cookie-less域名
- 固有的请求-响应模式,重要资源无法优先推送
- 目前应对方法:内联资源
HTTP/2对于上面这些问题自然是重拳出击。
HTTP/2概述
“HTTP/2 enables a more efficient use of network resources and a reduced perception of latency by introducing header field compression and allowing multiple concurrent exchanges on the same connection. It also introduces unsolicited push of representations from servers to clients.”
根据RFC7540的摘要,简明扼要地点出了HTTP/2带来的几个重要特性:
- 多路复用的二进制协议;一个TCP连接上不再只有1个请求/响应,同时采用二进制而非文本传输数据
- 头部压缩;用二进制分帧配合专门设计的头部压缩算法(HPACK)大大减少头部体积,HPACK有专门的RFC7541来规范。
- 服务器推送;在客户端发送请求前,主动将资源推送给客户端
整个HTTP/2实际上还是在HTTP的框架下的,对HTTP1.1也是完全兼容的,这意味着你可以像以前一样使用HTTP的API、方法、头部、状态码这些:
- HTTP/2必须维持HTTP的范式。它只是一个让客户端发送请求到服务器的基于TCP的协议
- 不能改变
http://
和https://
这样的URL,也不能对其添加新的结构。使用这类URL的网站太多了,没法指望他们全部改变。 - HTTP1.1的服务器和客户端依然会存在很久,所以必须提供HTTP1.1到HTTP/2服务器的代理
- 不再使用小版本号。服务器和客户端都必须确定自己是否完整兼容http2或者彻底不兼容
协商
SPDY依赖于TLS,不过从SPDY中诞生的HTTP/2却可以选择是否基于TLS。由此带来2种HTTP/2协商机制。对于普通的HTTP1.1,通过给服务器发送一个带升级头部的报文。如果服务器支持HTTP/2,它将以“101 Switching”作为回复的状态码,并从此开始在该连接上使用HTTP/2。这种连接方式也被称为h2c(HTTP/2 cleartext),此时HTTP/2在TCP之上运行。出于安全性考虑。几乎所有的主流浏览器都不支持这种协商实现(curl可以支持)。
对于在TLS之上的https,Next Protocol Negotiation (NPN)是一个用来在TLS服务器上协商SPDY的协议。IETF将这个非正式标准进行规范化,从而演变成了ALPN(Application Layer Protocol Negotiation)。ALPN会伴随HTTP/2中的使用而推广,考虑到SPDY会使用NPN,而许多服务器又会同时提供SPDY以及HTTP/2,所以在这些服务器上同时支持ALPN以及NPN显然会成为最理所当然的选择。ALPN和NPN的主要区别在于,ALPN中由服务端最终决定通信协议,NPN中由客户端最终决定。
HTTP/2特性
在HTTP/2的介绍中提到,协议通过定义一个优化的基础连接的HTTP语义映射来解决HTTP1.1的问题。具体地,它允许在同一连接上交错地建立请求和响应消息,并使用高效率编码的HTTP报头字段。它还允许请求的优先级,让更多的重要的请求更快速的完成,进一步提升了性能。最终协议设计为对网络更友好,因为它相对HTTP/1.x减少了TCP连接。最后,这种封装也通过使用二进制消息帧使信息处理更具扩展性。
里面加粗的部分即HTTP/2带来的几个新特性:
- 单一TCP连接
- 二进制分帧
- 请求优先级
- 服务端推送
- 流量控制
- 多路复用
- 头部压缩(HPACK)
二进制分帧“层”
首先,HTTP/2是个二进制协议。它的请求和响应都是流的形式,它基本的协议单位是帧。每个帧都有不同的类型和用途。HTTP/2所有性能增强的核心也在于这个新的二进制分帧层,它定义了如何封装 HTTP 消息并在客户端与服务器之间传输。
从图中能看到,在TLS之上,HTTP/2之下新增了一个二进制分帧层。这里所谓的“层”,指的是位于套接字接口与应用可见的高级HTTP API之间一个经过优化的新编码机制:HTTP的语义(包括各种动词、方法、标头)都不受影响,不同的是传输期间对它们的编码方式变了。不同于HTTP1.x里面用换行符作为分隔,HTTP/2中将信息分割成帧,并进行二进制编码。整个分帧过程由客户端和服务端替我们完成。
数据流和帧
上面这种二进制分帧机制改变了客户端与服务器之间交换数据的方式,也带来了流的概念。
- 流(Stream):一个双向字节帧流穿过HTTP/2连接中的虚拟通道,可以承载一条或多条消息。
- 消息:与逻辑请求或响应消息对应的完整的一系列帧。
流的生存周期包含idle、reserved(local)、reserved(remote)、open、half closed(local)、half closed(remote)、closed多个阶段。状态间通过特定的帧类型流转。在不同状态下对应着不同的能力,对于状态规范描述以外的操作请求都会给出协议错误(PROTOCOL_ERROR)。
1 | +--------+ |
流和帧的关系是:
- 所有通信都在一个TCP连接上完成,这个连接可以承载任意数量的流
- 每个流上面都有唯一标识符和可选的优先级信息,里面会承载要传递的消息
- 每条消息都是一条逻辑HTTP信息(如请求或相应),有完整的HTTP语义,其中可能有一个或多个帧
- 帧是最小的通信单位,承载着特定类型的数据,例如HTTP 标头、消息负载等等。帧可以交错发送,然后再根据帧头的数据流标识符进行组装
所有的帧以8字节的报头开始并且跟着0到16383字节长度的主体。帧格式如下:
1 | 0 1 2 3 |
其中:
- R:保留字段
- Length:14位无符号整数的帧主体长度
- Type:帧类型,它描述了剩余的帧报头和帧主体将如何被解释
- Flags:为帧类型保留的8位布尔类型字段,根据不同帧类型赋予不同语义
- Stream Identifier:31字节的流标识符(见StreamIdentifiers)。0是保留的,标明帧是与连接相关作为一个整体而不是一个单独的流。
请求与响应复用
在HTTP1.1中,客户端要想发起多个并行请求以提升性能,则必须使用多个TCP连接,这种对TCP效率低下的利用,在HTTP/2中得到改善。二进制分帧层将HTTP消息分解为互不依赖的帧,然后交错发送,最后再在另一端把它们重新组装起来。
在上图中,流1和流3交错在同一个TCP连接上并行运行。这种机制带来了下面一些具体的提升:
- 请求之间交错且互不影响
- 响应之间交错且互不影响
- 可以使用一个连接并行发送多个请求和响应
- 不必再为了优化HTTP1.1性能做雪碧图、分片等骚操作
- 一定程度上解决了线头阻塞问题
流控制
HTTP/2的流控制类似TCP,但是更为精细和更靠近应用层。借助HTTP/2流控制,可以实现在用户暂停一个大型视频流后,减少或阻塞视频里带来的HTTP流量,又或者中间代理匹配上下游流量速率。HTTP/2流控制提出了一些规则,但并没有指出特定算法,目标在于允许不需要协议改动的情况下改进流量控制算法。
- 流量控制是逐跳的,而不是头尾端点的
- 流量控制是基于窗口更新帧的。接收端广播自己准备在流及整个连接过程中接收的字节大小。这是一个信用为基础的方案。
- 流量控制是有方向性的,由接收端全权掌握。
- 流量控制窗口初始值是65,535字节,不过接收方可以设置一个更大的值
- 帧类型决定了是否适用流量控制规则。目前只有DATA帧受流量控制
- 不能被禁用
- 通过使用
WINDOW_UPDATE
帧类型来实现
流优先级
HTTP/2标准允许每个数据流都有一个关联的权重和依赖关系:
- 可以向每个数据流分配一个介于1至256之间的整数。
- 每个数据流与其他数据流之间可以存在显式依赖关系。
优先级的目的是让客户端可以构建和传递“优先级树”,表明它倾向于如何接收响应。反过来,服务器可以使用此信息通过控制CPU、内存和其他资源的分配设定数据流处理的优先级。
数据流依赖关系通过将另一个数据流的唯一标识符作为父项引用进行声明;如果忽略标识符,相应数据流将依赖于“根数据流”。分配时,会尽可能先向父数据流分配资源,然后再向其依赖项分配资源。共享相同父项的数据流按其权重比例分配资源。在上图中,数据流B获得的资源是数据流A所获资源的三分之一。
新建流的终端可以在报头帧中包含优先级信息来对流标记优先级。对于已存在的流,优先级帧可以用来改变流优先级。
更详细规范参考RFC。
帧类型
DATA
数据帧,类型0x0,传递和流关联的任意变量值长度的字节数据。例如,一个或多个数据帧被用来携带HTTP请求或者响应的载体。数据帧定义了以下标记:
- END_STREAM (0x1) :用来表示当前帧是确定的流发送的最后一帧。设置这个标记时流进入到一种半封闭状态或者关闭状态。
- END_SEGMENT (0x2) :表示是当前端的最后一帧。代理端绝对不能跨越多个端的边界来合并帧,转发帧的时候代理端必须保持片段的边界。
- PADDED (0x8) : 位4用来表示Pad Length字段是可见的。
数据帧绝对需要与流相关联,且遵从流量控制。
HEADERS
报头帧,类型0x1,用来打开一个流,并携带头部片段。能在流打开或者半封闭(远程)的状态下发送。
1 | +---------------+ |
它有以下标记:
- END_STREAM (0x1) :用来标识这是发送端对确定的流发送的最后报头区块。设置这个标记将使流进入一种半封闭状态。后面伴随带有END_STREAM标记的延续帧的报头帧表示流的终止。延续帧不用来终止流。
- END_SEGMENT (0x2) :表示这是当前端的最后一帧。中介端绝对不能跨片段来合并帧,且在转发帧的时候必须保持片段的边界。
- END_HEADERS (0x4) :表示帧包含了整个的报头块,且后面没有延续帧。不带有END_HEADERS标记的报头帧在同个流上后面必须跟着延续帧。
- PADDED (0x8) :表示Pad Length字段会呈现。
- PRIORITY (0x8) :设置指示专用标记(E),流依赖及权重字段将会呈现
PRIORITY
优先级帧,类型0x2。明确了发送者建议的流的优先级,它可以在任意时间的流中发送。优先级帧不包含任何标记(flag)。
1 | 0 1 2 3 |
RST_STREAM
类型0x3,允许流的立即终止。通常用来取消一个流,或表示有错误发生。绝不应该在idle状态下发出。
1 | 0 1 2 3 |
SETTINGS
设置帧,类型0x4。包含影响如何与终端通信的设置参数,并且用来确认这些参数的接收。设置帧必须由两个终端在连接开始的时候发送,并且可以由各个终端在连接生存期的任意时间发送。
PUSH_PROMISE
推送承诺帧,类型0x5。用来在流发送者准备发送流之前告知对等端。包含了终端准备创建的长流的31位无符号标记以及提供附加上下文的报头的集合。通常在服务器中使用。推送承诺的接收端可以选择给推送承诺的发送端返回一个与被承诺的流标识符相关的RST_STREAM标记来拒绝接收承诺流。
1 | 0 1 2 3 |
PING
类型0x6。从发送端测量最小的RTT时间的机制,同样也是一种检测连接是否可用的方法。PING帧可以被任何终端发送,且必须在载体中包含一个8字节长度的任意数据。
GOAWAY
超时帧,类型0x7。通知远端对等端不要在这个连接上建立新流。超时帧可以由客户端或者服务端发送。发送后,可以针对新的流创建一个新的连接。这个帧的目的是允许终端优雅的停止接收新的流,但仍可以继续完成之前已经建立的流的处理。
1 | 0 1 2 3 |
32位的错误码中包含了关闭连接的原因。
WINDOW_UPDATE
窗口更新帧,类型0x8。用来实现流控制。
1 | 0 1 2 3 |
CONTINUATION
延续帧,类型0x9,用来延续一个报头区块。在END_HEADERS标记前,可以在HEADERS帧、PUSH_PROMISE帧以及CONTINUATION帧后接续任意数量的CONTINUATION帧。它包含一个flag:
- END_HEADERS (0x4) : 设置指示这个帧的报头区块的终止
延续帧必须与流相关联。如果延续帧的相关流表示字段是0x0,终端必须响应一个类型为协议错误的连接错误。
服务器推送
HTTP/2中的服务器推送打破了原来HTTP中的请求-响应语义(对原有语义也做了改进),支持服务器可以对一个客户端请求发送多个响应。在原先的HTTP1.1中我们可能会将重要资源内联到网页中,减少网络延迟,这实际上等同于HTTP/2中的强制推送。在HTTP/2中的服务器推送还有下面一些功能:
- 推送的资源能被客户端缓存(服务器也只能推送可被缓存的资源)
- 在不同页面之间可以重用
- 可以由服务器设定优先级
- 可以被客户端拒绝
服务器推送数据流由PUSH_PROMISE帧发起,需要先于请求推送资源的响应数据传输。实现上的策略是先于父响应(即,DATA 帧)发送所有PUSH_PROMISE帧,其中包含所承诺资源的HTTP头部。客户端接收到PUSH_PROMISE帧后,它可以根据自身情况选择接受拒绝(通过RST_STREAM帧)数据流。(例如,如果资源已经位于缓存中)
客户端完全掌控服务器推送的使用方式。客户端可以限制并行推送的数据流数量;调整初始的流控制窗口以控制在数据流首次打开时推送的数据量;或完全停用服务器推送。这些在HTTP/2连接开始时通过SETTINGS帧传输,可能随时更新。
头部压缩
在HTTP1.1中,头部数据使用以纯文本的形式传输,所占空间较大,在使用HTTP Cookie后,更是会达到上千字节。为了减少此开销和提升性能,HTTP/2使用专门设计的HPACK压缩格式压缩请求和响应头部,这种格式通过静态霍夫曼编码对传输的头部字段进行编码。HPACK要求客户端和服务器同时维护和更新一个包含之前见过的标头字段的索引列表,利用之前传输值的索引列表,我们可以通过传输索引值的方式对重复值进行编码,索引值可用于有效查询和重构完整的头部键值对。
作为一种进一步优化方式,HPACK压缩上下文包含一个静态表和一个动态表:静态表在规范中定义,并提供了一个所有连接都可能使用的常用HTTP头部字段列表;动态表最初为空,将根据在特定连接内交换的值进行更新。
早期版本的HTTP/2和SPDY使用的DEFLATE对头部进行压缩,但是在2012年夏天出现了CRIME这种安全攻击。因此,之后HTTP/2的头部压缩采用了专门设计的HPACK方案。在使用HPACK后,初次访问后的压缩率能达到70%~80%,甚至90%+。
支持度与调试
支持度上,主流浏览器和服务器程序都已支持。你也可以访问这个网址体验HTTP/2和HTTP1.1在load大量图片时的延迟对比。
如果你想确认当前网页中的哪些请求是使用的HTTP/2,可以在chrome devTools下的network选项卡里查看“Protocol”列(未发现此列的可以在表头右键找到并勾选显示),其中HTTP/2将显示为h2。正如之前所说,支持HTTP/2的浏览器会和服务器使用特定协议协商,对于不支持HTTP/2的情况,会自动会退到HTTP1.1版本。
尽管HTTP/2使用二进制传输数据,然而浏览器为我们掩盖了实现细节。如果想要深入查看甚至是调试二进制分帧层的功能,如何去debug呢?
- Wireshark
- curl
- h2i,不过这个好像已经不维护了
TLS调优
目前各大浏览器只在https://
的基础上支持HTTP/2,即在TLS层之上的HTTP/2。多出的TLS的层也是会增加时延和成本的,具体涉及到的TLS握手、会话和加密套件协商过程还有优化空间,如减少证书层级、减少证书大小等。其余优化方向可以查看参考中一些文章介绍。
之前的优化还应该继续用么
继续保持的
- 减少DNS查询
- 减少域名
- 启用预读:dns-prefetch
- 使用CDN
- 避免重定向
- 资源压缩
- 代码压缩(JS、HTML、CSS)
- 资源压缩(图片、字体、音频、视频)
- 文本压缩(Gzip)
- 使用缓存
不再需要的
- 分片与域名散列:HTTP/2对于一个域名只使用一个TCP连接,分片反而会浪费资源,同时也会影响流控制、头部压缩的表现。
- 资源打包合并:HTTP/2支持多路复用,资源合并会降低缓存利用率,且会让开发流程更复杂。(snowpack了解一下)
- 资源内联:可以由服务器推送解决这类需求,资源内联一方面无法缓存,另一方面会让页面代码更大
常见问题
- Q: 既然HTTP/2是在SPDY工作基础上设计的,那HTTP/2推出后,SPDY还使用吗
- A: Google公开声明了他们会在2016年移除Chrome里对SPDY和NPN的支持,并且极力推动服务器迁移至HTTP/2。2016年2月他们声明了SPDY和NPN会在Chrome 51之后被移除。
- Q: 这个协议是否只对大型网站有效
- A: 由于缺乏内容分发网络,小网站的网络延迟往往较高,而多路复用的能力可以极大的改善在高网络延迟下的体验。
- Q: 基于TLS让速度变得更慢
- A: 正如上一节提到的,TLS的握手确实增加了额外的开销,也有越来越多的方案提出来减少TLS往返的时间。TLS同时也会更多消耗CPU等资源,更多例子可见istlsfastyet.com。不过一方面HTTP/2并不强制要求基于TLS,另一方面HTTP/2带来的性能提升使得即使基于TLS,通常也会比HTTP1.1更快
- Q: 为什么不使用文本传输
- A: 的确,如果可以直接读出协议内容,那么调试和追踪都会变得更为简单。但是二进制带来的灵活度更高,何况浏览器会自动帮你解析。
- Q: 看起来,HTTP1.1中的一些短板并没有改彻底
- A: 实际上,设计HTTP/2之初的目标就包括向前兼容HTTP/1.1的范式,来保证升级协议也不用重写底层的很多东西。老的HTTP功能,如协议头、状态码、可怕的Cookie,这些都保留了。性能优化更多通过增加了一个中间分帧层解决的。
- Q: 目前使用的广泛程度如何
- A: 在2015年年底大多数浏览器就已经支持HTTP/2,目前约96%的浏览器支持HTTP/2,同时约46%的网站支持HTTP/2。
后续
- 官网在给出HTTP/2规范时,也曾预计要10年时间和HTTP1.1并存,在这个期间,Web优化的思路也可能会有调整
- TLS1.3。2020年2月的RFC8740中给出了基于TLS1.3的HTTP/2的实现建议。
- 既然TCP容易遇到线头阻塞问题,那么能不用使用UDP呢?Google提出的QUIC(Quick UDP Internet Connection)它在很大程度上继承了SPDY的衣钵。QUIC可以理解成TCP + TLS + HTTP/2替代实现。
- 2018年10月,互联网工程任务组HTTP及QUIC工作小组正式将基于QUIC协议的HTTP(英语:HTTP over QUIC)重命名为HTTP/3以为确立下一代规范做准备。