干货丨HTTP2.0技术及应用解析
Posted 中兴开发者社区
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了干货丨HTTP2.0技术及应用解析相关的知识,希望对你有一定的参考价值。
导读:
南骏是开源软件爱好者,从事一线开发十多年,对网络技术,HTTP等都有深入理解。这篇文章讲述的HTTP/2协议尤其适合于大量小文件传输的场景。对于遇到此类HTTP性能瓶颈的朋友可以借鉴参考。
注:第三小节翻译和总结自RFC7540,请知悉。
1. 介绍
1.1 HTTP/1.1的问题
随着网站平均资源数的增长,HTTP/1.1暴露出诸多问题:TCP连接数过多,消耗资源;短连接,效率低;每个TCP连接三次握手增加延时;请求排队产生队头阻塞;头部冗余,使tcp慢启动的窗口阻塞……
为了克服这些缺点,曾经出现了各种变通技术:图片拼接、资源内联、资源分布式存储……但是这些技术也有各自的缺点:
▶图片拼接:在HTTP 1.1里,下载一张大图比下载100张小图快得多。因此网站将许多小图拼接成一张大图传输,由客户端切割来显示。缺点是如果只需要显示部分小图,也得传整张大图。
▶ 资源内联:将图片通过base64编码后直接嵌入CSS文件里。缺点同样也是如果一些图片不需要,也得全部传输;并且base64编码使资源字节数扩大。
▶资源拼接:将很多javascript拼接到一个文件里,特点同图片拼接。
▶资源分布式存储:将一个页面的各种资源分布到很多服务器上存储,使客户端分别对这些服务器建立连接并发下载。缺点一方面网站架设较复杂,另一方面虽然降低了单个服务器的连接数,但是过多的TCP连接对客户端、中间网元仍然造成压力。
HTTP/2的前身:谷歌开发了SPDY协议,很大程度上解决了上述问题,但是未被IETF标准化,支持厂商不多。
基于SPDY,IEFT提出了标准化的HTTP/2协议。HTTP/2协议已经由IEFT工作组在2015年标准化,到目前已经事实上取代了SPDY协议,得到各主流厂商的支持。
1.2 HTTP/2主要特性
▶多路复用特性减少了TCP连接数,服务器端允许更多的用户访问网站资源,提升服务器容量;减少对路由器、防火墙等中间网元资源占用,减少对其它网络流量的竞争;
▶流技术相对于HTTP/1.1的pipeline技术解决了队头阻塞问题。一条链路上不同流的请求和响应可以交错,一些流上资源响应慢或阻塞不会影响其它流;流支持优先级设置,可以使重要资源的传送占用更多带宽
▶支持服务端主动推送,减少请求次数,预填充客户端cache,提高网页加载速度
▶采用长连接,减少三次握手时间;避免TCP慢启动,有利于拥塞控制算法学习网络参数,提高TCP性能
▶支持头部压缩,提高了带宽利用率
▶采用二进制帧格式,提高协议处理效率
1.3 HTTP/1.1与HTTP/2的互操作性
HTTP/1.1与HTTP/2具有完全的互操作性。
在采用TLS情况下,采用ALPN扩展来进行协议协商:
客户端服务端
1.11.1采用HTTP/1.1
1.12.0服务端发现客户端未带ALPN扩展,采用HTTP/1.1
2.01.1服务端不识别ALPN扩展,客户端退回采用HTTP/1.1
2.02.0双方协商一致,使用HTTP/2.0
在采用纯TCP情况下,使用UPGRADE来进行协议协商:
客户端服务端
1.11.1采用HTTP/1.1
1.12.0服务端未收到UPGRADE头部,采用HTTP/1.1
2.01.1服务端不识别UPGRADE,报错误码,客户端退回采用HTTP/1.1
2.02.0双方协商一致,使用HTTP/2.0
2. 业界现状
▶目前国外全网已有5.2%的站点使用了HTTP/2、HTTPS流量有50%左右已使用HTTP/2。
▶ 主流HTTP客户端如Chrome、Curl、Firefox、IE/Edge都已经支持HTTP/2,大部分是基于TLS加密的HTTPS模式,不建议使用明文HTTP。
▶主流HTTP服务器如nginx、Apache、IIS、Tomcat都已经支持HTTP/2,大部分暂不支持主动推送模式。
▶网络抓包:Wireshark支持分析HTTP/2的包。
▶除了优先级区分、服务端主动推送功能目前应用还不广泛,其它特性已经有比较成熟的应用。
目前支持HTTP/2协议的项目包括各种语言共有约六七十个:
以下表格摘录自:https://github.com/http2/http2-spec/wiki/Implementations
3.协议详解
3.1 HTTP/2总览
与HTTP/1.1不同,HTTP/2定义了一个优化的传输层。语义层并未作改动,各个操作码、状态码不变,仍支持HTTP/1.1的核心功能。
基本协议单元:帧。帧类型:
▶HEADERS,DATA——组成HTTP请求和响应
▶SETTINGS,WINDOW_UPDATE,PUSH_PROMISE——支持HTTP/2的其他功能。
各个HTTP请求/响应归属于各自的流。流互相间很大程度上是独立的,一个流的阻塞不影响其他流。每个流上有流量控制和优先级,提高流复用的效率。
HTTP/2增加一种新的交互模式:服务端推送。服务端先用PUSH_PROMISE帧声明一个流,然后在这个流上主动发送响应给客户端。
头部压缩——允许多个请求压缩在一个包里。
3.2 启动HTTP/2
HTTP/2是TCP上层的应用层协议。和HTTP/1.1一样,使用http://和https://的URI,分别使用80和443端口。
HTTP/2版本标识:
h2: TLS上的HTTP/2
h2c: 明文TCP上的HTTP/2
▶明文TCP上启动HTTP/2
在不知道服务端是否支持HTTP/2的情况下,使用Upgrade机制。发送HTTP/1.1请求 ,带"Upgrade: h2c"头部,带HTTP2-Settings头部。
GET / HTTP/1.1
Host: server.example.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>
请求如果带内容体,必须在启动HTTP/2帧前一并发送内容。
如果第一个请求就想利用上HTTP/2的优点,可以先发送一个OPTIONS请求来执行upgrade。
收到第一个请求后,服务端如果不支持HTTP/2,则忽略upgrade头部,正常返回HTTP/1.1的响应。如果支持HTTP/2:先回101响应:
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: h2c
[空行]
[ HTTP/2 connection ...
服务器此后第一个HTTP/2帧必须是连接前缀(connection preface),含SETTINGS帧。客户端收到101后,必须发送包括SETTINGS帧的连接前缀。
upgrade前的首个HTTP/1.1请求认为是流1,默认是客户端到服务端半关闭状态(已发送请求,待响应)。建议在HTTP/2连接后,流1上发送响应。
upgrade的请求必须含HTTP2-Settings头部。内容就是SETTINGS帧的净荷的base64url编码。Connection头部必须含HTTP2-Settings作为connection option。
▶TLS上启动HTTP2
使用TLS 1.2版本的application-layer protocol negotiation(ALPN)扩展进行协商。客户端握手时,在TLS的ALPN扩展字段中带上自身支持的应用层协议列表,其中包括了“h2”即HTTP/2协议。服务端在TLS握手中获取这个标识,选择使用“h2”返回给客户端,告知选择HTTP/2协议。TLS握手完成后,双方发送连接前缀。
如果使用TLS1.1版本,也可以选择TLS的next protocol negotiation(NPN)扩展进行协商。NPN扩展是SPDY协议定义的,和ALPN不同之处在于它是由服务端给出支持的协议列表,由客户端选择使用的协议。
如果事先预知服务器可使用HTTP/2,TCP模式下,客户端直接发送连接前缀,然后发送HTTP/2帧;服务端发送连接前缀。TLS模式下,仍使用ALPN进行协商。
HTTP/2连接前缀:
服务端和客户端发送连接前缀作为使用HTTP/2协议,建立初始settings的最终确认。
客户端的连接前缀:PRI * HTTP/2.0 SM ,后面跟SETTINGS帧(净荷可以是空)。后面可以立即发送其他帧,不必等待服务端连接前缀。
服务端的连接前缀:SETTINGS帧(净荷可以是空)。
非启动过程中,单独出现连接前缀认为是连接错误。
3.3 HTTP/2帧格式
R:保留位
HTTP头部的处理:压缩成header block,然后拆分成若干个header block fragment,在HEADERS,PUSH_PROMISE,CONTINUATION帧净荷中发送。
Cookie头部使用HTTP mapping特殊处理(见后)。
完整的header block: HEADERS或PUSH_PROMISE帧,置END_HEADERS flag;或者HEADERS 或 PUSH_PROMISE 帧(未置END_HEADERS flag),+若干 CONTINUATION帧,最后一帧置END_HEADERS flag。
头部压缩是有状态的,压缩上下文使用于整个连接。由于头部压缩有状态,即使是可以丢弃的帧也要进行解压缩维持状态。
header block是一个整体,必须连续传送,不能和其他流交错,和其他帧类型交错。
3.4 流和复用
流的特点:
▶ 一个HTTP/2连接可以含多个同时打开的流,流之间可以帧交错;
▶流可以由服务端和客户端各自建立,共享;
▶ 流可以由任一方关闭;
▶流上的帧顺序是有保证的;
▶ 流的建立方给流分配整型序号。
流的生命周期:
send: endpoint sends this frame
recv: endpoint receives this frame
H: HEADERS frame (with implied CONTINUATIONs)
PP: PUSH_PROMISE frame (with implied CONTINUATIONs)
ES: END_STREAM flag
R: RST_STREAM frame
CONTINUATION帧不导致状态变迁,仅是HEADERS 或 PUSH_PROMISE 帧的后续部分。
PUSH_PROMISE帧在现有的流发送(即必须和一个客户端发起的请求相联系),使用Promised Stream ID字段标识新流。
closed状态仍可发送PRIORITY帧,用以调整优先级关系。
发送RST_STREAM帧时,对方可能已经在该流上发送了帧,因此接收方收到后必须忽略,
不认为错误。
RST_STREAM后收到PUSH_PROMISE也有效。如果不想要一个PUSH_PROMISE的流,需要在PROMISE的新流上发送RST_STREAM拒绝这个流。
客户端发起的流用奇数流号,服务端发起的流用偶数流号。流号0用于连接控制。
HTTP1.1的upgrade请求分配流号1。upgrade完成后,对客户端来说流1处于"half-closed (local)"状态。
流号必须递增。新流号的使用隐含关闭了所有低于此号的处于idle状态的流。
流号不能重用。31位的流号用尽后客户端可以重建tcp连接,服务端可以发送GOAWAY帧使客户端重建tcp连接。
WINDOW_UPDATE帧进行流量控制。流量控制可以对流,也可以对整个连接。
流量控制的特点:
▶ 针对单跳连接(客户端与proxy,proxy与服务端,等等),而不是整个端到端路径。
▶基于WINDOW_UPDATE帧。
接收端完全控制。客户端,服务端,中间网元各自独立宣告它的流控窗口,并遵守它上游的流控限制。
l 初始流控窗口65535字节。
l 只有DATA帧遵从流量控制,其他帧不占用流控窗口大小。
l 流控不能关闭。
l HTTP/2只定义WINDOW_UPDATE帧。流控算法由实现根据需要选择。
基于优先级的请求/响应发送策略由实现选择。
流优先级:
在打开流的HEADERS帧中嵌入优先级。在使用过程中可以用PRIORITY帧进行修改。
流的依赖关系形成依赖树。0号流是根节点。
依赖于同一流的兄弟流之间使用权重(1~256)按比例分配资源。
可以使用PROORITY帧调整优先级。流的依赖关系从依赖树上调整,它的子流也跟着调整。如果设置了"exclusive"标志,流所移动到的新的被依赖流下面原来的依赖流,调整到新流的下面。
流默认为非exclusive地依赖于0号流。PUSH流依赖于他所联系的流(PUSH_PROMISE本身所在的流)。流默认权重为16。
错误处理:
l 连接错误:发送GOAWAY帧,关闭tcp连接;
l 流错误:发送RST_STREAM帧,此后不能再发送任何帧。如果一个RTT后还收到数据,可以再发RST_STREAM。
HTTP/2扩展:
只对一个HTTP/2连接有效。
扩展包括:新的帧类型,新的settings值,新的错误码。
一个header block中间不允许有扩展。影响现有语义的扩展必须先协商(目前暂未定义,可能使用setting来完成)。
3.5帧定义
3.5.1 DATA帧(type=0x0)
可选标志:END_STREAM,PADDED(标志置位时,才有Pad Length字段)
3.5.2HEADERS帧(type=0x1)
E:exclusive依赖
只在PRIORITY标志置位时才有E,Stream Dependency,Weight字段。
可选标志:END_STREAM,END_HEADERS,PADDED,PRIORITY。
3.5.3PRIORITY帧(type=0x2)
3.5.4 RST_STREAM帧(type=0x3)
3.5.5SETTINGS帧(type=0x4)
净荷为0或多个identifier-value对。
SETTINGS帧只针对连接,不针对流。
可选标志:ACK(标志收到并确认对方的SETTINGS帧,净荷必须为空)。
已定义的参数:SETTINGS_HEADER_TABLE_SIZE、SETTINGS_ENABLE_PUSH、SETTINGS_MAX_CONCURRENT_STREAMS、SETTINGS_INITIAL_WINDOW_SIZE、SETTINGS_MAX_FRAME_SIZE、SETTINGS_MAX_HEADER_LIST_SIZE。
收到SETTINGS帧并处理后,必须立即返回SETTINGS帧并带ACK标志。在合理的时间内没有收到SETTINGS的ACK,认为连接错误。
3.5.6PUSH_PROMISE帧(type=0x5)
可选标志:END_HEADERS,PADDED。
接收方可以用RST_STREAM拒绝promise的流。
3.5.7PING帧(type=0x6)
接收端必须返回PING,带上ACK标志、同样的净荷。
PING优先级最高。
可选标志:ACK。
3.5.8GOAWAY帧(type=0x7)
发送GOAWAY时,允许现有的流继续处理完,拒绝新流。
接收方也应该发送一个GOAWAY再关闭链路。
如果发送多次GOAWAY, Last-Stream-ID不应该增长。大于Last-Stream-ID的流,接收方可另发起连接进行重试。
在关闭连接时,服务端可以先发送Last-Stream-ID=2^31-1来拒绝新的流,然后等待至少一个RTT时间。这期间有可能收到客户端在收到GOAWAY前新建的流。服务端处理完这些新建的流,然后发送Last-Stream-ID=所处理的最大流号。这样可以防止请求丢失。
发送GOAWAY后,仍需对收到的HEADERS/PUSH_PROMISE/CONTINUATION帧进行解压处理、DATA帧进行流量控制。其他帧可以忽略。
3.5.9 WINDOW_UPDATE帧(type=0x8)
流量控制是逐跳的。中间网元不前转WINDOW_UPDATE帧。下游的流控间接影响上游。
流控仅对DATA帧有效,不包括9字节的帧头。
可以针对流或者整个连接进行流控。
接收方处理完数据后发送WINDOW_UPDATE,流和连接层面上分别发送。
SETTINGS_INITIAL_WINDOW_SIZE设置初始窗口大小。由于在此之前可能已经发送过数据,窗口可能为负。接收方需要等待WINDOW_UPDATE才能继续发送数据。
3.5.10CONTINUATION帧(type=0x9)
净荷为header block fragment
可选标志:END_HEADERS。
3.6 错误码
NO_ERROR
PROTOCOL_ERROR
INTERNAL_ERROR
FLOW_CONTROL_ERROR
SETTINGS_TIMEOUT
STREAM_CLOSED
FRAME_SIZE_ERROR
REFUSED_STREAM
CANCEL
COMPRESSION_ERROR
CONNECT_ERROR
ENHANCE_YOUR_CALM(大量PUSH消息,大量SETTINGS、小帧、头部压缩等疑似DOS攻击时发)
INADEQUATE_SECURITY
HTTP_1_1_REQUIRED
3.7 HTTP消息交互过程
一个HTTP消息包括:
零或更多个HEADERS(可以跟CONTINUATION)的1xx响应(仅对HTTP响应消息而言) + 一个HEADERS(可以跟CONTINUATION) + 零或多个DATA + 可选的一个HEADERS(可以跟CONTINUATION)(内含trailer-part)
最后一帧含END_STREAM标志。如果最后是HEADERS+CONTINUATION,END_STREAM标志置于HEADERS帧。
HTTP/2不使用chunked编码。
HEADERS帧只能出现在流起始和结束。一个流只能有一对HTTP请求/响应。
响应可能在请求发完之前就发出,并且可以发送RST_STREAM取消请求的发送。
HTTP/2没有101(Switching Protocols)状态码。如果有其他协议需要协商,可以从HTTP/1.1的upgrade开始。
HTTP/2头部字段名使用小写字母。
HTTP/2使用伪头部来代替HTTP/1.1的起始行,伪头部必须在真正的头部之前。伪头部有:":method",":scheme",":authority",":path",":status",没有提供HTTP版本号的伪头部。
HTTP/2没有Connection-Specific头部(除"TE: trailers"外)。例如Keep-Alive,Proxy-Connection,Transfer-Encoding,Upgrade,这些都没有。
cookie头部可以拆分成多个头部,以便提高压缩效率:
cookie: a=b; c=d; e=f 等效于
cookie: a=b
cookie: c=d
cookie: e=f
HTTP/2对请求/响应格式要求更严格,畸形的请求被认为协议错误,以防止一些HTTP攻击。
例1:
GET /resource HTTP/1.1 HEADERS
Host: example.org ==> + END_STREAM
Accept: image/jpeg + END_HEADERS
:method = GET
:scheme = https
:path = /resource
host = example.org
accept = image/jpeg
例2:
HTTP/1.1 304 Not Modified HEADERS
ETag: "xyzzy" ==> + END_STREAM
Expires: Thu, 23 Jan ... + END_HEADERS
:status = 304
etag = "xyzzy"
expires = Thu, 23 Jan ...
例3:
POST /resource HTTP/1.1 HEADERS
Host: example.org ==> - END_STREAM
Content-Type: image/jpeg - END_HEADERS
Content-Length: 123 :method = POST
:path = /resource
{binary data} :scheme = https
CONTINUATION
+ END_HEADERS
content-type = image/jpeg
host = example.org
content-length = 123
DATA
+ END_STREAM
{binary data}
例4:
HTTP/1.1 200 OK HEADERS
Content-Type: image/jpeg ==> - END_STREAM
Content-Length: 123 + END_HEADERS
:status = 200
{binary data} content-type = image/jpeg
content-length = 123
DATA
+ END_STREAM
{binary data}
例5:
HTTP/1.1 100 Continue HEADERS
Extension-Field: bar ==> - END_STREAM
+ END_HEADERS
:status = 100
extension-field = bar
HTTP/1.1 200 OK HEADERS
Content-Type: image/jpeg ==> - END_STREAM
Transfer-Encoding: chunked + END_HEADERS
Trailer: Foo :status = 200
content-length = 123
123 content-type = image/jpeg
{binary data} trailer = Foo
0
Foo: bar DATA
- END_STREAM
{binary data}
HEADERS
+ END_STREAM
+ END_HEADERS
foo = bar
哪些情况下可以确认请求未被处理,可以安全地进行重试(请求一旦交给应用处理,就不能认为该请求未处理了,即使应用层没有处理。):
l GOAWAY帧给出了最大可能已处理的流号。更大流号的请求肯定未处理,可以安全进行重试;
RST_STREAM中给出REFUSED_STREAM错误码的,确认流在处理前已经关闭。该请求
l 可以安全重试。
PING帧可以安全地测试连接是否存活。
中间网元例如proxy可以不向下游转发PUSH,也可以自己发起向下游发PUSH。
PUSH请求和普通请求语义是一样的,区别就是请求也是由服务器通过PUSH_PROMISE发给客户端。请求有body的不能PUSH。PUSH请求必须是安全和可缓存的。
服务器必须先发PUSH_PROMISE请求,再发含这些请求的响应体,以避免客户端重复请求。
客户端不能发PUSH_PROMISE。
客户端可以RST_STREAM一个PUSH流,错误码可以是CANCEL或REFUSED_STREAM。
客户端要对服务端的PUSH请求鉴权,即确认PUSH的请求确实属于该服务器。
CONNECT方法:
可以使用一个流作为隧道,传输普通TCP数据。
":method: CONNECT",无":scheme"和":path"头部,":authority"含host和port号。
proxy根据请求向目的建立tcp连接,然后以HEADERS帧回复2xx响应。后续为DATA帧。END_STREAM标志对应于TCP的FIN。RST_STREAM对应于TCP连接错误。
3.8其它HTTP要求和考虑
连接管理:
HTTP/2连接为长连接。除非再无通信,否则就不关闭。客户端和一个服务端只建立一条连接。
流号用完、刷新TLS的keying material,或者遇到错误时,才新建连接。
使用多个Server Name,或者不同的客户端证书时,可以并发多个连接。同样的ServerName、IP-Port、证书的连接尽量复用(如果服务器发现复用连接上的请求不在本服务器,返回421响应)。
使用HTTP/2 proxy的,客户端复用到proxy的一条链路。
关闭连接应该先发GOAWAY。
使用TLS的,必须使用TLS 1.2或更高版本;必须支持Server Name Indication(SNI)扩展。SNI扩展用于一个TLS监听端口支持多个虚拟服务器的场景。当客户端发起TLS连接时,在TLS的SNI扩展字段中指示了本连接需要连接哪个虚拟服务器(Server Name),以便服务端选择使用适当的服务端证书进行握手。
使用TLS的,必须不使用压缩。压缩会导致信息有一定规律,容易被解密。并且HTTP/2已经有头部压缩了。
使用TLS的,必须禁用renegotiation(在connection preface之前可以)。服务端如果发现renegotiation,应该校验客户端的证书。
HTTP/2定义了一组加密算法黑名单,这些算法安全等级较低。选择其中的算法可能被认为是连接错误(错误码INADEQUATE_SECURITY)。
3.9安全性考虑
TCP上的HTTP2易受cross-protocol攻击,TLS上不存在这个问题。
HTTP/2允许头部字段名、值不符合HTTP/1.1规范,导致中间网元从HTTP/2翻译到HTTP/1.1是可能认为消息畸形。
2. 应用
目前HTTP/2在MOA产品中实现了客户端功能。
苹果的ios推送提醒服务(APNS)新版协议基于HTTP/2和JSON,可以对每个通知得到其是否成功的响应,而旧版的二进制协议则无法得到成功的确认。因此,为了使用新版APNS协议,我们在MOA产品中开发了对HTTP/2的支持。
从协议复杂度看,从零开始实现一个HTTP/2工作量比较大。想要在短期内推出产品,推荐采用开源实现。鉴于我们的软件是C语言开发的,我们需要寻求一款C语言环境下的HTTP/2支持库。Nghttp2是目前Linux/C语言环境下最活跃、成熟度最高、支持功能最全的开源项目,很多主流软件例如Curl就是采用了Nghttp2。它提供包括客户端/服务端/Proxy功能的动态库,以及功能较全面的客户端/服务端/Proxy命令行程序和性能测试程序;采用MIT License,可集成到商用产品中。我们基于此开源项目在自研软件HTTPPro中实现了HTTP/2客户端,应用于MOA产品中,与Apple的APNS服务器互通。
Nghttp2开源社区于2017.4.24号发布最新V1.22.0版本。
3. 推广建议及后续发展
HTTP/2协议最适合的应用场景是需要下载大量小文件的场景。因此,在我们的产品中如果有用到大量下载小文件的场合,建议推广使用HTTP/2。
协议的后续发展:
由于HTTP/2建立在TCP/TLS基础上,连接建立包含了TCP三次握手以及TLS握手,时延较大。另外由于TCP层不了解HTTP/2层的流,出现丢包时只能阻塞整个TCP流,不能单独阻塞发生丢包的HTTP/2流。为了解决这些问题,谷歌提出了QUIC协议。该协议建立在UDP之上,同样提供了流复用、可靠性保证、拥塞控制、加密、头部压缩等功能;该协议是从零开始重新设计的,从握手一开始就能传输数据,减小了RTT时延;该协议发生丢包时可以仅阻塞丢包所在的流,对其它流没有影响。QUIC协议目前还处在IETF工作组研究草案阶段。
以上是关于干货丨HTTP2.0技术及应用解析的主要内容,如果未能解决你的问题,请参考以下文章