HTTP2协议:https://httpwg.org/specs/rfc7540.html
HTTP2关键词:分帧,多路复用,HPACK,优先级,应用层流控
HTTP2相关技术:QUIC,HTTP3
文章相关的Nginx版本为1.12.2,该版本Nginx支持下游HTTP2卸载。
ngx_http_v2_connection_t
元素 | 含义 |
---|---|
*connection | 下游连接 |
*http_connection | HTTP连接上下文 |
processing | 目前存在的流数量 |
send_window | 发送窗口实际值 |
recv_window | 接收窗口实际值 |
init_window | 接收窗口初始值 |
frame_size | 指示客户端接收最大帧载荷 |
waiting | waiting队列,当发送窗口为0时,stream按优先级在waiting队列阻塞。 |
state | v2的处理以帧为单位,state存储当前处理帧的各类状态。 |
hpack | header解析上下文 |
*free_frames | 空闲帧 |
*free_fake_connection | 空闲连接 |
**streams_index | node节点哈希表 |
*last_out | 下游外出帧链表 |
dependencies | 优先级依赖树 |
closed | 空闲node队列 |
last_sid | 上一个处理帧的stream id |
closed_nodes | closed队列节点数 |
settings_ack | 是否收到setting帧的ACK |
blocked | blocked标志位 |
goaway | goaway标志位 |
ngx_http_v2_node_t
元素 | 含义 |
---|---|
id | 该node的stream id值 |
*index | 哈希节点链表下一node节点 |
*parent | 优先级树中的父节点 |
queue | 优先级树节点 |
children | 优先级树子节点队列 |
reuse | h2c->closed队列节点 |
rank | 节点在优先级树种的等级 |
weight | 节点权重 |
rel_weight | 节点真实权重 |
*stream | 节点对应的stream |
ngx_http_v2_stream_t
元素 | 含义 |
---|---|
*request | 流对应请求 |
*connection | h2c上下文 |
*node | 流对应node |
queued | 下游发送缓冲区的帧个数 |
send_window | http2流控:发送窗口 |
recv_window | http2流控:接收窗口 |
*preread | 预读preread缓存 |
*free_frames | 空闲帧 |
*free_freme_headers | 空闲帧头chain |
*free_bufs | 空闲帧体chain |
queue | 等待队列节点 |
*cookies | cookies数组 |
header_limit | 最大header长度限制 |
*pool | 内存池 |
waiting | 连接send_window为0阻塞标志位 |
blocked | block标志位 |
exhausted | 流send_window为0阻塞标志位 |
in_closed | 下游收关闭标志位 |
out_closed | 下游发关闭标志位 |
rst_sent r | st stream发送标志位 |
no_flow_control | 接收窗口无流控标志位 |
skip_data | 跳过data帧解析标志位 |
ngx_http_v2_state_t
元素 | 含义 |
---|---|
sid | 帧头stream id |
length | 帧头length值 |
padding | 帧体pad legth值 |
flags | 帧头flag值 |
incomplete | 帧未接收完整标志位 |
keep_pool | 内存池释放标志位 |
parse_name | 标记是否需要解析name |
parse_value | 标记是否需要解析value |
index | 表示性的key-value是否需要加入动态索引表 |
header | 保存当前解析的HTTP头的name-value |
header_limit | 配置项htt2_max_header_size |
field_state | |
*field_start | 保存正在解析的name或value |
*field_end | 保存正在解析的name或value |
field_rest | name或value长度 |
*pool | 内存池 |
*stream | 流指针 |
*buffer | 临时数组,保存未解析报文 |
buffer_used b | uffer使用字节数 |
handler | 帧解析回调函数 |
基于HTTP2流优先级的思想,在Nginx中用一棵优先级树表示各流之间的优先级关系,树以h2c->dependencies为根,挂载node节点。对于node节点,rank表示节点高度,weight表示原始权重,rel_weight表示真实权重。rank值越小,rel_weight值越大,则该node节点的优先级越大。这里重点关注rel_weight的计算:
即,相同父节点的流根据携带的weight值按比例分配被依赖流的资源。
优先级决定的资源在这里指带宽资源,优先级越高的流,其帧在h2c->last_out和h2c->waiting队列中越靠前,表示其将被越快被发送。
基于HTTP2流控的思想,在Nginx中用等待队列保存应用层下游发送窗口为零时被阻塞的流,当发送窗口被更新后,被阻塞的流将重新获得发送的机会。
当流关闭后,装载流的节点将被放入空闲节点队列以待重新使用,但需要注意的是,虽然node->stream被置为NULL,但其仍然存在于优先级树和node哈希表中。这里猜测Nginx采用惰性删除的思想,只有当该节点被重新使用时,才会从node哈希表删除并更新优先级树。
基于HTTP2动态索引表思想的FIFO数据结构,其中reused表示数据被释放但entries可被重用的条目,deleted表示表尾,added表示表头,这里需要注意的是,数组尾为动态索引表的表头,数组头为动态索引表的表尾,这便于新节点的插入。上图右侧Buffer为动态索引表的实际内存空间。
保存node节点的哈希表,以stream id作为key,hash表中不光保存有包含stream的节点,还包括stream已经释放的节点,以及优先收到priority帧时提前创建的依赖流节点,后两种节点的node->stream为NULL。
下游读事件的处理是HTTP2处理的核心,这里重点看一下时间轴下,不同阶段的下游读事件回调处理(rev->handler)
ngx_http_v2_init
ngx_http_v2_read_handler
核心处理函数,所有帧的解析工作都在这里完成。从代码流程也可以清晰看到,Nginx在不断的recv,然后调用h2c->state.handler对接收数据进行处理。h2c->state.handler存在ngx_http_v2_state_preface和ngx_http_v2_state_head两种情况,前者用于连接建立后解析HTTP2的前言内容,后者首先解析常规的HTTP2帧头包括length、flags、stream id、type,然后根据不同的帧类型,调用不同的解析函数。
首先为流分配stream和载体node,将node插入哈希表,根据流的依赖关系决定其是否加入优先级树。之后进入HEADERS帧体解析,即HTTP2 HPACK算法的实现。共列出一下几种情况:
Nginx根据上述HEADERS情况,或直接从索引表获取HEADER(ngx_http_v2_get_indexed_header),或对NAME和VALUE进行解析(ngx_http_v2_state_field_len / ngx_http_v2_state_field_raw / ngx_http_v2_state_field_huff),并根据上述情况决定是否将HEADER加入动态索引表(ngx_http_v2_add_header)。
与所有的NAME-VALUE后续被加入r->headers_in.headers保存不同,cookie被单独保存在r->stream->cookies数组中,这是因为HTTP2为提高cookie的传输效率,多组cookie不再通过一个cookie头字段传输,而是通过多个NAME-VALUE对传输,所以这里对cookie以数组形式保存以便后续形成HTTP1.X的cookie。
当所有HEADER都被解析完成后,开始处理请求事务(ngx_http_v2_run_request)。
对于没有请求体的情况,HTTP2的请求处理几乎和HTTP1.X完全一致,因为所有需要解析的东西都已经解析结束,后续事务无非建立上游并转发请求。但在存在请求体的情况下,HTTP2有特殊流程处理(ngx_http_v2_read_request_body)。
到这里为止,CPU仍陷入在一次读事件触发的HEADERS处理中未返回。因为请求体的存在,所以需要给请求体的收发分配空间(rb),那么请求体的收发空间如何分配。
到这里,一次读事件触发后所有的HEADERS处理全部结束。可以看到,因为报文格式的不同,HTTP2在请求头解析阶段存在特有的处理,一旦请求头解析结束,就会将请求头解析结果全部赋值到ngx_http_request_t中,并进入HTTP1.X常规处理流程。
另外,基于HTTP2多路复用的思想,HTTP2一条连接可以同时处理多条流,那么必然会同时存在多个ngx_http_request_t和多个upstream,所以Nginx为每个流创建一个fake_connection(ngx_connection_t),并赋于r->connection,从而保证进入到HTTP常规流程后的正常运行。
下游读事件再次触发,基础流程为ngx_http_v2_read_handler -> ngx_http_v2_state_head -> ngx_http_v2_state_data -> ngx_http_v2_state_read_data -> ngx_http_v2_process_request_body,帧头的解析不再说明,直接解析帧体的处理。
若r->request_body_no_buffering = 0,即包体缓存发送,则
if (buf->sync) {
buf->pos = buf->start = pos;
buf->last = buf->end = pos + size;
...
} else {
...
buf->last = ngx_cpymem(buf->last, pos, size);
}
如果rb未分配实际内存,则赋值指针,并调用ngx_http_v2_filter_request_body将包体写入文件;如果rb分配实际内存,则包体内容拷贝入rb后返回,待包体完全接收后建立上游。
若r->request_body_no_buffering = 1,即包体不缓存,将包体拷贝入rb,随后调用ngx_post_event(fc->read, &ngx_posted_events),这点需要重点关注,下游HTTP2的读触发最终只是将包体写入rb->buf,而最终将Buffer写入rb->bufs(chain),以及chain从上游发送,都是在以下两处完成:
主要说明一种情况,当PRIORITY帧到来时,依赖流(非被依赖流)仍不存在,那么此时Nginx提前分配node节点,将节点插入哈希表,并将节点根据PRIORITY帧表述的依赖情况加入优先级树,但该节点仍存在与h2c->closed队列中,直到流真正到来的时候(HEADERS处理),才会为其分配stream,并将其从closed队列删除。优先级树的插入逻辑如下:
void ngx_http_v2_set_dependency()
{
if (parent == NULL) {
/* 如果未查找到被依赖节点,则该节点为根节点的一级节点 */
} else {
if (node->parent != NULL) {
/* 如果插入node已在优先级树中,则先从优先级树中删除
并更新优先级树的权重 */
}
/* 根据新的依赖关系,确定节点rank和实际权重,并获取父节
点的子节点队列children */
}
if (exclusive) {
/* 如果该节点Exclusive Flag被置位,那么新node将被插入
parent和children之间新的一级,那么原始children变为新
node的子节点。 */
}
/* 将新node变为parent的子节点,并更新优先级树中节点的
rel_weight */
}
其它HTTP2帧的处理不再详述。
HTTP2响应处理的大量逻辑都和HTTP标准流程保持一致,只在部分逻辑上特有,所以这里重点说明HTTP2响应处理的介入点和主要功能。
标准流程中,通过ngx_http_send_header -> ngx_http_top_header_filter(ngx_http_header_filter)调用,最终将r->headers_out中的响应头形成Buffer并发送。对于HTTP2,ngx_http_top_header_filter被赋值为ngx_http_v2_header_filter,将r->headers_out中的响应头形成HEADERS帧,并挂在h2c->last_out下游发送队列,最终发送。
标准流程下,响应体最终调用c->send_chain发送。对于HTTP2,fc->send_chain被重新赋值为ngx_http_v2_send_chain,该函数将响应体形成DATA帧并发送。