client需要通过http1.1的upgrade header告诉server (值为h2c),进行协议升级,同时通过http2-settings header把HTTP/2 SETTINGS的payload base64url后的值告诉server,这样的设计让client 可以在进行http2通信前告诉server一些参数 ???
server端如果支持http2, 则需要接受upgrade,返回101状态(Swtiching Protocols),在空行后开始传输http2二进制帧数据。
一旦client收到server的101 response,就需要发送connection preface(包括SETTINGS frame)
如果client知道server一定支持http2,那么就可以直接发送connection preface,省去了upgrade这个rtt
用TLS的ALPN extension来确定http2协议,一旦TLS协商完成后,client和server都需要发送connection preface
是为了让两端都确定通信的协议以及一些通信的初始参数设定
conenction preface由一段特殊的固定字符串开始,紧跟着是SETTINGS frame。
client端要么在101 response后开始发送preface,要么在TLS的第一个appliaction data中开始发送。如果知道server端是支持http2的,那么直接就可以发送connection preface
client端发送完connection preface后直接就能开始发送后续的数据frame,不需要等待server端的conection preface。当client收到server的conneciton preface后需要去尊重这些参数,可能需要去变更一些配置 (这点和tcp有些不一样)
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
header占了9个字节。分别是
HEADERS、PUSH_PROMISE、CONTINUATION三种frame中都会包括http header的内容,其中HEADERS是client向server发送数据时的header内容,PUSH_PROMISE是server主动push给client数据时的http header内容,CONTINUATION则必须要跟着HEADERS或者PUSH_PROMISE一起出现,用于当一帧发不完时候继续帧。
Header list是header的key-value集合,http2中将header list压缩成header block,header block会被划分成一个或者多个header block fragments,作为HEADERS、PUSH_PROMIST、CONTINUATION的payload 接收端需要组装收到的这些fragments,然后decompress来重建header list.
Header compression 是有状态的,一个compression context和一个decompression context作用于整个connection生命周期。
Header blocks 必须要作为连续的frames来传输,中间不能有其他类型的frame或者其他的stream的frame穿插进来。
+--------+
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: endpoint sends this frame
recv: endpoint receives this frame
H: HEADERS frame (with implied CONTINUATIONs)
PP: PUSH_PROMISE frame (with implied CONTINUATIONs)
ES: END_STREAM flag
R: RST_STREAM frame
再开始介绍stream状态机之前需要先了解下http2的message exchange机制,http2设计上为了最大程度兼容目前http的语义,因为目前的request/response语义被保留了下来。通过在一个http2 stream上按照一定的约束交换各种http2 frame,达到组成语义上实现request/response的目的,这就构成http2的message,一个message通过由如下几个部分构成:
一般来说就只有2和3
一个完整的http2 stream则包括一个完整的http request/response交换,因此正常的stream的状态一般来说有idle -> open -> half close -> close。
一个request message从HEADERS frame开始(将stream从idle状态切到open状态),到一个带END_STREAM的frame结束(将stream切到half-closed状态, 对于client来说是half-closed local, 对于server来说是half-closed remote)。
一个response message从HEADERS frame开始到一个带END_STREAN的frame结束(同时会将stream置为closed状态)。
http2允许server根据client之前发起的request来先发制人的发送一些responses (和相应的“承诺过”的requests一起)给client。Server push在语义上等价于server去响应一个request,只不过这个request也是server发送的(通过PUSH_PROMISE帧发送这个request)。
PUSH_PROMISE frame和HEADERS frame一样,payload是http header block,而且要包括所有的必要的header fields。区别在于,HEADERS是会打开一个新的stream,而PUSH_PROMISE则是在之前的client发起的stream上发送。在payload里面包括一个promised stream identifer作为一个后面新的Server push stream。另外,还有一点对于理解什么是PUSH_PROMISE frame很重要的是PUSH_PROMISE是分散在组成http response的各个帧之间的,比如:client请求一个包括多个图片link的富文档,server在响应这个request的response中可能会穿插多个PUSH_PROMISE帧,每个图片link一个PUSH_PROMISE,这些PUSH_PROMIISE帧发送在富文档stream上,意图在于告诉client端,我后面会主动地push给你这些图片内容。这也是叫promise的原因。
server发送完PUSH_PROMISE之后,就可以开始交付push response了(相对自己说的话,现在来兑现了),这个response将会在新的stream上发送(就是之前PUSH_PROMISE frame里指定的promised stream identifer上发送),另外push response message的构成和普通的http response message的构成完全一样。区别在于对stream state的变更时机不太一样,普通的http response message在发送之前stream就已经处于half closed 状态了,但是push response不太一样,在push response开始之前,stream处于reserved状态,所以当push response发送完HEADERS帧后需要做一次stream state切换,从reserved状态切换到half-closed状态。
stream id有如下几个属性
关于stream的并发度再介绍一点: SETTINGS frame中有个参数可以限制允许一端最大同时发起的stream个数(SETTINGS_MAX_CONCURRENT_STREAMS)。要注意的是这个值是针对各自端的限制,如果client端想要关闭server push功能,那么可以在发给server的SETTIINGS帧中把SETTINGS_MAX_CONCURRENT_STREAMS置为0,意思就是不允许server端发起stream。只有处于open或者half-closed状态的stream才算做是并发的stream,reserved状态的stream还不算(因为还没有开始去发送push response)。
http2的flow control主要有以下几个特点:
priority的引入有两个目的:
那么怎么来表达stream的priority呢,HTTP2通过依赖树来表达stream的优先级,如果stream B依赖于Stream A,那么A的优先级就高于B。那问题来了,如果有多个streams B、C、D都依赖于A,HTTP2通过引入dependency weight来描述依赖于同一个stream的多个流之间的权重,这个权重决定了这些个流分配资源的比例。这样将当前所有的并发的流形成了一颗依赖树,root是stream 0,每个stream只有一个父依赖,每个stream可以被多个子stream依赖。
问题: 怎么表达一个stream依赖多个stream的情况???
怎么设置stream dependency呢?
在HEADERS frame中指定dependency和weight
HEADERS frame format:
+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|E| Stream Dependency? (31) |
+-+-------------+-----------------------------------------------+
| Weight? (8) |
+-+-------------+-----------------------------------------------+
| Header Block Fragment (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+
其中E flag标识该stream是排他性依赖父stream的,如果父stream目前有子stream依赖,那么所有的子依赖将成为当前stream的依赖。
在PRIORITY frame中指定dependency和weight
PRIORITY frame format:
+-+-------------------------------------------------------------+
|E| Stream Dependency (31) |
+-+-------------+-----------------------------------------------+
| Weight (8) |
+-+-------------+
值得注意的是PRIORITY帧可以在任何时候发送,也就是无论当前stream处于什么状态,都可以通过PRIORITY来更改stream的依赖状态。如果当前stream处于idle或者closed状态,主要作用是为了能够对依赖他的所有子stream进行Reprioritization。也就是说既可以对一个idle/closed的stream进行依赖变更,也可以让一个stream去depends-on一个idle/closed的stream。
这里协议规定了当stream的dependency发生变化时一些case的处理
例子:(让A重新依赖子stream D)
? ? ? ?
| / \ | |
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)
为了能够正确处理流的dependency信息的改变(术语就是Reprioritization),每端都需要维护stream的依赖状态(prioritization state)也就是维护依赖树。比如当一个stream从依赖树中删除时,那么可以把他的所有的子stream移动成为该stream的父stream的子,这时候这些被移动的子stream的weight需要根据被删除的stream原来的weight进行重新计算。那么什么时候会去从依赖树里删除一个stream呢?这要取决于具体的实现,因为存在有些stream会去依赖一个已经closed的流,所以依赖树的状态要能够尽量保留这些已经closed的stream。
所有的stream都默认depends on Stream 0, Pushed Streams则depends on他们关联的client端发起的stream,另外所有的stream的weight默认值都是16
+---------------+
|Pad Length? (8)|
+---------------+-----------------------------------------------+
| Data (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+
+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|E| Stream Dependency? (31) |
+-+-------------+-----------------------------------------------+
| Weight? (8) |
+-+-------------+-----------------------------------------------+
| Header Block Fragment (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+
+-+-------------------------------------------------------------+
|E| Stream Dependency (31) |
+-+-------------+-----------------------------------------------+
| Weight (8) |
+-+-------------+
+---------------------------------------------------------------+
| Error Code (32) |
+---------------------------------------------------------------+
payload包含0到多个参数配置,每个参数配置的格式如下:
+-------------------------------+
| Identifier (16) |
+-------------------------------+-------------------------------+
| Value (32) |
+---------------------------------------------------------------+
具体参数配置这里就不写了,参考协议文档
+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|R| Promised Stream ID (31) |
+-+-----------------------------+-------------------------------+
| Header Block Fragment (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+
+---------------------------------------------------------------+
| |
| Opaque Data (64) |
| |
+---------------------------------------------------------------+
+-+-------------------------------------------------------------+
|R| Last-Stream-ID (31) |
+-+-------------------------------------------------------------+
| Error Code (32) |
+---------------------------------------------------------------+
| Additional Debug Data (*) |
+---------------------------------------------------------------+
+-+-------------------------------------------------------------+
|R| Window Size Increment (31) |
+-+-------------------------------------------------------------+
+---------------------------------------------------------------+
| Header Block Fragment (*) ...
+---------------------------------------------------------------+