做者:景羅php
Nginx做爲一款開源的、高性能的HTTP服務器和反向代理服務器而聞名,本文基於nginx-1.15.0,將爲讀者簡要介紹其HTTP處理流程。nginx
一般nginx配置文件以下所示:git
worker_processes 1; events { worker_connections 1024; } http{ access_log logs/access.log main; server { listen 80; server_name example.com; location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; } } }
Nginx高度模塊化,每一個模塊實現某一具體功能,好比ngx_http_limit_req_module模塊實現按請求速率限流功能,ngx_http_fastcgi_module模塊實現fastcgi協議通訊功能。每一個模塊都須要解析配置文件中相關配置,每一個模塊須要解析的全部配置都定義爲ngx_command_t數組。github
例如ngx_http_module模塊,其ngx_command_t數定義以下:後端
struct ngx_command_s { ngx_str_t name; ngx_uint_t type; char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); ... }; static ngx_command_t ngx_http_commands[] = { { ngx_string("http"), NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS, ngx_http_block, ... }, };
http指令塊用於配置http請求處理相關,解析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_block主要解析http塊內部的main配置、server配置與location配置;同時會初始化HTTP處理流程所需的handler;以及初始化全部監聽端口。瀏覽器
函數ngx_http_optimize_servers將全部配置的IP端口進一步解析,並存儲在conf->cycle->listening字段,這是一個數組,後續操做會遍歷此數組,建立socket並監聽。服務器
conf->cycle->listening數組元素類型爲ngx_listening_t,建立該ngx_listening_t對象時,同時會設置其處理handler爲函數ngx_http_init_connection,當接受到客戶端連接請求時,會調用此handler。併發
那麼何時啓動監聽呢?全局搜索關鍵字cycle->listening能夠找到。main方法會調用ngx_init_cycle,其完成了服務器初始化的大部分工做,其中就包括啓動監聽(ngx_open_listening_sockets):socket
ngx_int_t ngx_open_listening_sockets(ngx_cycle_t *cycle) { for (i = 0; i < cycle->listening.nelts; i++) { s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, 0); bind(s, ls[i].sockaddr, ls[i].socklen); listen(s, ls[i].backlog); } }
假設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); } }
注意到向epoll添加讀事件時,設置該讀事件處理函數爲ngx_event_accept,即接收到客戶端socket鏈接請求事件時會調用該處理函數。
結構體ngx_connection_t存儲socket鏈接相關信息;nginx預先建立若干個ngx_connection_t對象,存儲在全局變量ngx_cycle->free_connections,稱之爲鏈接池;當新生成socket時,會嘗試從鏈接池中獲取空閒connection鏈接,若是獲取失敗,則會直接關閉此socket。
指令worker_connections用於配置鏈接池最大鏈接數目,配置在events指令塊中,由ngx_event_core_module解析。
events { use epoll; worker_connections 60000; }
當nginx做爲HTTP服務器時(從用戶的角度,http 1.1協議下,瀏覽器默認使用兩個併發鏈接),最大客戶端數目maxClient=worker_processes X worker_connections/2;當nginx做爲反向代理服務器時,最大客戶端數目maxClient=worker_processes X worker_connections/4。其worker_processes爲用戶配置的worker進程數目。
結構體ngx_connection_t定義以下:
struct ngx_connection_s { //空閒鏈接池中,data指向下一個鏈接,造成鏈表;取出來使用時,data指向請求結構體ngx_http_request_s void *data; //讀寫事件結構體 ngx_event_t *read; ngx_event_t *write; ngx_socket_t fd; //socket fd ngx_recv_pt recv; //socket接收數據函數指針 ngx_send_pt send; //socket發送數據函數指針 ngx_buf_t *buffer; //輸入緩衝區 struct sockaddr *sockaddr; //客戶端地址 socklen_t socklen; ngx_listening_t *listening; //監聽的ngx_listening_t對象 struct sockaddr *local_sockaddr; //本地地址 socklen_t local_socklen; ………… }
這裏須要重點關注幾個字段:
結構體ngx_http_request_t存儲整個HTTP請求處理流程所需的全部信息,字段很是多,這裏只進行簡要說明:
struct ngx_http_request_s { //連接 ngx_connection_t *connection; //讀寫事件處理handler ngx_http_event_handler_pt read_event_handler; ngx_http_event_handler_pt write_event_handler; //請求頭緩衝區 ngx_buf_t *header_in; //解析後的請求頭 ngx_http_headers_in_t headers_in; //請求體結構體 ngx_http_request_body_t *request_body; //請求行 ngx_str_t request_line; //解析後的若干請求行 ngx_uint_t method; ngx_uint_t http_version; ngx_str_t uri; ngx_str_t args; ………… }
請求行與請求體解析相對比較簡單,這裏重點講述請求頭的解析,解析後的請求頭信息都存儲在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_t ngx_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_t name; ngx_uint_t offset; ngx_http_header_handler_pt handler; } 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_t hash; //請求頭key的hash值 ngx_str_t key; ngx_str_t value; 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字段。
基礎結構體關係示意圖以下所示:
"初始化服務器"小節提到,在建立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); 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; } 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); }
注意到當讀事件超時時,nginx會直接關閉該連接;函數ngx_http_create_request建立並初始化ngx_http_request_t對象;解析請求行處理函數爲ngx_http_process_request_line。
解析請求行與請求頭的代碼較爲繁瑣,重點在於讀取socket數據,解析字符串,這裏不作詳述。HTTP請求解析過程主要函數調用以下圖所示:
注意,解析完成請求行與請求頭,nginx就開始處理HTTP請求,並無等到解析完請求體再處理。處理請求入口爲ngx_http_process_request。
nginx將HTTP請求處理流程分爲11個階段,絕大多數HTTP模塊都會將本身的handler添加到某個階段(將handler添加到全局惟一的數組phases中),nginx處理HTTP請求時會挨個調用每一個階段的handler。須要注意的是其中有4個階段不能添加自定義handler。11個階段定義以下:
typedef enum { NGX_HTTP_POST_READ_PHASE = 0, 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; } }
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以下圖所示:
上面提到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_t server_rewrite_index; //記錄NGX_HTTP_SERVER_REWRITE_PHASE階段handler的索引值 ngx_uint_t location_rewrite_index; //記錄NGX_HTTP_REWRITE_PHASE階段handler的索引值 } ngx_http_phase_engine_t; struct ngx_http_phase_handler_t { ngx_http_phase_handler_pt checker; //執行handler以前的校驗函數 ngx_http_handler_pt handler; ngx_uint_t next; //下一個待執行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);
GDB打印出轉換後的數組以下圖所示,第一列是cheker字段,第二列是handler字段,箭頭表示next跳轉;圖中有個返回的箭頭,即NGX_HTTP_POST_REWRITE_PHASE階段可能返回到NGX_HTTP_FIND_CONFIG_PHASE;緣由在於只要NGX_HTTP_REWRITE_PHASE階段產生了url重寫,就須要從新查找匹配location。
上面提到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; } }
內容產生階段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解析處理的詳細流程,還須要讀者去探索。