解析Nginx对HTTP2的支持

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

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

一、数据结构

1. 数据结构图

在这里插入图片描述

2. 重点结构体

ngx_http_v2_connection_t

元素含义
*connection下游连接
*http_connectionHTTP连接上下文
processing目前存在的流数量
send_window发送窗口实际值
recv_window接收窗口实际值
init_window接收窗口初始值
frame_size指示客户端接收最大帧载荷
waitingwaiting队列,当发送窗口为0时,stream按优先级在waiting队列阻塞。
statev2的处理以帧为单位,state存储当前处理帧的各类状态。
hpackheader解析上下文
*free_frames空闲帧
*free_fake_connection空闲连接
**streams_indexnode节点哈希表
*last_out下游外出帧链表
dependencies优先级依赖树
closed空闲node队列
last_sid上一个处理帧的stream id
closed_nodesclosed队列节点数
settings_ack是否收到setting帧的ACK
blockedblocked标志位
goawaygoaway标志位

ngx_http_v2_node_t

元素含义
id该node的stream id值
*index哈希节点链表下一node节点
*parent优先级树中的父节点
queue优先级树节点
children优先级树子节点队列
reuseh2c->closed队列节点
rank节点在优先级树种的等级
weight节点权重
rel_weight节点真实权重
*stream节点对应的stream

ngx_http_v2_stream_t

元素含义
*request流对应请求
*connectionh2c上下文
*node流对应node
queued下游发送缓冲区的帧个数
send_windowhttp2流控:发送窗口
recv_windowhttp2流控:接收窗口
*preread预读preread缓存
*free_frames空闲帧
*free_freme_headers空闲帧头chain
*free_bufs空闲帧体chain
queue等待队列节点
*cookiescookies数组
header_limit最大header长度限制
*pool内存池
waiting连接send_window为0阻塞标志位
blockedblock标志位
exhausted流send_window为0阻塞标志位
in_closed下游收关闭标志位
out_closed下游发关闭标志位
rst_sent rst 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_restname或value长度
*pool内存池
*stream流指针
*buffer临时数组,保存未解析报文
buffer_used buffer使用字节数
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)

在这里插入图片描述
基于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帧并发送。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值