菜鳥學習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大體流程已經介紹完畢,但是仍然沒有解決我們心中的疑惑?請看下一篇。