Nginx源码分析之 HTTP2

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函数

扫描二维码关注公众号,回复: 2174409 查看本文章
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_lenhpack解码。

    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_headerngx_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了。

猜你喜欢

转载自blog.csdn.net/mrpre/article/details/80711448