Nginx 分析之 HTTP2
首先,Nginx作为WEB服务器或者作为proxy,其基本的处理逻辑就是根据不同的Header、Method进行业务处理,无论前端协议是HTTP2还是HTTP 1.x,反正都是HTTP,所以Nginx业务处理中理论上并不关心前端是什么协议,而Nginx最开始支持的是HTTP1.x,所以当HTTP2协议过来时,它必然会先解析HTTP2,然后转换成Nginx现有流程中能够处理的数据结构(c、r等)。
HTTP2 客户端
大家可以用python构造HTTP2请求,hyper
模块高版本python自带;如果当前版本没有的话,pip安装即可。
from hyper import HTTP20Connection
c = HTTP20Connection('127.0.0.1', port=8080)
first = c.request('GET', '/', headers={'key': 'value'})
second = c.request('POST', '/post', body=b'hello')
first_response = c.get_response(first)
second_response = c.get_response(second)
HTTP2的介入
1:没有配置ssl
listen 指令后面配置了 http2,并没有配置ssl。
在accept后,调用了ngx_http_init_connection
,发现配置了http2,则修改handler:rev->handler = ngx_http_v2_init
。
2:配置了ssl
则在SSL握手完成之后,在函数ngx_http_ssl_handshake_handler
中,判断是否开启了http2以及SSL的ALPN拓展是否存在h2
,同时满足两点,才会在接下来进行HTTP2协议解析。
ngx_http_v2_init
作用:
创建并初始化http2会话 ngx_http_v2_connection_t
设置 read 的 handler为ngx_http_v2_read_handler
该函数主体是一个循环,Http2的所有处理均在该循环中:
do {
p = h2mcf->recv_buffer;
//当读取的数据没有满足header中length指定的的长度时,下面两句才会有效,相当于把之前缓存的数据
//放在当前buffer最前面。就是合并的意思。
ngx_memcpy(p, h2c->state.buffer, NGX_HTTP_V2_STATE_BUFFER_SIZE);
end = p + h2c->state.buffer_used;
n = c->recv(c, end, available);
............................
do {
//不同的处理阶段,handler不同,初始化为 ngx_http_v2_state_preface
p = h2c->state.handler(h2c, p, end);
if (p == NULL) {
return;
}
} while (p != end);
} while (rev->ready);
HTTP2握手建立
一个简单的HTTP2的报文交互如下:
HTTP2发送Header前,必然会进行类似握手的操作。
https://tools.ietf.org/html/rfc7540#page-11 中描述了client最先发送的数据:
In HTTP/2, each endpoint is required to send a connection preface as
a final confirmation of the protocol in use and to establish the
initial settings for the HTTP/2 connection. The client and server
each send a different connection preface.
The client connection preface starts with a sequence of 24 octets,
which in hex notation is:
0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a
That is, the connection preface starts with the string "PRI *
所以 这是 我们把 h2c->state.handler
初始化为ngx_http_v2_state_preface
的原因。
ngx_http_v2_state_preface
处理逻辑很简单,判断是不是 “PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n”,处理完成之后,把h2c->state.handler
设置为 ngx_http_v2_state_head
,然后调用ngx_http_v2_state_head
进行Http2的帧处理流程。
https://tools.ietf.org/html/rfc7540#page-12
4. HTTP Frames
Once the HTTP/2 connection is established, endpoints can begin
exchanging frames.
看一下ngx_http_v2_state_head
函数
static u_char *
ngx_http_v2_state_head(ngx_http_v2_connection_t *h2c, u_char *pos, u_char *end)
{
uint32_t head;
ngx_uint_t type;
//Frame的帧头必然是9个字节,Nginx处理逻辑是必须读完这9个字节才处理
if (end - pos < NGX_HTTP_V2_FRAME_HEADER_SIZE) {
return ngx_http_v2_state_save(h2c, pos, end, ngx_http_v2_state_head);
}
//取4字节
head = ngx_http_v2_parse_uint32(pos);
//4字节中高3字节是length、低1字节是type
h2c->state.length = ngx_http_v2_parse_length(head);
h2c->state.flags = pos[4];
//9字节中后4字节是stream id
h2c->state.sid = ngx_http_v2_parse_sid(&pos[5]);
pos += NGX_HTTP_V2_FRAME_HEADER_SIZE;
type = ngx_http_v2_parse_type(head);
ngx_log_debug4(NGX_LOG_DEBUG_HTTP, h2c->connection->log, 0,
"process http2 frame type:%ui f:%Xd l:%uz sid:%ui",
type, h2c->state.flags, h2c->state.length, h2c->state.sid);
if (type >= NGX_HTTP_V2_FRAME_STATES) {
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, h2c->connection->log, 0,
"http2 frame with unknown type %ui", type);
return ngx_http_v2_state_skip(h2c, pos, end);
}
//根据不同类型调用不同的处理函数
return ngx_http_v2_frame_states[type](h2c, pos, end);
}
帧头的格式
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
理解帧头的格式,那么久非常好理解ngx_http_v2_state_head
函数在干嘛了。
ngx_http_v2_frame_states数组定义了操作函数
static ngx_http_v2_handler_pt ngx_http_v2_frame_states[] = {
ngx_http_v2_state_data,
ngx_http_v2_state_headers,
ngx_http_v2_state_priority,
ngx_http_v2_state_rst_stream,
ngx_http_v2_state_settings,
ngx_http_v2_state_push_promise,
ngx_http_v2_state_ping,
ngx_http_v2_state_goaway,
ngx_http_v2_state_window_update,
ngx_http_v2_state_continuation
};
对于 setting帧,调用ngx_http_v2_state_settings
,对于HEAD帧,调用ngx_http_v2_state_headers
等。
SETTING帧处理
ngx_http_v2_state_settings
static u_char *
ngx_http_v2_state_settings(ngx_http_v2_connection_t *h2c, u_char *pos,
u_char *end)
{
//client发送的settting是携带ack标志位的,说明握手完成了
if (h2c->state.flags == NGX_HTTP_V2_ACK_FLAG) {
if (h2c->state.length != 0) {
ngx_log_error(NGX_LOG_INFO, h2c->connection->log, 0,
"client sent SETTINGS frame with the ACK flag "
"and nonzero length");
return ngx_http_v2_connection_error(h2c, NGX_HTTP_V2_SIZE_ERROR);
}
h2c->settings_ack = 1;
return ngx_http_v2_state_complete(h2c, pos, end);
}
//setting帧必须是6的整数倍
if (h2c->state.length % NGX_HTTP_V2_SETTINGS_PARAM_SIZE) {
ngx_log_error(NGX_LOG_INFO, h2c->connection->log, 0,
"client sent SETTINGS frame with incorrect length %uz",
h2c->state.length);
return ngx_http_v2_connection_error(h2c, NGX_HTTP_V2_SIZE_ERROR);
}
//client发送的setting,我们需要回复ack。这个函数构造ack的setting,然后挂队列,待会发出去用。
ngx_http_v2_send_settings(h2c, 1);
//根据settting帧中的内容设置我们的http2会话。
return ngx_http_v2_state_settings_params(h2c, pos, end);
}
上述处理逻辑是依据RFC中如下描述:
https://tools.ietf.org/html/rfc7540#page-36
ack
SETTINGS parameters are acknowledged by the receiving peer. To
enable this, the SETTINGS frame defines the following flag:
6字节整数倍
A SETTINGS frame with a length other than a multiple of 6 octets MUST
be treated as a connection error (Section 5.4.1) of type
FRAME_SIZE_ERROR.
HEADERS帧处理
1:根据stream id创建一个strem对象,用来描述这个stream
因为每个HEADER帧,stream id必然是递增的,所以处理这个HEADER时,必然会需要新建一个stream。
ngx_http_v2_state_headers:
node = ngx_http_v2_get_node_by_id(h2c, h2c->state.sid, 1);
stream = ngx_http_v2_create_stream(h2c);
h2c->state.stream = stream;
stream->node = node;
node->stream = stream;
ngx_http_v2_get_node_by_id
会去h2c->streams_index
中取一个node。这个node存储形式和哈希桶一样。根据streamid算index,然后使用链表处理冲突。
h2c->streams_index[]
---------------------------------------------------------------------------------
| ngx_http_v2_node_t* | ngx_http_v2_node_t* | ngx_http_v2_node_t* |........... |
-------—--------—--------—--------—-------—-------—-------—----------------------
|
|
ngx_http_v2_node_t *
|
|
ngx_http_v2_node_t *
.
.
node保存在链表中,而stream和一个node互指。这样就能根据一个streamid来找stream了。
获取header
ngx_http_v2_state_header_block负责处理每一个header,解析完把header放到h2c->state.header
中。一个header的格式,都有HPACK封装。
HPACK详细见文章
这里按照不同的HEADER编码格式,梳理流程。
header的name和value都在表中(静态或者动态)
ngx_http_v2_state_header_block
函数 中直接 调用ngx_http_v2_get_indexed_header
函数获取。
ngx_http_v2_state_header_block:
if (indexed) {
if (ngx_http_v2_get_indexed_header(h2c, value, 0) != NGX_OK) {
return ngx_http_v2_connection_error(h2c, NGX_HTTP_V2_COMP_ERROR);
}
return ngx_http_v2_state_process_header(h2c, pos, end);
}
ngx_http_v2_get_indexed_header:
index--;
//从静态表获取
if (index < NGX_HTTP_V2_STATIC_TABLE_ENTRIES) {
h2c->state.header = ngx_http_v2_static_table[index];
return NGX_OK;
}
//从动态表获取,详细的后面会说。
index -= NGX_HTTP_V2_STATIC_TABLE_ENTRIES;
.......
header的name在表中,但是value不在表中
ngx_http_v2_state_header_block
函数的value为非0,index变量为0。
name从ngx_http_v2_get_indexed_header
中取得,value需要在ngx_http_v2_state_field_len
hpack解码。
if (value == 0) {
h2c->state.parse_name = 1;
} else if (ngx_http_v2_get_indexed_header(h2c, value, 1) != NGX_OK) {
return ngx_http_v2_connection_error(h2c, NGX_HTTP_V2_COMP_ERROR);
}
h2c->state.parse_value = 1;
return ngx_http_v2_state_field_len(h2c, pos, end);
header的name和value都不在表中
ngx_http_v2_state_field_len
函数会递归调用2次,分别解析name和value。
header处理
1:如果HPACK指定某个header需要index,则在函数ngx_http_v2_state_process_header
函数中会调用ngx_http_v2_add_header
把header加入动态表中。
2:把header push进r->headers_in.headers,业务中需要。
3:如果所有head读取完成(length指定的data都处理完了),则调用ngx_http_v2_run_request
,其中会调用ngx_http_process_request_header
和ngx_http_process_request
标准的Nginx http处理流程进行处理。
HTTP2如何兼容Nginx的HTTP处理流程
HTTP1.1是如何在Nginx处理的?首先accept时,创建一个c,然后当有read信号是,创建一个r,然后read HTTP的头部,放到r中,然后处理r。一个r处理完成之后,r会被释放,c处于idle状态;如果当前TCP有另一个HTTP请求过来时,再新建一个r,继续如上处理。
但是HTTP2呢,一个TCP里面会有多个http请求同时发过来,即一个c,理论上会对应多个r的同时存在,Nginx是如何处理的呢?
fake connection
处理HTTP2时,c肯定只有accpt时创建的c,但是除了这个c,还会针对每个steam,创建一个fake的c,然后针对每个这个fake的c,创建一个r。
--> fake c1(stream 1) -> r1
|
c(real) --> fake c2(stream 3) -> r2
|
--> fake c3(stream 5) -> r3
fake c在ngx_http_v2_create_stream
时创建,在ngx_http_v2_close_stream
时释放。
static ngx_http_v2_stream_t *
ngx_http_v2_create_stream(ngx_http_v2_connection_t *h2c, ngx_uint_t push)
{
ngx_log_t *log;
ngx_event_t *rev, *wev;
ngx_connection_t *fc;
ngx_http_log_ctx_t *ctx;
ngx_http_request_t *r;
ngx_http_v2_stream_t *stream;
ngx_http_v2_srv_conf_t *h2scf;
ngx_http_core_srv_conf_t *cscf;
fc = h2c->free_fake_connections;
if (fc) {
h2c->free_fake_connections = fc->data;
rev = fc->read;
wev = fc->write;
log = fc->log;
ctx = log->data;
} else {
fc = ngx_palloc(h2c->pool, sizeof(ngx_connection_t));
if (fc == NULL) {
return NULL;
}
rev = ngx_palloc(h2c->pool, sizeof(ngx_event_t));
if (rev == NULL) {
return NULL;
}
wev = ngx_palloc(h2c->pool, sizeof(ngx_event_t));
if (wev == NULL) {
return NULL;
}
.........
ngx_memcpy(fc, h2c->connection, sizeof(ngx_connection_t));
.........
r = ngx_http_create_request(fc);
.........
r->stream = stream;
stream->request = r;
stream->connection = h2c;
}
fake c中的内容,完全拷贝自网络事件中的c(h2c->connection
)。
通过r能够找到stream,通过stream能够找到h2c,通过h2c就能够找到real c了。