5.5 掛鉤(HOOK)
5.5.1 為什么引入掛鉤
在Apache1.3版本中,對HTTP請求的處理包括若干個固定階段,比如地址轉換階段、身份確認階段、身份認證階段、權限確認階段、MIME類型識別階段等等,這也意味著Apache1.3中的掛鉤數目是有限的,固定的。這個反映在模塊結構中就是針對每個HOOK都對應一個函數指針。比如假如需要檢查用戶的身份是否合法則只需要調用ap_check_user_i;假如需要核查用戶的權限是否滿足則只需要調用auth_checker;而假如需要記錄日志,則只需要調用logger函數即可。這種對請求的處理策略非常清楚明了。不過這種方法也有明顯的局限性,首先就是所有的模塊都維護所有的掛鉤,但對于某個模塊而言只有很少的幾個掛鉤會被使用,比如ap_check_user_id掛鉤可能只有安全模塊才用到,模塊中不使用的掛鉤都被置為NULL;最重要的問題是很難增加新的掛鉤。假如需要增加一個新的掛鉤,就必須修改module結構,這涉及到所有的模塊的修改,工作量很大,而且需要重新進行編譯,因此這種策略使得模塊的擴展性能極差。
為了使得Apache 2.0版本為了能夠更具模塊化,更具擴展性,apache采取了更加靈活的處理策略,即“模塊自行治理”策略,即掛鉤(Hooks)的概念,Hooks的使用使得函數從靜態的變為動態的,模塊編寫者可以通過Hooks自行增加處理句柄,而不需要所有的模塊都千篇一律。因此每次增加新函數,唯一必須修改的就是增加函數的模塊而已。
為了對掛鉤有大體的了解,我們首先來看一下Apache2.0的HTTP請求處理流程。
從大的方面來看,Apache對HTTP的請求可以分為連接、處理和斷開連接三個階段。從小的方面而言,每個階段又可以分為更多的子階段。比如對HTTP的請求,我們可以進一步劃分為客戶身份驗證、客戶權限認證、請求校驗、URL重定向等階段,每一個階段調用相應的函數進行處理。在Apache中,這些子階段可以用術語“掛鉤(HOOK)”來描述。Apache中對請求的處理過程實質上就是依次調用一系列掛鉤的過程,不過由于HTTP請求類型的不同,它們所對應的掛鉤數目和類型也不盡相同。對于典型的HTTP請求,有一個默認的掛鉤調用順序,你可以按照這個默認的順序進行調用,也可以不遵守這個順序,你可以根據自己的情況調整調用順序。整個HTTP的請求可以用下圖來描述:

在上面的圖示中,存在六種不同的掛鉤,對于請求a,其指需要調用Hook2.、1;而對于請求b,則需要調用1、6、5;請求c則需要調用1、4、3、6四個掛鉤。
在Apache中,掛鉤總是和掛鉤函數聯系在一起的。掛鉤是用來表示在處理HTTP請求中一組類似的操作,與之對應,掛鉤函數就是操作函數。不過即使掛鉤相同,對應的掛鉤函數也未必相同。舉個簡單的例子,配置文件模塊和虛擬主機模塊都需要身份驗證掛鉤來確認訪問者能否訪問相應的資源,但兩個模塊的驗證方法則差別很大。因此一個掛鉤同時是和一組掛鉤函數聯系在一起的。因此請求處理過程中,調用掛鉤的時候實際上就是調用掛鉤函數組。調用過程可以用下圖示意。Apache對指定掛鉤的掛鉤函數調用有兩種,一種就是將掛鉤函數組中的每個函數都調用一次,比如圖中的掛鉤4;另一種就是從前往后調用,一旦找到合適的就停止繼續調用,圖中1、2、3就是這種情況。
通過掛鉤機制,你可以自行修改服務器的行為,比如修改掛鉤函數或者增加掛鉤函數,甚至增加掛鉤。不過增加掛鉤只是2.0才提供的功能。

5.5.2 聲明掛鉤
Apache中關于掛鉤的實現大部分是通過宏來實現的,而且這些宏大多數非常復雜。掛鉤的實現主要定義在文件a
PR_hook.h和apr_hook.c中,另外在config.c中也有部分定義。
Apache中對掛鉤的使用總是從定義一個掛鉤開始的,在Apache中聲明一個掛鉤,總是通過宏
#define AP_DECLARE_HOOK(ret,name,args) /
APR_DECLARE_EXTERNAL_HOOK(ap,AP,ret,name,args)
來實現的。在該宏中,ret是定義的掛鉤的返回類型;而name則是定義的掛鉤的名稱;args則是掛鉤函數需要的額外參數,通常是以bracket形式出現的。例如下面的語句就聲明了一個返回類型為整數,函數名稱為do_something,函數參數為(request_rec *r,int n)的掛鉤:
AP_DECLARE_HOOK(int , do_something , (request_rec *r , int n))
不過AP_DECLARE_HOOK內部則是調用的APR_DECLARE_EXTERNAL_HOOK宏。該宏定義如下:
#define APR_DECLARE_EXTERNAL_HOOK(ns,link,ret,name,args) /
typedef ret ns##_HOOK_##name##_t args; /
link##_DECLARE(void) ns##_hook_##name(ns##_HOOK_##name##_t *pf, /
const char * const *aszPre, /
const char * const *aszSUCc, int nOrder); /
link##_DECLARE(ret) ns##_run_##name args; /
APR_IMPLEMENT_HOOK_GET_PROTO(ns,link,name); /
typedef struct ns##_LINK_##name##_t /
{ /
ns##_HOOK_##name##_t *pFunc; /
const char *szName; /
const char * const *aszPredecessors; /
const char * const *aszSuccessors; /
int nOrder; /
} ns##_LINK_##name##_t;
從上面的定義可以看出,該宏定義冗長,而且事實上,Apache中的關于掛鉤定義的每個宏都是很長的。分析頗為費力。在該宏中出現最多的符號當之不讓的就是“##”。##宏主要用來實現連接前后的字符串。
APR_DECLARE_EXTERNAL_HOOK宏展來后實現了五個子功能的定義,這幾個功能我們分別介紹,其所用的示例則是配置模塊的post_config掛鉤,在Http_config.中Apache定義了post_config掛鉤如下:
AP_DECLARE_HOOK(int,post_config,(apr_pool_t *pconf,apr_pool_t *plog, apr_pool_t *ptemp,server_rec *s)),該宏進一步替換展開后為
APR_DECLARE_EXTERNAL_HOOK(ap,AP,int,post_config, (apr_pool_t *pconf,apr_pool_t *plog, apr_pool_t *ptemp,server_rec *s)),在該宏中:
(1)、typedef ret ns##_HOOK_##name##_t args;
該宏主要用來定義對應的post_config的掛鉤的執行函數原型,比如post_config掛鉤執行函數原型定義則是:
typedef int ap_HOOK_post_config_t (apr_pool_t *pconf,apr_pool_t *plog,apr_pool_t *ptemp,server_rec *s);
(2)、link##_DECLARE(void) ns##_hook_##name(ns##_HOOK_##name##_t *pf, /
const char * const *aszPre, /
const char * const *aszSucc, int nOrder); /
該宏則用來定義指定掛鉤的函數原型,其中ns##_HOOK_##name##_t *pf則是前面定義了對應于該掛鉤的執行函數指針,這個函數最終會加入到在請求處理期間的指定時刻進行調用的列表中,而aszPre和aszSucc在形式和功能上都很相近,都是以NULL為結束符的字符串數組,比如{“mod_mime.c”,NULL}。aszPre數組會為這個掛鉤規定必須在這個函數之前調用的函數模塊,而aszSucc則是規定必須在這個函數之后進行調用的函數模塊。上面的post_config掛鉤其定義展開后如下:
AP_DECLARE(void) ap_hook_post_config(ap_HOOK_post_config* pf;
const char * const *aszPre,
const char * const *aszSucc,
int nOrder);
函數的第四個參數nOrder則是該掛鉤的綜合排序參數,整數類型。該整數代表了這個函數與所有的其余同一階段的其他函數相比較時候的綜合位置。假如這個數值越低,則這個掛鉤函數將在列表中排列越靠前,因此也越早被調用。假如兩個模塊為它們的模塊設定相同的nOrder,而且模塊之間沒有依靠關系,則它們的調用順序則不確定。針對這個參數,Apache提供了5個通用的宏:APR_HOOK_REALLY_FIRST、APR_HOOK_FIRST、APR_HOOK_MIDDLE、APR_HOOK_LAST、APR_HOOK_REALLY_LAST,其定義如下:
#define APR_HOOK_REALLY_FIRST (-10)
#define APR_HOOK_FIRST 0
#define APR_HOOK_MIDDLE 10
#define APR_HOOK_LAST 20
#define APR_HOOK_REALLY_LAST 30
從上面可以看出,nOrder的值得范圍介于-10到30之間。假如函數是在列表中是必須第一個得到的,則必須將其設置為APR_HOOK_FIRST或者APR_HOOK_REALLY_FIRST;假如是必須最后一個調用的,則將其設置為APR_HOOK_LAST或者APR_HOOK_REALLY_LAST。假如其調用次序無關緊要,則可以設置為APR_HOOK_MIDDLE。
所有掛鉤最終通過排序函數進行,這個我們在后面的部分介紹。
(3)、link##_DECLARE(ret) ns##_run_##name args; /
掛鉤的定義最終是為了被調用,因此Apache中定義了對掛鉤的調用函數。通常掛鉤調用函數形式如下:ap_run_hookname();上面的宏真是用來定義掛鉤調用函數。
比如對于post_config掛鉤而言,其對外的調用函數則是ap_run_post_config()。
(4)、APR_IMPLEMENT_HOOK_GET_PROTO(ns,link,name); /
該宏展開后如下所示
#define APR_IMPLEMENT_HOOK_GET_PROTO(ns,link,name) /
link##_DECLARE(apr_array_header_t *) ns##_hook_get_##name(void)
該宏了用于返回掛鉤訪問函數原型,在模塊外部可以調用改函數獲得注冊為該掛鉤的所有函數。參數ns和link分別為掛鉤函數名字空間前綴和聯接申明前綴,一般情況為ap和AP,name為掛鉤名。訪問函數的原型一般為AP_DECLARE(apr_array_header_t *) ap_hook_get_name(void)。假如對于post_config掛鉤而言,該宏對應的則是AP_DECLARE(apr_array_header_t *) ap_hook_get_post_config(void)。通過該函數,Apache可以獲取掛鉤定義的原型。
(5)、typedef struct ns##_LINK_##name##_t /
{ /
ns##_HOOK_##name##_t *pFunc; /
const char *szName; /
const char * const *aszPredecessors; /
const char * const *aszSuccessors; /
int nOrder; /
} ns##_LINK_##name##_t;
該宏定義了一個結構類型,用來保存掛鉤的相關定義信息,比如掛鉤的調用函數指針、掛鉤名稱等等,這些信息與在掛鉤定義宏中的信息完全相同。比如對于post_config來說,其展開后的結果如下所示,結構中每個字段的含義前面已經說明,此處不再贅述。
typedef struct ap_LINK_post_config_t
{
ap_HOOK_post_config_t *pFunc;
const char *szName;
const char * const *aszPredecessors;
const char * const *aszSuccessors;
int nOrder;
} ap_LINK_post_config_t;
為此,我們可以看到,盡管對外我們聲明的僅僅是AP_DECLARE_HOOK宏,而內部卻一口氣實現了五個功能,每個功能彼此相關。
5.5.3 掛鉤鏈聲明(APR_HOOK_LINK)
在Apache中,系統定義了一定數量的掛鉤,這些掛鉤總的來說可以分為兩大類:啟動掛鉤和請求掛鉤。啟動掛鉤是隨著服務器啟動進行調用的掛鉤;而請求掛鉤則是服務器處理請求時候進行調用的掛鉤。這兩種掛鉤的實質唯一區別就是它們在服務器中的平均調用頻率以及它們在被調用時候服務器充當的角色。所有啟動掛鉤作為啟動服務器的用戶進行調用,通常是指UNIX超級用戶,而所有請求掛鉤都是作為配置文件中指定的用戶進行調用。
Apache中預定義了大量的掛鉤,啟動掛鉤有pre_config、post_config、open_logs以及child_init;請求掛鉤包括pre_connection、process_connection、create_request等等。Apache中聲明的掛鉤通常都是全局的,存在且僅存在一個。不過對應于掛鉤的調用函數則通常不止一個。比如對于掛鉤post_config,在核心模塊core.c中調用的掛鉤函數是core_post_config,在status模塊mod_status.c中調用的掛鉤函數是status_init,而在include模塊mod_include.c中對應的掛鉤函數則是include_post_config。對于同一個掛鉤,不同模塊對應于其的處理函數各不相同,為了能夠保存各個模塊中對同一掛鉤的使用信息,Apache使用apr_array_header_t數組進行保存。
為此該宏的定義很簡單,實際上無非就是聲明了一個apr_array_header_t類型的數組,用來保存對應于某個掛鉤的所有掛鉤函數。比如APR_HOOK_LINK(post_config)展開后定義如下:
apr_array_header_t *link_post_config;
各個模塊中對掛鉤post_config進行處理的信息都保存在數組link_post_config中,其中link_post_config數組中每個元素的類型都是ap_LINK_post_config_t結構;當然不同的數組,其類型也不相同,不過形式上都是ap_LINK_XXXX格式的。關于掛鉤的大部分信息都由這個結構提供。
5.5.4 掛鉤結構(APR_HOOK_STRUCT)
正如前面所說,對于每一個掛鉤,Apache都會定義一個apr_array_header_t數組來保存它的相關信息。通常,該數組定義在實現掛鉤的文件中,比如在config.c文件Apache實現了header_parser、pre_config、post_config、open_logs、child_init以及handler、quick_handler、optional_fn_retrieve八個掛鉤,那么該文件中就應該定義八個數組來保存它們的信息。一旦定義掛鉤數組,那么該數組將在整個Apache中保持唯一。當某個模塊想要使用該掛鉤的時候,其就向模塊內對于該掛鉤的處理結構壓入數組。為了便于各模塊對數組的訪問,原則上必須將數組聲明為全局變量,這是最簡單的實現方式。
不過Apache2.0中并不支持直接訪問掛鉤數組,因此你想直接將數據壓入處理結構那是“妄想”。為此Apache2.0中引入了APR_HOOK_STRUCT宏。該宏定義如下:
#define APR_HOOK_STRUCT(members) /
static struct { members } _hooks;
該宏展開后實際上定義了一個限于模塊內使用的結構_hook,該模塊內實現的所有掛鉤的對應數組都保存為_hook的成員。比如config.c中的APR_HOOK_STRUCT定義如下:
APR_HOOK_STRUCT(
APR_HOOK_LINK(header_parser)
APR_HOOK_LINK(pre_config)
APR_HOOK_LINK(post_config)
APR_HOOK_LINK(open_logs)
APR_HOOK_LINK(child_init)
APR_HOOK_LINK(handler)
APR_HOOK_LINK(quick_handler)
APR_HOOK_LINK(optional_fn_retrieve)
)
其展開后變為如下:
static struct{
apr_array_header_t *link_header_parser;
apr_array_header_t *link_pre_config;
apr_array_header_t *link_post_config;
apr_array_header_t *link_open_logs;
apr_array_header_t *link_child_init;
apr_array_header_t *link_handler;
apr_array_header_t *link_quick_handler;
apr_array_header_t *link_optional_fn_retrieve;
}_hook;
因此任何對掛鉤數組的訪問都必須通過_hook來實現。比如我們在某個模塊中使用掛鉤post_config,那么在數組中增加數據可以用下面的代碼:
ap_LINK_post_config_t *pHook;
if (!_hooks.link_post_config) {
_hooks.link_post_config = apr_array_make(apr_hook_global_pool, 1,
sizeof(ap_LINK_post_config_t));
apr_hook_sort_register("post_config", &_hooks.link_post_config);
}
pHook = apr_array_push(_hooks.link_post_config);
pHook->aszPredecessors = …;
pHook->aszSuccessors = …;
pHook->nOrder = …;
pHook->szName = …;
不過有幾點必須注重的是:
(1)、_hook在某個文件中假如定義,則只能定義一次。該文件中使用的定義的掛鉤數組統統添加到該結構中,正所謂“大肚能容,容天下難容之事”。
(2)、_hook的定義為static,這意味這該結構實際上是模塊內部的私有結構,外部模塊無法直接訪問_hook結構,而且_hook結構不僅僅在一個文件中出現,只要實現了掛鉤,按道理就應該有一個_hook結構。盡管如此,由于static屬性,它們相互之間“絕緣”,不會相互干擾。
(3)、當某個模塊想使用某個掛鉤,比如post_config的時候,其一不能直接訪問post_config掛鉤數組,二不能訪問被屏蔽的模塊內_hook,那么它應該怎么辦呢?它只能使用掛鉤注冊函數。正如前面所述,掛鉤注冊函數通常形如ap_hook_name。比如模塊需要使用post_config掛鉤,其必須調用ap_hook_post_config進行注冊?,F在假如我們將在編寫模塊的時候碰到類似下面的代碼,你就不會納悶了:
static void register_hooks(apr_pool_t *p)
{
ap_hook_handler(status_handler, NULL, NULL, APR_HOOK_MIDDLE);
ap_hook_post_config(status_init, NULL, NULL, APR_HOOK_MIDDLE);
}
register_hooks是模塊mod_status.c中的掛鉤注冊函數,在該模塊中,Apache注冊了兩個掛鉤:handler和status_init,分別對應的掛鉤函數為status_handler和status_init。
不過即使你查遍Apache的是所有的文件,你也不可能找到ap_hook_handler和ap_hook_post_config的函數聲明和實現。不僅如此,所有的ap_hook_HOOKNAME形式的函數你都不可能找到你平常希望的函數聲明和實現。那么Apache到底是在哪兒實現了這些掛鉤函數呢??一切答案都在宏APR_IMPLEMENT_EXTERNAL_HOOK_BASE里面。
關于作者
張中慶,目前主要的研究方向是嵌入式瀏覽器,移動中間件以及大規模服務器設計。目前正在進行Apache的源代碼分析,計劃出版《Apache源代碼全景分析》上下冊。Apache系列文章為本書的草案部分,對Apache感愛好的朋友可以通過flydish1234 at sina.com.cn與之聯系!
假如你覺得本文不錯,請點擊文后的“推薦本文”鏈接??!