John Jiang

a cup of Java, cheers!
https://github.com/johnshajiang/blog

   :: 首页 ::  :: 联系 :: 聚合  :: 管理 ::
  131 随笔 :: 1 文章 :: 530 评论 :: 0 Trackbacks
探索HTTP/2: HTTP/2协议简述
HTTP/2的协议包含着两个RFC:Hypertext Transfer Protocol Version 2 (RFC7540),即HTTP/2;HPACK: Header Compression for HTTP/2 (RFC7541),即HPACK。RFC7540描述了HTTP/2的语义,RFC7541则描述了用于HTTP/2的头部压缩的格式。本文只涉及HTTP/2协议,本系列的后续文章将会涉及HPACK协议。(2016.10.13最后更新)

1. HTTP/2要解决的问题
     HTTP/1.0只允许在一个TCP连接中出现一个请求。后来的HTTP/1.1虽然引入了请求流水线,以允许在一个连接中发送多个请求,但这只是部分地解决了请求并发的问题。服务器端在返回响应时,还是必须要按照它接收到的请求的顺序进行返回。如果排在前面的响应要消耗较长的时间,那依然会对后面的响应的造成阻塞,亦即线头阻塞(Head-of-line blocking)。所以,客户端必须要使用多条连接去发起多个的请求以实现并发,并进而减小延迟。更大的并发会增大服务器的负载,也会占用更大的网络带宽。另外,头部通常会包含有大量的信息,如cookie,而这也会增加网络传输的开销。
     HTTP/2允许在同一个TCP连接中交错地出现多个请求与响应,亦即多工(Multiplex)。同时,它使用了一个高效的编码方法对头部进行压缩。HTTP/2还允许对请求进行优先级排序,以便让更为重要的请求得以更快的完成,这会进一步提高性能。HTTP/2还改变了服务器端只能被动地向客户端返回响应的定式,允许服务器端主动地向客户端推送数据,这就可以减少客户端发起请求的数量。
     总之,HTTP/2主要是解决性能问题。

2. 发起HTTP/2
     HTTP/2会使用与HTTP/1相同的URI scheme,即http和https。而且实现HTTP/2的服务器端也不会使用不同的端口去分别支持HTTP/1和HTTP/2。这样有利于平滑地从HTTP/1升级到HTTP/2。毕竟目前已部署的绝大部分网络服务都只支持HTTP/1,当未来它们升级到HTTP/2时,如果换用了不同URI scheme或端口,那么肯定会对客户端产生极大的影响。但是HTTP/2协议为运行在http和https上的HTTP/2分别定义了两个不同的标识符:h2c和h2。h2c中的"c"指的是cleartext,即明文。本文后面会使用h2c指代运行在http2上(直接使用TCP)的HTTP/2,而用h2指代运行在https上(使用TLS)的HTTP/2。
     那么,支持HTTP/2的客户端如何知道它所连接的服务器端是否也支持HTTP/2呢?
     对于h2c,支持HTTP/2的客户端可以在发起的请求中使用HTTP/1.1的Upgrade头部去尝试要求服务器升级到HTTP/2。该请求的格式如下:
GET / HTTP/1.1
Host: server.example.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>
HTTP2-Settings是一个经由BASE64编码过的字符串,其原始内容是客户端将要发送的SETTINGS帧的载荷,即一些配置参数。
     如果服务器端支持HTTP/2,它就响应"101 Switching Protocols",表示可以进行升级。该响应的格式如下:
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: h2c
     如果服务器端不支持HTTP/2,则会忽略Upgrade请求头部,后续依然使用HTTP/1.1。
     对于h2,会使用到协议Transport Layer Security (TLS) Application-Layer Protocol Negotiation Extension (RFC7301),即TLS-ALPN。该协议允许客户端和服务器端就使用何种版本的HTTP进行协商。如果TLS-ALPN在现实中运行良好的话,也许某天还会使用该方法去协商使用别的协议。
     当客户端与服务器端都同意使用HTTP/2时,双方都需要各自发出一个连接序言(Connection Preface)以进行最后的确认。
     客户端在接收到服务器端的"101 Switching Protocols"响应(针对h2c)或TLS连接的第一个应用数据字节(针对h2)之后会立即发出连接序言。该序言的开头是"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"(其十六进制形式为"0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a")(1),后面必须再跟一个SETTINGS帧,哪怕这个帧是空的。
     服务器端的连接序言则由一个SETTINGS帧构成,该帧必须是服务器端在HTTP/2连接中发送的第一个帧。这个SETTINGS帧可以为空,也可以包含一些希望客户端如何与自己进行通信的必要配置信息。

3. 帧(Frame)
     HTTP/2消息使用二进制格式(实际编码时使用十六进制书写),相比于文本格式,这样可以提高消息处理的效率。HTTP/2消息的最小单元为帧,它由头部与载荷(Payload)组成。每个帧的长度必须是一个或多个8比特位字节(octet,下文将其简写为"字节")。
     帧头部依次包含有如下的5个字段:
     长度(Length):该字段占用24个比特位,代表帧载荷的长度。该长度是一个24位的无符号整数。
     类型(Type):该字段占用8个比特位,代表帧的类型。
     标志(Flags):该字段占用8个比特位,代表帧所定义的一个或多个标志。并不是所有的帧都定义了标志。
     保留位(R):该字段占用1个比特位,其语义尚未被定义。在读取帧时,该位需要被忽略;但在发送帧时,该位需要保持为0(0x0)。
     流标识符(Stream Identifier):该字段占用31个比特位,代表该帧所在流的标识符。
     在头部之后,紧接着的就是载荷。载荷的结构与内容完全由帧的类型决定,它的长度也是不定的。

     HTTP/2定义了如下10种不同类型的帧。
     DATA:用于携带一组长度不定的字节。一个或多个DATA可作为请求或响应的载荷。
     HEADERS:用于开启一个流,并可携带一个头部块片断。头部块指由一个HEADERS/PUSH_PROMISE帧和紧随它的零到多个CONTINUATION帧组成的集合,因为只有它们才可能携带头部信息。这个集合可被分割成一个或一组字节,这样的字节被称为头部块片断。头部块中各个特定类型的帧必须紧紧相邻,不能出现其它类型的帧。
     PRIORITY:用于指定发送端建议的流优先级。
     RST_STREAM:用于立即终止流。当希望取消一个流或发生错误时,就可发送RST_STREAM帧。
     SETTINGS:用于携带可以影响两端之间通信方式的配置参数。SETTINGS帧定义了一个ACK标志,用于指示该帧所设置的参数是否已被接收端获知。当收到一个SETTINGS且其中的ACK标志为0时,接收端必须尽可能快的应用其中已被更新的参数。
     PUSH_PROMISE:用于向接收端通知发送端将要创建的流。当接收端接收到该帧时,新的流尚未被发送端创建,但发送端承诺会创建该流。该帧用于实现HTTP/2的重要特性"服务器端推送(Server Push)"。
     PING:用于测量发送端与接收端之间的最小往返时间。这与使用众所周知的ping命令的目的相似,是为了测试某个空闲的连接是否还可用。
     GOAWAY:用于发起对连接的关闭,或触发严重的错误条件。该帧允许一端,在完成对之前已创建的流的处理的同时,优雅地停止接收新的流。一端在创建新的流,另一端在发送GOAWAY,这两者之间天然存在着竞争关系。为了就对这种情况,发送端在发送GOAWAY时会让它携带上(该发送端所知晓的)接收端最后创建的流的标识符,当该GOAWAY被发送之后,发送端将会忽视由接收端创建的任何一个标识符比该标识符大的流。
     WINDOW_UPDATE:用于流量控制。该帧的载荷由一个单比特保留位和一个31比特位的无符号整数组成。该整数向该帧的接收端指示了其向当前流量控制窗口所能增加传输量的值。
     CONTINUATION:用于继续发送头部块片断。只要同一个流中前面的帧是HEADERS,PUSH_PROMISE或CONTINUATION,并且该帧没有设置END_HEADERS标志,那么可无限量地发送CONTINUATION帧。
     部分帧,DATA,HEADERS和PUSH_PROMISE,的载荷中可能包含填白(Padding)。填白在业务上没有实际的用处,它的出现是基于安全目的。比如,可以用它来扰乱实际数据的长度,以减轻特定的HTTP攻击。

     发送端发送的帧的最大长度要尊重接收端设定的SETTINGS_MAX_FRAME_SIZE的值。但该值的范围要介于2^14至2^24-1个字节之间。

4. 流(Stream)
     流是用于在客户端与服务器端之间进行帧传送的通道,同一个TCP连接中可以同时有多个流,如下图所示,
┌────────┐          Connection           ┌────────┐
│        │ ============================= │        │
│        │    --------------------- <-- Stream    │
│        │    ┌─────┐┌─────────┐┌─┐      │        │
│        │    └─────┘└─────────┘└─┘ <-- Frame     │
│        │    ---------------------      │        │
│ Client │                               │ Server │
│        │    ----------                 │        │
│        │    ┌──┐┌────┐                 │        │
│        │    └──┘└────┘                 │        │
│        │    ----------                 │        │
│        │ ============================= │        │
└────────┘                               └────────┘
服务器端和客户端可以交错地向同一个连接中的不同流中传送帧。可以把一个流看作HTTP/1中的一个连接。客户端与服务器端在同一个流中的交互依然遵循发送请求-等待响应模式。两端都可以创建新的流,共享对方创建的流,也可以关闭对方创建的流。帧在流中的顺序是有意义的,接收端会以接收到的顺序去处理帧。
     每个流都有一个标识符,是一个31比特位的无符合整数。在同一个连接中,流标识符是唯一的。由客户端创建的流的标识符为奇数,由服务器创建的流的标识符为偶数。但标识符为0的流可看作连接,用于连接控制信息,创建新的流时不可使用该标识符。同一个连接中的任何一个流的标识符都不可重用,即便这个流已被关闭了。对于长时间没有中断的连接,可能会出现标识符不够用的情况,那时就必须强制创建一个新的连接。
     HTTP/2协议为流的生命周期定义了7种状态(2):idle,reserved(local),reserved(remote),open,half closed(local),half closed(remote)和closed。当一端接收或发送头部块或(帧DATA和HEADERS的)标志RST_STREAM后可使流的状态发生转变。
     使用流来实现多工就会引起对TCP连接使用的竞争,这会造成流的阻塞。基于帧WINDOW_UPDATE的流量控制方案可以确保相同连接中的流相互之间不会产生破坏性干扰。流量控制可以作用于两个层面,即单个流或整个连接。只有帧DATA需要遵守流量控制,所有其它的帧所有消耗的空间均不会占用流量控制窗口。HTTP/2协议只是定义了WINDOW_UPDATE帧的结构和语义,协议的实现可以选择任何适用自己的流量控制算法。
     流可以有优先级。客户端在创建一个新的流时,可在HEADERS中指定优先级权重。在后续任何时间,通过PRIORITY可以改变流的优先级权重。在并发能力有限的情况下,高权重流的帧会被优先传送。权重的值必须介于1至256之间,默认权重为16。流与流之间还可以有依赖关系,这种关系会组成一棵依赖关系树。一个流能够指定自己成为另一个流的子流。这一过程,可以是非排他的,也可以是排他的。非排他性依赖,是指一个流在将自己变成另一个流的子流的过程中,允许另一个流还有别的子流,即允许有自己的兄弟流存在。排他性依赖,指在前述过程中,不允许另一个流还有别的子流。如果另一个流已经有子流了,那么该流会把所有潜在的兄弟流先变成自己的子流,然后再使自己成为另一个流的唯一子流。其实,排他性依赖的作用就是为了能够打破已有的关系树,在既成的父子节点中插入新的节点。否则,只能为已有节点添加子节点,那么关系树将不可能进行重构。所有的流在被创建时,默认成为标识符为0x0的流的子流。在"服务器端推送"中生成的"推送"流将自动地成为生成该推送流的流的子流,其默认权重也为16。

5. 消息交换
5.1 请求/响应交换
     HTTP/2沿袭了HTTP/1的语义,即所有的请求与响应语义均得到了保留,尽管传递这些语义的语法已经改变了。
     一个HTTP/2消息由如下几个部分组成:
     [1]仅对于响应消息,可以包含一个携带有1xx响应头部的头部块。该头部块由一个HEADERS帧和紧随它的零到多个CONTINUATION帧组成。
     [2]一个头部块。该头部块由一个HEADERS帧和紧随它的零到多个CONTINUATION帧组成。
     [3]零到多个携带有体部(Body)消息的DATA帧。HTTP/1中使用的"分块(chunked)"体部将不适用于HTTP/2。因为一个体部可由多个DATA帧组成,所以HTTP/2的体部天然就是可分块的。
     [4]一个可能存在的包含着尾部消息的头部块。该头部块由一个HEADERS帧和紧随它的零到多个CONTINUATION帧组成。

     HTTP/2仍然沿用HTTP/1中的头部字段,但字段名称中的字母必须全部为小写。另外,还将HTTP/1消息开始行(请求中的请求行与响应中的状态行)中的消息,分解成了若干伪头部字段,此类字段均以冒号(:)开头。
     HTTP/1请求行格式为"method request-target HTTP-version",对应的HTTP/2伪头部字段有:method=method和:path=request-target,但HTTP-version无对应字段,默认为HTTP/2。
     HTTP/1状态行格式为"HTTP-version status-code reason-phrase",对应的HTTP/2伪头部字段有:status=status-code。但HTTP-version无对应字段,默认为HTTP/2;reason-phrase也无对应字段,因为可以通过状态代码查找到其对应的reason-phrase。HTTP/2协议是在尽量减少冗余消息。
     HTTP/2协议还为请求头部定义了另外两个伪字段:
     :scheme:URI中的scheme部分。它可以不仅仅是http或https,因为有时候可能会与非HTTP服务进行交互。
     :authority:URI中的授权部分。即,scheme://user:password@host:port/path?query#fragment中的"user:password@host:port"。
     HTTP/2协议8.1.3节中给出一些简单示例,展示了如何将HTTP/1消息对应到HTTP/2消息。
5.2 服务器端推送
     HTTP/2的服务器端推送是传统的请求/响应模式的一种特殊形式。服务器端在收到客户端的请求(主请求)之后,为了主动向客户端推送更多的内容,会自动地生成若干新的请求(推送请求)。服务器向客户端发送的响应中,不仅包含对主请求的响应(主响应),还包含对推送请求的响应(推送响应)。
     客户端可以通过发送包含有SETTINGS_MAX_CONCURRENT_STREAMS参数的SETTINGS帧去禁用服务器端推送,也可以通过发送RST_STREAM帧去取消已经发起的服务器端推送,但不能发送包含有END_STREAM标志的帧。

(1)"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"中的"PRI"与"SM"合起来就是"RRISM(棱镜)"。呵呵,HTTPbis工作组这是想表达什么意思呢 ;-)
(2)本系列的后续文章解读了流的状态。
posted on 2016-09-19 11:36 John Jiang 阅读(2648) 评论(0)  编辑  收藏 所属分类: 原创HTTP/2探索HTTP/2

只有注册用户登录后才能发表评论。


网站导航: