Apache2.0过滤器开发

来源:互联网 发布:高级java工程师充电班 编辑:程序博客网 时间:2024/04/30 02:47

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