客户端和服务器端一旦握手协商成功接建立连接,端点之间可以基于HTTP/2协议传递交换帧数据了。
一。帧通用格式
下图为HTTP/2帧通用格式:帧头+负载的比特位通用结构:
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
帧头为固定的9个字节((24+8+8+1+31)/8=9)呈现,变化的为帧的负载(payload),负载内容是由帧类型(Type)定义。
- 帧长度Length:无符号的自然数,24个比特表示,仅表示帧负载所占用字节数,不包括帧头所占用的9个字节。默认大小区间为为0~16,384(2^14),一旦超过默认最大值2^14(16384),发送方将不再允许发送,除非接收到接收方定义的SETTINGS_MAX_FRAME_SIZE(一般此值区间为2^14 ~ 2^24)值的通知。
- 帧类型Type:8个比特表示,定义了帧负载的具体格式和帧的语义,HTTP/2规范定义了10个帧类型,这里不包括实验类型帧和扩展类型帧
- 帧的标志位Flags:8个比特表示,服务于具体帧类型,默认值为0x0。有一个小技巧需要注意,一般来讲,8个比特可以容纳8个不同的标志,比如,PADDED值为0x8,二进制表示为00001000;END_HEADERS值为0x4,二进制表示为00000100;END_STREAM值为0X1,二进制为00000001。可以同时在一个字节中传达三种标志位,二进制表示为00001101,即0x13。因此,后面的帧结构中,标志位一般会使用8个比特表示,若某位不确定,使用问号?替代,表示此处可能会被设置标志位
- 帧保留比特为R:在HTTP/2语境下为保留的比特位,固定值为0X0
- 流标识符Stream Identifier:无符号的31比特表示无符号自然数。0x0值表示为帧仅作用于连接,不隶属于单独的流。
关于帧长度,需要稍加关注: - 0 ~ 2^14(16384)为默认约定长度,所有端点都需要遵守 - 2^14 (16,384) ~ 2^24-1(16,777,215)此区间数值,需要接收方设置SETTINGS_MAX_FRAME_SIZE参数单独赋值 - 一端接收到的帧长度超过设定上限或帧太小,需要发送FRAME_SIZE_ERR错误 - 当帧长错误会影响到整个连接状态时,须以连接错误对待之;比如HEADERS,PUSH_PROMISE,CONTINUATION,SETTINGS,以及帧标识符不该为0的帧等,都需要如此处理 - 任一端都没有义务必须使用完一个帧的所有可用空间 - 大帧可能会导致延迟,针对时间敏感的帧,比如RST_STREAM, WINDOW_UPDATE, PRIORITY,需要快速发送出去,以免延迟导致性能等问题
二。报文头压缩和解压
和HTTP/1一样,HTTP/2报头字段包含一个或多个相关的键值对。报头字段会在HTTP请求/响应报头和服务器推送操作中使用。原先为文本字段,现在需要使用HTTP报头压缩进行序列化成报头分块,作为HEADERS 、 PUSH_PROMISE、CONTINUATION等帧的负载传输出去。
解压缩采用的HPACK协议,具体可参考:http://http2.github.com/http2-spec/compression.html
接收端合并接收到的帧组装成报头分块,解压缩还原报头集合。
一个完整的报头分块包含: - 单个包含报头终止标记END_HEADERS的HEADERS、PUSH_PROMISE帧,或者 - HEADERS、PUSH_PROMISE帧不包含的END_HEADERS标记,后续跟随一个或多个CONTINUATION帧,最后一个CONTINUATION帧包含了END_HEADERS标记。
报头压缩是有状态的,在一个完整的连接中,一方的压缩上下文环境,另一方的解压的上下文环境,都是需要具备的。报头解码失败需要作为连接错误COMPRESSION_ERROR对待。
报头块彼此之间离散,作为连续的同一类型帧序列存在,不存在交错帧以及来自其他类型帧或流。举一个例子,一个连续的HEADERS/CONTINUATION/PUSH_PROMISE帧序列,最后一个帧包含了END_HEADERS标记,表示一个报头完结。一个报头块逻辑上是一个帧,但是否完整取决于同类型连续的帧的最后一个包含END_HEADERS标记。
报头块作为HEADERS/PUSH_PROMISE/CONTINUATION等帧负载被一端发向另一端。接收端需要从HEADERS/PUSH_PROMISE/CONTINUATION等帧负载中进行组装报头块,执行解压还原报头集合,不管帧需要不需要被丢弃。接收端在解压时若不能够正常解压报头块,需要回应COMPRESSION_ERROR错误,然后终止连接。
三。HTTP/2定义的帧
规范定义了10个正式使用到帧类型,扩展实验类型的ALTSVC、BLOCKED等不在介绍之列。下面按照优先使用顺序重新排排序。
1. SETTINGS
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| 0x4 (8) | 0000 000? (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier/0x0 (32) |
+=+=============================+===============================+
| Identifier (16) |
+-------------------------------+-------------------------------+
| Value (32) |
+---------------------------------------------------------------+
| Identifier (16) |
+-------------------------------+-------------------------------+
| Value (32) |
+---------------------------------------------------------------+
设置帧,接收者向发送者通告己方设定,服务器端在连接成功后必须第一个发送的帧。
字段Identifier定义了如下参数: - SETTINGS_HEADER_TABLE_SIZE (0x1),通知接收者报头表的字节数最大值,报头块解码使用;初始值为4096个字节,默认可不用设置 - SETTINGS_ENABLE_PUSH (0x2),0:禁止服务器推送,1:允许推送;其它值非法,PROTOCOL_ERROR错误 - SETTINGS_MAX_CONCURRENT_STREAMS (0x3),发送者允许可打开流的最大值,建议值100,默认可不用设置;0值为禁止创建新流 - SETTINGS_INITIAL_WINDOW_SIZE (0x4),发送端流控窗口大小,默认值2^16-1 (65,535)个字节大小;最大值为2^31-1个字节大小,若溢出需要报FLOW_CONTROL_ERROR错误 - SETTINGS_MAX_FRAME_SIZE (0x5),单帧负载最大值,默认为2^14(16384)个字节,两端所发送帧都会收到此设定影响;值区间为2^14(16384)-2^24-1(16777215) - SETTINGS_MAX_HEADER_LIST_SIZE (0x6),发送端通告自己准备接收的报头集合最大值,即字节数。此值依赖于未压缩报头字段,包含字段名称、字段值以及每一个报头字段的32个字节的开销等;文档里面虽说默认值不受限制,因为受到报头集合大小不限制的影响,个人认为不要多于2 _SETTINGS_MAX_FRAME_SIZE(即2^14_2=32768),否则包头太大,隐患多多。
标志位: * ACK (0x1),表示接收者已经接收到SETTING帧,作为确认必须设置此标志位,此时负载为空,否则需要报FRAME_SIZE_ERROR错误
注意事项: - 在连接开始阶段必须允许发送SETTINGS帧,但不一定要发送 - 在连接的生命周期内可以允许任一端点发送 - 接收者不需要维护参数的状态,只需要记录当前值即可 - SETTINGS帧仅作用于当前连接,不针对单个流,因此流标识符为0x0 - 不完整或不合规范的SETTINGS帧需要抛出PROTOCOL_ERROR类型连接错误 - 负载字节数为6个字节的倍数,6*N (N>=0)
处理流程: - 发送端发送需要两端都需要携带有遵守的SETTINGS设置帧,不能够带有ACK标志位 - 接收端接收到无ACK标志位的SETTINGS帧,必须按照帧内字段出现顺序一一进行处理,中间不能够处理其它帧 - 接收端处理时,针对不受支持的参数需忽略 - 接收端处理完毕之后,必须响应一个包含有ACK确认标志位、无负载的SETTINGS帧 - 发送端接收到确认的SETTINGS帧,表示两端设置已生效 - 发送端等待确认若超时,报SETTINGS_TIMEOUT类型连接错误
2. HEADER
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| 0x1 (8) | 00?0 ??0? (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============+===============================================+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|E| Stream Dependency? (31) |
+-+-------------+-----------------------------------------------+
| Weight? (8) |
+-+-------------+-----------------------------------------------+
| Header Block Fragment (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+
报头主要载体,请求头或响应头,同时呢也用于打开一个流,在流处于打开"open"或者远程半关闭"half closed (remote)"状态都可以发送。
字段列表: - Pad Length:受制于PADDED标志控制是否显示,8个比特表示填充的字节数。 - E:一个比特表示流依赖是否专用,可选项,只在流优先级PRIORITY被设置时有效 - Stream Dependency:31个比特表示流依赖,只在流优先级PRIORITY被设置时有效 Weight:8个比特(一个字节)表示无符号的自然数流优先级,值范围自然是(1~256),或称之为权重。只在流优先级PRIORITY被设置时有效 - Header Block Fragment:报头块分片 - Padding:填充的字节,受制于PADDED标志控制是否显示,长度由Pad Length字段决定
所需标志位: END_STREAM (0x1): 报头块为最后一个,意味着流的结束。后续可紧接着CONTINUATION帧在当前的流中,需要把CONTINUATION帧作为HEADERS帧的一部分对待 END_HEADERS (0x4): 此报头帧不需分片,完整的一个帧。后续不再需要CONTINUATION帧帮忙凑齐。若没有此标志的HEADER帧,后续帧必须是以CONTINUATION帧传递在当前的流中,否则接收者需要响应PROTOCOL_ERROR类型的连接错误。 PADDED (0x8): 需要填充的标志 PRIORITY (0x20): 优先级标志位,控制独立标志位E,流依赖,和流权重。
注意事项: - 其负载为报头块分片,若内容过大,需要借助于CONTINUATION帧继续传输。若流标识符为0x0,结束段需要返回PROTOCOL_ERROR连接异常。HEADERS帧包含优先级信息是为了避免潜在的不同流之间优先级顺序的干扰。 - 其实一般来讲,报文头部不大的情况下,一个HEADERS就可以完成了,特殊情况就是Cookie字段超过16KiB大小,不常见。
3. CONTINUATION
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| 0x9 (8) | 0x0/0x4 (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (32) |
+=+=============================================================+
| Header Block Fragment (*) |
+---------------------------------------------------------------+
字段列表: - Header Block Fragment,用于协助HEADERS/PUSH_PROMISE等单帧无法包含完整的报头剩余部分数据。
注意事项: - 一个HEADERS/PUSH_PROMISE帧后面会跟随零个或多个CONTINUATION,只要上一个帧没有设置END_HEADERS标志位,就不算一个帧完整数据的结束。 - 接收端处理此种情况,从开始的HEADERS/PUSH_PROMISE帧到最后一个包含有END_HEADERS标志位帧结束,合并的数据才算是一份完整数据拷贝 - 在HEADERS/PUSH_PROMISE(没有END_HEADERS标志位)和CONTINUATION帧中间,是不能够掺杂其它帧的,否则需要报PROTOCOL_ERROR错误
标志位: * END_HEADERS(0X4):表示报头块的最后一个帧,否则后面还会跟随CONTINUATION帧。
4. DATA
一个或多个DATA帧作为请求、响应内容载体,较为完整的结构如下:
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| 0x0 (8) | 0000 ?00? (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============+===============================================+
|Pad Length? (8)|
+---------------+-----------------------------------------------+
| Data (*) ...
+---------------------------------------------------------------+
| Padding? (*) ...
+---------------------------------------------------------------+
字段: Pad Length: 一个字节表示填充的字节长度。取决于PADDED标志是否被设置. Data: 这里是应用数据,真正大小需要减去其他字段(比如填充长度和填充内容)长度。 * Padding: 填充内容为若干个0x0字节,受PADDED标志控制是否显示。接收端处理时可忽略验证填充内容。若验证,可以对非0x0内容填充回应PROTOCOL_ERROR类型连接异常。
标志位: END_STREAM (0x1): 标志此帧为对应标志流最后一个帧,流进入了半关闭/关闭状态。 PADDED (0x8): 负载需要填充,Padding Length + Data + Padding组成。
注意事项: - 若流标识符为0x0,接收者需要响应PROTOCOL_ERROR连接错误 - DATA帧只能在流处于"open" or "half closed (remote)"状态时被发送出去,否则接收端必须响应一个STREAM_CLOSED的连接错误。若填充长度不小于负载长度,接收端必须响应一个PROTOCOL_ERROR连接错误。
5. PUSH_PROMISE
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| 0x5 (8) | 0000 ??00 (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (32) |
+=+=============================================================+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|R| Promised Stream ID (31) |
+-+-------------------------------------------------------------+
| Header Block Fragment (*) . . .
+---------------------------------------------------------------+
| Padding (*) . . .
+---------------------------------------------------------------+
服务器端通知对端初始化一个新的推送流准备稍后推送数据: - 要求推送流为打开或远端半关闭(half closed (remote))状态,否则报PROTOCOL_ERROR错误: - 承诺的流不一定要按照其流打开顺序进行使用,仅用作稍后使用 - 受对端所设置SETTINGS_ENABLE_PUSH标志位决定是否发送,否则作为PROTOCOL_ERROR错误对待 - 接收端一旦拒绝接收推送,会发送RST_STREAM帧告知对方推送无效
字段列表: - Promised Stream ID,31个比特表示无符号的自然数,为推送保留的流标识符,后续适用于发送推送数据 - Header Block Fragment,请求头部字段值,可看做是服务器端模拟客户端发起一次资源请求
标志位: END_HEADERS(0x4/00000010),此帧包含完整的报头块,不用后面跟随CONTINUATION帧了 PADDED(0x8/00000100),填充开关,决定了下面的Pad Length和Padding是否要填充,具体和HEADERS帧内容一致,不多说
6. PING
优先级帧,类型值为0x6,8个字节表示。发送者测量最小往返时间,心跳机制用于检测空闲连接是否有效。
+-----------------------------------------------+
| 0x8 (24) |
+---------------+---------------+---------------+
| 0x6 (8) | 0000 000? (8) |
+-+-------------+---------------+-------------------------------+
|R| 0x0 (32) |
+=+=============================================================+
| Opaque Data (64) |
+---------------------------------------------------------------+
字段列表: - Opaque Data:8个字节负载,值随意填写。
标志位: * ACK(0x1):一旦设置,表示此PING帧为接收者响应的PING帧,非发送者。
注意事项: - PING帧发送方ACK标志位为0x0,接收方响应的PING帧ACK标志位为0x1。否则直接丢弃。其优先级要高于其它类型帧。 - PING帧不和具体流相关联,若流标识符为0x0,接收方需要响应PROTOCOL_ERROR类型连接错误。 - 超过负载长度,接收者需要响应FRAME_SIZE_ERROR类型连接错误。
7. PRIORITY
优先级帧,类型值为0x2,5个字节表示。表达了发送方对流优先级权重的建议值,在流的任何状态下都可以发送,包括空闲或关闭的流。
+-----------------------------------------------+
| 0x5 (24) |
+---------------+---------------+---------------+
| 0x2 (8) | 0x0 (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
|E| Stream Dependency (31) |
+-+-------------+-----------------------------------------------+
| Weight (8) |
+---------------+
字段列表: - E:流是否独立 - Stream Dependency:流依赖,值为流的标识符,自然也是31个比特表示。 - Weight:权重/优先级,一个字节表示自然数,范围1~256
注意事项: - PRIORITY帧其流标识符为0x0,接收方需要响应PROTOCOL_ERROR类型的连接错误。 - PRIORITY帧可在流的任何状态下发送,但限制是不能够在一个包含有报头块连续的帧里面出现,其发送时刻需要,若流已经结束,虽然可以发送,但已经没有什么效果。 - 超过5个字节PRIORITY帧接收方响应FRAME_SIZE_ERROR类型流错误。
8. WINDOW_UPDATE
+-----------------------------------------------+
| 0x4 (24) |
+---------------+---------------+---------------+
| 0x8 (8) | 0x0 (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
|R| Window Size Increment (31) |
+-+-------------------------------------------------------------+
流量控制帧,作用于单个流以及整个连接,但只能影响两个端点之间传输的DATA数据帧。但需注意,中介不转发此帧。
字段列表: - Window Size Increment,31个比特位无符号自然数,范围为1-2^31-1(2,147,483,647)个字节数,表明发送者可以发送的最大字节数,以及接收者可以接收到的最大字节数。
注意事项: - 目前流控只会影响到DATA数据帧 - 流标识符为0,影响整个连接,非单个流 - 流标识符不为空,具体流的标识符,将只能够影响到具体流 - WINDOW_UPDATE在某个携带有END_STREAM帧的后面被发送(当前流处于关闭或远程关闭状态),接收端可忽略,但不能作为错误对待 - 发送者不能发送一个窗口值大于接收者已持有(接收端已经拥有一个流控窗口值)可用空间大小的WINDOW_UPDATE帧 - 当流控窗口所设置可用空间已耗尽时,对端发送一个零负载带有END_STREAM标志位的DATA数据帧,这是允许的行为 - 流量控制不会计算帧头所占用的9个字节空间 - 若窗口值溢出,针对单独流,响应RST_STREAM(错误码FLOW_CONTROL_ERROR)帧;针对整个连接的,响应GOAWAY(错误码FLOW_CONTROL_ERROR)帧 - DATA数据帧的接收方在接收到数据帧之后,需要计算已消耗的流控窗口可用空间,同时要把最新可用窗口空间发送给对端 - DATA数据帧发送方接收到WINDOW_UPDATE帧之后,获取最新可用窗口值 - 接收方异步更新发送方窗口值,避免流停顿/失速 - 默认情况下流量控制窗口值为65535,除非接收到SETTINGS帧SETTINGS_INITIAL_WINDOW_SIZE参数,或者WINDOWS_UPDATE帧携带的窗口值大小,否则不会改变 - SETTINGS_INITIAL_WINDOW_SIZE值的改变会导致窗口可用空间不明晰,易出问题,发送者必须停止受流控影响的DATA数据帧的发送直到接收到WINDOW_UPDATE帧获得新的窗口值,才会继续发送。eg:客户端在连接建立的瞬间一口气发送了60KB的数据,但来自服务器SETTINGS设置帧的初始窗口值为16KB,客户端只能够等到WINDOW_UPDATE帧告知新的窗口值,然后继续发送传送剩下的44KB数据 - SETTINGS帧无法修改针对整个连接的流量控制窗口值 - 任一端点在处理SETTINGS_INITIAL_WINDOW_SIZE值时一旦导致流控窗口值超出最大值,都需要作为一个FLOW_CONTROL_ERROR类型连接错误对待
9. RST_STREAWM
优先级帧,类型值为0x3,4个字节表示。表达了发送方对流优先级权重的建议值,任何时间任何流都可以发送,包括空闲或关闭的流。
+-----------------------------------------------+
| 0x4 (24) |
+---------------+---------------+---------------+
| 0x3 (8) | 0x0 (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Error Code (32) |
+---------------------------------------------------------------+
字段列表: - Error Code:错误代码,32位无符号的自然数表示流被关闭的错误原因。
注意事项: - 接收到RST_STREAM帧,需要关闭对应流,因此流也要处于关闭状态。 - 接收者不能够在此流上发送任何帧。 - 发送端需要做好准备接收接收端接收到RST_STREAM帧之前发送的帧,这个空隙的帧需要处理。 - 若流标识符为0x0,接收方需要响应PROTOCOL_ERROR类型连接错误。 - 当流处于空闲状态idle状态时是不能够发送RST_STREAM帧,否则接收方会报以PROOTOCOL_ERROR类型连接错误。
10. GOAWAY
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| 0x7 (8) | 0x0 (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (32) |
+=+=============================================================+
|R| Last-Stream-ID (31) |
+-+-------------------------------------------------------------+
| Error Code (32) |
+---------------------------------------------------------------+
| Additional Debug Data (*) |
+---------------------------------------------------------------+
一端通知对端较为优雅的方式停止创建流,同时还要完成之前已建立流的任务。
- 一旦发送,发送者将忽略接收到的流标识符大于Last-Stream-ID任何帧
- 接收者不能够在当前流上创建新流,若创建新流则创建新的连接
- 可用于服务器的管理行为,比如服务器进入维护阶段,不再准备接收新的连接
- 字段Last-Stream-ID为发送方取自最后一个正在处理或已经处理流的标识符
- 后续创建的流标识符高于Last-Stream-ID数据帧都不会被处理
- 终端应被鼓励在关闭连接之前发送GOAWAY隐式方式告知对方某些流是否已经被处理
- 终端可以选择关闭连接,针对行为不当的终端不发送GOAWAY帧
- GOAWAY应用于当前连接,非具体流
- 没有处理任何流的情况下,Last-Stream-ID值可为0,也是合法
- 流(标识符小于或等于已有编号的标识符)在连接关闭之前没有被完全关闭,需要创建新的连接进行重试
- 发送端在发送GOAWAY时还有一些流任务没有完成,将保持连接为打开状态直到任务完成
- 终端可以在自身环境发生改变时发送多个GOAWAY帧,但Last-Stream-ID不允许增长
- Additional Debug Data没有语义,仅用于联机测试诊断目的。若携带登陆或持久化调试数据,需要有安全保证避免未经授权访问。
四。帧的扩展
HTTP/2协议的扩展是允许存在的,在于提供额外服务。扩展包括: - 新类型帧,需要遵守通用帧格式 - 新的设置参数,用于设置新帧相关属性 - 新的错误代码,约定帧可能触发的错误
当定义一个新帧,需要注意 1. 规范建议新的扩展需要经过双方协商后才能使用 1. 在SETTINGS帧添加新的参数项,可在连接序言时发送给对端,或者适当机会发送 1. 双方协商成功,可以使用新的扩展
已知ALTSVC、BLOCKED属于扩展帧。
1. ALTSVC
服务器提供给客户端当前可用的替代服务,类似于CNAME,客户端不支持可用选择忽略
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| 0xa (8) | 0x0 (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (32) |
+=+=============================+===============================+
| Origin-Len (16) | Origin? (*) ...
+-------------------------------+-------------------------------+
| Alt-Svc-Field-Value (*) ...
+---------------------------------------------------------------+
字段列表: - Origin-Len: 16比特位整数,说明了Origin字段字节数 - Origin: ASCII字符串表示替代服务 - Alt-Svc-Field-Value: 包含了Alt-Svc HTTP Header Field,长度=Length (24) - Origin-Len (16)
需要注意: - 中介设备不能转发给客户端,原因就是中介自身替换处理,转发正常的业务数据给客户端就行
具体可参考:https://tools.ietf.org/html/draft-ietf-httpbis-alt-svc-06
2. BLOCKED
一端告诉另一端因为受到流量控制的作用有数据但无法发送。
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| 0xb (8) | 0x0 (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier/0x0 (32) |
+=+=============================================================+
- Stream Identifier若为0x0,则表示针对整个连接,否则针对具体流
- 在流量控制窗口生效之前不能发送BLOCKED
- 一旦遇到此项问题,说明我们的实现可能有缺陷,无法得到理想的传输速率
- 只能够在WINDOW_UPDATE帧接收之前或SETTINGS_INITIAL_WINDOW_SIZE参数增加之前发送
五。小结
以上记录了HTTP/2帧基本结构,10个文档定义的正式帧,以及额外的两个扩展帧。
无论是HTTP/1.*还是HTTP/2,HTTP的基本语义是不变的,比如方法语义(GET/PUST/PUT/DELETE),状态码(200/404/500等),Range Request,Cacheing,Authentication、URL路径。以前纯文本形式作为传输的载体,HTTP/2带来了与之不同的二进制传输语法语义。
下面为HTTP/2消息交换方便笔记。
请求/响应流程
一个典型的HTTP消息包含请求/响应,组成如下:
- 以响应为例,零或多个HEADERS帧(每一个HEADERS帧可能跟着>=0个CONTINUATION帧,以补充单个HEADERS容量不够的情况)包含状态码为1xx的报文头响应
- 或者一个HEADERS帧包含了完整的报文头(一般情况下)
- 零个或多个DATA数据帧包含了具体的消息负载内容
- 一个HEADERS帧,后面跟随零个或多个包含有报尾(trailer-part)的CONTINUATION帧,作为可选项
注意事项:
- 一个HEADERS帧携带有END_STREAM标志,后面可跟随有CONTINUATION帧用以补充剩余的包头块
- 来自于其它流的类型帧是不能够出现在HEADERS帧和CONTINUATION帧中间
- DATA数据帧不支持分块传输编码(chunked transfer encoding)
- 报尾字段(Trailing header field)当出现在报头块中时,可以终止当前流
- HEADERS帧以及关联的CONTINUATION帧只能够出现在一个流的开始或结束时
- HTTP请求-相应交换消耗一个流:请求准备HEADERS帧打开流,请求的帧包含有END_STREAM标志导致两端流处于半关闭状态,half closed(local/server);响应一个HEADERS帧,若某响应帧包含有END_STREAM标志,流将被关闭
- 一个HTTP响应完成指的响应帧是包含有END_STREAM标志,在服务器发送并且客户端接收成功。若响应不依赖于客户端的请求,服务器端可以在先于客户端发送请求之前发送完成,之后服务器通过再次发送一个RST_STREAM流(错误代码为NO_ERROR)请求客户端放弃发送请求。这要求客户端在接收到RST_STREAM帧后必须不能够丢弃响应,无论是处于什么谨慎原因。
1. 不支持升级机
HTTP/2多路复用,以及自身也可以通过HTTP/1.1 101切换古来,因此不支持101切换协议(Switching Protocols)机制也是情理之中。
2. HTTP Header Fields
HTTP/2报头字段注意点:
- 和HTTP/1.x报头字段一样,都是ASCII字符表示
- 字段要求全部小写,"Accept" -> "accept"
- 若大写,会被作为不完整数据对待,有被丢弃的风险
- 新增伪报头字段,但不属于常规HTTP头部字段,不允许终端自己产生,只允许规范中所定义的5个
- :method
- :scheme
- :authority
- :path
- :status
- 伪报头字段必须出现在常规HTTP报头字段之前
- 连接属性专用字段(Connection-Specific Header Fields)不再被使用(但Transfer-Encoding可以允许出现在请求头中),比如Keep-Alive, Proxy-Connection, Transfer-Encoding和Upgrade等
3. 简单示范
简单图片请求:
模拟一次提交,设置报头大于16KB(一般情况下,报头没有那么大,除非Cookie撑大):
以上示例,可以帮助理解HTTP/1.x和HTTP/2在HTTP语义表述上的不同。
4. 可靠性机制
HTTP/1.1,HTTP客户端无法重试非幂等请求,尤其在错误发生的时候,由于无法检测错误性质这会对重试带来不利的影响。
而HTTP/2在这方面有所增强,提供了两种方式可判断请求是否被完成:
- GOAWAY帧会携带上流标识符的最大值,低于此值的请求已经被执行过,高于此值的请求帧,则可以再次放心重试
- 包含有REFUSED_STREAM错误代码的RST_STREAM帧说明当前流早于任何处理发生之前就已经被关闭,因此发生在当前流上的请求可以安全重试。
另外PING帧有利于客户端检测当前连接是否可用,可以理解为心跳保活机制,因为一些网关、负载设备会关闭空闲状态下的连接以节省资源。
服务器推送机制
HTTP/2新增特性,服务器根据客户端一次请求内容主动推送与之相关的请求过去,避免客户端在解析出初次请求页面内容时,再逐一发送资源请求,节省网络资源利用效率。 一些注意事项:
- 客户端可以通过设置SETTINGS_ENABLE_PUSH为0值通知服务器端禁用推送
- 承诺请求应该是可缓存、安全,并且不能够携带请求的负载内容,这需要客户端做检测
- 推送的响应若不可缓存,客户端不能作为HTTP cache存储,这对单独的非浏览器环境特别适合
- 服务器必须包含一个:authority伪头部字段,标明自身被授权。客户端若检测不到需要作为PROTOCOL_ERROR类型流错误对待
- 中介设备接收到服务器的推送后,可以决定是否要转发给客户端,中介可以单独选择推送内容发送给客户端。这是一个特别需要注意的点
- 客户端必须拒绝来自服务器端的对SETTINGS_ENABLE_PUSH属性非0值的修改,也就是说服务器不能要求客户端打开PUSH开关,客户端一旦遇到需要响应PROTOCOL_ERROR类型连接错误
- 客户端不能够发送推送,PUSH_PROMISE帧只能够来自于服务器端(作为推送请求者发送),否则将会作为PROTOCOL_ERROR类型的连接错误对待
- PUSH_PROMISE需要包含伪头部:method,若客户端认为不安全,必须响应一个PROTOCOL_ERROR类型流错误
- 服务器端应该尽可能早的发送PUSH_PROMISE帧,以避免与来自客户端对相同资源的请求两者产生冲突
- 发送PUSH_PROMISE帧会创建一个新的流,然后处于两端的保留状态,reserved (local/remote)
- 发送完PUSH_PROMISE帧,服务器需要马上发送具体DATA数据帧
- 客户端接收完PUSH_PROMISE帧后,选择接收PUSH响应内容,这期间不能触发请求承诺的响应内容,直到承诺流关闭
- 客户端不需要接收推送内容时,可以选择发送RST_STREAM帧,包含CANCEL/REFUSED_STREAM代码,以及PUSH流标识符发送给服务器端,重置推送流
- 客户端可以通过设置SETTINGS_MAX_CONCURRENT_STREAMS限制响应数,值为0禁用。但不能阻止服务器发送PUSH_PROMISE帧
比如,服务器接收到来自客户端的请求某个HTML文档资源,该文档包含了若干图片连接,服务器应该优先发送图片数据到客户端,这需要优先发送推送承诺早于包含完整HTML文档内容的DATA帧,这样客户端优先接收到承诺资源,后面接收到DATA数据帧进行解析出图片连接的时候,就避免再次发送图片资源请求嘛。
CONNECT方法
在HTTP原始语义中是没有CONNECT方法的,这个伪方法(pseudo-method)在HTTP/1.x,HTTP代理用作转换HTTP连接通过隧洞方式到远程主机,HTTPS方式交互。 HTTP/2与之类似,伪方法CONNECT被HTTP代理用作在一个单独的HTTP/2流之上建立一个到远程主机的隧道,要求如下:
- :method=CONNECT
- ":scheme"和":path"被省略
- ":authority"字段为代理要连接的远程主机和端口信息
一旦不满足要求,会被视为不完整的需求。
- 连接成功建立,代理发送给客户端一个2xx的状态码
- 代理两端在HEADERS帧都发送完毕后,后续的DATA帧开始发送
- 代理转发客户端发送的DATA数据帧到远程服务器
- 代理接收到服务器数据组装成DATA数据帧
- 非DATA类型数据帧,包括流管理类型的RST_STREAM、WINDOW_UPDATE、PRIORITY帧都是不能够在已经连接的流上发送的,否则会被当做流错误对待
- 客户端接收到包含有END_STREAM标志位的DATA帧时,尽量也要发送一个包含有END_STREAM标志位的DATA帧
- DATA帧END_STREAM标志位被当做TCP FIN比特标志对待:
- 代理接收到DATA帧带有END_STREAM标志位,在转发时会设置TCP FIN比特位
- 代理接收到TCP段包含有FIN比特位设置时,会转发一个DATA帧并携带END_STREAM标志位
- 最后的TCP段或DATA帧可以为空
- TCP连接错误以RST_STREAM帧关联
- 代理对待在TCP连接中出现的错误,包括接收到一个包含有RST比特位的TCP段,作为CONNECT_ERROR类型的流错误抛出
- 一旦检测到流或HTTP/2连接的错误,代理必须发送一个TCP段并且其RST标志被设置
- 代理不能仅仅依靠SETTINGS_MAX_CONCURRENT_STREAMS属性值进行限制资源消耗
持久连接和重用
HTTP/2消息交换通过持久连接、重用实现,目的尽可能做到资源利用率最大化。
- HTTP/2为持久性连接,基于性能原因,规范建议客户端不要关闭已有连接除非不再需要和服务器保持通信。服务器端要是主动关闭连接的话,在请求量大的情况下,会导致系统出现大量的TIME_WAIT状态TCP,每一个TIME_WAIT状态默认情况下至少持续60秒,特别占用系统资源。因此最佳实践是客户端主动关闭连接,避免Linux服务器端出现TIME_WAIT。
- 基于具体主机和端口,客户端应该只打开一个HTTP/2连接
- 客户端可以额外创建连接作为替代补充:替换已耗尽可用流标识符,或刷新TLS连接,或替换遇到错误的连接
- 当任一端想关闭连接的时候,都应该第一时间发送一个GOAWAY帧到对端,告知对方先前发送的帧已经被处理过,终止之后的一些剩余任务,终止可放心关闭
- 有一些情况服务器若不希望客户端重用连接,可返回421 (Misdirected Request) 状态码作为响应,默认可缓存(POST方法或cache-control可控制),但代理不能够为客户端请求生成421状态码。
- HTTP代理与每一个服务器之间可以尽可能保持一个持久的连接方便专递客户端的请求;客户端到代理之间可以所有请求共享、重用一个连接
小结
以上为HTTP/2消息交换机制的一些简单梳理,需要注意点:
- HTTP/2不允许使用连接特定头部字段
- 新增的5个头部
- 推送机制的一些特性需求
- RST_STREAM等帧标志位的使用
一。HTTP/2错误
1. 错误定义
HTTP/2定义了两种类型错误:
- 导致整个连接不可使用的错误为连接错误(connection error)
- 单独出现在单个连接上的错误为流错误(stream error)
2. 错误代码
错误代码,32位正整数表示错误原因,RST_STREAM和GOAWAY帧中包含。
未知或不支持的错误代码可以选择忽略,或作为INTERNAL_ERROR错误对待都可以。
3. 连接错误处理
一般来讲连接错误很严重,会导致处理进程无法进行下去,或影响到整个连接的状态。
- 终端一旦遇上连接错误,需第一时间在最后一个可用流上发送包含错误原因GOAWAY帧过去,然后关闭连接
- GOAWAY有可能不被对端成功接收到,若成功接收可获得连接被终止的原因
- 终端可在任何时间终止连接,也可以把流错误作为连接错误对待。但都应该在关闭连接之前发送一个GOAWAY帧告知对方
4. 流错误
一般来讲具体流上的流错误不会影响到其它流的处理。
- 终端检测到流错误,需要发送一个RST_STREAM帧,其包含了操作到错误流标识符
- RST_STREAM应当是发送错误流最后一个帧,内含错误原因。
- 发送方在发送之后,需要准备接收对端将要或即将发送过来的帧数据,处理方式就是忽略之,除非是可以修改连接状态帧
- 一般来讲,终端不应该发送多个RST_STREAM帧,但若在一个往返时间之后已关闭的流上能够继续接收帧,则需要发送再次发送一个RST_STREAM帧,处理这种行为不端的实现。
- 终端在接收到RST_STREAM帧之后,不能响应一个RST_STREAM帧,避免死循环
5. 连接终止
TCP连接被关闭或重置时仍有处于"open"或"half closed"的流将不能自动重试。
二。HTTP/2安全注意事项
1. 跨协议攻击
跨协议攻击,字面上理解就很简单,比如攻击者构建HTTP/1.1请求直接转发给仅仅支持HTTP/2的服务器,以期待获取攻击效果。
这里有一篇讲解跨协议攻击的文章:http://www.freebuf.com/articles/web/19622.html
TLS的加密机制使得攻击者很难获得明文,另外TLS的ALPN协议扩展可以很轻松处理请求是否需要作为HTTP/2请求进行处理,总之可有效阻止对基于TLS的其它协议攻击。
基于标准版TCP没有TLS和ALPN的帮忙,客户端所发送连接序言前缀为PRI字符串用来混淆HTTP/1.1服务器,但对其它协议没有提供保护,仅限于此。但在处理时,若接收到HTTP/1.1的请求,没有包含Upgrade升级字段,则需要认为是一个跨协议攻击。
总之,程序要尽可能的健壮,容错,针对非法的请求,直接关闭对方连接。
2. 中介端数据转换封装的攻击
中介所做的HTTP/1.1和HTTP/2之间转换,会存在攻击点:
- HTTP/2头字段名称编码允许使用HTTP/1.1没有使用到的头字段名称,中介在转换HTTP/2到HTTP/1.1时就容易出现包含非法请求头字段HTTP/1.1数据。
- HTTP/2允许头字段值可以是非法值,诸如回车(CR, ASCII 0xd), 换行 (LF, ASCII 0xa), 零字符 (NUL, ASCII 0x0),这在逐字解析实现时是一个风险。
解决方式,一旦发现非法头字段名称,以及非法头字段值,都作为不完整、残缺数据对待,或丢弃,或忽略。
3. 推送内容的缓存
推送内容有保证的服务器提供,是否缓存由头字段Cache-Control控制。
但若服务器上多租户形式(SAAS),每一个租户使用一小部分URL空间,比如 tenant1.domain.com,tenant2.domain.com,服务器需要确保没有授权的租户不能够推送超于预期的资源,覆盖已有内容。
原始服务器没有被授权使用推送,既不能够违规发送推送,也不能够被缓存。
4. 拒绝服务攻击注意事项
- HTTP/2因为要为流、报头压缩、流量控制等特性占用资源较多,因此针对每一个连接的内存分配要设置限额,否则很少的连接占满内存,无法正常服务
- 针对单个连接,规范对PUSH_PROMISE帧数量没有约束,但客户端需要设置一个上限值,这也是确定需要维护的"reserved (remote)"状态的数量,超出限额需要报ENHANCE_YOUR_CALM类型流错误
- SETTINGS帧有可能会被滥用导致对端需要花费时间解析处理设置限制等,滥用情况包括包含未定义的参数,以及同一个参数多次出现等,类似于WINDOW_UPDATE和PRIORITY帧都会存在滥用的情况;这些帧被滥用导致资源耗费情况严重
- 大量小帧或空帧一样会被滥用,但又符合逻辑,耗费服务器资源在处理报文头部上面。比如空负载DATA帧,以及用于携带报文头部数据的CONTINUATION帧,都属于安全隐患
- 报头压缩存在潜在风险,也会被滥用,详情可参考HPACK协议第七章:http://http2.github.io/http2-spec/compression.html#Security
- 终端中途发送的SETTINGS帧所定义参数不是立即可以生效的,这会导致对端在实际操作时可能会超过最新的限制。建议直接在连接建立时在连接序言内包含设置值,就算如此,客户端也会存在超出服务器端连接序言中所设置的最新限定值。
总之,诸如SETTINGS帧、小帧或空帧,报头压缩被合理滥用时,表明上看符合逻辑,会造成资源过度消耗。这需要服务器端监控跟踪到此种行为,并且设置使用数量的上限,一旦发现直接报ENHANCE_YOUR_CALM类型连接错误。
5. 报头块大小限制
报头块过大导致实现需要维护大量的状态开销。另外,根据报头字段进行路由的情况,若此报头字段出现在一系列报头块帧的最后一个帧里面,可能会导致无法正常路由到目的地。若被缓存会导致耗费大量的内存。这需要设置SETTINGS_MAX_HEADER_LIST_SIZE参数限制报头最大值,以尽可能的避免出现以上情况。
服务器一旦接收到超过报头限制请求,需要响应一个431(请求头过大) HTTP状态码,客户端呢可直接丢掉响应。
6. 压缩使用的安全隐患
- 针对安全通道,不能使用同一个压缩字典压缩保密的关键数据和易受攻击者控制的数据
- 来源数据不能确定为完全可靠,就不应该使用压缩机制
- 通用流的压缩不能在基于TLS的HTTP/2上使用这一部分,可参考 http://http2.github.io/http2-spec/compression.html#Security
7. 填充使用的安全隐患
一般来讲,填充可用来混淆帧的真实负载长度,稍加保护,降低攻击的可能性。但若不当的填充策略:固定填充数、可轻松推导出填充规则等情况都会降低保护的力度,都有可能会被攻击者破解。
中介设备应该保留DATA帧的填充(需要避免如上所述一些情况),但可丢弃HEADERS和PUSH_PROMISE帧的填充。
三。TLS
HTTP/2加密建立在TLS基础,关于TLS,维基百科上有解释:http://zh.wikipedia.org/wiki/%E5%82%B3%E8%BC%B8%E5%B1%A4%E5%AE%89%E5%85%A8%E5%8D%94%E8%AD%B0
摘取一张图,可说明基于ALPN协议扩展定义的协商流程:
其它要求:
- 只能基于TLS >= 1.2版本。目前TLS 1.3为草案版本,正式版本目前尚未可知。目前只有TLS 1.2可选。
- 必须支持Server Name Indication (SNI) [TLS-EXT]扩展,客户端在连接协商阶段需要携带上域名
- 基于TLS 1.3或更高版本构建,仅需要支持SNI扩展。TLS 1.2要求较多
- 基于TLS 1.2构建
- 必须禁用压缩机制。不恰当压缩机制会导致信息外露,HTTP/2报头有压缩机制
- 必须禁用重新协商机制。终端对待TLS 1.2重新协商作为PROTOCOL_ERROR类型连接错误对待;密码套件加密次数限制导致连接一直挂起等待不可用
- 终端可以通过重新协商提供对客户端凭证保护功能在握手期间,重新协商必须发生在发送连接序言之前进行。服务器当看到重新协商请求时应该请求客户端证书在连接建立后
- 当客户端请求受保护的特定资源时,服务器可以响应HTTP_1_1_REQUIRED错误,可有效阻止重新协商机制