天天看點

nginx request body讀取流程詳解

nginx request body讀取流程詳解

前面的文章中我們分别講解了nginx是如何讀取請求行和請求頭資料的,在讀取完請求頭之後,nginx并不會直接讀取請求體,而是直接進入http子產品的11個階段開始處理請求的資料。在這個過程中,如果目前請求比對的location配置了proxy_pass,那麼就會在NGX_HTTP_CONTENT_PHASE階段讀取用戶端發送來的request body資料,以轉發給上遊伺服器。本文主要是對nginx是如何讀取用戶端的資料進行講解的。

  1. request body讀取入口

    nginx讀取資料是通過ngx_http_read_client_request_body()進行的,如下是該方法的源碼:

/**

  • 接收http請求的包體

    */

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++;

// 如果目前請求是子請求,或者已經接收過包體,或者需要忽略包體,則直接調用post_handler()方法,然後傳回

if (r != r->main || r->request_body || r->discard_body) {

r->request_body_no_buffering = 0;
post_handler(r);
return NGX_OK;           

}

// 主要是向使用者發送100 continue以期待擷取更多資料

if (ngx_http_test_expect(r) != NGX_OK) {

rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
goto done;           

// 申請ngx_http_request_body_t的記憶體

rb = ngx_pcalloc(r->pool, sizeof(ngx_http_request_body_t));

if (rb == NULL) {

rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
goto done;           

rb->rest = -1;

rb->post_handler = post_handler;

r->request_body = rb;

// 如果content-length小于0,并且不是大檔案,則直接回調post_handler(),并且傳回

if (r->headers_in.content_length_n < 0 && !r->headers_in.chunked) {

r->request_body_no_buffering = 0;
post_handler(r);
return NGX_OK;           

// preread表示緩沖區還有多少資料未處理。這裡這麼計算的原因在于,r->header_in中關于請求行和header的

// 資料都已經處理了,剩下的部分如果還有資料,必然是請求包體的,也即目前方法需要處理的部分

preread = r->header_in->last - r->header_in->pos;

// 已經讀取到了部分資料

if (preread) {

out.buf = r->header_in;
out.next = NULL;

// 将out中的資料嘗試寫入到臨時檔案中
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);
// 這裡是判斷r->header_in中是否已經讀取到的資料長度大于剩餘需要讀取的長度
if (!r->headers_in.chunked
    && rb->rest > 0
    && rb->rest <= (off_t) (r->header_in->end - r->header_in->last)) {
  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 {

if (ngx_http_request_body_filter(r, NULL) != NGX_OK) {
  rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
  goto done;
}           

// rb->rest為0說明沒有包體資料,或者包體資料已經讀取完畢

if (rb->rest == 0) {

r->request_body_no_buffering = 0;
post_handler(r);
return NGX_OK;           

// 這裡rb->rest肯定是大于0的,因而如果其小于0,說明讀取包體異常了

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;           

clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);

size = clcf->client_body_buffer_size;

size += size >> 2;

// 計算讀取資料緩沖區的大小

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

size = (ssize_t) rb->rest;
if (r->request_body_in_single_buf) {
  size += preread;
}           
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;

rc = ngx_http_do_read_client_request_body(r);

done:

if (r->request_body_no_buffering && (rc == NGX_OK || rc == NGX_AGAIN)) {

if (rc == NGX_OK) {
  r->request_body_no_buffering = 0;
} else {
  r->reading_body = 1;
}

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

if (rc >= NGX_HTTP_SPECIAL_RESPONSE) {

r->main->count--;           

return rc;

上述讀取request body的流程主要分為如下幾個步驟:

判斷目前請求如果是子請求,或者已經調用目前方法讀取過body,或者被設定為需要忽略body,則直接傳回;

申請ngx_http_request_body_t結構體的記憶體,這個結構體的作用是存儲目前讀取到的body資料的;

判斷目前請求的Content-Length是否為0,是則不進行body的讀取;

判斷目前請求的讀取緩沖區中是否已經讀取了一部分的body資料,如果已經讀取了,則調用ngx_http_request_body_filter()方法将已經讀取到的資料存儲到臨時檔案中,否則隻是調用ngx_http_request_body_filter()方法計算目前還剩餘多少資料未讀取,計算的到的值存儲在rb->rest中。這裡可能存在部分已經讀取的資料的原因在于,前面在讀取請求行和header資料的時候可能已經多讀取了一部分資料,而這部分資料就是body資料;

判斷目前剩餘需要讀取的資料長度為0,則直接傳回;

申請存儲body資料的緩沖區;

将目前請求的read_event_handler設定為ngx_http_read_client_request_body_handler()方法,而write_event_handler設定為ngx_http_request_empty_handler()方法。在ngx_http_read_client_request_body_handler()内部,其會調用ngx_http_do_read_client_request_body()方法以執行真正的請求體讀取過程。這裡的ngx_http_request_empty_handler()方法是一個空方法,由于目前正處于讀取資料階段,因而意外觸發的寫事件不需要處理;

調用ngx_http_do_read_client_request_body()以執行真正的body讀取過程。

這裡需要說明的是,第七步中将read_event_handler設定為ngx_http_read_client_request_body_handler(),這個read_event_handler()方法的調用方式在前面講解nginx如何驅動http子產品的11個階段的文章中已經進行了介紹。簡而言之,進入http子產品的11個階段的流程之後,如果觸發了讀事件,那麼就會調用read_event_handler()方法,這裡設定了之後,每次觸發了讀事件就會調用ngx_http_read_client_request_body_handler()方法,進而觸發body資料的繼續讀取流程。

上面的流程中,最主要的兩個方法是ngx_http_request_body_filter()和ngx_http_do_read_client_request_body()方法,第一個方法在緩沖區資料滿了的時候會将資料寫入到緩沖區中,第二個方法則用于讀取用戶端的body資料。

  1. 存儲緩沖區資料至臨時檔案

    存儲緩沖區資料到臨時檔案的方法為ngx_http_request_body_filter()方法,如下是該方法的源碼:

static ngx_int_t ngx_http_request_body_filter(ngx_http_request_t r, ngx_chain_t in) {

if (r->headers_in.chunked) {

return ngx_http_request_body_chunked_filter(r, in);           
// 我們這裡主要講解小塊body的處理方式
return ngx_http_request_body_length_filter(r, in);           
  • 這裡主要是将in中的資料寫入到臨時檔案中

static ngx_int_t ngx_http_request_body_length_filter(ngx_http_request_t r, ngx_chain_t in) {

size_t size;

ngx_chain_t cl, tl, out, *ll;

rb = r->request_body;

// rest為-1表示還未讀取過任何包體資料,因而将rest設定為content_length_n的值

if (rb->rest == -1) {

rb->rest = r->headers_in.content_length_n;           

out = NULL;

ll = &out;

for (cl = in; cl; cl = cl->next) {

if (rb->rest == 0) {
  break;
}

tl = ngx_chain_get_free_buf(r->pool, &rb->free);
if (tl == NULL) {
  return NGX_HTTP_INTERNAL_SERVER_ERROR;
}

b = tl->buf;
ngx_memzero(b, sizeof(ngx_buf_t));

b->temporary = 1;
b->tag = (ngx_buf_tag_t) &ngx_http_read_client_request_body;
b->start = cl->buf->pos;
b->pos = cl->buf->pos;
b->last = cl->buf->last;
b->end = cl->buf->end;
b->flush = r->request_body_no_buffering;

size = cl->buf->last - cl->buf->pos;
if ((off_t) size < rb->rest) {
  cl->buf->pos = cl->buf->last;
  rb->rest -= size;
} else {
  cl->buf->pos += (size_t) rb->rest;
  rb->rest = 0;
  b->last = cl->buf->pos;
  b->last_buf = 1;
}

*ll = tl;
ll = &tl->next;           

// 這裡的ngx_http_top_request_body_filter指向的是ngx_http_request_body_save_filter方法,

// 這裡主要是檢查現有的緩沖區是否寫滿了,如果滿了,則将緩沖區的資料寫入到臨時檔案中

rc = ngx_http_top_request_body_filter(r, out);

// 釋放busy中ngx_buf_t連結清單占用的緩沖區,不過不會釋放其tag為ngx_http_read_client_request_body的

// 緩沖區,而會将這些緩沖區添加到free連結清單的頭部

ngx_chain_update_chains(r->pool, &rb->free, &rb->busy, &out,

(ngx_buf_tag_t) &ngx_http_read_client_request_body);           

上述方法主要完成了如下幾部分工作:

周遊out連結清單,依次将其存儲的資料長度從rb->rest中扣除;

調用ngx_http_top_request_body_filter()判斷如果緩沖區中沒有剩餘空間,則将資料存儲到臨時檔案中;

調用ngx_chain_update_chains()方法将rb->busy中需要釋放的空間釋放到rb->free中;

這裡我們主要看看ngx_http_top_request_body_filter()方法是如何存儲資料到臨時檔案的,這個方法實際上指向的是ngx_http_request_body_save_filter()方法,如下是該方法的源碼:

  • 這裡主要是判斷是否還有剩餘的可用空間,如果沒有可用空間,則會将現有的資料寫入到包體中

ngx_int_t ngx_http_request_body_save_filter(ngx_http_request_t r, ngx_chain_t in) {

ngx_chain_t *cl;

// 将in的資料拼接到rb->bufs的末尾

if (ngx_chain_add_copy(r->pool, &rb->bufs, in) != NGX_OK) {

return NGX_HTTP_INTERNAL_SERVER_ERROR;           

if (r->request_body_no_buffering) {

return NGX_OK;           

if (rb->rest > 0) {

// 這裡rest大于0,說明還有資料未接收,但是這裡的buf->last等于buf->end,說明緩沖區中沒有剩餘可用于
// 存儲資料的空間了,因而這裡調用ngx_http_write_request_body()方法将已經接收到的包體寫入到臨時
// 檔案中
if (rb->buf && rb->buf->last == rb->buf->end 
    && ngx_http_write_request_body(r) != NGX_OK) {
  return NGX_HTTP_INTERNAL_SERVER_ERROR;
}

return NGX_OK;           

// 走到這裡,說明用戶端的包體資料已經接收完畢了

if (rb->temp_file || r->request_body_in_file_only) {

if (ngx_http_write_request_body(r) != NGX_OK) {
  return NGX_HTTP_INTERNAL_SERVER_ERROR;
}

if (rb->temp_file->file.offset != 0) {
  cl = ngx_chain_get_free_buf(r->pool, &rb->free);
  if (cl == NULL) {
    return NGX_HTTP_INTERNAL_SERVER_ERROR;
  }

  b = cl->buf;
  ngx_memzero(b, sizeof(ngx_buf_t));

  // 标記資料已經寫入到檔案中了
  b->in_file = 1;
  b->file_last = rb->temp_file->file.offset;
  b->file = &rb->temp_file->file;
  rb->bufs = cl;
}           

return NGX_OK;

在ngx_http_request_body_save_filter()方法中,其首先會将in中的資料拷貝到rb->bufs緩沖區中。然後會判斷目前緩沖區是否還有剩餘空間,如果沒有,則調用ngx_http_write_request_body()方法将資料寫入到臨時檔案中,并且更新表征目前request body的rb結構體中與檔案相關的屬性。關于ngx_http_write_request_body()是如何将資料寫入到臨時檔案的,其邏輯比較簡單,這裡不再贅述。

  1. 讀取request body資料

    前面我們講到,nginx讀取request body是通過ngx_http_do_read_client_request_body()方法進行的,如下是該方法的源碼:

static ngx_int_t ngx_http_do_read_client_request_body(ngx_http_request_t *r) {

off_t rest;

ssize_t n;

ngx_connection_t *c;

c = r->connection;

rb = r->request_body;

for (;;) {

for (;;) {
  // 判斷rb->buf中是否還有可用的空間
  if (rb->buf->last == rb->buf->end) {
    if (rb->buf->pos != rb->buf->last) {
      out.buf = rb->buf;
      out.next = NULL;
      // 将資料寫入到臨時檔案中
      rc = ngx_http_request_body_filter(r, &out);
      if (rc != NGX_OK) {
        return rc;
      }
    } else {
      rc = ngx_http_request_body_filter(r, NULL);
      if (rc != NGX_OK) {
        return rc;
      }
    }

    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;
    }

    // 更新rb->buf的指針資料
    rb->buf->pos = rb->buf->start;
    rb->buf->last = rb->buf->start;
  }

  // size表示rb->buf中剩餘的可用空間
  size = rb->buf->end - rb->buf->last;
  // 計算新的未讀取的資料長度
  rest = rb->rest - (rb->buf->last - rb->buf->pos);
  if ((off_t) size > rest) {
    size = (size_t) rest;
  }

  // 從連接配接的句柄中讀取size大小的資料到rb->buf->last位置
  n = c->recv(c, rb->buf->last, size);
  // NGX_AGAIN表示還未讀取完
  if (n == NGX_AGAIN) {
    break;
  }

  // n等于0表示用戶端關閉了連接配接
  if (n == 0) {
    ngx_log_error(NGX_LOG_INFO, c->log, 0, "client prematurely closed connection");
  }

  // 如果讀取異常,則傳回BAD_REQUEST
  if (n == 0 || n == NGX_ERROR) {
    c->error = 1;
    return NGX_HTTP_BAD_REQUEST;
  }

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

  // n等于rest表示剩餘的資料都讀取完了,此時将資料寫入到臨時檔案中
  if (n == rest) {
    out.buf = rb->buf;
    out.next = NULL;
    rc = ngx_http_request_body_filter(r, &out);
    if (rc != NGX_OK) {
      return rc;
    }
  }

  // 如果rb->rest為0,則退出目前内層循環
  if (rb->rest == 0) {
    break;
  }

  // 如果目前rb->buf中還有可用空間,則退出目前内層循環
  if (rb->buf->last < rb->buf->end) {
    break;
  }
}

// 如果沒有需要讀取的資料,則跳出外層循環
if (rb->rest == 0) {
  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;
}           

// 走到這裡,說明資料讀取完畢了,因而這裡會删除讀取事件,并且将讀取事件的處理方法設定為一個空方法

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);           

這裡ngx_http_do_read_client_request_body()方法使用了一個雙重無限for循環,對于内層循環,其主要分為四部分的工作:

檢查目前讀取緩沖區是否有剩餘的空間,如果沒有,則還是調用ngx_http_request_body_filter()方法将資料寫入到臨時檔案中;

計算下一次應該讀取的資料長度;

調用c->recv()方法讀取目前請求句柄上的資料,傳回值如果大于0,則表示此次讀取到的資料大小,如果等于0,則表示用戶端斷開了連接配接,如果為NGX_AGAIN,則表示需要再次長度讀取,如果為NGX_ERROR,則表示讀取異常了;

根據上一步調用的傳回值以判斷應該作何處理,對于NGX_AGAIN,直接跳出目前循環,對于0和NGX_ERROR,直接給用戶端傳回400響應碼,對于大于0的情況,則更新目前緩沖區的相關屬性,并且調用ngx_http_request_body_filter()嘗試将滿了的資料存儲到臨時檔案中。

本質上來講,内層循環如果讀取到了資料,并且還有資料需要讀取,那麼其是不會跳出循環的,并且循環會一直嘗試讀取資料,這樣做的優點在于,用戶端傳送的body資料一般都比較大,讀取事件可能會經常觸發,通過循環讀取可以盡快的讀取到連接配接句柄上接收到的資料。對于内層循環傳回NGX_AGAIN時,表示此次讀取的資料長度為0,此時可能句柄比較空閑,因而此時會break到外層循環(當然,還有其他的情況也都可以break到外層循環)。外層循環中,隻要目前剩餘需要讀取的資料不為0,并且連接配接事件不是ready狀态,則會将目前事件再次添加到事件架構中,以等待下次驅動讀事件讀取剩餘的資料。

  1. 小結

    本文首先對nginx讀取request body資料的入口進行了講解,然後講解了讀取的整體流程,接着着重講解了讀取過程中臨時檔案的存儲方式,以及讀取資料的具體細節。

原文位址

https://my.oschina.net/zhangxufeng/blog/3215381

繼續閱讀