1. 程式人生 > >nginx學習筆記七(nginx HTTP框架的執行流程)

nginx學習筆記七(nginx HTTP框架的執行流程)

之前已經介紹過nginx的事件框架。那麼,對於client發出的一個http的請求,nginx的http框架是如何一步步解析這個http請求?http框架又是如何和之前介紹過得epoll事件模組結合起來的,下面來簡要介紹下。

注:我手頭上的nginx工程是nginx-1.9.14的,與《深入理解nginx》的版本不一致,在http框架這塊的程式碼上也有著較大的區別。

一.ngx_http_init_connection

在http框架初始化的時候(參見《深入理解nginx》第10章),會將每個ngx_listening_t結構體的handler方法設為ngx_http_init_connection,這是框架初始化的時候完成的工作。在整個系統正常工作起來之後,client每發出一個新的http連線請求,nginx的事件模組會對這個請求進行處理,最後在ngx_event_accept函式裡面會呼叫accept系統呼叫來接收這個請求。而在ngx_event_accept函式的最後會呼叫ls->handler即ngx_http_init_connection函式。這樣一來,新的http連線就會來到http框架中的函式來進行後續的解析和處理。

void
ngx_http_init_connection(ngx_connection_t *c) 
//當建立連線後開闢ngx_http_connection_t結構,這裡面儲存該伺服器端ip:port所在server{}上下文配置資訊,和server_name資訊等,然後讓
//ngx_connection_t->data指向該結構,這樣就可以通過ngx_connection_t->data獲取到伺服器端的serv loc 等配置資訊以及該server{}中的server_name資訊


{
    ngx_uint_t              i;
    ngx_event_t            *rev;
    struct sockaddr_in     *sin;
    ngx_http_port_t        *port;
    ngx_http_in_addr_t     *addr;
    ngx_http_log_ctx_t     *ctx;
    ngx_http_connection_t  *hc;
#if (NGX_HAVE_INET6)
    struct sockaddr_in6    *sin6;
    ngx_http_in6_addr_t    *addr6;
#endif


    //注意ngx_connection_t和ngx_http_connection_t的區別,前者是建立連線accept前使用的結構,後者是連線成功後使用的結構
    hc = ngx_pcalloc(c->pool, sizeof(ngx_http_connection_t));
    if (hc == NULL) {
        ngx_http_close_connection(c);
        return;
    }


    //在伺服器端accept客戶端連線成功(ngx_event_accept)後,會通過ngx_get_connection從連線池獲取一個ngx_connection_t結構,也就是每個客戶端連線對於一個ngx_connection_t結構,
    //並且為其分配一個ngx_http_connection_t結構,ngx_connection_t->data = ngx_http_connection_t,見ngx_http_init_connection
    c->data = hc;


    /* find the server configuration for the address:port */


    port = c->listening->servers;  


    if (port->naddrs > 1) {  
    
        /*
         * there are several addresses on this port and one of them
         * is an "*:port" wildcard so getsockname() in ngx_http_server_addr()
         * is required to determine a server address
         */
        //說明listen ip:port存在幾條沒有bind選項,並且存在萬用字元配置,如listen *:port,那麼就需要通過ngx_connection_local_sockaddr來確定
    //究竟客戶端是和那個本地ip地址建立的連線
        if (ngx_connection_local_sockaddr(c, NULL, 0) != NGX_OK) { //
            ngx_http_close_connection(c);
            return;
        }


        switch (c->local_sockaddr->sa_family) {


#if (NGX_HAVE_INET6)
        case AF_INET6:
            sin6 = (struct sockaddr_in6 *) c->local_sockaddr;


            addr6 = port->addrs;


            /* the last address is "*" */


            for (i = 0; i < port->naddrs - 1; i++) {
                if (ngx_memcmp(&addr6[i].addr6, &sin6->sin6_addr, 16) == 0) {
                    break;
                }
            }


            hc->addr_conf = &addr6[i].conf;


            break;
#endif


        default: /* AF_INET */
            sin = (struct sockaddr_in *) c->local_sockaddr;


            addr = port->addrs; 


            /* the last address is "*" */
            //根據上面的ngx_connection_local_sockaddr函式獲取到客戶端連線到本地,本地IP地址獲取到後,遍歷ngx_http_port_t找到對應
            //的IP地址和埠,然後賦值給ngx_http_connection_t->addr_conf,這裡面儲存有server_name配置資訊以及該ip:port對應的上下文資訊
            for (i = 0; i < port->naddrs - 1; i++) {
                if (addr[i].addr == sin->sin_addr.s_addr) {
                    break;
                }
            }


          /*
                這裡也體現了在ngx_http_init_connection中獲取http{}上下文ctx,如果客戶端請求中帶有host引數,則會繼續在ngx_http_set_virtual_server
                中重新獲取對應的server{}和location{},如果客戶端請求不帶host頭部行,則使用預設的server{},見 ngx_http_init_connection  
            */
            hc->addr_conf = &addr[i].conf;


            break;
        }


    } else {


        switch (c->local_sockaddr->sa_family) {


#if (NGX_HAVE_INET6)
        case AF_INET6:
            addr6 = port->addrs;
            hc->addr_conf = &addr6[0].conf;
            break;
#endif


        default: /* AF_INET */
            addr = port->addrs;
            hc->addr_conf = &addr[0].conf;
            break;
        }
    }


    /* the default server configuration for the address:port */
    //listen add:port對於的 server{}配置塊的上下文ctx
    hc->conf_ctx = hc->addr_conf->default_server->ctx;


    ctx = ngx_palloc(c->pool, sizeof(ngx_http_log_ctx_t));
    if (ctx == NULL) {
        ngx_http_close_connection(c);
        return;
    }


    ctx->connection = c;
    ctx->request = NULL;
    ctx->current_request = NULL;


    c->log->connection = c->number;
    c->log->handler = ngx_http_log_error;
    c->log->data = ctx;
    c->log->action = "waiting for request";


    c->log_error = NGX_ERROR_INFO;


    rev = c->read;
    rev->handler = ngx_http_wait_request_handler;
    c->write->handler = ngx_http_empty_handler;


#if (NGX_HTTP_SPDY)
    if (hc->addr_conf->spdy) {
        rev->handler = ngx_http_spdy_init;
    }
#endif


#if (NGX_HTTP_SSL)
    {
    ngx_http_ssl_srv_conf_t  *sscf;


    sscf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_ssl_module);


    if (sscf->enable || hc->addr_conf->ssl) {


        c->log->action = "SSL handshaking";


        if (hc->addr_conf->ssl && sscf->ssl.ctx == NULL) {
            ngx_log_error(NGX_LOG_ERR, c->log, 0,
                          "no \"ssl_certificate\" is defined "
                          "in server listening on SSL port");
            ngx_http_close_connection(c);
            return;
        }


        hc->ssl = 1;


        rev->handler = ngx_http_ssl_handshake;
    }
    }
#endif


    if (hc->addr_conf->proxy_protocol) {
        hc->proxy_protocol = 1;
        c->log->action = "reading PROXY protocol";
    }


    /*
     如果新連線的讀事件ngx_event_t結構體中的標誌位ready為1,實際上表示這個連線對應的套接字快取上已經有使用者發來的資料,
     這時就可呼叫上面說過的ngx_http_init_request方法處理請求。
     */
    //這裡只可能是當listen的時候添加了defered引數並且核心支援,在ngx_event_accept的時候才會置1,才可能執行下面的if裡面的內容,否則不會只需if裡面的內容
    if (rev->ready) {
        /* the deferred accept(), iocp */
        if (ngx_use_accept_mutex) { //如果是配置了accept_mutex,則把該rev->handler延後處理,
        //實際上執行的地方為ngx_process_events_and_timers中的ngx_event_process_posted
            ngx_post_event(rev, &ngx_posted_events);
            return;
        }


        rev->handler(rev); //ngx_http_wait_request_handler
        return;
    }
這個函式最核心的地方是設定c->read->handler和c->write->handler。因為此時TCP連線已經建立了,後續當epoll_wait返回事件的時候,需要完成的就不是TCP連線操作而是資料接收處理操作。所以這裡把handler設定成了ngx_http_wait_request_handler二.ngx_http_wait_request_handler
//客戶端建立連線後,只有第一次讀取客戶端資料到資料的時候,執行的handler指向該函式,因此當客戶端連線建立成功後,只有第一次讀取
//客戶端資料才會走該函式,如果在保活期內又收到客戶端請求,則不會再走該函式,而是執行ngx_http_process_request_line,因為該函式
//把handler指向了ngx_http_process_request_line
static void
ngx_http_wait_request_handler(ngx_event_t *rev)
{
    u_char                    *p;
    size_t                     size;
    ssize_t                    n;
    ngx_buf_t                 *b;
    ngx_connection_t          *c;
    ngx_http_connection_t     *hc;
    ngx_http_core_srv_conf_t  *cscf;

    c = rev->data;
    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http wait request handler");

    if (rev->timedout) { //如果tcp連線建立後,等了client_header_timeout秒一直沒有收到客戶端的資料包過來,則關閉連線
        ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, "client timed out");
        ngx_http_close_connection(c);
        return;
    }

    if (c->close) {
        ngx_http_close_connection(c);
        return;
    }

    hc = c->data;
    cscf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_core_module);

    size = cscf->client_header_buffer_size; //預設1024

    b = c->buffer;

    if (b == NULL) {
        b = ngx_create_temp_buf(c->pool, size);
        if (b == NULL) {
            ngx_http_close_connection(c);
            return;
        }

        c->buffer = b;

    } else if (b->start == NULL) {

        b->start = ngx_palloc(c->pool, size);
        if (b->start == NULL) {
            ngx_http_close_connection(c);
            return;
        }

        b->pos = b->start;
        b->last = b->start;
        b->end = b->last + size;
    }

    //這裡如果一次沒有把所有客戶端的資料讀取完,則在ngx_http_process_request_line中會繼續讀取
    //與ngx_http_read_request_header配合讀
    n = c->recv(c, b->last, size);  //讀取客戶端來的資料    執行ngx_unix_recv

    if (n == NGX_AGAIN) {       //nginx裡面採用的都是非阻塞的recv,因此當執行recv時候可能會出現還沒傳送完的情形,這時候recv實際上就會返回EAGAIN錯誤
        if (!rev->timer_set) {
            ngx_add_timer(rev, c->listening->post_accept_timeout, NGX_FUNC_LINE);
            ngx_reusable_connection(c, 1);
        }

        if (ngx_handle_read_event(rev, 0, NGX_FUNC_LINE) != NGX_OK) {
            ngx_http_close_connection(c);
            return;
        }

        /*
         * We are trying to not hold c->buffer's memory for an idle connection.
         */

        if (ngx_pfree(c->pool, b->start) == NGX_OK) {
            b->start = NULL;
        }

        return;
    }

    if (n == NGX_ERROR) {
        ngx_http_close_connection(c);
        return;
    }

    if (n == 0) {
        ngx_log_error(NGX_LOG_INFO, c->log, 0,
                      "client closed connection");
        ngx_http_close_connection(c);
        return;
    }

    b->last += n;

    if (hc->proxy_protocol) {
        hc->proxy_protocol = 0;

        p = ngx_proxy_protocol_read(c, b->pos, b->last);

        if (p == NULL) {
            ngx_http_close_connection(c);
            return;
        }

        b->pos = p;

        if (b->pos == b->last) {
            c->log->action = "waiting for request";
            b->pos = b->start;
            b->last = b->start;
            ngx_post_event(rev, &ngx_posted_events);
            return;
        }
    }

    c->log->action = "reading client request line";

    ngx_reusable_connection(c, 0);
    //從新讓c->data指向新開闢的ngx_http_request_t
    c->data = ngx_http_create_request(c);
    if (c->data == NULL) {
        ngx_http_close_connection(c);
        return;
    }

    rev->handler = ngx_http_process_request_line;
    ngx_http_process_request_line(rev);
}
當進入這個函式的時候,一定是客戶端開始往client發實際的資料了(像HTTP頭,請求行等等)。那麼在這個函式裡面會先呼叫recv來接收下。由於nginx裡面的recv都是非阻塞的,因此當前的recv可能會沒接收到資料(比如出現client資料還沒傳送完這樣的情況,此時recv會返回EAGAIN,這個並不是出錯,而是讓程式過一會再來recv看看。在非阻塞程式裡面比較常見。)當出現EAGAIN的時候,需要再次把該事件註冊到epoll裡面去。這是因為nginx裡面的epoll採用的是ET觸發模式,epoll_wait模式將無法再次獲取該事件,所以需要重新進行註冊。然後函式會直接return,將控制權交換給HTTP框架。

ps. 有一個疑惑:如果epoll提示監聽的讀fd上有資料來了,但是取出該讀fd, 使用recv系統呼叫的返回值是EAGAIN。這具體是什麼原因導致的?epoll既然提示,那麼該fd的接收快取中應該存有一定的可讀資料才對?

如果recv返回的結果是n>0。說明此時接收到client傳來的資料了,但是隻recv一次可能沒法讀取到所有的資料,而且TCP傳送端的快取區也很可能存不下整個HTTP請求行。所以需要採取額外的措施來繼續接收資料,並且判斷是否接收到了完成的HTTP請求行。nginx是專門實現了一個函式ngx_http_process_request_line來完成這個事,本函式後來把handler指向了ngx_http_process_request_line。

三.ngx_http_process_request_line

static void
ngx_http_process_request_line(ngx_event_t *rev) //gx_http_process_request_line方法來接收HTTP請求行
{
    ssize_t              n;
    ngx_int_t            rc, rv;
    ngx_str_t            host;
    ngx_connection_t    *c;
    ngx_http_request_t  *r;

    c = rev->data;
    r = c->data;

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, rev->log, 0,
                   "http process request line");
    /*
     檢查這個讀事件是否已經超時,超時時間仍然是nginx.conf配置檔案中指定的client_header_timeout。如果ngx_event_t事件的timeout標誌為1,
     則認為接收HTTP請求已經超時,呼叫ngx_http_close_request方法關閉請求,同時由ngx_http_process_request_line方法中返回。
     */
    if (rev->timedout) {
        ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, "client timed out");
        c->timedout = 1;
        ngx_http_close_request(r, NGX_HTTP_REQUEST_TIME_OUT);
        return;
    }

    rc = NGX_AGAIN;

//讀取一行資料,分析出請求行中包含的method、uri、http_version資訊。然後再一行一行處理請求頭,並根據請求method與請求頭的資訊來決定
//是否有請求體以及請求體的長度,然後再去讀取請求體
    for ( ;; ) {

        if (rc == NGX_AGAIN) {
            n = ngx_http_read_request_header(r);

            if (n == NGX_AGAIN || n == NGX_ERROR) { 
            //如果核心中的資料已經讀完,但這時候頭部欄位還沒有解析完畢,則把控制器交還給HTTP,當資料到來的時候觸發
            //ngx_http_process_request_line,因為該函式外面rev->handler = ngx_http_process_request_line;
                return;
            }
        }
    
        rc = ngx_http_parse_request_line(r, r->header_in);

        if (rc == NGX_OK) { //請求行解析成功

            /* the request line has been parsed successfully */
            //請求行內容及長度    //GET /sample.jsp HTTP/1.1整行
            r->request_line.len = r->request_end - r->request_start;
            r->request_line.data = r->request_start;
            r->request_length = r->header_in->pos - r->request_start;

            ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
                           "http request line: \"%V\"", &r->request_line);

            //請求方法 GET  POST等    //GET /sample.jsp HTTP/1.1  中的GET
            r->method_name.len = r->method_end - r->request_start + 1;
            r->method_name.data = r->request_line.data;

            //GET /sample.jsp HTTP/1.1  中的HTTP/1.1
            if (r->http_protocol.data) {
                r->http_protocol.len = r->request_end - r->http_protocol.data;
            }

            if (ngx_http_process_request_uri(r) != NGX_OK) {
                return;
            }

            if (r->host_start && r->host_end) {

                host.len = r->host_end - r->host_start;
                host.data = r->host_start;

                rc = ngx_http_validate_host(&host, r->pool, 0);

                if (rc == NGX_DECLINED) {
                    ngx_log_error(NGX_LOG_INFO, c->log, 0,
                                  "client sent invalid host in request line");
                    ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
                    return;
                }

                if (rc == NGX_ERROR) {
                    ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
                    return;
                }

                if (ngx_http_set_virtual_server(r, &host) == NGX_ERROR) {
                    return;
                }

                r->headers_in.server = host;
            }

            if (r->http_version < NGX_HTTP_VERSION_10) { //1.0以下版本沒有請求頭部欄位,
                /*
                    使用者請求的HTTP版本小於1.0(如HTTP 0.9版本),其處理過程將與HTTP l.0和HTTP l.1的完全不同,它不會有接收HTTP
                    頭部這一步驟。這時將會呼叫ngx_http_find_virtual_server方法尋找到相應的虛擬主機?                    */
                if (r->headers_in.server.len == 0
                    && ngx_http_set_virtual_server(r, &r->headers_in.server) //http0.9應該是從請求行獲取虛擬主機?
                       == NGX_ERROR)
                {
                    return;
                }

                ngx_http_process_request(r);
                return;
            }

            //初始化用於存放http頭部行的空間,用來存放http頭部行
            if (ngx_list_init(&r->headers_in.headers, r->pool, 20,
                              sizeof(ngx_table_elt_t))
                != NGX_OK)
            {
                ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
                return;
            }

            c->log->action = "reading client request headers";

            rev->handler = ngx_http_process_request_headers;
            ngx_http_process_request_headers(rev);//開始解析http頭部行

            return;
        }

        if (rc != NGX_AGAIN) {//讀取完畢核心該套接字上面的資料,頭部行不全,則說明頭部行不全關閉連線

            /* there was error while a request line parsing */

            ngx_log_error(NGX_LOG_INFO, c->log, 0,
                          ngx_http_client_errors[rc - NGX_HTTP_CLIENT_ERROR]);
            ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
            return;
        }

        //表示該行內容不夠,例如recv讀取的時候,沒有把整行資料讀取出來,返回後繼續recv,然後接著上次解析的位置繼續解析直到請求行解析完畢
        /* NGX_AGAIN: a request line parsing is still incomplete */
        /*
             如果ngx_http_parse_request_line方法返回NGX_AGAIN,則表示需要接收更多的字元流,這時需要對header_in緩衝區做判斷,檢查
         是否還有空閒的記憶體,如果還有未使用的記憶體可以繼續接收字元流,則跳轉到第2步,檢查緩衝區是否有未解析的字元流,否則呼叫
         ngx_http_alloc_large_header_buffer方法分配更大的接收緩衝區。到底分配多大呢?這由nginx.conf檔案中的large_client_header_buffers配置項指定。
          */
        if (r->header_in->pos == r->header_in->end) {

            rv = ngx_http_alloc_large_header_buffer(r, 1);

            if (rv == NGX_ERROR) {
                ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
                return;
            }

            if (rv == NGX_DECLINED) {
                r->request_line.len = r->header_in->end - r->request_start;
                r->request_line.data = r->request_start;

                ngx_log_error(NGX_LOG_INFO, c->log, 0,
                              "client sent too long URI");
                ngx_http_finalize_request(r, NGX_HTTP_REQUEST_URI_TOO_LARGE);
                return;
            }
        }
        //表示頭部行沒有解析完成,繼續讀資料解析
    }
}

在ngx_http_process_request_line這個函式裡面,首先是判斷是否超時。如果不超時的話,那麼接下來首先是進入 n = ngx_http_read_request_header(r);函式。 在這個函式裡面,先是判斷當前使用者態快取區裡面是否有一些還沒解析的資料。如果存在一些未解析的資料,那麼會繼續下去呼叫ngx_http_parse_request_line來進行解析請求行。如果解析成功,則後面會繼續解析請求頭。但是也可能解析失敗,因為TCP是位元組流的服務,當前收到的位元組可能還沒有涵蓋整個請求行。所以rc的狀態會再變成EAGAIN,然後再次進入 ngx_http_read_request_header(r),在這個函式裡面會嘗試呼叫recv來接收資料。直至接收到足夠的資料,以成功解析請求行。