nginx http模塊開發入門
導語
本文對nginx http模塊開發需要掌握的一些關鍵點進行了提煉,同時以開發一個簡單的日誌模塊進行講解,讓nginx的初學者也能看完之後做到心裏有譜。本文只是一個用作入門的概述。
目錄
- 背景
- 主線
- 認識nginx
- http請求處理流程
- 日誌模塊開發準備
- 配置解析流程
- 掛載處理函數
- 獲取日誌數據
- 模塊編譯
- 完整源碼
- 結尾
背景
網上已經有很多介紹nginx http模塊開發入門的文章,但是大多以hello_word程序為例,簡單講解一下代碼,看完之後可能只能夠依葫蘆畫瓢,做不了其它事情,更不要談實際工程需要了。當然網上也有很多系統介紹nginx的系列文章,不過學習成本較高,需要一定時間成本才能開始"動手"。這是我初學nginx時的感受:網上很少有拿捏比較準的,既能屏蔽掉底層細節,又讓讀者清楚大致原理,做到心中不慌的文章,當自己對nginx有了一定了解後,決定也寫一篇關於http模塊開發入門的文章。當然由於自己也是半桶水,初衷雖好,但文章水平未必如意。以下都是以linux平臺為準。
主線
認識nginx
需要看本文的同學,想必對nginx多少都有一定了解了。nginx是web服務器程序(一般都是作為反向代理服務器)。傳統的apache服務器每當收到一個http請求就會創建一個進程,這種方式在高並發情況下,對系統資源(CPU,內存)的消耗十分浪費,與之相比,nginx具備如下優勢
1 多worker監聽處理模式(類比單Proxy多Worker的服務模型,請求量較大的時候單Proxy容易成為IO瓶頸),同時worker底 層依靠epoll進行IO調度(每個worker能同時處理多個請求),以及每個請求的低內存消耗(事實上對於現在的機器配置而言 ,CPU,網卡往往才是性能瓶頸),單機10萬QPS也是不在話下 2 通過良好的分層次/功能的模塊化設計,各模塊間耦合度極低,具備良好的擴展性及靈活性,受益於此,nginx有大量的 第三方模塊。 3 在熱部署方面,nginx的單Master-多worker進程模型,可讓nginx在不停止服務的情況下,進行二進制文件的升級,更 新配置,切割日誌文件等操作。 這裏還有一個有趣的話題,nginx為什麽要叫反向代理服務器呢?因為它是將外部請求轉發到內部服務。
http請求處理流程
要開發第三方http模塊,必須清楚nginx的http框架處理http請求的流程,才能知道自己的模塊應該掛載(作用)在哪個流程。http框架對請求的處理分為11個流程,其中的7個流程可以掛載第三方模塊。對於http請求的主要處理流程可歸納為如下5步:
1 完整讀取HTTP頭部
2 Uri查找及重寫階段
3 訪問控制階段
4 處理請求內容階段
5 日誌階段
其完整的請求流程如下:
1 NGX_HTTP_POST_READ_PHASE 讀取到完整的請求頭後進行處理的階段 2 NGX_HTTP_SERVER_REWRITE_PHASE 還未進行URI的Location匹配前的URL重寫階段 3 NGX_HTTP_FIND_COFIG_PHASE Location匹配階段(不可掛載) 4 NGX_HTTP_REWRITE_PHASE URI匹配之後的URL重寫階段 5 NGX_HTTP_POST_REWRITE_PHASE 重寫提交階段,用於在重寫URL之後,再次跳到NGX_HTTP_FIND_COFIG_PHASE階段(不可掛載) 6 NGX_HTTP_PREACCESS_PHASE 訪問控制階段前 7 NGX_HTTP_ACCESS_PHASE 訪問控制階段 8 NGX_HTTP_POST_ACCESS_PHASE 訪問控制提交階段,如不允許訪問,此階段將構造拒絕訪問的回包(不可掛載) 9 NGX_HTTP_TRY_FILES_PHASE 為try-files配置項設立的,用於處理靜態文件,可以不關註(不可掛載) 10 NGX_HTTP_CONTENT_PHASE 核心階段,處理請求內容 11 NGX_HTTP_LOG_PHASE 日誌階段
通過將請求處理劃分為十一個流程,每個流程由各自掛接的模塊進行處理,使得http框架具備良好的擴展性和靈活性,另外除去上述主流程之外,還有一個經常碰到的流程是content_filter流程,該流程可以視為是NGX_HTTP_CONTENT_PHASE的一個子流程,常用的gzip模塊就是作用在這一流程中,由於是入門,此塊暫且不提。
另外我們可以看到,在1-9階段其實都是http服務器的基礎功能,如uri重寫/匹配,訪問權限校驗等,一般我們都是在NGX_HTTP_CONTENT_PHASE掛載我們自定義的模塊來對特定請求進行處理,或者是在NGX_HTTP_LOG_PHASE,進行一些信息收集類的工作。
日誌模塊開發準備
通過上面的了解,我們已經清楚了nginx的http請求處理流程,很明顯,我們要開發的日誌模塊,肯定是掛在請求處理的最後階段,即NGX_HTTP_LOG_PHASE階段,掛載該階段還有兩個理由
1 因為在上一個階段,已經處理了回包,該階段不會影響請求的耗時
2 在該階段,我們可以獲取到該次請求的所有信息(包括請求信息以及回包信息)
下面開始設計該模塊,首先需要明白一點,http模塊由兩部分構成:
1 配置項命令,即nginx配置文件中的各個配置項
2 http模塊上下文,上下文中將設置配置解析過程中的各個回調函數
下面我們分別介紹。
配置項命令
首先我們需要增加兩個配置項命令,分別用於日誌開關和日誌格式(控制打印哪些信息),通過定義ngx_command_t來添加配置項命令,先看ngx_command_t結構的定義:
typedef struct ngx_command_t ngx_command_s;
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
name是配置項名稱
type
type是配置項類型,該值會指定該配置項可出現的位置(http,server,location等三個),以及可以指定攜帶多少個參數,nginx預定義了一些宏,這裏抽取部分進行介紹
NGX_HTTP_MAIN_CONF
該配置項可出現在http塊內(不包含server塊)
NGX_HTTP_SRV_CONF
該配置項可出現在server塊內(不包含location塊)
NGX_HTTP_LOC_CONF
該配置項可出現在location塊內
NGX_CONF_NOARGS
該配置項沒有參數
NGX_CONF_TAKE1
該配置項攜帶1個參數
NGX_CONF_TAKE2
該配置項攜帶2個參數
NGX_CONF_TAKE3
該配置項攜帶3個參數
...
NGX_CONF_TAKE12
該配置項攜帶1或者2個參數
NGX_CONF_TAKE13
該配置攜帶1或者3個參數
set
在解析配置時,碰到了該配置項名後,將會調用該方法來解析其後面的參數,nginx預設了一些解析函數,最常用到的有以下3個(還有很多,包括數組解析,kv對解析,甚至是枚舉,這些需要用到的話google一下就好):
1 ngx_conf_set_flag_slot
如果配置項的參數是on/off時(即希望這個參數像個開關一樣,打開或者關閉某項功能),可以使用這個預設函數,當參數為
on時,會把對應的變量設置為1,同理,為off時,會設置為0
2 ngx_conf_set_str_slot
如果該配置項只有一個參數,並且希望以ngx_str_t類型(nginx裏面的字符串類型)進行存儲的話,可使用這個預設函數
3 ngx_conf_set_num_slot
如果該配置項只有一個參數,並且這個參數是數字,而且存儲該參數的變量是整形時,可以使用該預設函數
conf
該參數決定存儲該配置項內容的內存偏移,在這裏很難講清,具體將會在下文提到。
offset
一般來說,我們會定義一個結構體,用於存放我們自定義模塊對應配置項解析出來的值,offset是指存儲該配置項的成員在該結構體中的內存偏移量。如果使用的set函數是nginx預設的,那麽offset參數必須要設置,一般我們使用這個宏:offsetof(struct,member)。如果使用的set函數是自定義的,那麽該值隨便你填(一般填0,避免誤解)都沒關系,它只是nginx提供的解析函數需要使用。
post
該參數一般置為NULL,由於是入門,這裏簡單提一下,如果上面的set函數是自定義方法,那麽post指針完全可以隨意發揮。如果set函數用的是nginx預設的,那麽需要根據使用的預設set方法來對post進行相應的設定或置為NULL。
了解了ngx_command_t結構後,我們可以先定義我們的配置項命令,以及存儲配置項值的結構體了。在這裏我們定義了兩個配置命令,分別是mylog_switch,以及mylog_format,其中mylog_switch是作為是否打開mylog日誌模塊的flag使用,mylog_format用於設置日誌格式,在這裏我們簡單處理,只用於存放需要打印的nginx內部變量。
//用於存儲解析出來的配置項值
typedef struct{
ngx_str_t name; //變量名
ngx_int_t index; //該變量的下標
}ngx_http_mylog_var_t;
typedef struct{
ngx_flag_t mylog_switch;
ngx_array_t mylog_formats;//數組,在這裏用來存儲ngx_http_mylog_var_t
}ngx_http_mylog_conf_t;
/*
*聲明配置項解析函數
* cf: 該變量保存著當前的配置內容
* cmd: 對應的上文提到的cmd指令
* conf:通過create_main/srv/loc_conf(下文會提到) 創建出來的結構體
*/
static char* ngx_http_mylog_set_switch(ngx_conf_t *cf,ngx_command_t *cmd,void *conf);
static char* ngx_http_mylog_set_format(ngx_conf_t *cf,ngx_command_t *cmd,void *conf);
//定義我們需要新增的配置項,該變量必須為數組類型,且以ngx_null_command結尾
static ngx_command_t ngx_http_mylog_commands[] = {
{
ngx_string("mylog_switch"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_http_mylog_set_swtich,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mylog_conf_t,switch),
NULL
},
{
ngx_string("mylog_format"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE,
ngx_http_mylog_set_format,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL
},
ngx_null_command
};
解釋一下ngx_http_mylog_commands數組中各command定義的含義
mylog_switch
name
ngx_string是nginx預定義好的宏,用於快速創建一個ngx_string_t變量,我們增加了一項名為"mylog_switch"的配置項
type
首先我們設置了該配置項可出現的配置塊,它可出現在http塊,srv塊,loc塊等配置中,同時定義了它只能攜帶一個參數,type參數與conf參數的設置是有一定關聯性的,待會講解conf參數時會提
set
這裏我們使用自定義的解析函數,具體實現我們在下文會講解
conf
conf參數決定存儲該配置項內容的內存偏移(具體來由在下文還會提),這個參數我們使用哪個值,需要根據type來決定,簡單來說該值需要填type中的最低級別對應的值,比如我們的配置項可出現在http塊,srv塊,loc塊中,那麽我們的conf參數理應填NGX_HTTP_LOC_CONF_OFFSET,如果我們的配置項只可出現在http塊和srv塊中,那麽就填NGX_HTTP_SRV_CONF_OFFSET,當然如果只能出現在http塊的話,那就填NGX_HTTP_MAIN_CONF_OFFSET即可(如果讀者去讀nginx的http模塊源碼的話,可能會發現有些模塊傳了0進去,NGX_HTTP_MAIN_CONF_OFFSET的值其實就是0)
offset
mylog_format由於使用的自定義解析函數,直接填0,mylog_switch雖然也使用的我們的自定義函數,但是在下文的示例代碼裏該函數會使用預定義解析函數ngx_conf_set_flag_slot,因此使用offsetof宏,設置一下偏移
post
由於使用的自定義解析函數,該值可以隨便我們使用,由於我們並不想搞事,填NULL即可
mylog_format
各參數值與mylog_switch基本一致,這裏就不再贅述了,除了type參數中指定配置項參數個數用的是NGX_CONF_1MORE,該參數表示至少有一個參數。
Note:
對於剛剛定義的兩個配置項命令,我們可以有如下使用方式:
mylog_switch on;
mylog_format ‘$remote_addr $http_user_agent $http_cookie‘
‘$status $body_bytes_sent‘;
含義是打開mylog日誌模塊,需要打印remote_addr,http_user_agent ,http_cookie,status,
body_bytes_sent等nginx內置變量
模塊上下文
有了配置項命令後,接下來我們需要定義模塊上下文,模塊上下文用於設置該模塊在解析配置文件時的各個階段的回調函數。我們看一下http模塊上下文結構體的定義,其由一堆函數指針組成,這些函數會在配置解析的對應階段中被調用。
typedef struct {
ngx_int_t (*preconfiguration)(ngx_conf_t *cf);/*配置解析前*/
ngx_int_t (*postconfiguration)(ngx_conf_t *cf);/*配置解析完畢後*/
void* (*create_main_conf)(ngx_conf_t *cf);/*當碰到http塊時的回調函數,用於創建http塊配置項*/
char* (*init_main_conf)(ngx_conf_t *cf, void *conf);/*初始化http塊配置項,該函數會在配置項合並前調用*/
void* (*create_srv_conf)(ngx_conf_t *cf);/*當碰到server塊時的回調函數,用於創建server塊配置項*/
char* (*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);/*用於合並Main級別和Srv級別的server塊配置項*/
void* (*create_loc_conf)(ngx_conf_t *cf);/*當碰到location塊時的回調函數,用於創建location塊的配置項*/
char* (*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);/*用於合並Main,Srv級別和Loc級別的location塊配置項*/
} ngx_http_module_t;
Note:
create_main_conf,create_srv_conf,create_loc_conf三個函數分別創建不同配置塊對應的結構體,因此它們是可以返回
不同類型的結構體的,開發者可根據自己的需求來決定
下面定義我們的日誌模塊的上下文結構
//函數聲明
static ngx_int_t ngx_http_mylog_post_init(ngx_conf_t *cf);
static void* ngx_http_mylog_create_main_conf(ngx_conf_t *cf);
static void* ngx_http_mylog_create_srv_conf(ngx_conf_t *cf);
static char* ngx_http_mylog_merge_srv_conf(ngx_conf_t *cf,void *prev,void *conf);
static void* ngx_http_mylog_create_loc_conf(ngx_conf_t *cf);
static char* ngx_http_mylog_merge_loc_conf(ngx_conf_t *cf,void *prev,void *conf);
//該函數用於掛載到日誌階段,執行mylog模塊的邏輯
static ngx_int_t ngx_http_mylog_handler(ngx_http_request_t *r);
//定義日誌模塊上下文結構
static ngx_http_module_t ngx_http_mylog_module_ctx = {
NULL,
ngx_http_mylog_post_init,
ngx_http_mylog_create_main_conf,
NULL,
ngx_http_mylog_create_srv_conf,
ngx_http_mylog_merge_srv_conf,
ngx_http_mylog_create_loc_conf,
ngx_http_mylog_merge_loc_conf
};
Note:
沒錯,配置項命令和模塊上下文都是在圍繞著nginx配置文件在轉,可以說nginx配置文件是第三方模塊與nginx的橋梁
為什麽我們這裏聲明的函數和日誌模塊上下文結構都加上了static修復符呢? 全局變量和全局靜態變量有什麽區別呢?
這一點交給讀者自己解決。
上面聲明的函數的具體實現我們先放在一邊,下文會具體講解。我們來解釋下為何我們的ngx_http_mylog_module_ctx如此定義
1 ngx_http_mylog_post_init
這是我們設置的配置解析完畢後的回調函數,我們將在這個回調函數中,將我們的自定義邏輯處理函數掛載到nginx的日誌階段中去
2 ngx_http_mylog_create_main_conf
當nginx解析配置文件碰到http塊時會調用該函數生成對應的main級別的http塊配置項結構體,註意!如果create_srv/loc_con
f函數指針不為空的話,也將調用生成main級別的server和location塊配置項結構體。
3 ngx_http_mylog_create_srv_conf
同上,在碰到server塊時會調用該函數生成srv級別的server塊結構體,同時如果create_loc_conf函數指針不為空的話,也
將調用生成srv級別的location塊結構體
4 ngx_http_mylog_merge_srv_conf
用於合並main級別的server塊結構體和srv級別的server塊結構體,當然如果某一方為NULL的話,也就不用合並了
5 ngx_http_mylog_create_loc_conf
同3,在碰到location塊時會調用該函數生成loc級別的location塊結構體
6 ngx_http_mylog_merge_loc_conf
用於合並不同級別的location塊結構體
Note:
上下文結構的定義跟定義ngx_command_t時各command的賦值是有關系的。舉個例子,如果說有command它的conf參數是NGX_HTT
P_MAIN_CONF_OFFSET的話,那麽create_main_conf不能為空,否則就沒有main塊結構體供它存儲配置內容了。同理,如果有co
mmand的conf參數是NGX_HTTP_SRV_CONF_OFFSET的話,那麽create_srv_conf不能為空,否則就沒有servr塊結構體供它存儲配
置內容。至於兩個merge函數,如果說可能出現不同級別的server塊或者location塊結構體時,那麽就需要對應的merge函數來
合並配置項,當然事實上你不實現merge函數也不會導致nginx運行錯誤,只是這可能不符合設計規範
實際上由於我們的mylog日誌模塊的兩個配置項命令的內存偏移都使用的是NGX_HTTP_LOC_CONF_OFFSET,我們其實並不需要定義create_main_conf,create_srv_conf,以及merge_srv_conf,因此我們可以把這三個函數指針置為NULL,當然對應的那三個函數聲明也可以刪掉了,前面之所以寫出來,就是為了在這裏說明其實可以刪掉它們→_→
現在mylog日誌模塊的配置項命令以及模塊上下文都定義好了,我們可以定義mylog日誌模塊了,只需要定義一個ngx_module_t變量即可。我們先看ngx_module_t結構的定義,再定義我們的模塊變量。
typedef struct ngx_module_s ngx_module_t;
... /*省略號部分的變量我們不需要關註(它們中有版本號,保留字段等,以及兩個很核心,但是是由框架來賦值的變量)
,在我們定義變量時基本都是用nginx的預定義宏NGX_MODULE_V1來賦值,因此這裏略過*/
void *ctx; //變量名即註釋,傳入模塊上下文
ngx_command_t *command; //傳入配置項命令數組
ngx_uint_t type; /*模塊類型,nginx預定義了一些類型(可自定義新的
類型),其中就包括我們現在在定義的Http模塊,因此填入NGX_HTTP_MODULE*/
/*以下7個函數,是nginx在啟動停止時的回調函數,由於nginx還不支持多線程模式,所以init/exit_thread不會被調用
,設置為NULL即可,其中init/exit_master為master進程調用,其余均為work進程調用,參數ngx_cycle_t
*cycle其實是nginx的核心變量,但是由於這7個函數,大多數模塊都不會用到,作為入門,這裏就不展開了*/
ngx_int_t (*init_master)(ngx_log_t *log);
ngx_int_t (*init_module)(ngx_cycle_t *cycle);
ngx_int_t (*init_process)(ngx_cycle_t *cycle);
ngx_int_t (*init_thread)(ngx_cycle_t *cycle);
void (*exit_thread)(ngx_cycle_t *cycle);
void (*exit_process)(ngx_cycle_t *cycle);
void (*exit_master)(ngx_cycle_t *cycle);
.../*同上面的省略號,以下變量均為保留字段,在我們定義變量時基本都是用nginx的預定義宏NGX_MODULE_V1_PADDING
來填充,因此這裏略過*/
接下來定義我們的nginx模塊變量
ngx_module_t ngx_http_mylog_module = {
NGX_MODULE_V1,
&ngx_http_mylog_module_ctx,
ngx_http_mylog_commands,
NGX_HTTP_MODULE,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NGX_MODULE_V1_PADDING
};
定義完模塊變量已經是萬事具備了,接下來的工作只需要將模塊掛載到對應的http請求階段中即可生效,那麽在哪裏掛載?如何掛載?掛載的是什麽?還記得我們定義的ngx_command_t變量中它的set函數(解析到對應配置項時的回調函數)以及ngx_http_module_t中的各回調函數麽?第三方模塊可以說全是在這些回調函數中進行掛載的。至於如何掛載在講解完配置解析流程後會提。掛載的自然是我們的自定義邏輯處理函數(通過函數指針賦值,因此函數的參數列表是需要固定的,只不過裏面的邏輯由我們決定)。到這裏,是不是覺得開發一個第三方http模塊十分輕松?
接下來了解一下nginx的配置解析流程,清楚了配置解析流程後,讀者就能把上面的碎片化講解給串起來了。
配置解析流程
nginx是靠配置來驅動的,每一份配置文件就是一個定制化的nginx服務器。nginx在啟動過程中會對配置內容進行解析,在解析配置的過程中會回調相應的回調函數,對第三方模塊來說我們可以在這些回調函數中將自己的模塊掛接到相關的請求處理階段中。這裏我們只關註http配置項,而http配置項中最關鍵的幾個配置塊就是http配置塊,server配置塊和location配置塊,這裏簡要介紹一下nginx的http模塊配置解析流程。
nginx配置解析過程中,有一個核心結構必須了解,我們先看一下這個核心結構的定義:
//配置上下文結構,由三個二級指針構成,分別指向http/server/location塊配置項結構體
typedef struct{
void **main_conf;
void **srv_conf;
void **loc_conf;
}ngx_http_conf_ctx_t;
下面簡述nginx在解析配置文件時遇到http,server,location塊及配置項命令時的處理過程:
1 遇到http塊時,將調用各模塊的create\_main\_conf函數,同時還會調用create\_srv\_conf,create\_loc\_conf
2 調用各模塊的http模塊上下文的preconfiguration回調函數
3 解析http塊內的配置項命令,具體見#
4 遇到server塊時,將調用各模塊的create\_srv\_conf函數,以及create\_loc\_conf函數
5 解析server塊內的配置項命令,具體見#
6 遇到location塊時,將調用各模塊的create\_loc\_conf函數,由於location內可嵌套location,此過程可遞歸
7 解析location塊內的配置項命令,具體見#
8 調用各模塊的init\_main\_conf方法
9 合並各模塊main,srv,loc級別下的srv和loc配置項
10 調用各模塊的http模塊上下文的postconfiguration回調函數
# 在配置解析過程中,當遇到配置項命令時,nginx將尋找該配置項(根據配置項名稱,即定義ngx\_command\_t中的name
成員)對應的回調函數(即定義ngx\_command\_t變量時的set函數指針),並將配置內容以及nginx的上下文保存在ngx\_con
f\_t結構中,傳入set函數。
由上面可知,正常情況下:
1 create\_main\_conf方法只會被調用一次(http塊在nginx配置文件中只能出現一次)
2 2 create\_srv\_conf方法至少會被調用兩次(碰到http塊時將調用一次,而一個http塊下至少會有一個server塊)
3 3 create\_loc\_conf方法至少會被調用三次(碰到http塊,server塊和location塊時都將被調用)
為什麽nginx需要生成不同級別的server塊和location塊配置變量呢?這是因為我們需要解決配置項合並的問題。配置項合並的規則也是由我們來決定的,比如你可以完全采用上一級別的配置,也可以完全采用當前級別的配置,或者兩者各取一部分。
配置解析過程中,當碰到http塊時,將會創建一個ngx_http_conf_ctx_t變量,也就是上面我們提到的配置上下文結構,之後按順序調用各http模塊的create_main/srv/loc_conf方法,然後將生成的http/server/location塊的配置項結構體添加到創建好ngx_http_conf_ctx_t變量中(如果某個http模塊的create_main_conf方法為NULL呢?那對應位置就為NULL)。那麽是按怎樣的順序調用各http模塊呢?在我們定義ngx_module_t變量時,曾提到過有兩個變量是框架來賦值的,其中的一個就是index變量,每個第三方模塊都會被分配一個遞增的下標,對第三方模塊而言,其index值由它編譯進nginx的順序決定,而nginx正是通過index值從小到大的順序來進行調用。
index值的大小跟configure腳本的--add-module=Path命令添加進去時的順序相關,原因是該腳本執行之後將生成一個ngx\_modules.c文件,該文件中會定義一個ngx\_modules數組,其index值與其在數組中的下標相關,下標越大,index值越大,則越靠後被調用
當碰到server塊時(每次碰到都會),nginx又會創建一個ngx_http_conf_ctx_t變量,同時按順序調用各模塊的create_srv/loc_conf方法,然後將對應的srv/loc塊的結構體添加到創建好的ngx_http_conf_ctx_t變量中,同時將該變量的main_conf指針指向其所屬的http塊的ngx_http_conf_ctx_t的main_conf指針指向的值(以此保證能尋找到它所屬的http塊配置)。
當碰到location塊時(每次碰到都會,要註意一點是location塊是可以嵌套的,不過子location塊依然也是這樣的邏輯,因此在合並loc級別的配置項時,還可能合並父location與子location塊的配置),nginx又會創建一個ngx_http_conf_ctx_t變量,同時按順序調用各模塊的create_loc_conf方法,然後將生成的location塊的結構體添加到創建好的ngx_http_conf_ctx_t變量中,同時將該變量的main_conf變量和srv_conf變量分別指向其所屬http塊的ngx_http_conf_ctx_t的main_conf指針和所屬server塊的ngx_http_conf_ctx_t的srv_conf指針指向的值(以此保證能尋找到它所屬的http塊和server塊配置項)。
Note:
現在我們再回顧一下定義http模塊上下文時的三個函數指針,create_main/srv/loc/_conf,這裏我們以mylog日誌模塊的c
reate_loc_conf函數指針為例,我們看一下它對應的函數聲明:
static void* ngx_http_mylog_create_loc_conf(ngx_conf_t *cf);
ngx_conf_t *cf變量中會存儲當前的配置項內容,同時它還有一個重要的成員是void *ctx,這個ctx指針指向著的就是當前的配置上下文結構。
這裏還忽略了一個問題,上面一共生成了3種級別的ngx_http_conf_ctx_t變量,分別是main級別,srv級別和loc級別,如果說我們拿到了srv級別的ngx_http_conf_ctx_t變量,那麽我們能查找到它所屬的main級別的ngx_http_conf_ctx_t變量,可如果我們拿到的是main級別的ngx_http_conf_ctx_t變量,卻無法對其下屬的server塊和location塊中的配置進行管理。那麽nginx是如何處理的呢?
在定義ngx_module_t變量時,第二個由框架來賦值的變量是ctx_index變量,該變量的值表示其所屬模塊在同一模塊類型中的位置,其中ctx_index為0的模塊是ngx_http_core_module(nginx的http框架的核心模塊),因此在http模塊初始化過程中,它是最先被調用的,我們看一下該模塊定義的http/server/location塊配置項結構體:
//http塊配置項結構體的部分定義
typedef struct {
ngx_array_t servers; /* ngx_http_core_srv_conf_t */
...
ngx_http_phase_t phases[NGX_HTTP_LOG_PHASE + 1];//這是我們掛載第三方模塊的關鍵變量,保存了各個階段的回調函數
} ngx_http_core_main_conf_t;
//server塊配置項結構體的部分定義
typedef strcut {
...
ngx_http_conf_ctx_t *ctx;
} ngx_http_core_srv_conf_t;
//location塊配置項結構體的部分定義
typedef struct ngx_http_core_loc_conf_s ngx_http_core_loc_conf_t;
struct ngx_http_core_loc_conf_s {
...
ngx_http_handler_pt handler;//這是我們在NGX_HTTP_CONTENT_PHASE階段掛載第三方模塊的關鍵變量
void **loc_conf;
ngx_queue_t *locations;
};
在ngx_http_core_main_conf_t結構中,servers數組裏面存儲的元素正是ngx_http_core_srv_conf_t,而ngx_http_core_srv_conf_t中的ctx變量保存了當前server塊的配置上下文,因此main級的ngx_http_conf_ctx_t變量將其下的所有server塊的配置都管理起來了。
在ngx_http_core_loc_conf_t結構中,locations隊列裏面最終會存放的依然是ngx_http_core_loc_conf_t變量,而ngx_http_core_loc_conf_t中的loc_conf保存了當前location塊的配置上下文(因為碰到location塊時,只會調用create_loc_conf方法),所以srv級別的ngx_http_conf_ctx_t變量成功將其下的location塊的配置管理起來了,原因是srv級別的ngx_http_conf_ctx_t的ngx_http_core_loc_conf_t變量(由ngx_http_core_module的create_loc_conf生成)會初始化locations隊列,該server塊下的直屬location塊配置項都將添加進該locations隊列中。另外我們提到過location塊是可以嵌套的,比如下面這種結構
...
location L1{
...
location L2{
...
}
location L3{
...
}
...
}
這種結構下在解析location塊L1時生成的loc級ngx_http_conf_ctx_t裏的ngx_http_core_loc_conf_t變量中的locations將會初始化,L2塊和L3塊的配置項將添加進該locations隊列中。也就是說,location塊配置項會添加到上一級的ngx_http_core_loc_conf_t中的locations隊列中。
到這裏,我們基本理清了nginx對於http塊的配置解析流程以及配置管理。
ctx_index和index值有什麽聯系呢?比如說同一個模塊類型的模塊A和B,A的ctx_index值 < B的ctx_index值,那麽A的index值 < B的index值。那麽為什麽有了index變量後,還需要一個ctx_index變量呢?因為我們可以根據ctx_index值和http配置上下文結構,直接找到它對應的配置項結構體。
Note:
此處我們再回顧一下ngx_command_t變量中的set函數指針,先看它的聲明
char *(*set)(ngx_conf_t *cf,ngx_command_t *cmd,void *conf);
在這裏講解一下各個參數的意義
cf:
上面已經提到了,它存儲著該command對應當前的配置內容,以及當前的配置nginx的上下文
cmd:
即查找到的對應配置項的ngx_command_t結構
conf:
通過http模塊上下文中的create_main/srv/loc_conf等函數創建出來的結構體,那麽這裏到底使用的是create_main_
conf創建出來的結構體還是create_srv_conf,或者create_loc_conf創建出來的結構體呢?這一點其實通過上文的了解已
經能夠知道了,這裏就交給讀者自己思考
掛載處理函數
首先,我們需要把我們的邏輯處理函數掛載至日誌階段,nginx的第三方模塊掛載一共有兩種方式:
1 當需要介入的階段為NGX_HTTP_CONTENT_PHASE階段時,如
//在配置命令的set回調函數中,進行掛載
static char* ngx_http_example_set_func(ngx_conf_t *cf,ngx_command_t *cmd,void *conf){
ngx_http_core_loc_conf_t clcf = ngx_http_conf_get_module_loc_conf(cf,ngx_http_core_module);
clcf->handler = ngx_http_example_handler;
return NGX_CONF_OK;
}
此種掛載方法只能用於NGX_HTTP_CONTENT_PHASE階段,且只對匹配了該location的請求生效。使用此種方法進行掛載時,我們一般都是在對應的ngx_command_t的set回調方法中添加處理方法。
2 獲取需要介入階段的邏輯處理函數數組對象,將我們的邏輯處理函數添加進去,如將mylog日誌模塊的處理函數掛載上去:
//在postConfiguration函數中進行掛載
//以我們的mylog模塊為例
static ngx_int_t ngx_http_mylog_post_init(ngx_conf_t *cf){
ngx_http_handler_pt *h;
ngx_http_core_main_conf_t *cmcf = ngx_http_conf_get_module_main_conf(cf,ngx_http_core_module);
/*
*上文我們已經提過了ngx_http_core_main_conf_t的phases成員保存了各個
*階段的回調函數
*/
h = ngx_array_push(&cmcf->phases[NGX_HTTP_LOG_PHASE].handlers);
if(!h){
return NGX_ERROR;
}
//掛載mylog模塊的處理函數
*h = ngx_http_mylog_handler;
return NGX_OK;
}
此種掛載方法,可適用於所有階段,且將會對所有請求生效。那麽對於NGX_HTTP_CONTENT_PHASE階段其實是有兩種掛載方法的,當同時使用時,只會調用第一種方式掛載的函數。
另外在不同階段掛載的函數,其函數的返回值也會有各種含義,這裏我們只講解一下NGX_HTTP_LOG_PHASE階段,其余階段如果需要的話可google
/* Return:
* NGX_OK:
執行下一階段中的第一個ngx_http_handler_pt處理方法,即日誌階段的其余方法將被跳過
* NGX_DECLINED:
* 按順序執行下一個ngx_http_handler_pt方法
* NGX_AGAIN/NGX_DONE:
* 這兩個返回值表示當前處理方法還未結束,一般是函數將控制權交給事件模塊,當事件發生時再次執行該方法,這裏不展開
* NGX_ERROR:
* 將調用ngx_http_finalize_request結束請求
*/
static ngx_int_t ngx_http_mylog_handler(ngx_http_request_t *r);
通過第二種方法,將自定義函數Push進邏輯處理函數數組對象時,該階段的函數的執行順序是怎樣的?當然這裏既然提出
來了,讀者想必也猜到了是逆序執行的。
因此mylog日誌模塊如果不想影響到nginx http框架的日誌模塊的話,返回值應該用NGX_DECLINED。
如果說想直接跳過的話,那返回值設為NGX_OK即可。在這裏我們用NGX_DECLINED,即不影響nginx http框架的日誌模塊。
獲取日誌數據
在上文中我們定義了mylog_format配置命令用於設置日誌格式(一些內置變量),我們將在我們的自定義處理函數ngx_http_mylog_handler函數中獲取mylog_format裏設置的各變量的值。
在mylog_format裏存儲了mylog模塊需要打印的nginx內部變量值,nginx的變量機制如果要展開講的話,需要很多筆墨,
由於是入門篇,這裏就不詳細說明了。但是有一點需要提一下,對變量進行索引時,必須在配置解析過程中的postConfig
uration函數調用前(強行要在這個函數裏進行其實也是可以的,但對於此函數我們一般都是用於掛載第三方模塊),因為在配置解析過
程的末尾階段還會對索引過的變量進行初始化操作,因此我們一般就是在對應的配置項命令的set函數中對需要的變量進行索引。
在講解ngx_http_mylog_handler函數的實現前,我們先看一下定義ngx_command_t時,mylog_format命令對應的set函數的實現。該函數用於解析mylog_format配置命令,並將相應的內部變量索引化(提升訪問速度),供ngx_http_mylog_handler函數使用。
static char* ngx_http_mylog_set_format(ngx_conf_t *cf,ngx_command_t *cmd,void *conf){
ngx_str_t *fmt;
ngx_str_t value;
ngx_str_t var;
ngx_uint_t i,j,k;
ngx_int_t index;
ngx_http_mylog_var_t *mylog_var;
ngx_http_mylog_conf_t *mlcf = conf;
//初始化formats數組
if(mlcf->mylog_formats == NGX_CONF_UNSET_PTR){
//數組初始化大小為2
mlcf->mylog_formats = ngx_array_create(cf->pool,2,sizeof(ngx_http_mylog_var_t));
}
fmt = cf->args->elts;
for(i = 0; i < cf->args->nelts; i++){
value = fmt[i];
j = 0;
while(j < value.len){
if(value.data[j] == ‘$‘){
k = ++j;
var.data = &value.data[k];
while(j < value.len && value.data[j] != ‘ ‘
&& value.data[j] != ‘$‘) j++;
var.len = j - k;
if(var.len == 0){
ngx_conf_log_error(NGX_LOG_EMERG,cf,0,"mylog_format can‘t has empty variable");
return NGX_CONF_ERROR;
}
//獲取變量索引
index = ngx_http_get_variable_index(cf,&var);
if(index == NGX_ERROR){
ngx_conf_log_error(NGX_LOG_EMERG,cf,0,"mylog_format get variable index error,name:%V",&var);
}
mylog_var = ngx_array_push(mlcf->mylog_formats);
mylog_var->name = var;
mylog_var->index = index;
ngx_conf_log_error(NGX_LOG_EMERG,cf,0,"mylog_format add variable:%V success",&var);
continue;
}
j++;
}
}
return NGX_CONF_OK;
}
接下來我們在ngx_http_mylog_handler函數中獲取對應變量的值,並將其打印出來,其代碼如下:
static ngx_int_t ngx_http_mylog_handler(ngx_http_request_t *r){
ngx_log_error(NGX_LOG_EMERG,r->connection->log,0,"ngx_http_mylog_handler");
ngx_http_mylog_conf_t *lccf = ngx_http_get_module_loc_conf(r,ngx_http_mylog_module);
//開啟了開關的才會執行mylog日誌模塊的邏輯,loc conf裏mylog_switch只可能為0或者1,不可能為NGX_CONF_UNSET
if(lccf->mylog_switch == 1){
//loc conf裏mylog_switch只可能為NULL或者正常初始化,不可能為NGX_CONF_UNSET_PTR
if(lccf->mylog_formats){
ngx_uint_t i;
ngx_http_mylog_var_t var;
ngx_http_variable_value_t *value;
ngx_http_mylog_var_t *vars = lccf->mylog_formats->elts;
for(i = 0; i < lccf->mylog_formats->nelts; i++){
var = vars[i];
value = ngx_http_get_indexed_variable(r,var.index);
if(!value || value->not_found){
continue;
}
ngx_log_error(NGX_LOG_EMERG,r->connection->log,0,"variable:%V , value:%s",&var.name,value->data);
}
}
}
return NGX_DECLINED;
}
模塊編譯
nginx的源碼包提供了一個名為configure的腳本,我們可以通過--add-module=Path的方式將我們的第三方模塊設置進去,configure腳本將會去尋找Path路徑下的config文件(沒錯,這個文件的文件名就叫config),根據config文件生成對應的編譯指令至Makefile文件中。因此,我們需要先創建我們的config文件,對於mylog日誌模塊,其config文件內容如下:
ngx_addon_name=ngx_http_mylog_module
HTTP_MODULES="$HTTP_MODULES ngx_http_mylog_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_mylog_module.c"
接下來解釋一下,在config文件中常用的變量及含義
ngx_addon_name
該變量一般設置為模塊名,並沒有什麽特殊的用處,不管它也沒關系,標識作用
ngx_addon_dir
該變量保存著模塊所在的路徑,跟我們使用configure腳本的--add-module=Path命令中的Path一致
HTTP_MODULES
該變量保存著所有的模塊名稱,不能直接覆蓋,在這裏需要把我們的mylog模塊添加進去。另外如果開發的是過濾模塊的話需要使用的是HTTP_FILTER_MODULE或者HTTP_FILTER_HEADERS_MODULE
NGX_ADDON_SRCS
第三方模塊的源代碼文件
NGX_ADDON_DEPS
第三方模塊依賴的文件
CORE_INCS
可在此添加依賴路徑,註意不能直接覆蓋,需要像HTTP_MODULES變量一樣append進去
CORE_LIBS
可在此添加生成nginx時的鏈接指令,比如需要鏈接第三方庫,可在此添加對應的鏈接指令,同樣不能直接覆蓋,需要append進去
如果config文件不能滿足你的需求,你也可以直接修改configure腳本生成的Makefile文件。比如對於我們的mylog模塊,在執行過configure腳本後的nginx目錄中找到obs/Makefile文件,可以看到我們的目標文件的相關編譯指令:
//寫過makefile文件的同學一眼就能理解,就不贅述了
objs/addon/ngx_http_mylog_module/ngx_http_mylog_module.o: $(ADDON_DEPS) ../ngx_http_mylog_module//ngx_http_mylog_module.c
$(CC) -c $(CFLAGS) $(ALL_INCS) -o objs/addon/ngx_http_mylog_module/ngx_http_mylog_module.o ../ngx_http_mylog_module//ngx_http_mylog_module.c
不建議不通過在configure腳本中添加第三方模塊的方法來添加第三方模塊。初學者可忽略這句話。
完整源碼
下面給出mylog日誌模塊的完整源碼,以及對應的config文件,以及將其編譯進nginx所需的配置命令。
//ngx_http_mylog_module.c源碼
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
//用於存儲解析出來的配置項值
typedef struct{
ngx_str_t name; //變量名
ngx_int_t index; //該變量的下標
}ngx_http_mylog_var_t;
typedef struct{
ngx_flag_t mylog_switch;
ngx_array_t *mylog_formats;//數組,在這裏用來存儲ngx_http_mylog_var_t
}ngx_http_mylog_conf_t;
/*
*聲明配置項解析函數
* cf: 該變量保存著當前的配置內容
* cmd: 對應的上文提到的cmd指令
* conf:通過create_main/srv/loc_conf(下文會提到) 創建出來的結構體
*/
static char* ngx_http_mylog_set_switch(ngx_conf_t *cf,ngx_command_t *cmd,void *conf);
static char* ngx_http_mylog_set_format(ngx_conf_t *cf,ngx_command_t *cmd,void *conf);
//模塊上下文函數聲明
static ngx_int_t ngx_http_mylog_post_init(ngx_conf_t *cf);
static void* ngx_http_mylog_create_loc_conf(ngx_conf_t *cf);
static char* ngx_http_mylog_merge_loc_conf(ngx_conf_t *cf,void *prev,void *conf);
//該函數用於掛載到日誌階段,執行mylog模塊的邏輯
static ngx_int_t ngx_http_mylog_handler(ngx_http_request_t *r);
//定義我們需要新增的配置項,該變量必須為數組類型,且以ngx_null_command結尾
static ngx_command_t ngx_http_mylog_commands[] = {
{
ngx_string("mylog_switch"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_http_mylog_set_switch,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_mylog_conf_t,mylog_switch),
NULL
},
{
ngx_string("mylog_format"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE,
ngx_http_mylog_set_format,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL
},
ngx_null_command
};
//定義日誌模塊上下文結構
static ngx_http_module_t ngx_http_mylog_module_ctx = {
NULL,
ngx_http_mylog_post_init,
NULL,
NULL,
NULL,
NULL,
ngx_http_mylog_create_loc_conf,
ngx_http_mylog_merge_loc_conf
};
ngx_module_t ngx_http_mylog_module = {
NGX_MODULE_V1,
&ngx_http_mylog_module_ctx,
ngx_http_mylog_commands,
NGX_HTTP_MODULE,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NGX_MODULE_V1_PADDING
};
static char* ngx_http_mylog_set_switch(ngx_conf_t *cf,ngx_command_t *cmd,void *conf){
ngx_str_t *value = cf->args->elts;
ngx_conf_log_error(NGX_LOG_EMERG,cf,0,"ngx_http_mylog_set_switch:%V",&value[1]);
return ngx_conf_set_flag_slot(cf,cmd,conf);
}
static char* ngx_http_mylog_set_format(ngx_conf_t *cf,ngx_command_t *cmd,void *conf){
ngx_str_t *fmt;
ngx_str_t value;
ngx_str_t var;
ngx_uint_t i,j,k;
ngx_int_t index;
ngx_http_mylog_var_t *mylog_var;
ngx_http_mylog_conf_t *mlcf = conf;
//初始化formats數組
if(mlcf->mylog_formats == NGX_CONF_UNSET_PTR){
//數組初始化大小為2
mlcf->mylog_formats = ngx_array_create(cf->pool,2,sizeof(ngx_http_mylog_var_t));
}
fmt = cf->args->elts;
for(i = 0; i < cf->args->nelts; i++){
value = fmt[i];
j = 0;
while(j < value.len){
if(value.data[j] == ‘$‘){
k = ++j;
var.data = &value.data[k];
while(j < value.len && value.data[j] != ‘ ‘
&& value.data[j] != ‘$‘) j++;
var.len = j - k;
if(var.len == 0){
ngx_conf_log_error(NGX_LOG_EMERG,cf,0,"mylog_format can‘t has empty variable");
return NGX_CONF_ERROR;
}
//獲取變量索引
index = ngx_http_get_variable_index(cf,&var);
if(index == NGX_ERROR){
ngx_conf_log_error(NGX_LOG_EMERG,cf,0,"mylog_format get variable index error,name:%V",&var);
}
mylog_var = ngx_array_push(mlcf->mylog_formats);
mylog_var->name = var;
mylog_var->index = index;
ngx_conf_log_error(NGX_LOG_EMERG,cf,0,"mylog_format add variable:%V success",&var);
continue;
}
j++;
}
}
return NGX_CONF_OK;
}
//配置塊結構體創建
static void* ngx_http_mylog_create_loc_conf(ngx_conf_t *cf){
ngx_conf_log_error(NGX_LOG_EMERG,cf,0,"ngx_http_mylog_create_loc_conf");
ngx_http_mylog_conf_t *conf = ngx_pcalloc(cf->pool,sizeof(ngx_http_mylog_conf_t));
if(!conf)
return NULL;
//由於要使用預定義解析函數,此處必須初始化為NGX_CONF_UNSET,否則ngx_conf_set_flag_slot會認為已經賦值過,而不處理
conf->mylog_switch = NGX_CONF_UNSET;
//初始化formats數組
conf->mylog_formats = NGX_CONF_UNSET_PTR;
return conf;
}
//配置合並函數
static char* ngx_http_mylog_merge_loc_conf(ngx_conf_t *cf, void *prev, void *conf){
//child為子塊的配置,parent為父塊的配置,這裏我們的合並規則是,優先用子塊自己的配置,如果子塊沒設置,則用父塊的配置,默認為空
ngx_http_mylog_conf_t *parent = prev;
ngx_http_mylog_conf_t *child = conf;
ngx_conf_merge_value(child->mylog_switch,parent->mylog_switch,0);
ngx_conf_merge_ptr_value(child->mylog_formats,parent->mylog_formats,NULL);
return NGX_CONF_OK;
}
//在postConfiguration函數中進行掛載
static ngx_int_t ngx_http_mylog_post_init(ngx_conf_t *cf){
ngx_http_handler_pt *h;
ngx_http_core_main_conf_t *cmcf = ngx_http_conf_get_module_main_conf(cf,ngx_http_core_module);
h = ngx_array_push(&cmcf->phases[NGX_HTTP_LOG_PHASE].handlers);
if(!h){
return NGX_ERROR;
}
//掛載mylog模塊的處理函數
*h = ngx_http_mylog_handler;
return NGX_OK;
}
static ngx_int_t ngx_http_mylog_handler(ngx_http_request_t *r){
ngx_log_error(NGX_LOG_EMERG,r->connection->log,0,"ngx_http_mylog_handler");
ngx_http_mylog_conf_t *lccf = ngx_http_get_module_loc_conf(r,ngx_http_mylog_module);
//開啟了開關的才會執行mylog日誌模塊的邏輯,loc conf裏mylog_switch只可能為0或者1,不可能為NGX_CONF_UNSET
if(lccf->mylog_switch == 1){
//loc conf裏mylog_switch只可能為NULL或者正常初始化,不可能為NGX_CONF_UNSET_PTR
if(lccf->mylog_formats){
ngx_uint_t i;
ngx_http_mylog_var_t var;
ngx_http_variable_value_t *value;
ngx_http_mylog_var_t *vars = lccf->mylog_formats->elts;
for(i = 0; i < lccf->mylog_formats->nelts; i++){
var = vars[i];
value = ngx_http_get_indexed_variable(r,var.index);
if(!value || value->not_found){
continue;
}
ngx_log_error(NGX_LOG_EMERG,r->connection->log,0,"variable:%V , value:%s",&var.name,value->data);
}
}
}
return NGX_DECLINED;
}
//config文件內容
ngx_addon_name=ngx_http_mylog_module
HTTP_MODULES="$HTTP_MODULES ngx_http_mylog_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_mylog_module.c"
//將其加入nginx的編譯指令所需的配置命令,由於我存放的mylog模塊所在目錄與nginx同級,因此使用../ngx_http_mylog_module
./configure ...(編譯nginx需要的其它命令) --add-module=../ngx_http_mylog_module
//如果需要使用,可以在nginx配置文件中加上如下兩條指令
mylog_switch on;
mylog_format ‘$status $body_bytes_sent $upstream_response_time‘;
發起請求後,查看error log文件,可通過日誌觀察mylog模塊的執行情況
結尾
在背景部分已經提過了,寫這篇文章的初衷,是因為在網上很少看到關於nginx http模塊開發入門的拿捏的比較準的文章,但自己也開始動手時,才發現要寫下來確實很不容易,很多地方可能描述的不夠清晰,主要還是受限自己的水平和文筆。另外也因為在網上看過很多介紹tcp的文章,發現很多的文章描述都不準確,而且缺乏對tcp協議的思考,有感於此,自己也寫了一篇tcp相關的文章,裏面融入了我對於tcp協議的部分設計的理解,有興趣的讀者可查看本博客中的tcp隨筆一文。
nginx http模塊開發入門