1. 程式人生 > >菜鳥學習nginx之HTTP body接收(2)

菜鳥學習nginx之HTTP body接收(2)

上一篇介紹Nginx是如何丟棄body,雖然是丟棄body但是仍然需要乖乖的從socket緩衝區中讀取報文才行。本篇將介紹實際接收流程。

一、概要

接收流程是比較複雜的,主要涉及到兩個方面考慮:body過長如何儲存以及一次接收不完body應該如何設定下次接收。Nginx採用如下方式解決上述問題:

1、如果一個buffer緩衝區不能夠容納body,則會把body寫入到臨時檔案中。

2、如果一次接收不完則會重新設定epoll可讀事件,並且修改回撥函式。這點在上一篇中也有提到。第一次接收body的回撥函式和第二次接收的回撥函式不一樣。

二、首次接收body函式ngx_http_read_client_request_body

2.1、流程圖

該函式是入口函式且上層應用唯一能夠使用的介面函式,我們先來看一下它的流程圖:

2.2、程式碼

由於函式ngx_http_read_client_request_body邏輯比較複雜,這裡將分段顯示

/**
 * 呼叫接收body
 * @param r 請求
 * @param post_handler body處理的回撥函式
 */
ngx_int_t
ngx_http_read_client_request_body(ngx_http_request_t *r,
    ngx_http_client_body_handler_pt post_handler)
{
    size_t                     preread;
    ssize_t                    size;
    ngx_int_t                  rc;
    ngx_buf_t                 *b;
    ngx_chain_t                out;
    ngx_http_request_body_t   *rb;
    ngx_http_core_loc_conf_t  *clcf;

    r->main->count++;
    
    /**
     * 如果不是原始請求、已經標記是丟棄body、request_body不空則呼叫回撥函式
     * request_body不空說明已經讀取到完整body
     * 為什麼說明這裡request_body不空就能說明已經讀取到完整body呢?
     * 原因: 此函式在業務處理流程只會被呼叫一次 即使一次epoll讀取事件不能把所有
     * body都讀取成功,下一次讀取操作不會由該函式處理而是由
     * ngx_http_read_client_request_body_handler處理
     */
    if (r != r->main || r->request_body || r->discard_body) {
        r->request_body_no_buffering = 0;
        post_handler(r); //回撥函式必須呼叫類似ngx_http_finalize_request將count進行自減
        return NGX_OK;
    }

#if (NGX_HTTP_V2)
    if (r->stream) {
        rc = ngx_http_v2_read_request_body(r, post_handler);
        goto done;
    }
#endif

    if (ngx_http_test_expect(r) != NGX_OK) {
        rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
        goto done;
    }

說明:

1)函式第二引數post_handler是使用者指定的回撥函式。當body接收完畢後會主動呼叫該引數指向的回撥函式。 

2)判斷body是否有效,如果有效則直接呼叫post_handler回撥函式。註釋中已經詳細說明。

    /* 表明開始接收body 分配request_body物件 */
    rb = ngx_pcalloc(r->pool, sizeof(ngx_http_request_body_t));
    if (rb == NULL) {
        rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
        goto done;
    }

    /*
     * set by ngx_pcalloc():
     *
     *     rb->bufs = NULL;
     *     rb->buf = NULL;
     *     rb->free = NULL;
     *     rb->busy = NULL;
     *     rb->chunked = NULL;
     */

    rb->rest = -1; /* 後續流程會賦值 初始值為content-length */
    rb->post_handler = post_handler;

    r->request_body = rb;
    
    /* 表示body小於0且不是chunked模式 則認為沒有接收到body 立即呼叫回撥函式 */
    if (r->headers_in.content_length_n < 0 && !r->headers_in.chunked) {
        r->request_body_no_buffering = 0;
        post_handler(r);
        return NGX_OK;
    }

說明:

1)建立用於儲存body的request_body物件

2)判斷headers_in中content_length是否小於0,如果小於0則說明沒有body,則直接呼叫post_handler回撥函式。

    /* 進入此函式表示 HTTP header內容已經處理完畢 剩餘的內容就是body */
    preread = r->header_in->last - r->header_in->pos;

    if (preread) {

        /**
         * there is the pre-read part of the request body 
         * 表示已經讀取到一部分body
         */

        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                       "http client request body preread %uz", preread);

        out.buf = r->header_in;
        out.next = NULL;
        /* 過濾出body */
        rc = ngx_http_request_body_filter(r, &out);

        if (rc != NGX_OK) {
            goto done;
        }

        r->request_length += preread - (r->header_in->last - r->header_in->pos);

        /**
         * 進入下面if分支條件:
         * 1、非chunked模式
         * 2、body還沒接收完成,且header_in剩餘空間足夠接收剩下的body
         * 做的主要工作:
         * 重置讀寫事件,進行socket讀取
         */
        if (!r->headers_in.chunked
            && rb->rest > 0
            && rb->rest <= (off_t) (r->header_in->end - r->header_in->last))
        {
            /**
             * the whole request body may be placed in r->header_in
             * 
             */
            b = ngx_calloc_buf(r->pool);
            if (b == NULL) {
                rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
                goto done;
            }

            b->temporary = 1;
            b->start = r->header_in->pos;
            b->pos = r->header_in->pos;
            b->last = r->header_in->last;
            b->end = r->header_in->end;

            rb->buf = b;
            /* 設定讀寫事件回撥函式 */
            r->read_event_handler = ngx_http_read_client_request_body_handler;
            r->write_event_handler = ngx_http_request_empty_handler;

            rc = ngx_http_do_read_client_request_body(r);
            goto done;
        }

    } else {
        /** 表示當前header_in中沒有接收到body 
         * 同時對rb->rest進行賦值
         */
        if (ngx_http_request_body_filter(r, NULL) != NGX_OK) {
            rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
            goto done;
        }
    }

說明:

1、preread大於0,說明header_in緩衝區記憶體在部分body(也可能是完整body),這個是需要剝離body(也可以理解成解析出body資料)存到request_body物件中。

2、 當執行完剝離body之後,會再次判斷request_body中rest是否為0,如果不為0表示還有body沒有接收完畢。因此需要建立一個buf用接收新的body資料。

3、如果preread為0,表示header_in緩衝區中沒有body,這裡需要設定request_body物件中rest為content_length。

    if (rb->rest == 0) {/* 表示整個body已經讀取完畢 執行回撥函式進行處理 */
        /* the whole request body was pre-read */
        r->request_body_no_buffering = 0;
        post_handler(r);
        return NGX_OK;
    }

    if (rb->rest < 0) {
        ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0,
                      "negative request body rest");
        rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
        goto done;
    }

    /**
     * 以下流程表示 rest > 0 表示需要再次讀取body 
     * 主要工作是建立buf、設定回撥函式以及讀取socket緩衝區     
     */
    clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);

    size = clcf->client_body_buffer_size; //預設1M
    size += size >> 2;// 1M+256K

    /* TODO: honor r->request_body_in_single_buf */

    if (!r->headers_in.chunked && rb->rest < size) {
        size = (ssize_t) rb->rest;

        if (r->request_body_in_single_buf) {
            size += preread;
        }

    } else {
        size = clcf->client_body_buffer_size;
    }
    /* 建立接收緩衝區 */
    rb->buf = ngx_create_temp_buf(r->pool, size);
    if (rb->buf == NULL) {
        rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
        goto done;
    }
    /* 設定讀寫事件回撥函式 */
    r->read_event_handler = ngx_http_read_client_request_body_handler;
    r->write_event_handler = ngx_http_request_empty_handler;
    /* 真正從socket中讀取資料 */
    rc = ngx_http_do_read_client_request_body(r);

 說明:這部分程式碼主要流程,建立一個全新的buffer用於接收body,設定回撥函式,執行函式進行socket recv操作。

done:
    
    if (r->request_body_no_buffering
        && (rc == NGX_OK || rc == NGX_AGAIN))
    {
        if (rc == NGX_OK) {
            r->request_body_no_buffering = 0;

        } else {
            /* rc == NGX_AGAIN */
            r->reading_body = 1;
        }

        r->read_event_handler = ngx_http_block_reading;
        post_handler(r);
    }

    if (rc >= NGX_HTTP_SPECIAL_RESPONSE) {
        r->main->count--; //如果返回是大於300表示出現錯誤 需要將count自減
    }

    return rc;
}

這部分程式碼,理解不是很清楚,但是大部分流程都進入標籤done中。

2.3、request_body結構

在上面流程中,涉及到一個非常重要的結構--ngx_http_request_body_t,該結構體對於讀取body相關功能,非常重要。其中比較難於理解的就是bufs、buf,在下面的註釋已經寫得很清楚了,具體定義如下:

typedef struct {
    ngx_temp_file_t                  *temp_file; /* 當指標不空說明以檔案方式儲存body */
    /**
     * 儲存body的連結串列 完整body在這裡面 因此我們在編寫業務邏輯需要特別注意 
     * 這裡還需要注意一點 bufs中ngx_buf_t結構既支援記憶體結構又支援檔案結構
     * 當我們處理body時 取出buf後需要判斷in_file變數是否為1
     */
    ngx_chain_t                      *bufs;
    /**
     * 用於接收socket資料 即接收body 在ngx_http_read_client_request_body中賦值
     * buf是用於socket recv函式 所以當body很大的時候 這個buf可能不能滿足body長度
     * 因此會buf指向的記憶體拷貝到bufs中
     */
    ngx_buf_t                        *buf;
    off_t                             rest; /* 該值代表還有多少位元組的body未讀取 */
    off_t                             received; /* 用於http V2版本 */
    ngx_chain_t                      *free; /*  */
    ngx_chain_t                      *busy;
    ngx_http_chunked_t               *chunked; /* chunked資訊 */
    ngx_http_client_body_handler_pt   post_handler; /* 使用者設定的回撥函式 用於處理body */
} ngx_http_request_body_t;

三、非首次接收body函式ngx_http_read_client_request_body_handler

我們在上面,看到兩處重置讀寫事件,其中讀事件設定的函式為ngx_http_read_client_request_body_handler。表明下次接收並且處理body的函式為ngx_http_read_client_request_body_handler,而不在是ngx_http_read_client_request_body。函式實現比較簡單,具體如下所示:

static void
ngx_http_read_client_request_body_handler(ngx_http_request_t *r)
{
    ngx_int_t  rc;

    if (r->connection->read->timedout) {//如果是超時事件 則直接結束http請求
        r->connection->timedout = 1;
        ngx_http_finalize_request(r, NGX_HTTP_REQUEST_TIME_OUT);
        return;
    }

    rc = ngx_http_do_read_client_request_body(r); //真正讀取body函式

    if (rc >= NGX_HTTP_SPECIAL_RESPONSE) {
        ngx_http_finalize_request(r, rc); //返回錯直接結束http請求
    }
}

四、真正接收body函式ngx_http_do_read_client_request_body

/**
 * 真正讀取body的函式
 * @param r http請求
 */
static ngx_int_t
ngx_http_do_read_client_request_body(ngx_http_request_t *r)
{
    off_t                      rest;
    size_t                     size;
    ssize_t                    n;
    ngx_int_t                  rc;
    ngx_chain_t                out;
    ngx_connection_t          *c;
    ngx_http_request_body_t   *rb;
    ngx_http_core_loc_conf_t  *clcf;

    c = r->connection;
    rb = r->request_body;

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
                   "http read client request body");

    for ( ;; ) {
        for ( ;; ) {
            if (rb->buf->last == rb->buf->end) {//空間已滿

                if (rb->buf->pos != rb->buf->last) {

                    /**
                     * pass buffer to request body filter chain 
                     * 表示當前buf中報文還沒有處理 將當前buf追加到request_body中
                     */

                    out.buf = rb->buf;
                    out.next = NULL;
                    /* 將body儲存在request_body物件中 */
                    rc = ngx_http_request_body_filter(r, &out);

                    if (rc != NGX_OK) {
                        return rc;
                    }

                } else {
                    /* update chains */
                    rc = ngx_http_request_body_filter(r, NULL);

                    if (rc != NGX_OK) {
                        return rc;
                    }
                }
                
                /* 表示緩衝區中還有body沒有被處理 需要再次觸發事件驅動 */
                if (rb->busy != NULL) {
                    if (r->request_body_no_buffering) {//重新註冊讀事件
                        if (c->read->timer_set) {
                            ngx_del_timer(c->read);
                        }

                        if (ngx_handle_read_event(c->read, 0) != NGX_OK) {
                            return NGX_HTTP_INTERNAL_SERVER_ERROR;
                        }

                        return NGX_AGAIN;
                    }

                    return NGX_HTTP_INTERNAL_SERVER_ERROR;
                }
                /** 
                 * 這裡為什麼可以重置標誌位:
                 * Nginx內部實現 當buf緩衝區滿時會把緩衝區記憶體寫到臨時檔案中
                 */
                rb->buf->pos = rb->buf->start;
                rb->buf->last = rb->buf->start;
            }

            size = rb->buf->end - rb->buf->last; //當前buf可用接收資料的空間大小
            rest = rb->rest - (rb->buf->last - rb->buf->pos); //還有多少body沒有接收

            if ((off_t) size > rest) {
                size = (size_t) rest;
            }
            /* 真正從socket中 讀取報文 */
            n = c->recv(c, rb->buf->last, size);

            ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
                           "http client request body recv %z", n);

            if (n == NGX_AGAIN) {
                break;
            }

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

            if (n == 0 || n == NGX_ERROR) {
                c->error = 1;
                return NGX_HTTP_BAD_REQUEST;
            }

            rb->buf->last += n;
            r->request_length += n;

            if (n == rest) {
                /* pass buffer to request body filter chain */

                out.buf = rb->buf;
                out.next = NULL;
                //rb->rest在此函式中會被修改
                rc = ngx_http_request_body_filter(r, &out);

                if (rc != NGX_OK) {
                    return rc;
                }
            }

            if (rb->rest == 0) {//表示body全部內容都已經接收完畢
                break;
            }

            if (rb->buf->last < rb->buf->end) {
                break;
            }
        }//最內層for迴圈結束

        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
                       "http client request body rest %O", rb->rest);

        if (rb->rest == 0) {//表示body全部內容都已經接收完畢
            break;
        }

        if (!c->read->ready) {//如果是失效事件 需要重新註冊事件

            if (r->request_body_no_buffering
                && rb->buf->pos != rb->buf->last)
            {
                /* pass buffer to request body filter chain */

                out.buf = rb->buf;
                out.next = NULL;

                rc = ngx_http_request_body_filter(r, &out);

                if (rc != NGX_OK) {
                    return rc;
                }
            }
            //重新註冊 讀取事件
            clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
            ngx_add_timer(c->read, clcf->client_body_timeout);

            if (ngx_handle_read_event(c->read, 0) != NGX_OK) {
                return NGX_HTTP_INTERNAL_SERVER_ERROR;
            }

            return NGX_AGAIN;
        }
    }//外層for迴圈
    
    /* 注意: 進入下面程式碼 表示讀取body完成 */
    if (c->read->timer_set) {
        ngx_del_timer(c->read);
    }

    if (!r->request_body_no_buffering) {
        r->read_event_handler = ngx_http_block_reading;
        rb->post_handler(r);//呼叫回撥 處理body
    }

    return NGX_OK;
}

雖然該函式內容比較多,單邏輯不是很複雜,在註釋中我已經做了標記。 

五、總結

至此,本篇將Nginx是如何接收body大體流程已經介紹完畢,但是仍然沒有解決我們心中的疑惑?請看下一篇