(十) HTTP/2的消息交换

HTTP/2的目的是尽可能地兼容目前正在使用的HTTP协议。这意味着,从应用的角度来看,协议的大部分特性并没有改变。为了实现这个目标,所有请求和响应的语义都被保留,尽管表达这些语义的语法已经变化。

因此,HTTP1.1协议中“语义和内容”、“有条件的请求”、“范围请求”、“缓存”和“认证”等规范和要求同样适用于HTTP/2。对于HTTP1.1“消息语法”和“路由”的选定部分,例如HTTP和HTTPS的URI方案,也仍然适用于HTTP/2,但是这些语义的表达方式将在下面定义。

HTTP请求/响应交换

客户端在一个新的流上发送HTTP请求,使用之前未使用过的流标识。服务端在相同的流上发送HTTP响应。

HTTP消息(请求或响应)由以下几部分构成:

  1. 仅对响应消息而言,0个或多个HEADERS帧(后面跟随0个或多个CONTINUATION帧),包含信息性HTTP响应的报头(1xx)。
  2. 一个报头帧(后面跟随0个或多个CONTINUATION帧),包含报头。
  3. 0个或多个DATA帧,包含有效载荷体。
  4. 可选的,一个HEADER帧,后面跟随0个或多个CONTINUATION帧(如果存在,那么包含尾部部分)。

序列中的最后一帧设置了END_STREAM标志位,这表明设置了END_STREAM标志位的HEADERS帧后面可以跟随着携带报头块的剩余部分的CONTINUATION帧。

其它帧(来自任意流)都不能出现在HEADERS帧和其后可能跟随的CONTINUATION帧之间。

HTTP/2使用DATA帧来携带有效载荷。在HTTP/2中一定不能使用“分块”传输编码。

尾部头字段在报头块中携带,这个报头块也会终止流。这样一个报头块是一个以HEADERS帧开头的序列,后面跟随0个或多个CONTINUATION帧,其中,HEADERS帧中设置了END_STREAM标志位。第一个报头块后面的不终止流的报头块不是当前HTTP请求或响应的一部分。

一个HEADERS帧(包括关联的CONTINUATION帧)只能出现在流的开头和结尾。在收到一个最终(非信息)状态码后,端点接收到未设置END_STREAM标志位的HEADERS帧时,必须将相应的请求和响应视为格式异常。

一个HTTP请求/响应交换完全地消耗掉单个流。一个请求以HEADERS帧开始,使流进入“打开”状态。这个请求最终以一个设置了END_STREAM标识位的帧结束,对于客户端来说,流进入“半关闭(本地)”状态;对于服务端来说,流进入“半关闭(远程)”状态。一个响应以一个HEADERS帧开始,并且,以一个设置了END_STREAM标识位的帧结束,最后的这一帧使流进入“关闭”状态。

在服务端发送完(或客户端接收完)一个设置了END_STREAM标志位的帧(也包含任意构成完整报头块的CONTINUATION帧)之后,HTTP响应就完全结束了。服务端可以在客户端发送完整个请求之前就发送一个完整响应,前提是这个响应不依赖于请求的任何未发送或未接收的部分。在满足这个条件时,服务端可能会在发送完整个响应之后,通过发送一个错误码是NO_ERROR的RST_STREAM帧,来要求客户端终止请求的传输过程并且不抛出任何错误。当收到这样一个RST_STREAM帧时,客户端一定不能丢弃响应;尽管客户端总是可以根据其它原因自由裁决丢弃响应。

从HTTP/2升级

HTTP/2移除了对101(切换协议)信息状态码的支持。

101(切换协议)的语义不适用于一个多路复用协议。替代协议可以使用与HTTP/2相同的机制来协商它们的使用。

HTTP报头字段

HTTP报头字段以一系列key-value对的形式携带信息。

和在HTTP/1.x中一样,报头字段名是ASCII字符组成的字符串。在HTTP/1.x中,报头字段名是以不区分大小写的方式进行比较的。然而,在HTTP/2中,报头字段必须在编码之前先转换为小写形式。一个包含大小字母报头字段名的请求或相应必须被视为格式异常的。

伪报头字段

HTTP/1.x以消息开始行来传输目标URI、请求方法、相应状态码等信息。HTTP/2使用特殊的以英文冒号开头的伪报头字段来达到相同的目的。

伪报头字段不是HTTP报头字段。端点不能使用未定义的伪报头字段。

伪报头字段只在定义它们的上下文中有效。在请求中定义的伪报头字段一定不能出现在响应中,反之,在响应中定义的伪报头字段一定不能出现在请求中。伪报头字段一定不能出现在尾部中。如果请求或响应中包含了未定义或无效的伪报头字段,那么端点一定要将其视为格式异常的。

在报头块中,所有的伪报头字段都应该出现在正式的报头字段之前。任何请求或响应的报头块中,如果伪报头字段出现在正式报头字段之后,那么都应该被视为格式异常的。

连接特定的报头字段

HTTP/2不使用Connection字段来表明连接特定的报头字段,而是以其它方式来传达连接特定的元信息。端点不能生成包含连接特定的报头字段的HTTP/2消息。任何包含连接特定报头字段的消息都可以被视为格式异常的。

唯一的例外是TE报头字段,它可以出现在HTTP/2的请求中,TE字段的值只能是”trailers”。

这意味着,将HTTP/1.x消息转换为HTTP/2消息的中间代理需要移除Connection字段中列举的全部报头字段以及Connection报头字段自身。这种中间代理也应该移除其它连接特定的报头字段,例如:Keep-Alive、 Proxy-Connection、Transfer-Encoding和Upgrade,即使它们没有被Connection报头字段列举出来。

注意:HTTP/2故意不支持升级到其它协议。HTTP/2的握手方法足够用了协商其它协议的使用。

请求伪报头字段

以下伪报头字段为HTTP/2请求定义:

  • “:method”包含HTTP请求方法。
  • “:scheme”包含目标URI的scheme部分。”:scheme”并不限于”http”或”https”。代理或网关可以为非HTTP的scheme翻译请求,使得HTTP可以与非HTTP服务交互。
  • “:authority”包含目标URI的authority部分。”:authority”不能包含”http”或”https”的URI中已经废弃的”userinfo”子模块。为了保证HTTP/1.1的请求行能够被精确地再现,当从具有原始或星号形式请求目标的HTTP/1.1请求转换为HTTP/2时,这个伪报头字段必须被忽略。直接生成HTTP/2请求的客户端应该使用”:authority”代替Host报头字段。如果原始请求中没有提供Host报头字段,那么,将HTTP/2转换为HTTP/1.1的中间代理必须通过复制”:authority”中的值来创建一个Host报头字段,
  • “:path”包含目标URI的path和query部分。星号形式的请求将值”*”包含在”:path”中。对于”http”或”https”的URI,”:path”不能为空。不包含路径组件的”http”或”https”的URI中必须包含值”/”。这个规则的一个例外是,对”http”或”https”的URI的OPTIONS请求中不包含路径组件,这必须包含一个”:path”伪报头,值为”*”。

所有HTTP/2的请求都必须为”:method”、”:scheme”和”:path”等伪报头字段包含一个精确的值,除非是一个CONNECT请求。忽略这些强制伪报头字段的的HTTP请求是格式异常的。

HTTP/2没有定义如何携带HTTP/1.1请求行中包含的版本标识符。

响应伪报头字段

对于HTTP/2响应,只定义了一个”:status”伪报头字段,它携带HTTP状态码。所有响应中都必须包含这个伪报头字段,否则,响应就是格式异常的。

HTTP/2没有定义如何携带HTTP/1.1状态行中的版本或原因短语。

压缩Cookie报头字段

Cookie报头字段使用英文分号来分隔cookie对。这个报头字段不遵守HTTP的列表构造规则,从而避免cookie对被分离到不同的key-value对中。当单个cookie对被更新时,这会显著地降低压缩效率。

为了更好的压缩效率,Cookie报头字段可以被分隔为几个独立的报头字段,其中,每个报头字段包含一个或多个cookie对。如果解压缩后有多个Cookie报头字段,那么在被传给非HTTP/2上下文(例如HTTP/1.1连接,或者通用HTTP服务)之前,这些字段必须被连接为一个字符串(以英文分号加空格”; “连接)。

因此,下面两个Cookie报头字段列表在语义上是等价的:

cookie: a=b; c=d; e=f

cookie: a=b
cookie: c=d
cookie: e=f

格式异常的请求和响应

格式异常的请求或响应是指一个非常有效的HTTP/2帧序列,但是由于外来帧的存在、被禁止的报头字段、缺少强制头字段以及包含大写报头字段名等原因,实际上是无效的序列。

包含有效载荷体的请求或响应可以包含content-length报头字段。如果content-length报头字段的值不等于DATA帧有效载荷的长度之和,那么,请求或响应也是格式异常的。被定义没有无有效载荷的响应,可以有一个非0的content-length报头字段,即使DATA帧中没有内容。

处理HTTP请求或响应的中间代理(任何中间代理都不会充当隧道)一定不能转发格式异常的请求或响应。检测到格式异常的请求或响应时,必须将其视为类型是PROTOCOL_ERROR的流错误。

对于格式异常的请求,服务端可以在关闭流或重置流之前发送一个HTTP响应。客户端一定不能接受格式异常的响应。注意,这些要求旨在防止一些类型的针对HTTP的常见攻击。这些要求是刻意严格的,因为一旦允许这样做,就可能暴露这些漏洞的实现。

示例

下面展示了HTTP/1.1的请求和响应,以及等价的HTTP/2的请求和响应的说明。

HTTP GET请求包含请求报头字段,没有有效载荷体,因此,被作为一个单独的HEADERS帧传输(后面可以跟随0个或多个CONTINUATION帧),包含请求报头字段的序列化的块。下面的HEADERS帧设置了END_HEADERS和END_STREAM标志位。没有CONTINUATION帧被发送。

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

类似地,一个响应只包含响应报头字段,被作为HEADERS帧传输(后面可以跟随0个或多个CONTINUATION帧),包含响应报头字段的序列化的块。

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 ...

HTTP POST请求包含请求报头字段和有效载荷数据,被作为一个HEADERS帧传输,后面跟随包含请求报头字段的0个或多个CONTINUATION帧,再跟随一个或多个DATA帧,其中,最后一个CONTINUATION帧(或HEADERS帧)设置了END_HEADERS标志位,最后的DATA帧设置了END_STREAM标志位。

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}

需要注意的是,任何报头字段的数据都可以被散布到报头块碎片之中。示例中的报头字段在帧中的分布仅仅是个说明。

包含报头字段和有效载荷数据的响应,被作为一个HEADERS帧传输,后面跟随0个或多个CONTINUATION帧,再跟随一个或多个DATA帧,其中,最后一个DATA帧设置了END_STREAM标志位。

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}

使用1xx状态码(除了101之外)的信息性的响应,被作为一个HEADERS帧传输,后面跟随0个或多个CONTINUATION帧。

在请求和响应报头块以及全部DATA帧都被发送之后,尾部报头字段被作为一个报头块发送。开始尾部报头块的HEADERS帧设置了END_STREAM标志位。

下面的示例包含100 (Continue)状态码,以及尾部报头字段。

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

HTTP/2的请求可靠性机制

在HTTP/1.1中,当错误发生时,客户端不能重试一个非幂等的请求,因为没有方式可以确定错误的性质。一些服务器的处理有可能在错误之前发生,这就导致了如果重新尝试请求可能会得到非预期的结果。

HTTP/2提供两种机制来让客户端了解请求没有被处理:

  • GOAWAY帧表明了可能被处理的最大流标识。大于此流标识的请求可以被安全地重试。
  • RST_STREAM帧中可以包含REFUSED_STREAM错误码,表明在任何处理发生前流已经被关闭。在被重置的流上发送的任何请求都可以被安全地重试。

未经处理的请求没有失败,客户端可以自动重试它们,即使是非幂等的请求。

服务端一定不能表明流已经被处理过了,除非它可以确保这个事实。如果流上的帧已经被传给应用层的任何流,那么REFUSED_STREAM一定不能在这个流上使用。而且,一个GOAWAY帧必须要包含一个大于给点流标识的流标识符。

除了这些记者,PING帧提供了一种让客户端容易地测试一个连接的方式。保持空闲的连接可能被打破,因为一些中间件(例如网络地址转换器和负载均衡器)静默丢弃了连接绑定。PING帧允许客户端无须发送请求就能安全地测试一个连接是否还是激活的。

服务端推送

HTTP/2允许服务端根据以前客户端发起的请求,主动推送响应(伴随着相应的“已承诺”的请求)给客户端。当服务端知道客户端将需要这些响应来完整地处理最初的请求的时候,这将变得非常有用。

客户端可以请求禁用服务端推送,尽管这是在每一跳上独立协商的。将SETTINGS_ENABLE_PUSH设置为0时表明服务端推送是被禁用的。

被承诺的请求必须是可缓存的、安全的,并且,一定不能包含请求body。客户端收到不可缓存的、不是已知安全的或者存在请求body的“承诺请求”时,必须使用类型是PROTOCOL_ERROR的流错误来重置所承诺的流。注意:这可能导致客户端将一个不认识的新定义的方法认为是不安全的,从而导致所承诺的流被重置。

被推送的响应是可缓存的,如果客户端实现了HTTP缓存,那么可以被存储在客户端。当被承诺的流仍然处于“打开”状态的时候,被推送的响应被认为是经过原始服务端成功验证过的(例如,如果出现了”no-cache”缓存指令)。

被推送的响应如果不能被缓存,那么就不能在任何HTTP缓存中存储。被推送的响应可以分别地提供给应用程序使用。

服务端必须在”:authority”伪报头字段中包含一个服务器授权的值。客户端收到服务器未授权的PUSH_PROMISE帧时,必须将其视为一个类型是PROTOCOL_ERROR的流错误。

中间代理可以接收来自服务器的推送响应,并且选择不把它们转发给客户端。也就是说,如何使用被推送的信息取决于中间代理。同样地,中间代理可能选择推送额外的信息给客户端,而这些信息并不是来自服务端的。

客户端不可以推送。因此,服务端必须将收到PUSH_PROMISE帧的情况视为类型是PROTOCOL_ERROR的连接错误。客户端必须拒绝任何试图将SETTINGS_ENABLE_PUSH设置为非0值的修改,并将此消息视为类型为PROTOCOL_ERROR的连接错误。

推送请求

服务端推送在语义上等价于服务端响应一个请求。然而,在这种情况下,请求也是由服务端发送的,作为一个PUSH_PROMISE帧。

PUSH_PROMISE帧包含一个报头块,块中包含服务端赋予的请求报头字段的完整集合。不可能为一个包含请求body的请求推送响应。

被推送的响应总是与一个客户端的显式请求相关联。服务端发送的PUSH_PROMISE帧就在这个显式请求的流上发送。PUSH_PROMISE帧中也包含一个承诺流标识符,是从服务端可用的流标识符中挑选的。

PUSH_PROMISE帧以及后续CONTINUATION帧中的报头字段必须是一个有效的、完整的请求报头字段集合。服务端必须在”:method”伪报头中包含一个安全并且可缓存的方法。如果客户端收到的PUSH_PROMISE帧中不包含完整且有效的请求报头集合,或者”:method”伪报头字段的方法是不安全的,那么它必须响应一个类型是PROTOCOL_ERROR的流错误。

服务端应该在发送引用被承诺响应的任意流之前发送PUSH_PROMISE帧。这就避免了客户端在收到PUSH_PROMISE帧之前发送请求所引起的竞争。

例如,如果服务端收到一个对内嵌多个图片链接的文档的请求,并且,服务端选择推送这些附加的图片信息到客户端,在包含图片连接点DATA帧之前发送PUSH_PROMISE帧,可以确保客户端能够看到一个资源即将被推送,然后才会发现内嵌的链接。类似地,如果服务端推送的响应被报头块引用(例如,在Link报头字段中),那么,在发送报头块之前发送PUSH_PROMISE帧确保了客户端不会请求这些资源。

PUSH_PROMISE帧一定不能被客户端发送。

PUSH_PROMISE帧可以由服务端发送,用来响应任意由客户端发起的流,但是,流在服务端的状态只能是“打开”或“半关闭(远程)”。尽管PUSH_PROMISE不能穿插散布在构成一个单独报头块的HEADERS和CONTINUATION帧之中,但是,PUSH_PROMISE帧穿插散布在构成响应的帧之中。

发送一个PUSH_PROMISE帧就创建了一个新的流,并且让这个流在服务端进入了“保留(本地)”状态,在客户端进入了“保留(远程)”状态。

推送响应

在发送PUSH_PROMISE帧之后,服务端可以开始传输被推送的响应信息,作为PUSH_PROMISE的响应。被推送的响应是在服务端发起的流上传输的,这个流使用的是被承诺的流标识符。服务端使用这个流来传输一个HTTP响应,使用与前文(HTTP请求/响应交换)描述相同的帧序列。在初始HEADERS帧发送之后,对客户端来说,这个流就进入了“半关闭”状态。

一旦客户端接收到一个PUSH_PROMISE帧并选择接受推送的响应,客户端就不应该再对被承诺的响应发出任何请求,直到被承诺的流关闭之后。

如果客户端因为任何原因决定不希望接受服务端推送的响应,或者服务端很长时间都没有开始发送承诺的响应,那么,客户端可以发送一个RST_STREAM帧,其内部使用CANCEL或REFUSED_STREAM错误码,并引用推送流的标识符。

客户端可以使用SETTINGS_MAX_CONCURRENT_STREAMS参数来限制服务端并发推送的响应的数量。公告SETTINGS_MAX_CONCURRENT_STREAMS的值为0会通过阻止服务端创建必要的流来禁用服务端推送。这并不会禁止服务端发送PUSH_PROMISE帧,客户端需要重置所有不需要的被承诺的流。

接收推送响应的客户端必须验证服务端是授权提供响应的,或者提供被推送响应的代理是被配置用于相应的请求的。例如,只为”example.com”DNS-ID或通用名称提供了证书的服务器是不允许为”https://www.example.org/doc“推送响应的。

对PUSH_PROMISE流的响应以一个HEADERS帧开头,并且以设置了END_STREAM标志位的帧结尾。开头的HEADERS帧会立即将流的状态改变为“半关闭(远程)”(对服务端而言)和“半关闭(本地)”(对客户端而言)。结尾的帧将流的状态改变为“关闭”。

注意:客户端永远不会为服务端推送发送一个设置了END_STREAM标志位的帧。

CONNECT方法

在HTTP/1.x中,伪方法CONNECT用于将HTTP连接转换为到远程主机的隧道。CONNECT方法主要被HTTP代理用来与原始服务器建立TLS会话,以达到与”https”资源交互的目的。

在HTTP/2中,为了达到类似的目的,CONNECT方法用于在一个单独的HTTP/2流上建立到远程主机的隧道。HTTP报头字段映射像前文描述(请求伪报头字段)的一样工作,但是有一些不同:

  • “:method”伪报头字段被设置为”CONNECT”。
  • “:scheme”和”:path”伪报头字段必须被忽略。
  • “:authority”伪报头字段包含要连接的主机和端口。

不符合这些限制的CONNECT请求是格式异常的。

支持CONNECT的代理与”:authority”伪报头中标识的服务器之间建立一条TCP连接。一旦这个连接被成功建立,代理就发送一个含有2xx系列状态码的HEADERS帧给客户端。

两端各自发送了初始的HEADERS帧之后,所有的后续数据相关的DATA帧都在这条TCP连接上发送。客户端发送的DATA帧的有效载荷通过代理传输给TCP服务器;从TCP服务器接收的数据被代理组装成DATA帧。除了DATA帧和流管理帧(RST_STREAM、WINDOW_UPDATE和PRIORITY)外,其它类型的帧都不能在连接的流上发送,如果收到,必须将其视为一个流错误。

两端都可以关闭TCP连接。DATA帧上的END_STREAM标志位可以被认为与TCP的FIN标识等价。客户端在接收到设置了END_STREAM标识位的帧之后,应该发送带有END_STREAM标识位的DATA帧。收到带有END_STREAM标志位的DATA帧的代理在最后一个TCP报文段上发送带有FIN比特位的附带数据。收到带有FIN比特位的TCP报文段的代理发送设置了END_STREAM标志位的DATA帧。注意,最后一个TCP报文段或DATA帧可以为空。

TCP连接错误由RST_STREAM帧来通知。代理将TCP连接上的任何错误(包括接收到设置了RST比特位的TCP报文段)都视为类型是CONNECT_ERROR的流错误。相应地,如果检测到HTTP/2的流错误或连接错误,代理必须发送一个设置了RST比特位的TCP报文段。

你可能感兴趣的:(HTTP/2,http,web,互联网)