HTTP2中的帧和流

  1. 1. HTTP帧
    1. 1.1. 帧格式
    2. 1.2. 帧尺寸
    3. 1.3. 报头压缩与解压
  2. 2. 流和多路复用(Steams and Multiplexing)
    1. 2.1. 流状态
      1. 2.1.1. 流标识符
      2. 2.1.2. 流并发
    2. 2.2. 流量控制
      1. 2.2.1. 流量控制原则
      2. 2.2.2. 恰当的应用流量控制
    3. 2.3. 流优先级
      1. 2.3.1. 流依赖
      2. 2.3.2. 依赖权重
      3. 2.3.3. 优先级重组(Reprioritization)
      4. 2.3.4. 优先级状态管理
      5. 2.3.5. 默认优先级
    4. 2.4. 错误处理
      1. 2.4.1. 连接错误处理
      2. 2.4.2. 流错误处理
      3. 2.4.3. 连接终止
    5. 2.5. 扩展HTTP/2
  3. 3. 流总结
    1. 3.1. 流的总体组成
      1. 3.1.1. 流中的帧分类
      2. 3.1.2. 流的自身属性
      3. 3.1.3. 流的管理
    2. 3.2. 流的状态总结
      1. 3.2.1. idle状态
      2. 3.2.2. reserved状态
      3. 3.2.3. open状态
      4. 3.2.4. half-closed
      5. 3.2.5. closed
  4. 4. 参考文章

本部分介绍帧和流相关概念。

HTTP帧

一旦HTTP/2连接建立, 终端就可以开始帧交换了。

帧格式

帧都是由9字节的报头开始,后面紧跟着一个可变长度的载体(payload)。下面是帧结构图:

1
2
3
4
5
6
7
8
9
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+

帧报头字段定义如下:

  • Length: 24位的无符号整数,表达帧负载体的长度。除非接收者的最大帧尺寸(SETTINGS_MAX_FRAME_SIZE)设置了更大的值,否则绝对不能发送超过2^14(16,384)字节的数据。注意这里发送数据的字节数不包含帧前面9哥字节的报头序列。
  • Type: 一个8位的字段,表示帧的类型。帧类型决定了帧格式和帧语义。实现者必须忽略并丢掉那些未知帧类型的帧。
  • Flags: 为帧类型保留的8位布尔标识字段。标识针对确定的帧类型赋予特定的语义。没有为特定帧类型定义语义的标志必须忽略掉,并且发送的时候必须重置(0x0)。
  • R: 一位保留字段。该位的语义未定义, 发送的时候必须保持重置(0x0), 接受的时候必须被忽略掉。
  • 流标志符: 流标志符使用31位整数表达。值0x0是为那些和整个连接相关联的流保留的,也就是说0x0流标志符代表的不是某个单独的流。

帧尺寸

帧负载体的尺寸被接受端公告的最大帧尺寸设置的最大值限制。该设置可能值为[2^14(16,384), 2^24-1(16,777,215)]字节。所有实现都必须有最低限度的处理2^14+9字节长度帧的能力。其中2^14是帧负载体所允许的最小的尺寸,9是帧报头的尺寸。 而我们在描述帧大小的时候,帧报头是不计算在内的。

注意: 特定的帧类型,例如PING, 会在负载体数据允许的数量上面加强限制。

如果发送的帧尺寸超出最大帧尺寸设置,超出帧类型定义限制,或者没有足够空间容纳必须的数据时,终端必须发送帧尺寸错误的错误码(FRAME_SIZE_ERROR)。如果帧中的帧尺寸错误影响到了整个连接的状态,那么这个错误必须视为连接错误。这包括携带报头区块(即报头帧,推送承诺帧和延续帧),设置帧, 以及带有流标识符为0x0的任何帧。

不强制终端用完帧里的所有可用空间。通过使用比允许的最大尺寸小的帧可以提高响应性。对于时间敏感性强的帧,如果发送较大的帧可能会延迟,(比如重置帧,窗口更新帧或优先级帧),如果大的帧传输时被阻塞,可能会影响性能。

报头压缩与解压

和HTTP/1中一样,HTTP/2的报头字段就是带有一个或多个相关值的名字。报头字段用于HTTP请求和响应信息以及服务器推送操作中。

报头列表是零个以上的报头字段集合。当通过连接传输的时候,使用HTTP报头压缩将报头列表序列化到报头区块中。序列化的报头区块然后被分割到一个或多个字节序列中,这称为报头分区,然后在报头帧,推送承诺帧或延续帧(CONTINUATION)的载体中传输。

Cookie报头字段通过HTTP映射特殊处理。

接受端点通过连接片段并解压那些块来重构报头列表来重组(reassembles)报头区块。

完整的报头块包含:

  • 单独的带有结束报头标志设置的报头帧或推送承诺帧。
  • 或者明确带有结束报头标志,并有一到多个延续帧的报头帧, 最后一个延续帧有结束报头的标志设置。

报头压缩是有状态的。整个连接中使用同样的压缩上下文和解压上下文。报头区块的解压错误必须视为压缩错误类型连接错误。
每个报头区块以离散单元(discrete unit)处理。 报头区块必须使用连续的帧序列传输,不能与其他类型或其他流的帧交叉。报头帧或延续帧序列中的最后一个帧必须要有结束报头标志设置。逻辑上来说,报头区块就等价于单独的帧。

报头区块只能通过报头帧,推送承诺帧或延续帧的负载体发送,因为这些帧携带的数据可以修改由接受者维护的压缩上下文。终端接受报头帧,推送承诺帧或延续帧, 就算这些帧会被丢弃掉,也需要重新组织报头区块,执行解压。接受者如果没有解压报头区块,必须使用压缩错误的连接错误来终止连接。

流和多路复用(Steams and Multiplexing)

流是客户端和服务器之间使用HTTP/2连接建立的独立的,双向的帧交换序列。流具有几个重要特点:

  • 单个HTTP/2连接可以包含多个并行打开的流,两个端点在多个流中交叉帧。
  • 流可以被客户端和服务器单方面或以共享的方式建立和使用。
  • 流可以被两个端点中的任一端关闭。
  • 帧在流中发送顺序很重要。接受端按帧接收顺序处理它们。特别的,报头帧和数据帧的顺序是有重要语法意义的。
  • 流使用整数标识。流标志符由发起流的终端分配。

流状态

流的生命周期如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
+--------+
send PP | | recv PP
,--------| idle |--------.
/ | | \
v +--------+ v
+----------+ | +----------+
| | | send H / | |
,------| reserved | | recv H | reserved |------.
| | (local) | | | (remote) | |
| +----------+ v +----------+ |
| | +--------+ | |
| | recv ES | | send ES | |
| send H | ,-------| open |-------. | recv H |
| | / | | \ | |
| v v +--------+ v v |
| +----------+ | +----------+ |
| | half | | | half | |
| | closed | | send R / | closed | |
| | (remote) | | recv R | (local) | |
| +----------+ | +----------+ |
| | | | |
| | send ES / | recv ES / | |
| | send R / v send R / | |
| | recv R +--------+ recv R | |
| send R / `----------->| |<-----------' send R / |
| recv R | closed | recv R |
`----------------------->| |<----------------------'
+--------+
send: 终端发送帧
recv: 终端接收帧
H: 报头帧(带有隐含的延续帧)
PP: 推送承诺帧(带有隐含的延续帧)
ES: END_STREAM flag
R: RST_STREAM frame

注意该图只展示流状态转换以及影响这些转换的帧和标识。在这方面,延续帧不会对状态转换产生影响;它们实际上是隶属于所归属的报头帧和推送承诺帧。

为了状态转换目的需要,结束流标志作为包含它的帧的一个单独事件处理。带有结束流标志设置的报头帧可以引起两种状态转换。

两端点都有一个流状态的主管视图,帧在传输的时候,它们可能有所不同。流不是由双方终端协调创建的,而是由某个终端单方面(unilaterally)创建的。
状态的不匹配导致的消极后果是在发送重置帧之后, 它们是被closed状态限制的,因为有可能在关闭之后才会收到帧。

流具有下面的状态:

1) 空闲状态(idle)
所有流都是从空闲状态启动的。
该状态中下面转换是有效的:

  • 发送或接收报头帧,流状态变为open状态。流标识符由5.1.1节描述的那样来选择。同样的报头帧也可以导致流立即变成半关闭状态。
  • 在其他流中发送推送承诺帧,保留idle状态的流以备后续使用。被保留流的状态转换为”reserved(local)”。
  • 在其他流中接收推送承诺帧,保留idle状态的流以备后续使用。被保留流的状态转换为”reserved(remote)”。

注意推送承诺帧不是在空闲流上发送的,而是在承诺流ID字段中的这个新保留的流。

在这个状态中只能接收报头帧和优先级帧,接收其他类型的帧都必须视为协议错误类型连接错误。

2) reserved (local)
reserved(local)状态中的流就是通过发送推送承诺帧允诺的那个流。
推送承诺帧通过将空闲流与远程对等端初始化的打开流关联来保留该空闲流。

这种状态中,只有下面的转换是可能的:

  • 终端能发送报头帧。流打开为”半关闭(remote)”状态。
  • 两个终端都可以发送重置帧,流变成关闭状态。 这样将释放流的保存。(注意上图中的下面send R/recv R, 可发可接,都变为closed)

这种状态下,终端不能发送除了报头帧,重置帧和优先帧之外的任意类型的帧。

这种状态中,可以接受优先级帧或窗口更新帧。这种情况下接受非重置帧, 优先级帧和窗口更新帧的都必须被视为协议错误类型的连接错误。

3) reserved (remote)
reserved(remote)状态中的流是远程对等端保留的。
在这个状态中,只有下面的转换是可能的:

  • 接收报头帧,流转换为”half-closed (local)”。
  • 两个端点都可发送重置帧,让流变成”closed”状态。释放保留的流。

该状态终端可以发送优先级帧,让被保留的流优先。该状态终端除了能发送重置帧,窗口更新帧或优先级帧,别的任何类型的帧都不能发送。
该状态只能接受报头帧,重置帧或优先级帧,否则就视为协议错误类型的连接错误。

4) open
处于open状态的流可以用于两端发送任意类型的帧。在该状态中,发送对等端观察流级别的流量控制限制的公告。

在该状态下,两个终端都可以发送带有结束流标识设置的帧,将流状态转换为半关闭状态。其中一个终端发送结束流标识导致流状态变为”half-closed(local)”,另外一个终端发送结束流标志设置将流状态变为”half-closed(remote)”。

该状态下,两个终端都可以发送重置帧,让状态立即变成”closed”。

5) half-closed (local):
处于”half-closed(local)”状态的流不能发送除窗口更新帧,优先级帧以及RST_STREAM之外的其他类型的帧。

该状态时,流如果接收到包含有END_STREAM标识或者任意一端发送RST_STREAM(重置帧),状态就会变成closed。

该状态时,终端可以接收任意类型的帧。
继续接收流量控制帧时,使用窗口更新帧提供流量控制信任是必要的。在该状态中,接收者可以忽略窗口更新帧,这些可能是在已经发送包含END_STREAM的帧之后一段时间才到达的。

优先级(PRIORITY)帧可以在这种状态下接收并用来对依赖当前流的流进行优先级重排序。

6) half-closed (remote)
处于”half-closed(remote)”的流不在被用于对等端发送帧。该状态中,终端不在有义务维持接收者的流量控制窗口。

该状态中,终端接收到除了窗口更新帧,优先级帧,重置帧之外的其他帧, 必须使用STREAM_CLOSED类型的流错误来响应。

“half-closed(remote)”状态的流可以用于终端发送任意类型的帧。该状态中,终端继续观察公告的流级别的流量控制。

发送一个包含END_STREAM的帧或者任意一端发送重置帧时候,状态都可以转换为closed。

7) closed
closed就是终止状态。

终端已定不能在种植状态流上发送优先级之外的其他类型的帧。如果接收到非优先级帧之外的帧,必须视为类型为STREAM_CLOSED的流错误。类似的,终端接收到任意帧之后接收到带有END_STREAM标识设置的帧必须视为连接错误, 除非该帧被允许为下面描述的。

该状态中,在数据帧或报头帧包含有END_STREAM标识被发送之后的一小段时间,可以接收窗口更新帧或重置帧。直到远程对等端接收并处理重置帧,或者帧与END_STREAM标识有关,它可以发送这些类型的帧。终端必须忽略该状态下接收到的窗口更新或重置帧, 因此终端可以选择将那些到达明显在发送END_STREAM之后的帧视为PROTOCOL_ERROR类型的连接错误。

在关闭状态流上可发送优先级帧来优先那些依赖关闭流的帧。终端应该处理优先级帧,虽然它们可以忽略掉,如果流已经从依赖树中被移除掉。

如果该状态是作为发送重置帧的结果,对等端接收重置帧可以已经发送或进入发送队列,流上的帧不能收回(withdrawn)。终端必须忽略在关闭流上已经发送了重置帧的帧上接收的帧。终端必须选择忽略帧以及视帧达到时间为错误的限制周期。

流量控制帧(比如数据帧DATA)在发送重置帧之后接收到,被辑入连接流量控制窗口。即便这些帧可能会被忽略掉,因为它们在发送者接收到重置帧之前被发送,发送着讲考虑这些帧在流量控制窗口计数。

终端可以在发送了重置帧之后接收推送承诺帧。推送承诺帧将流状态变成保留状态,即便相关流已经被重置。因此,重置帧需要关闭不希望的承诺流。

本文档中没有明确说明的地方,具体实现时接收描述状态中没有明确许可的信息都应作为类型为PROTOCOL_ERROR的连接错误(章节5.4.1)来处理。注意优先级帧可以在任意的流状态被发送和接收。帧类型为unknown的帧会被忽略。

8.1节有一个HTTP请求/响应状态转换的例子。服务器推送状态转换的例子在8.2.1节和8.2.2节。

流标识符

流采用31位整数来标识。由客户端初始化的流必须使用奇数流标识符;服务器初始化的流必须使用偶数流标识符。流标识符零(0x0)用于流量控制信息;零流标识符不能用于建立新的流。

HTTP/1.1请求升级到HTTP/2将收到一个标识符为1(0x1)的响应。升级成功,流0x1对客户端是”half-closed(local)”状态的。因此,流0x1在客户端从HTTP/1.1升级时,也不能选择作为新流标识符。

新建立流的标识符数值必须必所有初始化中终端已经打开或保留的要大。这个管理使用报头帧打开的以及使用推送承诺帧保留的流。终端接收到意料外的流标识符必须使用类型为PROTOCOL_ERROR连接错误来响应。

新的流标识符第一次被使用时将隐式关闭所有处于“空闲”状态下可能已经被对等端初始化而且流标识符数字小于新标识符的流。例如,一个客户端发送一个流7的报头帧,那么在流7发送或者接收帧后从没有发送帧的流5将转换为“关闭”状态。

流标识符不能重复使用。生存期长的连接可能导致一个终端耗尽流标识符可用范围。不能建立新流标识符的客户端可以为新流建立新的流标识符。不能建立新标识符的服务器可以发送GOAWAY帧,这样客户端端被强制喂新流打开新连接。

流并发

对等端可以使用设置帧里面的SETTINGS_MAX_CONCURRENT_STREAMS参数来限制流的并发量。最大并发流设置仅适用于终端并且只对接收到此设置的对等端有效。也就是说:客户端可以指定服务端能启动的流最大并发量,而且服务端能指定客户端能启动的流最大并发量。终端绝对不能超过对等端设置的限制。

处于open状态或两种half-closed状态的流都会计入终端允许打开的最大流数量。处于这三种状态之一的流都会计入SETINGS_MAX_CONCURRENT_STREAMS设置的公告限制。处于两种”reserved”状态的流不计入流数目限制。

终端一定不能超出它的对等端的限制。接收报头帧的终端导致它的公告的并发超过流限制,必须当做类型为PROTOCOL_ERROR或REFUSED_STREAM的流错误处理。具体错误码选择取决于终端是否希望启用自动重试。

终端希望将SETTINGS_MAX_CONCURRENT_STREAMS的值减少到比当前打开的流更小时,可以关闭超过新的设置值的流或者允许流结束。

流量控制

使用多路复用流引入了TCP连接使用的竞争, 导致堵塞流。流量控制方案确保同一连接上的流不会破坏性的相互干扰。流量控制在单个流以及整个连接中使用。

HTTP/2通过窗口更新帧提供的流量控制。

流量控制原则

HTTP/2流的流量控制目的是允许各种流量控制算法都能使用,而且不需要协议的更改。HTTP/2的流量控制具有下面一些特点:

  1. 流量控制是针对连接的。流量控制的两种类型都是在两端之间的逐跳,而非经历整个端到端的路径。
  2. 流量控制是基于窗口更新帧的。接收者广播自己准备在流已经整个连接要接收的字节数。这个是以信用为基础的方案。
  3. 流量控制是有方向性的,整个控制都是由接收者提供的。接收者可以选择针对流以及整个连接设置任意的窗口尺寸。发送者必须接受接收方强制的流量控制限制。客户端,服务器以及中介作为接收者都独立公告它们的流量控制窗口,而当它们作为发送者时必须遵守对等端的流量控制设置。
  4. 新流以及整个连接的流量控制窗口初始值是65,535字节。
  5. 帧类型决定流量控制是否应用到给定帧。本文档提到的帧,只有数据帧是面向流量控制的;所有其他类型的帧都不小号流量控制窗口公告的空间。这样确保重要控制帧不会被流量控制阻塞。
  6. 流量控制不能禁用。
  7. HTTP/2只定义窗口更新帧的格式和语义。本文档不明确规定接收者如何决定什么时候发送这些帧以及发送帧的值,也不指定发送者如何选择发送包。实现能选择任何适合它们需要的算法。

实现也负责管理请求和响应如何基于优先级发送,选择如何避免请求头阻塞,以及管理如新流的创建。为这些选择的算法能和任何流量控制算法交互。

恰当的应用流量控制

流量控制定义是保护终端在资源限制的情况下的操作。例如,代理需要在很多连接间共享内存, 也可能有慢速的上游连接和快速的下游连接。流量控制解决那些接收者没有能力在一个流中处理数据,但还想要继续在同一连接处理其他流的情况。

不需要有这种能力调度,它能公告最大尺寸(2^31-1)的流量控制窗口,并能在接收数据的时候通过发送窗口更新帧来维护这个窗口。这就有效的对接收者禁用了流量控制。相反的,发送者总是面对由接收者公告的流量控制窗口。

资源约束下的(比如,内存)调度可以利用流量控制来限制对等端可以消费的内存量。然而,请注意如果流量控制不能知道带宽延迟结果的情况下可能会导致不理想的网络资源使用。

即便完全意识到当前带宽延迟结果,流量控制的实现可能会非常复杂。当使用流量控制时,接收者必须及时的从TCP接收缓冲中读取数据。失败的话可能会导致当诸如窗口更新帧那样的重要帧没有读取和起作用而死锁。

流优先级

客户端可以在打开的流中的报头帧包含优先级信息为新流赋予优先级。在其他任意时间,优先级帧可以用于改变流的优先级。

优先级的目的是允许终端表达它如何希望对等端在管理并发流的时候分配资源。更重要的是,优先级可以在发送能力受限的时候,用于选择流传输帧。

流可以通过标识自己依赖其他流的完成来实现优先级。每个以来被赋予一个相关权重,即用于确定分配给依赖同一流的流的可用资源的相关比率。

流的优先级明确设置将输入到优先级处理过程中。它并不能保证能相对其他相关流有特殊的处理或者传输顺序。终端并不能使用优先级强制要求对等端按照特定顺序处理并发流。因此优先级的表达仅仅是一个建议。

优先级信息可以从信息中忽略掉。默认值优先于所提供的任何明确值。

流依赖

每个流都可以显式的依赖其他流。包括依赖表达偏爱分配资源给特定流,而不是依赖流。

不依赖其他流的流可以赋予流依赖为0x0。换句话说,不存在的流0构成了树根。
依赖其他流的流为依赖流。 被依赖的流为父流。被依赖的流当前不在树中–例如处于idle状态的流–流将会被赋予一个默认的优先级。

当指定另一个流的依赖时,这个流将添加到父节点流的子流中。共有相同父节点的流互相之间顺序是不固定的。例如,如果B和C依赖流A,而且如果新创建的流D依赖流A,最终依赖树中的结果就是A被B,C和D以任意顺序依赖。

1
2
3
A A
/ \ ==> /|\
B C B D C

专用标识允许插入新的级别的依赖。专用标志导致插入的流成为它父流唯一(sole)的依赖, 使得其他依赖成为依赖此专用流。在之前的例子中,如果流D是使用专用依赖流A创建的,结果D会成为B和C的依赖父级。

1
2
3
4
5
A
A |
/ \ ==> D
B C / \
B C

在依赖树内,依赖流只应该在所有它依赖的流(链条的父流到达0x0)关闭或不可能处理它们的时候才分配资源。

流不能依赖自身。终端必须视依赖自身的流为PROTOCOL_ERROR流错误。

依赖权重

所有依赖流都被分配一个整数权重[1,256]。

具有相同父亲的流应该基于它们的权重来按比例分配资源。因此,如果流B依赖流A,带有权重4,流C依赖流A, 权重为12,那么如果A流上不会有进展了,B流理论上将获取到相对于C流资源的三分之一。

优先级重组(Reprioritization)

流权重使用优先级帧可改变。设置依赖导致流变得依赖指定的父流。

如果父流优先级重组,依赖它的流也会跟随父流更改优先级。使用专属标志为优先级重组流设置依赖导致所有依赖新父流的流变得依赖重组优先级的流。

如果流被设置成依赖其子流,之前依赖这个流的所有流将首先转成依赖优先级改变前的流的父节点流。依赖的改变保持其权重不变。

例如,考虑原始依赖树中B和C依赖A,D和E依赖C,且F依赖D。如果A改成依赖D,那么D替换A的位置。其他所有的依赖关系保持不变,不过如果优先级修改使用的是专用标记,那么F将变成依赖A。

1
2
3
4
5
6
7
8
9
10
x x x x
| / \ | |
A D A D D
/ \ / / \ / \ |
B C ==> F B C ==> F A OR A
/ \ | / \ /|\
D E E B C B C F
| | |
F E E
(intermediate) (non-exclusive) (exclusive)

优先级状态管理

当某个流从依赖树中移除,它的依赖可以移动变成依赖之前流的父节点流。新依赖的权重会根据移除流的权重以及流自身的权重重新计算。

从依赖树移除流导致有些优先级信息丢失。资源在具有相同父节点的流之间是共享的,意思就是如果位于那个集合的流关闭或变得阻塞了的话,任何空闲容量将分配给最近的邻居流。然而,如果公公依赖被从树中移除,这些流将与上层流共享资源。

例如,假设流A和B共享一个父节点,C和D都依赖A。移除流A之前, 如果流A和D不能进行,那么C接收分配给流A的所有资源。如果流A从树中移除, 流A的权重被流C和流D瓜分。如果D仍然不能进行,将导致C流获取到的资源比例变少。对于同等的初始权重,C流获取到三分之一而不是二分之一的可用资源。

流有可能在优先级信息在自身创建的依赖还在传输的时候变成关闭状态。如果依赖关系中的一个流存在任何相关的优先级信息被销毁,那么依赖它的流将被分配为默认的优先级。这有可能导致不理想的优先级,因为流可能被赋予一个高于预期的优先级。

为了避免这些问题,终端应该在流变为关闭状态之后一段时间保持流优先级状态。状态被保留的时间越长,流被分配错误的或者默认的优先级值的可能性就越小。

类似的,处于idle状态的流可以赋予优先级或变成其他流的父节点流。这允许在依赖树中创建一组节点,这就让优先级表达更加灵活。idle的流由默认优先级开始。

不计入SETTINGS_MAX_CONCURRENT_STREAMS设置限制的流优先级信息的保留, 会创建一个大的状态对终端带来负担。因此,保留的优先级状态的数量可以限制一下。

终端维持优先级的附加状态的数量可依赖负载;高负载下,优先级状态可以丢弃以限制资源委托。在极端情况下(extrem cases), 终端甚至能丢掉活动流或保留流的优先级状态。如果应用了限制,终端应该维持至少多至它们设置SETTINGS_MAX_CONCURRENT_STREAMS的状态。实现也应该尝试为在优先级树中处于活动状态的流保留状态。

如果已经保留了足够状态,终端收到改变关闭流的优先级的优先级帧,应该改变它依赖的流的依赖。

默认优先级

所有流初始化被赋予依赖流0x0的非专属依赖。推送的流初始化依赖它们相关联的流。这两种情况,流都是赋予一个默认的权重16。

错误处理

HTTP/2帧许可两种类型的错误:

  1. 使整个连接不可用的错误, 为连接错误。
  2. 单独流中的错误为流错误。

在第7节会列出错误代码。

连接错误处理

连接错误是阻止帧层的进一步处理或破坏任何连接状态的任意错误。

终端遇到连接错误应该首先发送一个GOAWAY(超时)帧, 并带有最近的一个成功从对等端接收帧的流的标识符。超时帧包含一个错误码表明连接为什么终止。在为错误条件发送了超时帧之后, 终端必须关闭该TCP连接。

超时帧有可能不被接受终端有效接收。在连接错误事件中,超时(GOAWAY)帧尽可能的尝试跟对等端通信告知连接终止原因。
终端可以在任意时间终止一个连接。 特别的, 终端可以选择奖流错误视为连接错误。终端在结束连接的时候应该发送一个超时帧, 如果允许的话提供所发生的情况。

流错误处理

流错误是与特定流相关的错误,不会影响到其他流的处理。

终端检测到流错误发送一个RST_STREAM帧,包含错误发生时的流的标识符。RST_STREAM帧包含一个错误码表示错误类型。

RST_STREAM是终端可以发送一个流的最后一帧。发送RST_STREAM帧的对等端必须准备好接收任何由远端对等端发送或者准备发送的帧。这些帧可以被忽略,除非连接状态被修改(例如报头压缩或流量控制中的状态)。

通常,终端不应该在任何流上发送多个RST_STREAM帧。但是,终端如果在一个关闭的流上超过来回时间(round-trip)后收到帧,则可以发送的额外的RST_STREAM帧。这种做法是被允许用来处理这种非常规情况。

终端绝不能在收到RST_STREAM帧后响应一个RST_STREAM帧,避免死循环。

连接终止

如果TCP连接被关闭或重置,而流保持open或half-closed状态,那么受影响的流不能自动重试。

扩展HTTP/2

HTTP/2协议允许扩展。在本节描述的限制内,协议扩展可用于提供附加服务或改变协议的任何方面。扩展至在单个HTTP/2连接范围内有效。

这适用定义在本文档中的协议元素。这不影响扩展HTTP已经存在的选项,比如定义新方法,状态码或报头字段。

允许扩展使用新的帧类型,新的设置,或新的错误码。建立注册器来管理这些扩展点: 帧类型,设置以及错误码。

实现必须忽略所有可扩展协议元素中的未知或不支持值。实现必须丢弃那些未知或不支持类型的帧。这就意味着直邮这些扩展点可以被扩展安全使用, 不需要事先安排或协商。然而,出现在报头区块中的扩展帧是不允许的;这必须视为类型为PROTOCOL_ERROR的连接错误。

可能改变既有协议组件语义的扩展必须在使用前协商。例如,改变报头布局的扩展必须在对等端给出肯定信号,表示可以接受的时候才能使用。在这种情况下,修改过的帧布局结构生效前需要通过协商。注意将任意数据帧以外的帧纳入流量控制的修改也是语义上的修改,也必须在协商后才能生效。

本文档并不强制要求使用特定的方法来完成使用扩展的协商,但注意可以通过使用设置来实现这个目的。如果对等端双方都设置了指示愿意使用扩展的值,那么这个扩展就可以被使用。如果某个设置用来协商扩展的使用,那么必须定义初始值来设置扩展初始时是被禁用的。

流总结

流是一种抽象出来的概念,就是为了实现多路复用。 流是帧序列,也就是说,流实际上是帧组成的序列,可见的是帧。流是帧组成的。

流被抽象出来,在实现中,流有自身的标识符,流有自己的生命周期,流还有优先级。这几个属性又构成了流的自身属性。

流除了自身的一些属性外,流还是被管理起来的,并发数量控制和流量控制,这都是为了保证网络的顺畅。

流的总体组成

流中的帧分类

流中的帧可以大致分为: 报头帧、数据帧、管理帧和其他类型的帧。
报头帧包含HEADERS, PUSH_PROMISE, CONTINUATION; 数据帧就是DATA; 管理帧有PRIORITY, WINDOW_UPDATE, SETTINGS; 其他类型帧包括PING, GOAWAY, RST_STREAM。

HTTP/2的规范文档中基本上就定义了上述10种类型的帧。

流的自身属性

流具有标识符,31位整数表示。客户端初始化的流以奇数表示,服务器初始化的流以偶数表示。特殊的流标识符0x0用于流量控制信息,不能用于建立新的流。

同一连接中可以存在多个流,因此需要给流定义优先级以及流的依赖关系。这些可以通过PRIORITY帧来实现。

另外,流是由两个终端共享或独立的方式建立起来的,它具有一定生命周期,对于流两端来说,都维护了一套自己的流状态视图。

流的状态有: idle, reserved, half-closed, closed, open。 其中idle, open, closed是没有方向性的,或者说没有相对性的,就是不论两端的哪一端,都是一样的, 在它们的端状态视图中,状态相对性一致。而reserved, half-closed的则对端点具有相对性,分别派生出来reserved(local)和reserved(remote),以及half-closed(local)和half-closed(remote)。

流的管理

流的管理涉及到流的并发数量和流的流量控制两个方面。

流的状态总结

idle状态

idle状态是所有流的开始状态值。

  • 接收或发送HEADERS帧,状态变成open。
  • 推送承诺帧只能在已有的流上发送,导致创建的本地推送流变成reserved(local)。
  • 在已有的流中接收推送承诺帧,导致本地预留一个流变成reserved(remote)。
  • 报头帧、推送承诺帧或者连续的延续帧只要后面带有一个结束流标志,那么流可以进入half-closed状态。
  • 只能接收报头帧、优先级帧。

reserved状态

该状态是为推送保留一个流已备后续使用。

reserved(local),服务器端发送完推送承诺帧, 本地预留的一个用于推送流所处于的状态。

  • 只能发送报头帧、重置帧、优先级帧
  • 只能接受重置帧、优先级帧、窗口更新帧

reserved(remote),客户端接收到推送承诺帧, 本地预留的一个用于接收推送流所处于的状态。

  • 只能发送窗口更新帧、重置帧、优先级帧
  • 只能接收重置帧、优先级帧、报头帧

不满足条件,需要报PROTOCOL_ERROR类型连接错误

open状态

用于两端发送帧,需要发送数据的对等端需要遵守流量控制的通告。

  • 每一端可以发送包含结束流标志位的帧,导致流进入”half-closed”状态
  • 每一端都可以发送重置帧,流进入”closed”状态

half-closed

half-closed(local),发送包含有结束流标志位帧的一端,流进入本地半关闭状态。

  • 不能发送窗口更新帧、优先级帧和重置帧
  • 可以接收到任何类型帧
  • 接收者可以忽略窗口更新帧,后续可能会马上接收到包含有结束流标志位帧
  • 接收到优先级PRIORITY帧,可用来变更依赖流的优先级顺序,有些小复杂了
  • 一旦接收到包含结束流标志位的帧,将进入”closed”状态

half-closed(remote),接收到包含有结束流标志位帧的一端,流进入远程半关闭状态

  • 对流量控制窗口可不用维护
  • 只能接收重置流帧、PRIORITY、窗口更新帧,否则报STREAM_CLOSED流错误
  • 终端可以发送任何类型帧,但需要遵守对端的当前流的流量控制限制
  • 一旦发送包含END_STREAM标志位的帧,将进入”closed”状态
  • 一旦接收或发送RST_STREAM帧,流将进入”closed”状态。

closed

流的最终关闭状态

  • 只允许发送PRIORITY帧,对依赖关闭的流进行重排序
  • 终端接收RST_STREAM帧之后,只能接收PRIORITY帧,否则报STREAM_CLOSED流错误
  • 接收的DATA/HEADERS帧包含有END_STREAM标志位,在一个很短的周期内可以接收WINDOW_UPDATE或RST_STREAM帧;超时后需要作为错误对待
  • 终端必须忽略WINDOW_UPDATE或RST_STREAM帧
  • 终端发送RST_STREAM帧之后,必须忽略任何接收到的帧
  • 在RST_STREAM帧被发送之后收到的流量受限DATA帧,转向流量控制窗口连接处理。尽管这些帧可以被忽略,因为他们是在发送端接收到RST_STREAM之前发送的,但发送端会认为这些帧与流量控制窗口不符。
  • 终端在发送RST_STREAM之后接收PUSH_PROMISE帧,尽管相关流已被重置,但推送帧也能使流变成“保留”状态。因此,可用RST_STREAM帧关闭一个不想要的承诺流

参考文章

  1. http://www.blogjava.net/yongboy/archive/2015/03/19/423611.aspx