Apache的請求處理

 Apache的請求處理

儘管不是全部的,但是絕大部分模塊都關注處理HTTP請求的某些方面。不過,一個模塊不能考慮處理HTTP的所有方面——這是httpd要做的工作。模塊化方法的一個好處就是:一個模塊可以只關注一個具體的任務,而不去考慮那些和它不相關的HTTP的其他方面。

  內容生成

Web服務器最簡單的形式就是一個程序,它偵聽HTTP請求,在收到一個HTTP請求之後做出一個回覆。在Apache中,這個工作主要由內容生成器負責,它是Web服務器的核心。


確切地說內容生成器必須處理每一個HTTP請求。一個處理函數可以通過在httpd.conf文件中使用SetHandler和AddHandler指令進行配置。通常都是通過定義一個被處理函數所引用的函數,這樣任何一個模塊都可以註冊內容生成器。在沒有任何模塊指定特殊生成器時,使用默認的生成器,它只是簡單地返回一個直接將請求映射到文件系統上所得到的文件。實現內容生成器的模塊有時被稱作“內容生成器”模塊或者“處理函數”模塊。

 請求處理階段

原則上,一個內容生成器可以操作Web服務器的所有函數。例如,一個CGI程序獲取請求併產生回覆,它可以控制獲取請求和產生回覆之間所有的事務。和其他的Web服務器一樣,Apache把這個請求分成幾個不同的階段。例如,在內容生成器執行某些任務之前,Apache會檢查這個用戶是否被授權去執行這些任務。

有些請求階段發生在內容生成器之前。這些階段用來檢查(也可能處理)請求報頭,然後決定如何處理該請求。例如:

請求的URL將和配置信息做比較,決定將使用哪一個內容生成器。

請求的URL通常將被映射到文件系統。映射可能是一個靜態的文件,或者一個CGI腳本,甚至是任何內容生成器所使用的方式。

如果啓用了內容協商,mod_negotiation模塊將查找最適合瀏覽器偏好的資源版本。例如,Apache的用戶手冊頁面可以通過瀏覽器的語言要求進行指定。

mod_alias模塊和mod_rewrite模塊可能會改變請求的有效URL。

最後還有一個請求日誌記錄階段,發生在內容生成器之後,用來給瀏覽器發送一個回覆。


 非標準請求處理

有時由於一些特殊原因,請求處理可能不會遵守上面描述的標準處理流程。

在應答被髮送之前的任何一個點上,模塊可能轉向處理一個新的請求,或者處理錯誤文檔(見第6章)。

模塊可能定義了附加的階段,並且允許其他模塊增加鉤子以便進行它們自己的處理流程(見第10章)。

quick_handler鉤子有時會旁路通常的處理流程,它被mod_cache模塊調用(本書沒有就此討論)。

 處理鉤子

在Apache中,一個模塊是通過一系列的鉤子函數影響或者掌管處理過程的某些方面。在Apache 2.0中用來處理請求的常用鉤子如下所示。

post_read_request:在正常請求處理流程中,這是模塊可以使用的第一個鉤子。對於那些想很早進入處理請求的模塊來說,這個鉤子比較有用。

translate_name:Apache用來將請求URL映射到文件系統的鉤子。模塊可以在這裏插入一個鉤子來執行自己的邏輯——例如mod_alias。

map_to_storage:當URL被映射到V文件系統後,我們需要應用每個目錄配置(<Directory>和<Files>部分和它們的變量,如果允許的話包括任何相關的.htaccess文件)。這個鉤子使得Apache能夠決定應用到這個請求的配置選項。

它將一般的配置指令應用到所有的活動模塊,因此一般較少的模塊需要採用這個鉤子。mod_proxy是唯一這樣做的模塊。

header_parser:這個鉤子檢查請求的頭部。由於模塊可以在請求處理流程的任何一個點上執行檢查請求頭部的任務,而且通常是在另外一個鉤子的上下文中執行這個任務,因此這個鉤子很少被使用。mod_setenvif根據請求的頭部,使用這個鉤子設置內部的環境變量。

access_checker:Apache檢查是否根據服務器的配置(httpd.conf)允許訪問請求的資源。Apache的標準邏輯實現了允許/拒絕指令(在httpd1.x和2.0中是mod_access,在httpd2.2中是mod_authz_host),模塊可以增加或者替換這個標準邏輯。

check_user_id:如果使用了任何一個認證方法,Apache將採用相關的認證方法,並將用戶名區域設定爲r->user,模塊可以通過這個鉤子實現一個認證方法。

auth_checker:這個鉤子檢查是否允許認證的用戶執行請求的操作。

type_checker:這個鉤子應用請求資源的MIME類型(可用的)的相關規則,判定將要使用的內容處理函數(如果還沒有設定的話)。標準模塊mod_negotiation(基於HTTP內容協商的資源被選中部分)和mod_mime實現了這個鉤子(根據標準配置指令和配置慣例,例如文件的擴展名,設定MIME類型和處理函數信息)。

fixups:這是一個通用的鉤子,允許模塊在內容生成器之前,上述描述的鉤子運行之後,運行任何必要的處理流程。和post_read_request類似,這是一個能夠捕獲任何信息的鉤子,也是最常使用的鉤子。

handler:這是一個內容生成器鉤子。它負責給客戶端發送一個恰當的回覆。如果有輸入數據,handler也要負責讀取這些數據。其他的鉤子可能需要多個函數參與處理一個請求,handler鉤子則不同,每一個請求都是由一個handler處理。

log_transaction:這個鉤子在回覆已經發送給客戶端之後記錄事務,模塊可能修改或者替換Apache的標準日誌記錄。

模塊可能會在任何一個處理階段中爲自己的處理函數掛鉤子。模塊提供了一個回調函數並且爲其掛鉤子。Apache將在恰當的處理階段調用這個函數。那些在內容生成階段之間關注自身的模塊有時被稱爲元數據模塊。第6章將詳細介紹這些模塊。進行日誌記錄的模塊被稱爲日誌模塊。除了使用這些標準的鉤子,模塊也可能定義更加深入的處理鉤子,我們將在第10章對這些內容進行介紹。

 數據軸和過濾器

我們目前所討論的和每一個通用的Web服務器架構都非常相似。雖然在細節上有些不同,但是請求處理(元數據→生成器→記錄器)機制都是一樣的。

Apache 2的主要創新就是過濾鏈,它將Apache 2從一個“基本的”Web服務器(像Apache 1.3和其他的服務器)變成了功能強大的應用平臺。過濾鏈可以表示成爲一個數據軸,和請求處理軸垂直(見圖2-4)。請求數據可以在到達內容生成器之前被輸入過濾器進行處理,回覆數據可以在發送到客戶端之前被輸出過濾器進行處理。過濾器使得數據處理的實現比以往的版本更加簡潔有效,同時將內容生成器從它的變換(transformation)和集合(aggregation)中分離出來。


  處理函數(handler)還是過濾器(filter)

很多應用程序都可以作爲處理函數或者過濾器進行應用。有時候可能很明確作爲它們其中的一種方式比較合適,而另外一種則不太適合,但是在這兩種極端情況之間存在着一個灰色地帶。如何決定是一個處理函數呢,還是一個過濾器呢?

當作這個決定時,以下的幾個問題需要考慮。

可行性:這兩種情況都適合麼?如果不是都適合,那麼,馬上就可以決定使用一種方式。

有效性:這兩種情況中的一種比另外一種提供了更加有效的功能嗎?過濾器通常比處理函數更加有用,因爲它可以被不同的內容生成器重用,也可以連接生成器和其他的過濾器。不過每一個請求需要經某些處理函數進行處理,即使它們沒有做任何工作。

複雜性:其中的一種方式比另外一種方式更加複雜嗎?它需要消耗更多的時間和能力去開發嗎?它運行得更慢嗎?過濾器模塊通常比同樣的處理函數更加複雜,因爲處理函數通常可以完全控制它的數據,隨意進行讀寫操作,而過濾器需要實現一個回調函數,該回調函數伴隨部分數據被調用幾次,並且這些數據必須被當作非結構化的塊進行處理。我們將在第8章仔細討論這個問題。

例如,Apache 1.3的用戶可以將一個XSLT變換構建在處理函數中(例如CGI或者PHP),然後就可以進行XSLT變換操作。另外一個選擇就是,他們可以使用一個XSLT模塊,不過這種方式運行非常慢,而且比較麻煩(筆者曾試着爲Apache 1.3編寫了一個XSLT模塊,不過後來發現對臨時文件進行操作時,這個模塊比在CGI腳本中運行XSLT要慢上幾百倍)。在處理函數中運行XSLT是可以的,不過這就破壞了代碼的模塊性和可重用性。任何需要XSLT處理的應用都需要重新發明這個輪子,使用任何一個提供給編程或者腳本語言的庫,經常不得不採用笨拙的方法,例如使用臨時文件。

和上述方法相比,Apache 2允許我們在一個過濾器中運行XSLT。內容處理函數請求XSLT可以簡單的輸出這個XML,並且把轉換工作留給Apache。由Phillip Dunkel編寫併發布的第一個針對Apache 2的XSLT模塊目前正在處於Beta測試中,工作纔剛剛開始,還是不完善的,不過已經比Apache 1.3中的XSLT好太多了。目前它還在進一步的完善,是XSLT模塊的選擇之一。本書作者還開發了另外一個XSLT模塊。

通常,如果一個模塊既有數據輸入,又有數據輸出,那麼它可以被多個應用所使用,並且很大程度上會被實現爲一個過濾器。

 內容生成器示例

默認的處理函數從本地磁盤的DocumentRoot目錄中發送一個文件。儘管處理器也可以這麼做,不過這樣並沒有任何好處。

作爲服務器端編程通用API的CGI是一個處理函數。由於CGI腳本想成爲Web服務器架構的中心,它必須是一個處理函數。不過,mod_ext_filter也爲外部過濾器提供了一個有些類似的框架。

Apache代理是一個從後端(back-end)服務器上取得內容的處理函數。

表單處理(form-processing)應用程序通常被實現成爲處理函數——特別是那些接收POST數據的表單處理應用程序,或者可以轉換服務器自身狀態的其他操作。同樣的,從任何後端生成報告的應用程序也通常應用爲處理函數。不過,如果處理函數是基於嵌入編程元素的HTML或者XML頁面時,它通常應用爲一個過濾器。

  過濾器示例

mod_include在實現了服務器端包含(include),在頁面中嵌入簡單的腳本語言。它應用爲一個過濾器,因此它可以從任何內容生成器那裏後處理(post-process)內容。

mod_ssl作爲一個連接層過濾器實現了安全傳輸,由於它的存在,服務器只需要處理未加密的明文數據。這是對Apache 1.x版本的一個很大的改進,之前版本的安全傳輸很複雜,其他的應用如果要使用安全傳輸還需要大量的融合工作。

標記解析模塊用來進行後處理(post-process),以及用更合適的方法轉換XML和HTML,包括從簡單地通過XSLT、Xinclude處理鏈接重寫,到一個針對標記過濾的完整API,或者到一個複雜的安全過濾器(像PHP腳本,能夠阻止攻擊帶應用程序漏洞的嘗試)。第8章將會介紹一些例子。

圖像處理可以發生在過濾器內部。筆者爲一個開發者的移動手機瀏覽器開發了一個定製代理。由於瀏覽器會把自身的能力通知給代理,圖像就可以被剪裁以適應屏幕的顯示空間,在適當情況下還會轉換灰度,這樣就會減少數據的發送量,在緩慢的連接速度下加速圖像的瀏覽。

表單處理模塊需要將Web瀏覽器發送過來的數據進行數據提取。輸入過濾模塊,例如mod_form和mod_upload已經實現了這種功能,別的應用不需要再次開發。

mod_deflate模塊實現了數據壓縮和解壓縮功能。過濾器架構使得這個模塊比mod_gzip模塊(Apache 1.3中的一個數據壓縮模塊)更加簡單,對於需要使用數據壓縮的臨時文件也更加方便。

  處理的順序

在轉向討論一個模塊如何對自己掛鉤子進入請求/數據處理的各個階段之前,我們需要暫停一下,澄清一個經常讓人迷惑的問題——也就是處理的順序。

請求處理軸線是一個前向的軸線,每一個階段都是嚴格按照順序產生的,而數據處理軸線就存在着一些疑惑。考慮到最大程度的有效性,這個軸線是管道狀的,因此內容生成器和過濾器的運行並沒有確定的順序。舉例來說,在輸入過濾器進行某些設定之後,你通常不應該期待它將被內容生成器或者輸出過濾器所採用。

處理順序的中心在內容生成器,它負責將數據從輸入過濾器的堆棧中提取出來,並放入輸出過濾器中(如果可行的話,兩種情形都會發生)。當一個生成器或者過濾器需要進行某些總體上影響請求的設定時,它必須在將這些數據向鏈中的後端(生成器和輸出過濾器)發送之前進行設定,或者在將數據返回給調用者(輸入過濾器)之前進行設定。

  處理鉤子

既然我們已經對Apache的請求處理流程進行了概述,下面就可以演示一個模塊鉤子如何掛在模塊中,併成爲模塊執行的一部分了。

Apache模塊結構聲明瞭幾個(可選的)數據和函數成員:

module AP_MODULE_DECLARE_DATA my_module = {

    STANDARD20_MODULE_STUFF,    /* 保證模塊版本一致性的宏 */

    my_dir_conf,                  /* 創建一個每目錄配置記錄 */

    my_dir_merge,                /* 合併目錄配置記錄*/

    my_server_conf,              /* 創建服務器配置記錄*/

    my_server_merge,             /* 融合服務器配置記錄*/

    my_cmds,                      /* 配置指令 */

    my_hooks                      /* 在內核中註冊的模塊函數 */

};

配置指令使用一個數組進行表示,剩下的模塊入口都是函數。模塊用來創建請求處理鉤子的相關函數是一個final成員。

static void my_hooks(apr_pool_t *pool) {

    /*創建請求處理的鉤子*/

}

我們要根據模塊對請求處理部分中的哪個具體部分感興趣來創建我們需要的鉤子。例如,一個實現了內容生成器(處理函數)的模塊可能需要一個處理函數的鉤子,如下所示。

    ap_hook_handler(my_handler, NULL, NULL, APR_HOOK_MIDDLE) ;

這樣my_handler在一個請求到達內容生成器階段時就可以被調用了。對於其他的請求階段的鉤子都是類似的。

下面的例子演示瞭如何在任一個階段中使用一個處理函數。

static int my_handler(request_rec *r) {

    /* 對這個請求進行處理 */

}


最後當然是實現處理函數helloworld_handler啦。它是一個回調函數,他會在Apache處理HTTP請求的適當時機調用。處理函數可以選擇處理或是忽略這個請求。

static int helloworld_handler(request_rec *r)
{
    if (!r->handler || strcmp(r->handler, "helloworld")) {
        return DECLINED;
    }

    if (r->method_number != M_GET) {
        return HTTP_METHOD_NOT_ALLOWED;
    }
    ap_set_content_type(r, "text/html;charset=ascii");
    ap_rputs("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\">\n", r);
    ap_rputs("<html><head><title>Hello Apache Module</title></head>", r);
    ap_rputs("<body><h1>Hello Apache Module</h1>", r);
    ap_rputs("</body></html>", r);
    return OK;
}


我們開始做一些檢查,來決定模塊是處理這個請求還是忽略它。返回DECLINED表示忽略,OK表示成功處理,也可返回HTTP狀態代碼(HTTP status code)表示錯誤處理。

全部放在一起

/* The simplest HelloWorld module */
#include <httpd.h>
#include <http_protocol.h>
#include <http_config.h>

static int helloworld_handler(request_rec *r)
{
    if (!r->handler || strcmp(r->handler, "helloworld")) {
        return DECLINED;
    }

    if (r->method_number != M_GET) {
        return HTTP_METHOD_NOT_ALLOWED;
    }
    ap_set_content_type(r, "text/html;charset=ascii");
    ap_rputs("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\">\n", r);
    ap_rputs("<html><head><title>Hello Apache Module</title></head>", r);
    ap_rputs("<body><h1>Hello Apache Module</h1>", r);
    ap_rputs("</body></html>", r);
    return OK;
}

static void helloworld_hooks(apr_pool_t *pool)
{
    ap_hook_handler(helloworld_handler, NULL, NULL, APR_HOOK_MIDDLE);
}

module AP_MODULE_DECLARE_DATA helloworld_module = {
    STANDARD20_MODULE_STUFF,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    helloworld_hooks
};


注意:helloworld_hooks和helloworld_handler都是static的。一般來說,只有模塊結構被導出,其他的都是模塊自己私有的。這是一個好的實踐。

編譯安裝

apxs -c mod_helloworld.c (編譯)
apxs -i mod_helloworld.la (安裝)


這樣我們的modules目錄中就多了一個mod_helloworld.so的文件

運行
要運行我們還要修改配置文件httpd.conf
在最後添加

LoadModule helloworld_module modules/mod_helloworld.so
<Location /helloworld>
    SetHandler helloworld
</Location>


第一行是讓Apache動態的載入這個模塊。
第二行設置瀏覽器的請求url的路徑。
SetHandler helloworld其實是設置r->handler的值。

別忘了重啓Apache哦!

最後在瀏覽器中敲入http://localhost/hellworld,就可以看到令人興奮的效果了。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章