零。前言
本部分将讲解HTTP/2协议中对流的定义和使用,其实就是在说HTTP/2是若何做到多路复用的。
一。流和多路复用的关系
1. 流的概念
流(Stream),服务器和客户端在HTTP/2连接内用于交换帧数据的独立双向序列,逻辑上可看做一个较为完整的交互处理单元,即表达一次完整的资源请求-响应数据交换流程;一个业务处理单元,在一个流内进行处理完毕,这个流生命周期完结。
特点如下:
- 一个HTTP/2连接可同时保持多个打开的流,任一端点交换帧
- 流可被客户端或服务器单独或共享创建和使用
- 流可被任一端关闭
- 在流内发送和接收数据都要按照顺序
- 流的标识符自然数表示,1~2^31-1区间,有创建流的终端分配
- 流与流之间逻辑上是并行、独立存在
2. 多路复用
流的概念提出是为了实现多路复用,在单个连接上实现同时进行多个业务单元数据的传输。逻辑图如下:
实际传输可能是这样的:
只看到帧(Frame),没有流(Stream)嘛。
需要抽象化一些,就好理解了:
- 每一个帧可看做是一个学生,流可以认为是组(流标识符为帧的属性值),一个班级(一个连接)内学生被分为若干个小组,每一个小组分配不同的具体任务。
- HTTP/1.* 一次请求-响应,建立一个连接,用完关闭;每一个小组任务都需要建立一个班级,多个小组任务多个班级,1:1比例
- HTTP/1.1 Pipeling解决方式为,若干个小组任务排队串行化单线程处理,后面小组任务等待前面小组任务完成才能获得执行机会,一旦有任务处理超时等,后续任务只能被阻塞,毫无办法,也就是人们常说的线头阻塞
- HTTP/2多个小组任务可同时并行(严格意义上是并发)在班级内执行。一旦某个小组任务耗时严重,但不会影响到其它小组任务正常执行
- 针对一个班级资源维护要比多个班级资源维护经济多了,这也是多路复用出现的原因
这样简单梳理,就有些小清晰了。
3. 流的组成
流的概念提出,就是为了实现多路复用。影响因素:
- 流的优先级(priority)属性建议终端(客户端+服务器端)需要按照优先级值进行资源合理分配,优先级高的需要首先处理,优先级低的可以稍微排排队,这样的机制可保证重要数据优先处理。
- 流的并发数(或者说同一时间存在的流的个数)初始环境下不少于100个
- 流量控制阀协调网络带宽资源利用,由接收端提出发送端遵守其规则
- 流具有完整的生命周期,从创建到最终关闭,经历不同阶段
流总体组成如下:
搞清楚了流和多路复用之间关系,下面稍微深入一点,学习流的一些细节。
二。流的属性
1. 流状态/生命周期
帧的行为以及END_STREAM标志位都会对流的状态的产生变化。因为流由各个端独立创建,没有协商,消极后果就是(两端无法匹配的流的状态)导致发送完毕RST_STREAM帧之后“关闭”状态受限,因为帧的传输和接收需要一点时间。
帧的状态列表:
- idle,所有流的开始状态值
- 发送/接收HEADERS帧,进入open状态
- PUSH_PROMISE帧只能在已有流上发送,导致创建的本地推送流处于"resereved(local)"状态
- 在已有流上接收PUSH_PORMISE帧,导致本地预留一个流处于"resereved(remote)"状态
- HEADERS/PUSH_PROMISE帧以及后面的零个或多个CONTINUATION帧,只要携带有END_STREAM标志位,流状态将进入"half closed"状态
- 只能接收HEADERS和PRIORITY,否则报PROTOCOL_ERROR类型连接错误
reserved,为推送保留一个流稍后使用
- 只能发送WINDOW_UPDATE、RST_STREAM、PRIORITY帧
- 只能接收RST_STREAM、PRIORITY、HEADERS帧
- 只能发送HEADERS、RST_STREAM、PRIORITY帧
- 只能接收RST_STREAM、PRIORITY、WINDOW_UPDATE帧
- reserved (local),服务器端发送完PUSH_PROMISE帧本地预留的一个用于推送流所处于的状态
- reserved (remote),客户端接收到PUSH_PROMISE帧,本地预留的一个用于接收推送流所处于的状态
不满足条件,需要报PROTOCOL_ERROR类型连接错误
open,用于两端发送帧,需要发送数据的对等端需要遵守流量控制的通告。
- 每一端可以发送包含END_STREAM标志位的帧,导致流进入"half closed"状态
- 每一端都可以发送RST_STREAM帧,流进入"closed"状态
half closed
- 对流量控制窗口可不用维护
- 只能接收RST_STREAM、PRIORITY、WINDOW_UPDATE帧,否则报STREAM_CLOSED流错误
- 终端可以发送任何类型帧,但需要遵守对端的当前流的流量控制限制
- 一旦发送包含END_STREAM标志位的帧,将进入"closed"状态
- 不能发送WINDOW_UPDATE,PRIORITY和RST_STREAM帧
- 可以接收到任何类型帧
- 接收者可以忽略WINDOW_UPDATE帧,后续可能会马上接收到包含有END_STREAM标志位帧
- 接收到优先级PRIORITY帧,可用来变更依赖流的优先级顺序,有些小复杂了
- 一旦接收到包含END_STREAM标志位的帧,将进入"closed"状态
- half closed (local),发送包含有END_STREAM标志位帧的一端,流进入本地半关闭状态
- half closed (remote),接收到包含有END_STREAM标志位帧的一端,流进入远程半关闭状态
一旦接收或发送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帧关闭一个不想要的承诺流
要求如下:
- 针对具体状态中出现没有允许出现的帧,需要作为协议错误(PROTOCOL_ERROR)类型的连接错误处理
- 在流的任何状态下,PRIORITY帧都可以被发送或接收
- 未知帧可以被忽略
2. 流标识符
- 31个字节表示无符号的整数,1~2^31-1
- 客户端创建的流以奇数表示,服务器端创建流以偶数表示
- 0x0用来表示连接控制信息流,不能够创建新流
- 通过http/1.1 101 协议切换升级切换到HTTP/2,0x1所指代流处于"half closed(local)",不能用于创建新流
- 新建流的标识符要大于已有流和预留的流的标识符
- 新建流第一次被使用时,低于此标识符的并且处于空闲"idle"状态的流都会被关闭
- 已使用的流标识符不能被再次使用
- 终端的流标识符若被耗尽的情况下
- 若是客户端,需要关闭连接,创建新的连接创建新流
- 若是服务器端,需要发送一个GOAWAY帧通知客户端,强迫其打开一个新连接
3. 流的并发数量
- 每一端都可以发送包含有SETTINGS_MAX_CONCURRENT_STREAMS参数的SETTINGS帧限制对等端流的最大并发量
- 对等端接收之后遵守终端最大并发量限制约定
- 状态为"open"或"half closed"的流需要计入限制总数
- 保留态"reserved"流不算入限制总数内
- 终端接收到HEADERS帧导致创建的流总数超过限制,需要响应PROTOCOL_ERROR或REFUSED_STREAM错误,具体哪一种错误,需要根据终端是否可以检测得到允许自动重复重试
- 终端想降低SETTINGS_MAX_CONCURRENT_STREAMS设置的活动流的上限,若低于当前已经打开流的数值,可以选择光比溢出的流或者允许流继续存在直到完成
4. 流的优先级
流的优先级在于允许终端向对端表达所期待的给予具体流更多资源支持的意见的表达,不能保证对端一定会遵守,非强制性需求建议;默认值16。在资源有限时,可以保证基本数据的传输。
优先级改变:
- 终端可在新建的流所传递HEADERS帧中包含优先级priority属性
- 可单独通过PRIORITY帧专门设置流的优先级属性
5. 流依赖
- 流与流之间存在依赖、被依赖关系。所有流默认依赖流0x0;推送流依赖于传输PUSH_PROMISE的关联流。
- 依赖权重值1~256区间,对于依赖同一父级的子节点,应该根据权重比列进行分配资源。
- 对于依赖同一个父级流的子节点被指定相关权重值,以及可用资源的分配比重。子节点之间顺序不固定。
A A / \ ==> /| B C B D C
- 一旦设置独家专属标志(exclusive flag)将为现有依赖插入一个水平的依赖关系,其父级流只能被插入的新流所依赖。比如流D设置专属标志并依赖于流A:
A A | / \ ==> D B C / B C
- 流的依赖树形模型,底层的流只能等到上层流被关闭或无法正常运转/失效时,才会被分配到资源
- 流无法依赖自身,否则为PROTOCOL_ERROR流错误
- 在流依赖树形模型中,父节点优先级,以及专属依赖流的加入等,都会导致已有优先级重排序
? ? ? ? | / \ | | 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)
6. 流优先级状态管理
- 流的依赖树形模型,任一节点被移除,都需要重建优先级顺序,重新分配资源
- 终端建议在流关闭一段时间内保留优先级信息,减少潜在的指派错误
- 处于"idle"状态流可被指派默认优先级16,这时可以变成其它流的父节点,可以指派新的优先级值
- 终端持有的流优先级信息不受SETTINGS_MAX_CONCURRENT_STREAMS限制,但可能会造成终端状态维护负担,其数量可以被限制不多于SETTINGS_MAX_CONCURRENT_STREAMS所定义数量
- 优先级状态信息的维持在负载较高时可以被丢弃,以减少资源占用。
- 终端若有能力保留足够状态,在接收到PRIORITY帧目的修改已被关闭流的优先级时,可以为其子节点重建优先级顺序
7. 流量控制
多路复用会引入资源竞争,流量控制可以保证流之间不会严重影响到彼此。流量控制通过使用WINDOW_UPDATE帧实现,可作用于单个流以及整个的连接。一些原则如下:
- 逐跳,具有方向性
- 不能够被禁止
- 初始窗口值为65535字节,针对单个流,以及整个连接都有效
- 基于WINDOW_UPDATE帧传输实现,接收端通告对端准备在流/连接上接收的字节数
- 接收端完全控制权限,接受端可通告针对流/连接的窗口值,发送者需要遵守
- 目前只有DATA帧可被流量控制,仅针对其有效负载计算;超出窗口值,其负载可以为空
需要注意事项:
- 流量控制是为解决线头阻塞问题,同时在资源约束情况下保护一些操作顺利进行,针对单个连接,某个流可能被阻塞或处理缓慢,但同时不会影响到其它流上正在传输的数据
- 虽然流量控制可以用来限制一个对等端消耗的内存,但若在不知道网络带宽延迟乘积的情况下可能未必能够充分利用好网络资源
- 流量控制机制很复杂,需要考虑大量的细节,实现很困难
三。小结
HTTP/2规范中所定义的流概念、属性很复杂,在请求量很大以及应对海量并发的情况下,整个连接的流量控制+单个流的流量控制+流的状态+流优先级属性+优先级的状态+流依赖树形模型等一系列新特性,可能会造成:
- 服务器端/客户端单个连接内存占用过高,维护一个长连接的成本比以往多了若干倍
- 流量控制是一个复杂功能,实现不好会导致一端流量窗口值已被耗尽,需要等待客户端发送新的流控窗口值,若有热数据进行发送,需要等待成本,无形中增加了额外的交互步骤
- 流依赖和优先级重排序等,无形中增加了程序的复杂度,处理不好触发潜在BUG
- 为了性能和内存考虑,很多知名应用不见得有动力实现全部特性,流的一些高级特性毕竟有些过于理想化,诸如当前实现列表:https://github.com/http2/http2-spec/wiki/Implementations,可以看出一二
- 实际非浏览器环境,诸如HTTP API等,实际上仅需要部分关键特性,这属于情理之中的选择
- 凡是状态皆需要维护,无论横向还是纵向的扩展都需要倍加注意;无状态才是最有利于扩展