【Nginx原始碼研究】初探nginx HTTP處理流程
運營研發團隊 李樂
1.初始化伺服器
server指令用於配置virtual server,我們通常會在一臺機器配置多個virtual server,監聽不同埠號,對映到不同檔案目錄;nginx解析使用者配置,在所有埠建立socket並啟動監聽。
nginx解析配置檔案是由各個模組分擔處理的,每個模組註冊並處理自己關心的配置,通過模組結構體ngx_module_t的欄位ngx_command_t *commands實現;
例如ngx_http_module是一個核心模組,其commands欄位定義如下:
struct ngx_command_s { ngx_str_tname; ngx_uint_ttype; char*(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); }; static ngx_command_tngx_http_commands[] = { { ngx_string("http"), NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS, ngx_http_block, }, };
- name指令名稱,解析配置檔案時按照名稱能匹配查詢;
- type指令型別,NGX_CONF_NOARGS該配置無引數,NGX_CONF_BLOCK該配置是一個配置塊,NGX_MAIN_CONF表示配置可以出現在哪些位(NGX_MAIN_CONF、NGX_HTTP_SRV_CONF、NGX_HTTP_LOC_CONF);
- set指令處理函式指標;
可以看到解析http指令的處理函式為ngx_http_block,實現如下:
static char * ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { //解析main配置 //解析server配置 //解析location配置 //初始化HTTP處理流程所需的handler //初始化listening if (ngx_http_optimize_servers(cf, cmcf, cmcf->ports) != NGX_OK) { return NGX_CONF_ERROR; } }
ngx_http_optimize_servers方法迴圈所有配置埠,建立ngx_listening_t物件,並將其新增到conf->cycle->listening(後續操作會遍歷此陣列,建立socket並監聽)。方法主要操作如下圖:
注意到這裡設定了ngx_listening_t的handler為ngx_http_init_connection,當接收到socket連線請求時,會呼叫此handler處理。
那麼什麼時候啟動監聽呢?全域性搜尋關鍵字cycle->listening可以找到。main方法會呼叫ngx_init_cycle,其完成了伺服器初始化的大部分工作,其中就包括啟動監聽(ngx_open_listening_sockets)。
假設nginx使用epoll處理所有socket事件,什麼時候將監聽事件新增到epoll呢?全域性搜尋關鍵字cycle->listening可以找到。ngx_event_core_module模組是事件處理核心模組,初始化此模組時會執行ngx_event_process_init函式,其中將監聽事件新增到epoll。
static ngx_int_t ngx_event_process_init(ngx_cycle_t *cycle) { ls = cycle->listening.elts; for (i = 0; i < cycle->listening.nelts; i++) { //設定讀事件處理handler rev->handler = ngx_event_accept; ngx_add_event(rev, NGX_READ_EVENT, 0); } }
注意到接收到客戶端socket連線請求事件的處理函式是ngx_event_accept。
2.HTTP請求解析
2.1 基礎結構體
結構體ngx_connection_t儲存socket連線相關資訊;nginx預先建立若干個ngx_connection_t物件,儲存在全域性變數ngx_cycle->free_connections,稱之為連線池;當新生成socket時,會嘗試從連線池中獲取空閒connection連線,如果獲取失敗,則會直接關閉此socket。
指令worker_connections用於配置連線池最大連線數目,配置在events指令塊中,由ngx_event_core_module解析。
vents { use epoll; worker_connections60000; }
當nginx作為HTTP伺服器時,最大客戶端數目maxClient=worker_processes worker_connections/2;當nginx作為反向代理伺服器時,最大客戶端數目maxClient=worker_processes worker_connections/4。其worker_processes為使用者配置的worker程序數目。
結構體ngx_connection_t定義如下:
struct ngx_connection_s { //空閒連線池中,data指向下一個連線,形成連結串列;取出來使用時,data指向請求結構體ngx_http_request_s void*data; //讀寫事件結構體,兩個關鍵欄位:handler處理函式、timer定時器 ngx_event_t*read; ngx_event_t*write; ngx_socket_tfd;//socket fd ngx_recv_ptrecv; //socket接收資料函式指標 ngx_send_ptsend; //socket傳送資料函式指標 ngx_buf_t*buffer; //輸入緩衝區 struct sockaddr*sockaddr; //客戶端地址 socklen_tsocklen; ngx_listening_t*listening; //監聽的ngx_listening_t物件 struct sockaddr*local_sockaddr; //本地地址 socklen_tlocal_socklen; ………… }
結構體ngx_http_request_t儲存整個HTTP請求處理流程所需的所有信息,欄位非常多,這裡只進行簡要說明:
struct ngx_http_request_s { ngx_connection_t*connection; //讀寫事件處理handler ngx_http_event_handler_ptread_event_handler; ngx_http_event_handler_ptwrite_event_handler; //請求頭緩衝區 ngx_buf_t*header_in; //解析後的請求頭 ngx_http_headers_in_theaders_in; //請求體結構體 ngx_http_request_body_t*request_body; //請求行 ngx_str_trequest_line; //解析後請求行若干欄位 ngx_uint_tmethod; ngx_uint_thttp_version; ngx_str_turi; ngx_str_targs; ………… }
請求行與請求體解析相對比較簡單,這裡重點講述請求頭的解析,解析後的請求頭資訊都儲存在ngx_http_headers_in_t結構體中。
ngx_http_request.c檔案中定義了所有的HTTP頭部,儲存在ngx_http_headers_in陣列,陣列的每個元素是一個ngx_http_header_t結構體,主要包含三個欄位,頭部名稱、頭部解析後欄位儲存在ngx_http_headers_in_t的偏移量,解析頭部的處理函式。
ngx_http_header_tngx_http_headers_in[] = { { ngx_string("Host"), offsetof(ngx_http_headers_in_t, host), ngx_http_process_host }, { ngx_string("Connection"), offsetof(ngx_http_headers_in_t, connection), ngx_http_process_connection }, ………… } typedef struct { ngx_str_tname; ngx_uint_toffset; ngx_http_header_handler_pthandler; } ngx_http_header_t;
解析請求頭時,從ngx_http_headers_in陣列中查詢請求頭ngx_http_header_t物件,呼叫處理函式handler,儲存到r->headers_in對應欄位。以解析Connection頭部為例,ngx_http_process_connection實現如下:
static ngx_int_t ngx_http_process_connection(ngx_http_request_t *r, ngx_table_elt_t *h, ngx_uint_t offset) { if (ngx_strcasestrn(h->value.data, "close", 5 - 1)) { r->headers_in.connection_type = NGX_HTTP_CONNECTION_CLOSE; } else if (ngx_strcasestrn(h->value.data, "keep-alive", 10 - 1)) { r->headers_in.connection_type = NGX_HTTP_CONNECTION_KEEP_ALIVE; } return NGX_OK; }
輸入引數offset在此處並沒有什麼作用。注意到第二個輸入引數ngx_table_elt_t,儲存了當前請求頭的鍵值對資訊:
typedef struct { ngx_uint_thash;//請求頭key的hash值 ngx_str_tkey; ngx_str_tvalue; u_char*lowcase_key;//請求頭key轉為小寫字串(可以看到HTTP請求頭解析時key不區分大小寫) } ngx_table_elt_t;
再思考一個問題,從ngx_http_headers_in陣列中查詢請求頭對應ngx_http_header_t物件時,需要遍歷,每個元素都需要進行字串比較,效率低下。因此nginx將ngx_http_headers_in陣列轉換為雜湊表,雜湊表的鍵即為請求頭的key,方法ngx_http_init_headers_in_hash實現了陣列到雜湊表的轉換,轉換後的雜湊表儲存在cmcf->headers_in_hash欄位。
2.2 解析HTTP請求
第1節提到,在建立socket啟動監聽時,會新增可讀事件到epoll,事件處理函式為ngx_event_accept,用於接收socket連線,分配connection連線,並呼叫ngx_listening_t物件的處理函式(ngx_http_init_connection)。
void ngx_event_accept(ngx_event_t *ev) { s = accept4(lc->fd, (struct sockaddr *) sa, &socklen, SOCK_NONBLOCK); //客戶端socket連線成功時,都需要分配connection連線,如果分配失敗則會直接關閉此socket。 //而每個worker程序連線池的最大連線數目是固定的,當不存在空閒連線時,此worker程序accept的所有socket都會被拒絕; //多個worker程序通過競爭執行epoll_wait;而當ngx_accept_disabled大於0時,會直接放棄此次競爭,同時ngx_accept_disabled減1。 //以此實現,當worker程序的空閒連線過少時,減少其競爭epoll_wait次數 ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n; c = ngx_get_connection(s, ev->log); ls->handler(c); }
socket連線成功後,nginx會等待客戶端傳送HTTP請求,預設會有60秒的超時時間,即60秒內沒有接收到客戶端請求時,斷開此連線,列印錯誤日誌。函式ngx_http_init_connection用於設定讀事件處理函式,以及超時定時器。
void ngx_http_init_connection(ngx_connection_t *c) { c->read = ngx_http_wait_request_handler; c->write->handler = ngx_http_empty_handler; ngx_add_timer(rev, c->listening->post_accept_timeout); }
全域性搜尋post_accept_timeout欄位,可以查詢到設定此超時時間的配置指令,client_header_timeout,其可以在http、server指令塊中配置。
函式ngx_http_wait_request_handler為解析HTTP請求的入口函式,實現如下:
static void ngx_http_wait_request_handler(ngx_event_t *rev) { //讀事件已經超時 if (rev->timedout) { ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, "client timed out"); ngx_http_close_connection(c); return; } size = cscf->client_header_buffer_size;//client_header_buffer_size指令用於配置接收請求頭緩衝區大小 b = c->buffer; n = c->recv(c, b->last, size); //建立請求物件ngx_http_request_t,HTTP請求整個處理過程都有用; c->data = ngx_http_create_request(c); rev->handler = ngx_http_process_request_line; //設定讀事件處理函式(此次請求行可能沒有讀取完) ngx_http_process_request_line(rev); }
函式ngx_http_create_request建立並初始化ngx_http_request_t物件,注意這賦值語句r->header_in =c->buffer。
解析請求行與請求頭的程式碼較為繁瑣,終點在於讀取socket資料,解析字串,這裡不做詳述。HTTP請求解析過程主要函式呼叫如下圖所示:
注意,解析完成請求行與請求頭,nginx就開始處理HTTP請求,並沒有等到解析完請求體再處理。處理請求入口為ngx_http_process_request。
3.處理HTTP請求
3.1 HTTP請求處理的11個階段
nginx將HTTP請求處理流程分為11個階段,絕大多數HTTP模組都會將自己的handler新增到某個階段(將handler新增到全域性唯一的陣列phases中),注意其中有4個階段不能新增自定義handler,nginx處理HTTP請求時會挨個呼叫每個階段的handler;
typedef enum { NGX_HTTP_POST_READ_PHASE = 0, //第一個階段,目前只有realip模組會註冊handler,但是該模組預設不會執行(nginx作為代理伺服器時有用,後端以此獲取客戶端原始ip) NGX_HTTP_SERVER_REWRITE_PHASE,//server塊中配置了rewrite指令,重寫url NGX_HTTP_FIND_CONFIG_PHASE,//查詢匹配的location配置;不能自定義handler; NGX_HTTP_REWRITE_PHASE,//location塊中配置了rewrite指令,重寫url NGX_HTTP_POST_REWRITE_PHASE,//檢查是否發生了url重寫,如果有,重新回到FIND_CONFIG階段;不能自定義handler; NGX_HTTP_PREACCESS_PHASE,//訪問控制,比如限流模組會註冊handler到此階段 NGX_HTTP_ACCESS_PHASE,//訪問許可權控制,比如基於ip黑白名單的許可權控制,基於使用者名稱密碼的許可權控制等 NGX_HTTP_POST_ACCESS_PHASE,//根據訪問許可權控制階段做相應處理;不能自定義handler; NGX_HTTP_TRY_FILES_PHASE,//只有配置了try_files指令,才會有此階段;不能自定義handler; NGX_HTTP_CONTENT_PHASE,//內容產生階段,返回響應給客戶端 NGX_HTTP_LOG_PHASE//日誌記錄 } ngx_http_phases;
nginx使用結構體ngx_module_s表示一個模組,其中欄位ctx,是一個指向模組上下文結構體的指標(上下文結構體的欄位都是一些函式指標);nginx的HTTP模組上下文結構體大多都有欄位postconfiguration,負責註冊本模組的handler到某個處理階段。11個階段在解析完成http配置塊指令後初始化。
static char * ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { //解析http配置塊 //初始化11個階段的phases陣列,注意多個模組可能註冊到同一個階段,因此phases是一個二維陣列 if (ngx_http_init_phases(cf, cmcf) != NGX_OK) { return NGX_CONF_ERROR; } //遍歷索引HTTP模組,註冊handler for (m = 0; ngx_modules[m]; m++) { if (ngx_modules[m]->type != NGX_HTTP_MODULE) { continue; } module = ngx_modules[m]->ctx; if (module->postconfiguration) { if (module->postconfiguration(cf) != NGX_OK) { return NGX_CONF_ERROR; } } } //將二維陣列轉換為一維陣列,從而遍歷執行陣列所有handler if (ngx_http_init_phase_handlers(cf, cmcf) != NGX_OK) { return NGX_CONF_ERROR; } }
以限流模組ngx_http_limit_req_module模組為例,postconfiguration方法簡單實現如下:
static ngx_int_t ngx_http_limit_req_init(ngx_conf_t *cf) { h = ngx_array_push(&cmcf->phases[NGX_HTTP_PREACCESS_PHASE].handlers); *h = ngx_http_limit_req_handler;//ngx_http_limit_req_module模組的限流方法;nginx處理HTTP請求時,都會呼叫此方法判斷應該繼續執行還是拒絕請求 return NGX_OK; }
GDB除錯,斷點到ngx_http_block方法執行所有HTTP模組註冊handler之後,列印phases陣列
p cmcf->phases[*].handlers p *(ngx_http_handler_pt*)cmcf->phases[*].handlers.elts
11個階段註冊的handler如下圖所示:
3.2 11個階段初始化
上面提到HTTP的11個處理階段handler儲存在phases陣列,但由於多個模組可能註冊handler到同一個階段,使得phases是一個二維陣列,因此需要轉換為一維陣列,轉換後儲存在cmcf->phase_engine欄位,phase_engine的型別為ngx_http_phase_engine_t,定義如下:
typedef struct { ngx_http_phase_handler_t*handlers;//一維陣列,儲存所有handler ngx_uint_tserver_rewrite_index;//記錄NGX_HTTP_SERVER_REWRITE_PHASE階段handler的索引值 ngx_uint_tlocation_rewrite_index; //記錄NGX_HTTP_REWRITE_PHASE階段handler的索引值 } ngx_http_phase_engine_t; struct ngx_http_phase_handler_t { ngx_http_phase_handler_ptchecker;//執行handler之前的校驗函式 ngx_http_handler_pthandler; ngx_uint_tnext;//下一個待執行handler的索引(通過next實現handler跳轉執行) }; //cheker函式指標型別定義 typedef ngx_int_t (*ngx_http_phase_handler_pt)(ngx_http_request_t *r, ngx_http_phase_handler_t *ph); //handler函式指標型別定義 typedef ngx_int_t (*ngx_http_handler_pt)(ngx_http_request_t *r);
陣列轉換函式ngx_http_init_phase_handlers實現如下:
static ngx_int_t ngx_http_init_phase_handlers(ngx_conf_t *cf, ngx_http_core_main_conf_t *cmcf) { use_rewrite = cmcf->phases[NGX_HTTP_REWRITE_PHASE].handlers.nelts ? 1 : 0; use_access = cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers.nelts ? 1 : 0; n = use_rewrite + use_access + cmcf->try_files + 1 /* find config phase */; //至少有4個階段,這4個階段是上面說的不能註冊handler的4個階段 //計算handler數目,分配空間 for (i = 0; i < NGX_HTTP_LOG_PHASE; i++) { n += cmcf->phases[i].handlers.nelts; } ph = ngx_pcalloc(cf->pool, n * sizeof(ngx_http_phase_handler_t) + sizeof(void *)); //遍歷二維陣列 for (i = 0; i < NGX_HTTP_LOG_PHASE; i++) { h = cmcf->phases[i].handlers.elts; switch (i) { case NGX_HTTP_SERVER_REWRITE_PHASE: if (cmcf->phase_engine.server_rewrite_index == (ngx_uint_t) -1) { cmcf->phase_engine.server_rewrite_index = n;//記錄NGX_HTTP_SERVER_REWRITE_PHASE階段handler的索引值 } checker = ngx_http_core_rewrite_phase; break; case NGX_HTTP_FIND_CONFIG_PHASE: find_config_index = n;//記錄NGX_HTTP_FIND_CONFIG_PHASE階段的索引,NGX_HTTP_POST_REWRITE_PHASE階段可能會跳轉回此階段 ph->checker = ngx_http_core_find_config_phase; n++; ph++; continue;//進入下一個階段NGX_HTTP_REWRITE_PHASE case NGX_HTTP_REWRITE_PHASE: if (cmcf->phase_engine.location_rewrite_index == (ngx_uint_t) -1) { cmcf->phase_engine.location_rewrite_index = n;//記錄NGX_HTTP_REWRITE_PHASE階段handler的索引值 } checker = ngx_http_core_rewrite_phase; break; case NGX_HTTP_POST_REWRITE_PHASE: if (use_rewrite) { ph->checker = ngx_http_core_post_rewrite_phase; ph->next = find_config_index; n++; ph++; } continue;//進入下一個階段NGX_HTTP_ACCESS_PHASE case NGX_HTTP_ACCESS_PHASE: checker = ngx_http_core_access_phase; n++; break; case NGX_HTTP_POST_ACCESS_PHASE: if (use_access) { ph->checker = ngx_http_core_post_access_phase; ph->next = n; ph++; } continue;//進入下一個階段 case NGX_HTTP_TRY_FILES_PHASE: if (cmcf->try_files) { ph->checker = ngx_http_core_try_files_phase; n++; ph++; } continue; case NGX_HTTP_CONTENT_PHASE: checker = ngx_http_core_content_phase; break; default: checker = ngx_http_core_generic_phase; } //n為下一個階段第一個handler的索引 n += cmcf->phases[i].handlers.nelts; //遍歷當前階段的所有handler for (j = cmcf->phases[i].handlers.nelts - 1; j >=0; j--) { ph->checker = checker; ph->handler = h[j]; ph->next = n; ph++; } } }
GDB打印出轉換後的陣列如下圖所示,第一列是cheker欄位,第二列是handler欄位,箭頭表示next跳轉;圖中有個返回的箭頭,即NGX_HTTP_POST_REWRITE_PHASE階段可能返回到NGX_HTTP_FIND_CONFIG_PHASE;原因在於只要NGX_HTTP_REWRITE_PHASE階段產生了url重寫,就需要重新查詢匹配location。
3.3 處理HTTP請求
2.2節提到HTTP請求的處理入口函式是ngx_http_process_request,其主要呼叫ngx_http_core_run_phases實現11個階段的執行流程;
ngx_http_core_run_phases遍歷預先設定好的cmcf->phase_engine.handlers陣列,呼叫其checker函式,邏輯如下:
void ngx_http_core_run_phases(ngx_http_request_t *r) { ph = cmcf->phase_engine.handlers; //phase_handler初始為0,表示待處理handler的索引;cheker內部會根據ph->next欄位修改phase_handler while (ph[r->phase_handler].checker) { rc = ph[r->phase_handler].checker(r, &ph[r->phase_handler]); if (rc == NGX_OK) { return; } } }
checker內部就是呼叫handler,並設定下一步要執行handler的索引;比如說ngx_http_core_generic_phase實現如下:
ngx_int_t ngx_http_core_generic_phase(ngx_http_request_t *r, ngx_http_phase_handler_t *ph) { ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "rewrite phase: %ui", r->phase_handler); rc = ph->handler(r); if (rc == NGX_OK) { r->phase_handler = ph->next; return NGX_AGAIN; } }
3.4 內容產生階段
內容產生階段NGX_HTTP_CONTENT_PHASE是HTTP請求處理的第10個階段,一般情況有3個模組註冊handler到此階段:ngx_http_static_module、ngx_http_autoindex_module和ngx_http_index_module。
但是當我們配置了proxy_pass和fastcgi_pass時,情況會有所不同;
使用proxy_pass配置上游時,ngx_http_proxy_module模組會設定其處理函式到配置類conf;使用fastcgi_pass配置時,ngx_http_fastcgi_module會設定其處理函式到配置類conf。例如:
static char * ngx_http_fastcgi_pass(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_core_loc_conf_t*clcf; clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); clcf->handler = ngx_http_fastcgi_handler; }
階段NGX_HTTP_FIND_CONFIG_PHASE查詢匹配的location,並獲取此ngx_http_core_loc_conf_t物件,將其handler賦值給ngx_http_request_t物件的content_handler欄位(內容產生處理函式)。
而在執行內容產生階段的checker函式時,會執行content_handler指向的函式;檢視ngx_http_core_content_phase函式實現(內容產生階段的checker函式):
ngx_int_t ngx_http_core_content_phase(ngx_http_request_t *r, ngx_http_phase_handler_t *ph) { if (r->content_handler) {//如果請求物件的content_handler欄位不為空,則呼叫 r->write_event_handler = ngx_http_request_empty_handler; ngx_http_finalize_request(r, r->content_handler(r)); return NGX_OK; } ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "content phase: %ui", r->phase_handler); rc = ph->handler(r);//否則執行內容產生階段handler }
總結
nginx處理HTTP請求的流程較為複雜,因此本文只是簡單提供了一條線索:分析了nginx伺服器啟動監聽的過程,HTTP請求的解析過程,11個階段的初始化與呼叫過程。至於HTTP解析處理的詳細流程,還需要讀者去探索。