Apache2.0過濾器開發

Apache 2.0提供了許多 API 改進。本文將給出一個 Apache 2.0 過濾器模塊示例,並將用示例說明新的 API。

Apache 之所以變成最流行的Web服務器,部分是因爲可以獲得大量由第三方開發的服務器擴展,同時還因爲其開放的體系結構使得開發自己的擴展十分容易。當然,從來沒有什麼是絕對容易的,因此,在開發 Apache 2.0 過程中,一個主要的目標是改進 Apache API 以使得開發擴展更容易。

一個關鍵變化是對典型的擴展模塊這一非常通用的選擇進行了專門化,這使得開發這種專門化的子集十分容易。Apache 2.0 有專門的 API 用於開發模塊,這些模塊只需修改對用戶響應的內容,或者只需修改用戶的HTTP 請求的詳細信息。這些 API 分別被稱爲輸出過濾器和輸入過濾器。輸出過濾器最爲常見,一個好的示例是標準 Apache 2.0模塊,它被用於計算返回給用戶的內容的長度以便更新適當的頭和日誌項。另一個示例是用於對出站內容進行自動拼寫檢查的模塊(例如,Apache 1.3 中巧妙地命名爲“mod_speling”的模塊)。

安裝

Apache 2.0仍然不夠完美,其文檔就是一個方面,開發人員不能完全明白這些文檔(我相信這對於代碼和文檔的編寫者來說是件好事)。我將概述爲了設置適合於開發模塊的 Apache 2.0 安裝所採取的步驟。我從 Apache FTP 站點獲取了httpd-2.0.39.tar.gz(參閱參考資料以獲取鏈接)並將其解壓縮到一個適當的目錄中。接下來,我使用任何最近的 UNIX 用戶都非常熟悉的三條命令組合來構建代碼:

./configure --prefix=/usr/local/apache/
make
make install如果不願意使用缺省值“/usr/local/”,那麼請使用 configure 的 --prefix 選項。現在,您需要構建完整的 API 文檔,因爲它們好象無法在線得到,而它們又是 Apache 模塊開發人員所必需的。爲了構建這些文檔,您需要 ScanDoc(參閱參考資料),它象 JavaDoc 一樣從代碼中的特殊註釋生成 HTML 文檔。我下載了 ScanDoc 0.14 並把它作爲 Apache 源碼解壓縮到相同的目錄下。然後,從創建的目錄:

$ cp scandoc ../httpd-2.0.39/srclib/apr/build/scandoc.pl
$ cp -r images/ ../httpd-2.0.39/docs/我還必須對 Apache 源代碼應用補丁程序以避免影響 scandoc。此小補丁程序如清單 1 中所示。

清單 1. 允許生成文檔的補丁程序

--- include/util_time.h.old     2002-07-26 00:59:28.000000000 -0600
+++ include/util_time.h 2002-07-26 00:59:37.000000000 -0600
@@ -65,7 +65,7 @@
 #endif
 /**
- * @package Apache date/time handling functions
+ * @package Apache date-time handling functions
  */
 /* Maximum delta from the current time, in seconds, for a past time由於進行了這一工作,因此您得到了部分回報 — source 目錄下的 docs/api 子目錄中的 API 文檔。讓我們從 docs/api/index.html 開始。make install 命令好象並沒有對 API 文檔做任何事,因此,您可能需要手工創建從 api 目錄到 make install 創建的“manual”目錄之間的軟鏈接。

我說“部分回報”是因爲Apache頭文件中的文檔註釋中好象有一些明顯的缺陷。生成的文檔顯示了許多未給出名稱的函數。我不再使用生成的文檔作爲良好的起點,來查找我所需要的信息,轉而在必要時查找源代碼。我確實發現尋找 API 函數的方便技術就是使用類似下面的命令搜索 include 目錄:

grep -C7 AP.*_DECLARE /usr/local/include/* | grep -C7 [search-keyword]簡單的輸出過濾器

輸出過濾器以某種方式修改其它模塊生成的內容或頭。我將演示的簡單示例是一個過濾器,它尋找神奇的字符串“***TIME-COOKIE***”,然後用服務器上的當前本地時間顯示來替換它。當然,可以使用 Apache 服務器端包含(server-side include)或其它類似這樣的實用程序來輕易地完成這一任務,但本示例允許我們演示 Apache API。我們並不總是使用當前時間:如果神奇的字符串在內容中出現多次,那麼過濾器每次都將使用相同的時間戳記,即使第一次找到這類字符串與隨後找到字符串的時間之間存在時間間隔。這種做法演示了過濾器上下文的管理。

Apache 2.0 運行時模型設計得十分巧妙,它就象內容液從一個過濾器流動到另一個過濾器那樣運作。實際上,Apache 團隊選擇的比喻就是將其比作排成長隊傳水桶救火的隊伍。一個過濾器將“桶”裝滿內容,然後將其傳遞給鏈中的下一個過濾器。這樣,在處理一個 HTTP 事務期間可能會多次調用某個過濾器,就象不同的塊通過“桶隊列”。對於所有最普通的過濾器來說,這意味着過濾器必須能夠在兩次調用之間保存某種上下文。對於我的示例所描述的情形,過濾器需要記住替換第一個字符串時的時間戳記。清單 2 給出了該過濾器。

#include "httpd.h"
#include "util_filter.h"
#include "http_config.h"
#include "http_log.h"

/* The string which is to be replaced by the time stamp */
static char TIME_COOKIE[] = "***TIME-COOKIE***";

/* Declare the module name */
module AP_MODULE_DECLARE_DATA time_cookie;

typedef struct tc_context_ {
    apr_bucket_brigade *bb;
    apr_time_t timestamp;
} tc_context;

/*
This function passes in the system filter information (f)
and the bucket brigade representing content to be filtered (bb)
*/
static int time_cookie_filter(ap_filter_t *f, apr_bucket_brigade *bb)
{
  tc_context *ctx = f->ctx;       /* The filter context */
  apr_bucket *curr_bucket;
  apr_pool_t *pool = f->r->pool;  /* The pool for all memory requests  */

  /* The buffer where we shall place the time stamp string.
     APR_RFC822_DATE_LEN the fixed length of such strings */
  char time_str[APR_RFC822_DATE_LEN+1];
  apr_time_t timestamp;

  if (ctx == NULL) {
    /* The first time this filter has been invoked for this transaction */
    f->ctx = ctx = apr_pcalloc(f->r->pool, sizeof(*ctx));
    ctx->bb = apr_brigade_create(f->r->pool, f->c->bucket_alloc);
    timestamp = apr_time_now();
    ctx->timestamp = timestamp;
  }
  else {
    /* Get the time stamp we've already set */
    timestamp = ctx->timestamp;
  }

  /* Render the time into a string in RFC822 format */
  apr_rfc822_date(time_str, timestamp);
  /*
    Iterate over each bucket in the brigade.
    Find each "cookie" in the "kitchen" and replace with the time stamp
   */
  APR_BRIGADE_FOREACH(curr_bucket, bb) {
    const char *kitchen, *cookie;
    apr_size_t len;

    if (APR_BUCKET_IS_EOS(curr_bucket) || APR_BUCKET_IS_FLUSH(curr_bucket)) {
      APR_BUCKET_REMOVE(curr_bucket);
      APR_BRIGADE_INSERT_TAIL(ctx->bb, curr_bucket);
      ap_pass_brigade(f->next, ctx->bb);
      return APR_SUCCESS;
    }

    apr_bucket_read(curr_bucket, &kitchen, &len, APR_NONBLOCK_READ);

    while (kitchen && strcmp(kitchen, "")) {
      /* Return a poiner to the next occurrence of the cookie */
      cookie = ap_strstr(kitchen, TIME_COOKIE);
      if (cookie) {
        /* Write the text up to the cookie, then the cookie
           to the next filter in the chain
        */
        ap_fwrite(f->next, ctx->bb, kitchen, cookie-kitchen);
        ap_fputs(f->next, ctx->bb, time_str);
        kitchen = cookie + sizeof(TIME_COOKIE) - 1;
        /*
          The following is an example of writing to the error log.
          The message is actually not really appropriate for the error log,
          but it serves as example.
        */
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, f->r,
                      "Replacing cookie with /"%s/"", time_str);
      } else {
        /* No more cookies found, so just write the rest of the
           string and flag that we're done
        */
        ap_fputs(f->next, ctx->bb, kitchen);
        kitchen = "";
      }
    }
  }

  return APR_SUCCESS;
}

/* Register the filter function as a filter for modifying the HTTP body (content) */
static void time_cookie_register_hook(apr_pool_t *pool)
{
  ap_register_output_filter("TIMECOOKIE", time_cookie_filter,NULL,
                           AP_FTYPE_CONTENT_SET);
}

/* Define the module data */
module AP_MODULE_DECLARE_DATA time_cookie =
{
  STANDARD20_MODULE_STUFF,
  NULL,                        /* dir config creater */
  NULL,                        /* dir merger --- default is to override */
  NULL,                        /* server config */
  NULL,                        /* merge server config */
  NULL,                        /* command apr_table_t */
  time_cookie_register_hook    /* register hook */
};確定需要哪些 #include 也有些學問,而當 2.0 API 文檔完善時,確定起來就比較容易。模塊名稱聲明名稱非常重要。用戶必須在其配置文件中顯式地裝入您的模塊。(我們將在下面通過向我的模塊中添加時間 cookie 模塊來顯示它是如何工作的。)他們將指定已編譯的目標文件和模塊名稱,您的模塊將從該目標文件裝入,而模塊名稱必須與 AP_MODULE_DECLARE_DATA 聲明相匹配。下一個全局構造,即上下文結構,也很重要。由於爲完成其操作可能會幾次調用過濾器,因此大多數實例需要在每個調用之間維持信息。因爲處理多個同時發生的請求時可能會導致調用同一個函數,所以不應該選擇全局或靜態局部變量,因爲它們通常不出現在多線程程序中。Apache API 通過在調用之間維持模塊的上下文解決了這一問題。

在上下文中我們跟蹤:

· 桶隊列,我們將它們傳遞給隊列中的下一個過濾器。引伸一下比喻,我們正在收集一些嶄新的桶,就象我們前面的過濾器交給我們的裝滿內容的桶一樣,我們將按照需要修改內容並將內容放到新桶中,這個新桶將被交給下一個過濾器。我們在上下文中對這批新桶進行了跟蹤。

· 時間戳記,它是在第一次調用時生成的。它確保:不管處理請求花費了多長時間,都會用相同的字符串替換文檔中的所有 cookie 實例。

在過濾器函數中,上下文作爲過濾器信息結構的一部分傳入,傳入的桶隊列也是如此,我們將使用這個桶隊列作爲供處理的內容來源。在每次爲請求而調用函數時,一開始上下文實際上爲 NULL。Apache 提供了非常完善的函數,使得程序員再也不用受大多數內存分配問題的困擾了。我們選擇使用提供給我們的用於資源請求的池,而不是創建自己的池,後者可能只在可能只在一些非常高級的情況下才需要。Apache 還提供了時間函數庫。類型 apr_time_t 是單個數,它表示一個時間點。可以將它轉換成幾種時間表示法中的任何一種,包括人類可讀的字符串表示法。

第一次調用過濾器時,進行上下文結構分配,並創建輸出桶隊列。同時,還獲取當前時間戳記。在任何情況下,這個時間都被轉換成人們易讀的表示法。接下來是使用 APR_BRIGADE_FOREACH 處理內容的時候了,APR_BRIGADE_FOREACH 是 Apache 提供的用於對桶隊列進行迭代的宏。業務的第一道工序是查找用於隊列結尾的特殊記號桶。過濾器總是能夠說:“我目前已經處理了足夠的內容:我要把它向下傳,重新等待輪到我”。這是使用 ap_fflush 函數來完成的。當我們前面一個過濾器調用該函數時,我們得到了一個由 APR_BUCKET_IS_FLUSH 標記的特殊桶。一旦到達了該桶或內容的實際末尾,那麼我們就清空桶隊列,然後將它傳遞給隊列中的下一個過濾器,並返回 APR_SUCCESS,它讓 Apache 知道沒有任何問題。

最後,到了文章的最重要部分。我們從每個桶讀取內容(它變成 kitchen 變量)並尋找 cookie 實例,將所有其它文本按現狀複製到輸出隊列並在任何找到 cookie 的地方寫入時間戳記(注:通過“cookie”我實際上指的是常規編程意義上的作爲神奇值的 cookie — 而不是 Web 瀏覽器 cookie)。我自然會使用 Apache 版本的流讀寫函數對桶進行操作,而我也使用 Apache 版本的 strstr 函數,這個函數初看起來好象不必要,因爲所有相關參數都是簡單的字符指針。Apache 提供一個完整的字符串庫,出於資源管理和安全性原因,您應該總是使用 Apache 的版本。

Apache API 和許多其它編程框架一樣使用 hook 來調用定製代碼。hook 是一個在 Apache 上註冊了的函數,它在特定點被調用。有些 hook 在 Apache 啓動等情況下的配置期間被調用。本示例並沒有使用任何這類 hook,因此它們用 NULL 值表示。也有一些在請求期間調用的用於代碼的 hook,本示例使用了這類 hook。hook 註冊函數在啓動時調用,它指定將其它一些 hook 註冊到 Apache。在本示例中,過濾器在正常的過濾器鏈中註冊它自己。我將過濾器指定爲類型 AP_FTYPE_CONTENT_SET,表明它對內容進行操作。還有一些過濾器類型用於對頭和網絡參數進行操作。接下來,填充在文件頂部聲明的模塊聲明。我只需註冊用於註冊模塊的 hook。更復雜的過濾器可能需要在結構中註冊其它 hook。

嘗試

嘗試完成該模塊非常容易。如下所示構建該模塊:

gcc -fPIC -I$INCLUDE -c time_cookie.c -o time_cookie.o
gcc -shared -L$LIB -lapr -laprutil time_cookie.o -o time_cookie.so請確保 $INCLUDE 和 $LIB 包含了到 Apache 頭文件和庫文件的路徑。然後將生成的 time_cookie.so 文件複製到您的 Apache 模塊文件(比如 /usr/local/apache/modules)。接下來,添加諸如下面的幾行到您的 httpd.conf:

LoadModule time_cookie modules/time_cookie.so
AddOutputFilter TIMECOOKIE .html然後使用 apachectl start 或 apachectl restart 啓動或重新啓動服務器。接下來,將清單 3 複製到文件 time-cookie-test.html,並將該文件放到一個可以接受 Apache 服務的地方。最簡單的方法是將它複製到 Apache 安裝的 htdocs 目錄,如果複製到了該目錄,那麼您可以通過瀏覽 http://localhost/time-cookie-test.html 看到結果。由於並非人人都有超級用戶的訪問權,因此我以普通用戶的身份來進行本文中的工作 — 包括 Apache 安裝本身 — 以確保它對任何人都適用。由於 Apache 是這樣安裝的,所以它排除了在端口 80 上的偵聽,因此我在 httpd.conf 中將偵聽端口設置成 8000,因而就使用 URL http://localhost:8000/time-cookie-test.html。圖 1 顯示了結果。

清單 3. 過濾器的 HTML 演示

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>Apache 2.0 Time Cookie Filter Test (***TIME-COOKIE***)</title>
  </head>
  <body bgcolor="#FFFFFF" text="#000000" link="#0000FF"
        vlink="#000080" alink="#FF0000">
      <h3>Apache HTTP Server Version 2.0</h3>
    <h1>Apache 2.0 Time Cookie Filter Test</h1>
    <p>This page request was generated on ***TIME-COOKIE***</p>
    <p>--Uche Ogbuji</p>
  </body>
</html>圖 1. 通過時間 cookie 過濾器呈現的 HTML 頁面(圖略)

模塊

過濾器只是修改其它組件生成的數據。要編寫一個 Apache 擴展,使它根據 HTTP 請求生成最初的內容,您需要編寫模塊。衆所周知的模塊包括對簡單文件提供服務的內置模塊、用於服務器端包含的 Apache 模塊、用於公共網關接口腳本的 mod_cgi 以及用於有效直接地調用腳本語言的 mod_perl 和 mod_python。編寫模塊往往要比編寫過濾器要複雜一點。但由於過濾器滿足瞭如此多的擴展需要,所以 Apache 2.0 將它們分成簡單的 API 是很有幫助的。

模塊使用同一個 AP_MODULE_DECLARE_DATA 結構將 hook 註冊到服務器。主處理程序入口點有更簡單的說明,它只從 HTTP 消息接收一個請求記錄。以下是一個來自 mod_python 3.0 的示例,爲 Apache 2.0 對它進行了定製:

static int PythonHandler(request_rec *req) {
  /* Handler stuff */
}通常註冊模塊以處理配置文件中的特殊文件類型或其它此類標準。這涉及用“神奇字符串”來標識指定用於特殊標

<Directory /pydir>
    AddHandler python-program .py
    PythonHandler script
</Directory>上面的語句表明:那些在 /pydir 路徑中有 .py 擴展名的請求將由理解神奇字符串“python-program”(換言之,也就是 mod_python)的處理程序處理。第三行是一個特殊的配置僞指令,它由 mod_python 模塊在服務器啓動時定義和處理(而模塊是通過處理配置文件的註冊 hook 調用的)。Apache 爲每個請求調用所有處理程序,因此每個處理程序應該迅速決定請求是否是衝着它來的。因此,大多數頭文件都從類似下面的語句開始:

if (!req->handler || strcmp(req->handler, "python-program"))
        return DECLINED;上面的語句檢查與基於路徑和其它標準的請求相關的神奇處理程序字符串是否與 mod_python 理解的神奇字符串匹配。如果不匹配,那麼它就拒絕請求。

其餘大部分編寫模塊所需的基礎知識都與編寫過濾器所需的基礎知識相同。創建一個桶隊列並在生成該隊列時在裏面放入內容。在完成時,也可使用 ap_pass_brigade 函數,但您傳遞給它的接收方參數卻是 r->output_filters,假定您將請求記錄結構稱爲 r。Apache 然後接管通過過濾器運行內容的任務。

開始工作並編寫自己的東西

Apache 2.0 API 顯然受益於 Apache 開發人員所進行的長期考慮與設計。它遠沒有 1.3 API 那麼隨意,並且讓人覺得非常有連貫性。模塊編寫者常見的大多數常規任務在函數中都有提供,並且資源管理要比許多其它 C API 要簡單得多。主要問題在於文檔不夠完善,而且生成它們特別費勁。至少希望隨着時間的推移它可以變得更好。而另一方面,糟糕的 API 設計卻不會隨時間的推移而有所改進,因此 Apache 解決了最重要的問題。

我覺得編寫 2.0 模塊的過程比起編寫 1.x 的過程更輕鬆。您也應該敢於試一試。從過濾器開始肯定是做熱身的好方法,因而,我現在建議:如果可能的話,就請爲您可能擁有的任何模塊項目編寫一個原型過濾器變體。雖然我只討論了輸出過濾器,但其它類型與此非常類似。如果您真的要編寫模塊,那麼您可能需要研究一下 mod_asis 源代碼,它作爲優秀且簡單的示例隨 Apache 一起提供(modules/generators/mod_asis.c)。

由於 Apache 2.0 API 得到了改進,因此,如果您發現 Apache 不能滿足您的所有需求,那麼您也不必過分擔心。用您最喜歡的搜索引擎做一下搜索,確信沒有人已經完成了您要完成的任務之後,那麼就請自己動手開發您自己的 Apache 擴展吧。

相關文章:

http://httpd.apache.org/docs/2.0/developer/
http://www.onlamp.com/pub/ct/38

 
發佈了3 篇原創文章 · 獲贊 0 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章