首頁>技術>

前言

Nginx是當前最流行的HTTP Server之一,根據W3Techs的統計,目前世界排名(根據Alexa)前100萬的網站中,Nginx的佔有率為6.8%。與Apache相比,Nginx在高併發情況下具有巨大的效能優勢

使用Nginx的第一步是下載Nginx原始碼包,例如1.0.0的下載地址為http://nginx.org/download/nginx-1.0.0.tar.gz。下載完後用tar命令解壓縮,進入目錄後安裝過程與Linux下通常步驟無異,例如我想將Nginx安裝到/usr/local/nginx下,則執行如下命令:

./configure --prefix=/usr/local/nginxmakemake install

安裝完成後可以直接使用下面命令啟動Nginx:

/usr/local/nginx/sbin/nginx

Nginx預設以Deamon程序啟動,輸入下列命令:

/usr/local/nginx/sbin/nginx -s stop

Nginx配置檔案基本結構配置檔案可以看做是Nginx的靈魂,Nginx服務在啟動時會讀入配置檔案,而後續幾乎一切動作行為都是按照配置檔案中的指令進行的,因此如果將Nginx本身看做一個計算機,那麼Nginx的配置檔案可以看成是全部的程式指令。下面是一個Nginx配置檔案的例項:

#user nobody;worker_processes 8;error_log logs/error.log;pid logs/nginx.pid;events { worker_connections 1024;}http { include mime.types; default_type application/octet-stream;  sendfile on; #tcp_nopush on; keepalive_timeout 65; #gzip on;  server { listen 80; server_name localhost; location / { root /home/yefeng/www; index index.html index.htm; } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } }}

Nginx配置檔案是純文字檔案,通常它會在nginx安裝目錄的conf下,如我的nginx安裝在/usr/local/nginx,主配置檔案預設放在/usr/local/nginx/conf/nginx.conf。其中“#”表示此行是註釋,由於筆者為了學習擴充套件開發安裝了一個純淨的Nginx,因此配置檔案沒有經過太多改動。最後要提到的是配置檔案是可以包含的,如上面配置檔案中“include mime.types”就包含了mine.types這個配置檔案,此檔案指定了各種HTTP Content-type。一般來說,一個server block表示一個Host,而裡面的一個location則代表一個路由對映規則,這兩個block可以說是HTTP配置的核心。下圖是Nginx配置檔案通常結構圖示。

關於Nginx配置的更多內容請參看Nginx官方文件。Nginx模組工作原理概述(Nginx本身支援多種模組,如HTTP模組、EVENT模組和MAIL模組,本文只討論HTTP模組)Nginx本身做的工作實際很少,當它接到一個HTTP請求時,它僅僅是透過查詢配置檔案將此次請求對映到一個location block,而此location中所配置的各個指令則會啟動不同的模組去完成工作,因此模組可以看做Nginx真正的勞動工作者。通常一個location中的指令會涉及一個handler模組和多個filter模組(當然,多個location可以複用同一個模組)。handler模組負責處理請求,完成響應內容的生成,而filter模組對響應內容進行處理。因此Nginx模組開發分為handler開發和filter開發(本文不考慮load-balancer模組)。下圖展示了一次常規請求和響應的過程。

Nginx模組開發實戰下面本文展示一個簡單的Nginx模組開發全過程,我們開發一個叫echo的handler模組,這個模組功能非常簡單,它接收“echo”指令,指令可指定一個字串引數,模組會輸出這個字串作為HTTP響應。例如,做如下配置:

location /echo { echo "hello nginx";}

則訪問http://hostname/echo時會輸出hello nginx。直觀來看,要實現這個功能需要三步:1、讀入配置檔案中echo指令及其引數;2、進行HTTP包裝(新增HTTP頭等工作);3、將結果返回給客戶端。下面本文將分部介紹整個模組的開發過程。定義模組配置結構首先我們需要一個結構用於儲存從配置檔案中讀進來的相關指令引數,即模組配置資訊結構。根據Nginx模組開發規則,這個結構的命名規則為ngx_http_[module-name]_[main|srv|loc]_conf_t。其中main、srv和loc分別用於表示同一模組在三層block中的配置資訊。這裡我們的echo模組只需要執行在loc層級下,需要儲存一個字串引數,因此我們可以定義如下的模組配置:

typedef struct { ngx_str_t ed;} ngx_http_echo_loc_conf_t;

其中欄位ed用於儲存echo指令指定的需要輸出的字串。注意這裡ed的型別,在Nginx模組開發中使用ngx_str_t型別表示字串,這個型別定義在core/ngx_string中:

typedef struct { size_t len; u_char *data;} ngx_str_t;

其中兩個欄位分別表示字串的長度和資料起始地址。注意在Nginx原始碼中對資料型別進行了別稱定義,如ngx_int_t為intptr_t的別稱,為了保持一致,在開發Nginx模組時也應該使用這些Nginx原始碼定義的型別而不要使用C原生型別。除了ngx_str_t外,其它三個常用的nginx type分別為:

typedef intptr_t ngx_int_t;typedef uintptr_t ngx_uint_t;typedef intptr_t ngx_flag_t;

具體定義請參看core/ngx_config.h。關於intptr_t和uintptr_t請參考C99中的stdint.h或http://linux.die.net/man/3/intptr_t。定義指令一個Nginx模組往往接收一至多個指令,echo模組接收一個指令“echo”。Nginx模組使用一個ngx_command_t陣列表示模組所能接收的所有模組,其中每一個元素表示一個條指令。ngx_command_t是ngx_command_s的一個別稱(Nginx習慣於使用“_s”字尾命名結構體,然後typedef一個同名“_t”字尾名稱作為此結構體的型別名),ngx_command_s定義在core/ngx_config_file.h中:

struct ngx_command_s { ngx_str_t name; ngx_uint_t type; char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); ngx_uint_t conf; ngx_uint_t offset; void *post;};

其中name是詞條指令的名稱,type使用掩碼標誌位方式配置指令引數,相關可用type定義在core/ngx_config_file.h中:

#define NGX_CONF_NOARGS 0x00000001#define NGX_CONF_TAKE1 0x00000002#define NGX_CONF_TAKE2 0x00000004#define NGX_CONF_TAKE3 0x00000008#define NGX_CONF_TAKE4 0x00000010#define NGX_CONF_TAKE5 0x00000020#define NGX_CONF_TAKE6 0x00000040#define NGX_CONF_TAKE7 0x00000080#define NGX_CONF_MAX_ARGS 8#define NGX_CONF_TAKE12 (NGX_CONF_TAKE1|NGX_CONF_TAKE2)#define NGX_CONF_TAKE13 (NGX_CONF_TAKE1|NGX_CONF_TAKE3)#define NGX_CONF_TAKE23 (NGX_CONF_TAKE2|NGX_CONF_TAKE3)#define NGX_CONF_TAKE123 (NGX_CONF_TAKE1|NGX_CONF_TAKE2|NGX_CONF_TAKE3)#define NGX_CONF_TAKE1234 (NGX_CONF_TAKE1|NGX_CONF_TAKE2|NGX_CONF_TAKE3 \ |NGX_CONF_TAKE4)#define NGX_CONF_ARGS_NUMBER 0x000000ff#define NGX_CONF_BLOCK 0x00000100#define NGX_CONF_FLAG 0x00000200#define NGX_CONF_ANY 0x00000400#define NGX_CONF_1MORE 0x00000800#define NGX_CONF_2MORE 0x00001000#define NGX_CONF_MULTI 0x00002000

其中NGX_CONF_NOARGS表示此指令不接受引數,NGX_CON F_TAKE1-7表示精確接收1-7個,NGX_CONF_TAKE12表示接受1或2個引數,NGX_CONF_1MORE表示至少一個引數,NGX_CONF_FLAG表示接受“on|off”……set是一個函式指標,用於指定一個引數轉化函式,這個函式一般是將配置檔案中相關指令的引數轉化成需要的格式並存入配置結構體。Nginx預定義了一些轉換函式,可以方便我們呼叫,這些函式定義在core/ngx_conf_file.h中,一般以“_slot”結尾,例如ngx_conf_set_flag_slot將“on或off”轉換為“1或0”,再如ngx_conf_set_str_slot將裸字串轉化為ngx_str_t。conf用於指定Nginx相應配置檔案記憶體其實地址,一般可以透過內建常量指定,如NGX_HTTP_LOC_CONF_OFFSET,offset指定此條指令的引數的偏移量。下面是echo模組的指令定義:

static ngx_command_t ngx_http_echo_commands[] = { { ngx_string("echo"), NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_http_echo, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_echo_loc_conf_t, ed), NULL }, ngx_null_command};

指令陣列的命名規則為ngx_http_[module-name]_commands,注意陣列最後一個元素要是ngx_null_command結束。引數轉化函式的程式碼為:

static char *ngx_http_echo(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_echo_handler; ngx_conf_set_str_slot(cf,cmd,conf); return NGX_CONF_OK;}

這個函式除了呼叫ngx_conf_set_str_slot轉化echo指令的引數外,還將修改了核心模組配置(也就是這個location的配置),將其handler替換為我們編寫的handler:ngx_http_echo_handler。這樣就遮蔽了此location的預設handler,使用ngx_http_echo_handler產生HTTP響應。建立合併配置資訊下一步是定義模組Context。這裡首先需要定義一個ngx_http_module_t型別的結構體變數,命名規則為ngx_http_[module-name]_module_ctx,這個結構主要用於定義各個Hook函式。下面是echo模組的context結構:

static ngx_http_module_t ngx_http_echo_module_ctx = { NULL, /* preconfiguration */ NULL, /* postconfiguration */ NULL, /* create main configuration */ NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ ngx_http_echo_create_loc_conf, /* create location configration */ ngx_http_echo_merge_loc_conf /* merge location configration */};

可以看到一共有8個Hook注入點,分別會在不同時刻被Nginx呼叫,由於我們的模組僅僅用於location域,這裡將不需要的注入點設為NULL即可。其中create_loc_conf用於初始化一個配置結構體,如為配置結構體分配記憶體等工作;merge_loc_conf用於將其父block的配置資訊合併到此結構體中,也就是實現配置的繼承。這兩個函式會被Nginx自動呼叫。注意這裡的命名規則:ngx_http_[module-name][create|merge][main|srv|loc]_conf。下面是echo模組這個兩個函式的程式碼:

static void *ngx_http_echo_create_loc_conf(ngx_conf_t *cf){ ngx_http_echo_loc_conf_t *conf; conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_echo_loc_conf_t)); if (conf == NULL) { return NGX_CONF_ERROR; } conf->ed.len = 0; conf->ed.data = NULL; return conf;}static char *ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child){ ngx_http_echo_loc_conf_t *prev = parent; ngx_http_echo_loc_conf_t *conf = child; ngx_conf_merge_str_value(conf->ed, prev->ed, ""); return NGX_CONF_OK;}

其中ngx_pcalloc用於在Nginx記憶體池中分配一塊空間,是pcalloc的一個包裝。使用ngx_pcalloc分配的記憶體空間不必手工free,Nginx會自行管理,在適當是否釋放。create_loc_conf新建一個ngx_http_echo_loc_conf_t,分配記憶體,並初始化其中的資料,然後返回這個結構的指標;merge_loc_conf將父block域的配置資訊合併到create_loc_conf新建的配置結構體中。其中ngx_conf_merge_str_value不是一個函式,而是一個宏,其定義在core/ngx_conf_file.h中:

#define ngx_conf_merge_str_value(conf, prev, default) \ if (conf.data == NULL) { \ if (prev.data) { \ conf.len = prev.len; \ conf.data = prev.data; \ } else { \ conf.len = sizeof(default) - 1; \ conf.data = (u_char *) default; \ } \ }

同時可以看到,core/ngx_conf_file.h還定義了很多merge value的宏用於merge各種資料。它們的行為比較相似:使用prev填充conf,如果prev的資料為空則使用default填充。編寫Handler下面的工作是編寫handler。handler可以說是模組中真正幹活的程式碼,它主要有以下四項職責:讀入模組配置。處理功能業務。產生HTTP header。產生HTTP body。下面先貼出echo模組的程式碼,然後透過分析程式碼的方式介紹如何實現這四步。這一塊的程式碼比較複雜:

static ngx_int_tngx_http_echo_handler(ngx_http_request_t *r){ ngx_int_t rc; ngx_buf_t *b; ngx_chain_t out; ngx_http_echo_loc_conf_t *elcf; elcf = ngx_http_get_module_loc_conf(r, ngx_http_echo_module); if(!(r->method & (NGX_HTTP_HEAD|NGX_HTTP_GET|NGX_HTTP_POST))) { return NGX_HTTP_NOT_ALLOWED; } r->headers_out.content_type.len = sizeof("text/html") - 1; r->headers_out.content_type.data = (u_char *) "text/html"; r->headers_out.status = NGX_HTTP_OK; r->headers_out.content_length_n = elcf->ed.len; if(r->method == NGX_HTTP_HEAD) { rc = ngx_http_send_header(r); if(rc != NGX_OK) { return rc; } } b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t)); if(b == NULL) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Failed to allocate response buffer."); return NGX_HTTP_INTERNAL_SERVER_ERROR; } out.buf = b; out.next = NULL; b->pos = elcf->ed.data; b->last = elcf->ed.data + (elcf->ed.len); b->memory = 1; b->last_buf = 1; rc = ngx_http_send_header(r); if(rc != NGX_OK) { return rc; } return ngx_http_output_filter(r, &out);}

handler會接收一個ngx_http_request_t指標型別的引數,這個引數指向一個ngx_http_request_t結構體,此結構體儲存了這次HTTP請求的一些資訊,這個結構定義在

http/ngx_http_request.h中:struct ngx_http_request_s { uint32_t signature; /* "HTTP" */ ngx_connection_t *connection; void **ctx; void **main_conf; void **srv_conf; void **loc_conf; ngx_http_event_handler_pt read_event_handler; ngx_http_event_handler_pt write_event_handler;#if (NGX_HTTP_CACHE) ngx_http_cache_t *cache;#endif ngx_http_upstream_t *upstream; ngx_array_t *upstream_states; /* of ngx_http_upstream_state_t */ ngx_pool_t *pool; ngx_buf_t *header_in; ngx_http_headers_in_t headers_in; ngx_http_headers_out_t headers_out; ngx_http_request_body_t *request_body; time_t lingering_time; time_t start_sec; ngx_msec_t start_msec; ngx_uint_t method; ngx_uint_t http_version; ngx_str_t request_line; ngx_str_t uri; ngx_str_t args; ngx_str_t exten; ngx_str_t unparsed_uri; ngx_str_t method_name; ngx_str_t http_protocol; ngx_chain_t *out; ngx_http_request_t *main; ngx_http_request_t *parent; ngx_http_postponed_request_t *postponed; ngx_http_post_subrequest_t *post_subrequest; ngx_http_posted_request_t *posted_requests; ngx_http_virtual_names_t *virtual_names; ngx_int_t phase_handler; ngx_http_handler_pt content_handler; ngx_uint_t access_code; ngx_http_variable_value_t *variables; /* ... */}

由於ngx_http_request_s定義比較長,這裡我只截取了一部分。可以看到裡面有諸如uri,args和request_body等HTTP常用資訊。這裡需要特別注意的幾個欄位是headers_in、headers_out和chain,它們分別表示request header、response header和輸出資料緩衝區連結串列(緩衝區連結串列是Nginx I/O中的重要內容,後面會單獨介紹)。第一步是獲取模組配置資訊,這一塊只要簡單使用ngx_http_get_module_loc_conf就可以了。第二步是功能邏輯,因為echo模組非常簡單,只是簡單輸出一個字串,所以這裡沒有功能邏輯程式碼。第三步是設定response header。Header內容可以透過填充headers_out實現,我們這裡只設置了Content-type和Content-length等基本內容,ngx_http_headers_out_t定義了所有可以設定的HTTP Response Header資訊:

typedef struct { ngx_list_t headers; ngx_uint_t status; ngx_str_t status_line; ngx_table_elt_t *server; ngx_table_elt_t *date; ngx_table_elt_t *content_length; ngx_table_elt_t *content_encoding; ngx_table_elt_t *location; ngx_table_elt_t *refresh; ngx_table_elt_t *last_modified; ngx_table_elt_t *content_range; ngx_table_elt_t *accept_ranges; ngx_table_elt_t *www_authenticate; ngx_table_elt_t *expires; ngx_table_elt_t *etag; ngx_str_t *override_charset; size_t content_type_len; ngx_str_t content_type; ngx_str_t charset; u_char *content_type_lowcase; ngx_uint_t content_type_hash; ngx_array_t cache_control; off_t content_length_n; time_t date_time; time_t last_modified_time;} ngx_http_headers_out_t;

這裡並不包含所有HTTP頭資訊,如果需要可以使用agentzh(春來)開發的Nginx模組HttpHeadersMore在指令中指定更多的Header頭資訊。設定好頭資訊後使用ngx_http_send_header就可以將頭資訊輸出,ngx_http_send_header接受一個ngx_http_request_t型別的引數。第四步也是最重要的一步是輸出Response body。這裡首先要了解Nginx的I/O機制,Nginx允許handler一次產生一組輸出,可以產生多次,Nginx將輸出組織成一個單鏈表結構,連結串列中的每個節點是一個chain_t,定義在core/ngx_buf.h:

struct ngx_chain_s { ngx_buf_t *buf; ngx_chain_t *next;};

其中ngx_chain_t是ngx_chain_s的別名,buf為某個資料緩衝區的指標,next指向下一個連結串列節點,可以看到這是一個非常簡單的連結串列。ngx_buf_t的定義比較長而且很複雜,這裡就不貼出來了,請自行參考core/ngx_buf.h。ngx_but_t中比較重要的是pos和last,分別表示要緩衝區資料在記憶體中的起始地址和結尾地址,這裡我們將配置中字串傳進去,last_buf是一個位域,設為1表示此緩衝區是連結串列中最後一個元素,為0表示後面還有元素。因為我們只有一組資料,所以緩衝區連結串列中只有一個節點,如果需要輸入多組資料可將各組資料放入不同緩衝區後插入到連結串列。下圖展示了Nginx緩衝連結串列的結構:

Linux後臺開發系列】Nginx原始碼從模組開發開始,不再對nginx原始碼陌生丨原始碼分析

10
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 創新!阿里首發微服務實施手冊我粉了,原來微服務還可以這樣玩