HTTP2协议:https://httpwg.org/specs/rfc7540.html
HTTP2关键词:分帧,多路复用,HPACK,优先级,应用层流控
HTTP2相关技术:QUIC,HTTP3

文章相关的Nginx版本为1.12.2,该版本Nginx支持下游HTTP2卸载。

一、数据结构

1. 数据结构图

nginx支持aspx nginx 支持http2_Nginx

2. 重点结构体

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

帧解析回调函数

3. 重点数据结构
优先级树(h2c->dependencies)

基于HTTP2流优先级的思想,在Nginx中用一棵优先级树表示各流之间的优先级关系,树以h2c->dependencies为根,挂载node节点。对于node节点,rank表示节点高度,weight表示原始权重,rel_weight表示真实权重。rank值越小,rel_weight值越大,则该node节点的优先级越大。这里重点关注rel_weight的计算:

  • 一级节点:node->rel_weight = (1.0 / 256) * node->weight;
  • 其它节点:node->rel_weight = (parent->rel_weight / 256) * node->weight;

即,相同父节点的流根据携带的weight值按比例分配被依赖流的资源。

优先级决定的资源在这里指带宽资源,优先级越高的流,其帧在h2c->last_out和h2c->waiting队列中越靠前,表示其将被越快被发送。

等待队列(h2c->waiting)

基于HTTP2流控的思想,在Nginx中用等待队列保存应用层下游发送窗口为零时被阻塞的流,当发送窗口被更新后,被阻塞的流将重新获得发送的机会。

空闲节点队列(h2c->closed)

当流关闭后,装载流的节点将被放入空闲节点队列以待重新使用,但需要注意的是,虽然node->stream被置为NULL,但其仍然存在于优先级树和node哈希表中。这里猜测Nginx采用惰性删除的思想,只有当该节点被重新使用时,才会从node哈希表删除并更新优先级树。

动态索引表( h2c->hpack)

nginx支持aspx nginx 支持http2_sed_02


基于HTTP2动态索引表思想的FIFO数据结构,其中reused表示数据被释放但entries可被重用的条目,deleted表示表尾,added表示表头,这里需要注意的是,数组尾为动态索引表的表头,数组头为动态索引表的表尾,这便于新节点的插入。上图右侧Buffer为动态索引表的实际内存空间。

节点哈希表(h2c->streams_index)

保存node节点的哈希表,以stream id作为key,hash表中不光保存有包含stream的节点,还包括stream已经释放的节点,以及优先收到priority帧时提前创建的依赖流节点,后两种节点的node->stream为NULL。

二、流程概述

1. 请求处理

下游读事件的处理是HTTP2处理的核心,这里重点看一下时间轴下,不同阶段的下游读事件回调处理(rev->handler)

ngx_http_v2_init

  • HTTP2处理的入口函数,实现关键数据结构的分配和初始化;
  • 下游发送队列入队SETTING帧和WINDOW_UPDATE帧,将接收窗口调整到最大;
  • 赋值一下阶段的读事件回调为ngx_http_v2_read_handler。

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,然后根据不同的帧类型,调用不同的解析函数。

1.1 HEADERS帧解析

首先为流分配stream和载体node,将node插入哈希表,根据流的依赖关系决定其是否加入优先级树。之后进入HEADERS帧体解析,即HTTP2 HPACK算法的实现。共列出一下几种情况:

  • ch >= (1 << 7):表示Indexed Header Field,即请求头字段的NAME和VALUE被索引。
  • ch >= (1 << 6):表示Literal Header Field With Incremental Indexing,即NAME和VALUE分别可能在也可能不在索引表中,且该项允许被加入动态索引表。
  • ch >= (1 << 5):表示Dynamic Table Size Update,即动态索引表大小更新。
  • 其它情况:表示Literal Header Field Never Indexed和Literal Header Field Without Indexing,即NAME和VALUE分别可能在也可能不在索引表中,且该项不允许被加入动态索引表。在这里,Nginx并没有区分NAME和VALUE是否允许被重新编码的情况。

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),那么请求体的收发空间如何分配。

  • 对于包体不缓存的情况(r->request_body_no_buffering = 1),根据实际限制情况分配时间内存空间,并直接建立upstream;
  • 对于包体缓存的情况(r->request_body_no_buffering = 0),在包体完全接收前不建立upstream,如果包体大小超过缓存Buffer上限,则不分配实际内存,只分配ngx_buf_t指针,并使rb->buf->sync = 1(可以理解为该buf没有实际空间),否则分配实际内存空间。

到这里,一次读事件触发后所有的HEADERS处理全部结束。可以看到,因为报文格式的不同,HTTP2在请求头解析阶段存在特有的处理,一旦请求头解析结束,就会将请求头解析结果全部赋值到ngx_http_request_t中,并进入HTTP1.X常规处理流程。

另外,基于HTTP2多路复用的思想,HTTP2一条连接可以同时处理多条流,那么必然会同时存在多个ngx_http_request_t和多个upstream,所以Nginx为每个流创建一个fake_connection(ngx_connection_t),并赋于r->connection,从而保证进入到HTTP常规流程后的正常运行。

1.2 DATA帧解析

下游读事件再次触发,基础流程为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从上游发送,都是在以下两处完成:

  • post event处理中,调用ngx_http_upstream_read_request_handler ->> ngx_http_v2_read_unbuffered_request_body。
  • upstream的写事件处理中,调用ngx_http_upstream_send_request_handler ->> ngx_http_v2_read_unbuffered_request_body。
1.3 PRIORITY帧解析

主要说明一种情况,当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帧的处理不再详述。

2. 响应处理

HTTP2响应处理的大量逻辑都和HTTP标准流程保持一致,只在部分逻辑上特有,所以这里重点说明HTTP2响应处理的介入点和主要功能。

2.1 响应头处理

标准流程中,通过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下游发送队列,最终发送。

2.2 响应体处理

标准流程下,响应体最终调用c->send_chain发送。对于HTTP2,fc->send_chain被重新赋值为ngx_http_v2_send_chain,该函数将响应体形成DATA帧并发送。