Apache 2.0手册中文版翻译项目 [本文译者: suncjs * ]

项目说明 | 项目进度 | 项目讨论区 | Apache手册中文版

 


Apache 1.3 API 备忘录 - Apache HTTP服务器
<-
Apache主站 > HTTP服务器 > 文档 > 2.0版本 > Developer Documentation

Apache 1.3 API 备忘录

警告: 该文档没有完全包含在Apache HTTP服务器2.0中已经更新的内容,有些内容可能仍然有效,使用中请注意。

对于你必须处理的Apache API和数据结构内容有一些需要注意的地方。 它们迄今并不十分完整,但是很有希望帮助你解决压力。 牢记这一点:这些API将会因为我们从其中得到的经验增长而容易改变。 (参看TODO文件了解可能会出现的变化)。但是,对任何可能的变化都将能够容易地改造模块。 (我们比你有更多的模块需要改造)。

这儿有一些关于普遍教学风格的备忘录。符合简明的目的, 这里所有的数据声明都是不完整的---真正的声明有更多的内容, 但我将不会告诉你关于那些的细节。通常,它们为服务器核心的组件保留, 并且模块改动它应该非常谨慎。但是,在有些情况下, 它们真的是我还没有时间处理的东西。欢迎来到非常边缘。

最后,这里有个轮廓让你完全了解发生了什么以及以什么顺序发生的:

top

基本概念

我们从API背后基本概念的概貌及其是怎样出现在代码中的问题来开始吧。

处理器、模块和请求

Apache把请求处理分解成一系列步骤,或多或少与Netscape服务器的API是同样的方式。 (虽然这个API比NetSite要更正规一点,例如钩子(hook)我想将来会有用)。它们是:

这些阶段由搜索一连串的模块中的每一个来处理, 寻找是否每一步都有相应的处理器,如果存在则尝试调用它。 典型地,处理器做下列三件事之一:

大部分阶段都是由处理它们的第一个模块来终止;然而,对于日志、'修正'、 以及非访问验证检查来说,所有的处理器一直保持运行(除非出现错误)。 而且,在那些可以声明多个处理器的模块当中,响应阶段的工作方式是独一无二的, 模块可以通过关于被请求对象的MIME类型的一个关键字调度表声明多个处理器。 模块可以通过给出关键字*/*(例子, 一个通配符类型描述)来声明一个可以处理任何请求的响应阶段的处理器。但是, 通配符处理器只在服务器对于特定MIME类型的被请求对象, 已经尝试过查找所有其它更明确匹配该对象的处理器并且全都已经失败的时候被调用 (或者其他处理器都不存在,或者都拒绝访问)。

如上所述,处理器本身只是带一个参数 (一个request_rec结构。参阅)的一组函数,返回值是一个整数。

模块的简明介绍

现在,我们需要解释模块的结构。我们的候选人是繁杂模块中的一个, CGI模块---它处理CGI脚本和ScriptAlias配置文件命令。 它实际上比多数模块都复杂得多,但是如果我们打算只要一个例子, 它就是不二之选。

让我们由处理器开始。为了处理CGI脚本,模块为它们声明了一个相应处理器。 因为ScriptAlias, 也需要有名字转换段(用于识别匹配ScriptAlias的URIs) 和类型检查段(任何匹配ScriptAlias的请求都归类为CGI脚本)。

模块需要维护关于每个(包括虚拟)服务器的信息, 即若干个有效的ScriptAlias; 因此模块结构包含一些指针,指向一个建立这些结构的函数和另一个合并它们的函数 (在主服务器和虚拟服务器都有ScriptAlias声明的情况下)。

最后,模块本身包含处理ScriptAlias命令的代码。 这个特例只声明了一个命令,但是可以不只一个, 因此模块有声明命令和描述命令何处可用及如何调用的命令表

最后一个关于声明这些命令的参数类型的备忘录: pool是一个指向资源池结构的指针;被服务器用于 跟踪已分配的内存、打开的文件资源,也为特定的请求服务, 或者用于自身配置过程。那样,当请求完成的时候(或者,对于配置池,当服务器重起), 全部内存能够被释放,全部打开的文件能够被关闭,而不需要有人为了释 放它们专门写出口代码来跟踪资源。另外,cmd_parms结构包含了读入的 配置文件的各种信息和其它状态信息,用于提供给处理配置文件命令的函数使用 (比如ScriptAlias)。 我们先不管别的问题,来看看模块本身:

/* 声明处理器*/

int translate_scriptalias (request_rec *);
int type_scriptalias (request_rec *);
int cgi_handler (request_rec *);

/* 响应段处理器的辅助调度表,根据MIME type */

handler_rec cgi_handlers[] = {
{ "application/x-httpd-cgi", cgi_handler },
{ NULL }
};

/* 操纵模块配置信息的例程的声明。
* 注意那些使用无类型指针传入和返回参数的例程;
* 服务器核心会跟踪它们,但是不会也不能
* 知道它们的内部结构。
*/

void *make_cgi_server_config (pool *);
void *merge_cgi_server_config (pool *, void *, void *);

/* 处理配置文件命令的例程的声明*/

extern char *script_alias(cmd_parms *, void *per_dir_config, char *fake, char *real);

command_rec cgi_cmds[] = {
{ "ScriptAlias", script_alias, NULL, RSRC_CONF, TAKE2,
"a fakename and a realname"},
{ NULL }
};

module cgi_module = {

  STANDARD_MODULE_STUFF,
   NULL,                     /* 初始化者 */
   NULL,                     /* 目录配置创建者 */
   NULL,                     /* 目录合并者 --- 缺省的将被覆盖 */
   make_cgi_server_config,   /* 服务器配置 */
   merge_cgi_server_config,  /* 合并服务器配置 */
   cgi_cmds,                 /* 命令表 */
   cgi_handlers,             /* 处理器 */
   translate_scriptalias,    /* 转换文件名 */
   NULL,                     /* 检查用户身份 */
   NULL,                     /* 检查验证 */
   NULL,                     /* 访问检查 */
   type_scriptalias,         /* 类型检查 */
   NULL,                     /* 修正 */
   NULL,                     /* 日志 */
   NULL                      /* 头分析器 */
};
top

处理器如何工作

处理器唯一的参数是一个request_rec结构。 这个结构描述了一个已提交给服务器的特定的代表客户端的请求。 多数情况下,每个与客户端的连接只产生一个request_rec结构。

request_rec的简明介绍

request_rec结构包括一些指针,这些指针指向: 服务器完成请求处理后将会清除的资源池,包含每个服务器与每个连接信息的结构, 以及更加重要的请求自身信息的结构。

这些当中最重要的信息是一小套描述被请求对象属性的字符串,包括它的URI、文件名、 内容类型与编码(这些信息由处理该请求的转换和类型检查处理器分别填入)。

其它经常用到的数据项是一些表,给出了客户端原生请求的MIME头、 准备回送的响应中的MIME头(模块们可以随意加入) 和为向请求提供服务过程中产生的子处理过程提供的环境变量。 这些表用ap_table_getap_table_set例程来处理。

注意Content-type头的值不能被模块的内容处理器使用 ap_table_*()例程来设置。而是通过把request_rec结构中的 content_type字段指向一个适合的字符串来设置。例如

r->content_type = "text/html";

最后,还有指向两个数据结构的指针,依次来说,一个是指向每模块配置结构。 具体地,这结构拥有指向这样一些数据结构的指针: 有模块创建来描述自身是如何被配置在给定的目录(通过htaccess文件 或<Directory>部分)中工作的; 以及在为请求提供服务的过程中建立的私有数据(因此模块中处理各段的处 理器能够传送一些“注解”给负责另一个阶段的处理器)。另有一个server_rec 数据结构中的配置向量,也由request_rec指向, 包含着每个(包括虚拟)服务器的配置数据。

这里是一个简化的声明,给出了最常用的域:

struct request_rec {

pool *pool;
conn_rec *connection;
server_rec *server;

/* 请求的是什么对象 */

char *uri;
char *filename;
char *path_info;

char *args;           /* 查询参数,如果有的话 */
  struct stat finfo;    /* 由服务器核心设置;
                         * 如果不存在这样的文件,st_mode设为零 */

char *content_type;
char *content_encoding;

/* MIME头环境,输入和输出。同时也是一个存放传递
* 给子处理过程的环境变量的数组,因此可以
* 写模块来加入一些东西到环境中去。
*
* headers_out和err_headers_out的区别是
* 后者即使出错时也被输出并持续通过内部重定向传递
* (因此为错误文档处理器输出的头信息将会包含它们)。
*/

table *headers_in;
table *headers_out;
table *err_headers_out;
table *subprocess_env;

/* 关于请求自身的信息 */

  int header_only;     /* 头请求,相对于GET */
  char *protocol;      /* 协议, 或者由客户请求给出,或者是HTTP/0.9 */
  char *method;        /* GET, HEAD, POST, 等等 */
  int method_number;   /* M_GET, M_POST, 等等 */


/* 日志信息 */

char *the_request;
int bytes_sent;

/* 一个模块可以设置的标志,用于显示返回的
* 信息是不是易变所以客户端不应该缓存它。
*/

int no_cache;

/* 各种跟随.htaccess文件变化的其他配置信息
* 这些是对于每个模块都有一个无类型指针的配置向量
* (具体指向的东西由各模块决定)。
*/

void *per_dir_config;   /* 配置文件中设置的选项,等等 */
  void *request_config;   /* 针对*这个*请求本身的专门信息 */


};

request_rec结构从哪里来

大多数request_rec结构是通过读取客户端的HTTP请求并填充各个域建立起来的。 但是有几个例外:

处理请求,拒绝服务和返回错误代码

像上面讨论提到的,每个处理器当被调用来处理特定的request_rec结构时, 必须返回一个int值来说明发生了什么情况。这个值可以是

注意如果返回的错误代码是REDIRECT, 则模块应该放一个Location到请求结构的headers_out域中, 这样来表明客户应该被重定向什么地方。

对响应处理器的特别考虑

大多数阶段的处理器的工作只是简单地设置request_rec结构中的一些域 (或者,在访问检查的情况下,简单地返回正确的错误代码)。然而, 响应处理器事实上需要发送一个响应(原文这里是request,我觉得应该就是response的意思)到客户端。

响应应该使用函数ap_send_http_header,以发送一个HTTP响应头开始。 (对于HTTP/0.9的请求你不必做专门任何事情来跳过发送响应头这一步; 函数自己会知道它不必作任何事)。如果请求已被标明为 header_only,那么这就已经是我们的全部工作了; 在此标志之后立刻返回,不需要再尝试更多的输出。

否则,就应该产生一个合适的响应该客户端的响应正文。用于内部产生输出的原语 是ap_rputcap_rprintf,此外ap_send_fd 用于直接将某些FILE *变量的内容复制给客户端。

到了这个时候,你或多或少应该了解一下下面的代码, 那就是用于处理没有指定其他处理器的GET请求的处理器; 它也显示了有条件的GET请求怎样处理, 如果在特定的响应处理器中这样做可取的话—— 如果存在客户提供的If-modified-sinceap_set_last_modified会检查它的值, 并返回一个合适的代码(如果非零会是USE_LOCAL_COPY)。 对于ap_set_content_length没有类似的情况要考虑, 但是为了协调,它会返回一个错误代码。

int default_handler (request_rec *r)
{
int errstatus;
FILE *f;

if (r->method_number != M_GET) return DECLINED;
if (r->finfo.st_mode == 0) return NOT_FOUND;

if ((errstatus = ap_set_content_length (r, r->finfo.st_size))
    || (errstatus = ap_set_last_modified (r, r->finfo.st_mtime)))
return errstatus;

f = fopen (r->filename, "r");

if (f == NULL) {
log_reason("file permissions deny server access", r->filename, r);
return FORBIDDEN;
}

register_timeout ("send", r);
ap_send_http_header (r);

if (!r->header_only) send_fd (f, r);
ap_pfclose (r->pool, f);
return OK;
}

最后,如果这些都太困难,也有一些别的办法。首先,如上述, 在服务器将自动产生一个错误应答的情况下, 一个还没有产生任何输出响应处理器会简单地返回一个错误码。 其次,它可以通过调用ap_internal_redirect被传送给其它的处理器, 就像前面讨论的内部重定向机制那样工作。 一个被内部重定向的响应处理器应该总是返回OK

(从非响应处理器内部调用ap_internal_redirect会导致严重的混乱).

对验证处理器的特别考虑

需要详细讨论的材料如下:

对日志处理器的特别考虑

当一个请求被内部进行重定向时,就有一个究竟应该在日志里记录什么的=问题。 Apache这样来处理这个问题:把整个重定向链绑定到一个request_rec 结构的列表,这个列表使用r->prevr->next指针连在一起。 这样传递给日志处理器的request_rec结构就是原来为初始的客户请求建立的那一个; 注意只有bytes_sent域在链中的最后一个请求里被校正(就是相对实际发送应答内容的那个请求)。

top

资源分配和资源池

编写和设计一个池化的服务器要考虑的问题之一是防止泄漏, 就是说分配了资源(内存,打开文件,等等)而随后没有释放它们。 资源池机制就是设计来使得防止泄漏发生更容易,通过允许让资源以使用完成以后可以 自动释放的方式来分配。

这种工作方式是这样:用于处理特定请求而分配的内存、打开的文件 与一个为该请求分配的资源池绑定。池是一个自身跟踪问题资源的数据结构。

处理请求时,池被清空。这时,所有关联的内存都被释放以供重用, 所有关联的文件都被关闭,并且执行任何与之关联的执行清除工作的函数。 完成以后,我们可以确信全部与这个池关联的资源都已经释放了,其中任何一项都没有泄漏。

服务器重起的情况下,对于每服务器配置的内存和资源的分配也是用相似的方式处理的。 存在一个配置池,它跟踪读取配置文件和处理其中的命令时分配的资源 (例如,为每服务器配置分配的内存、日志文件和其他打开的文件等等)。 当服务器重起和必须重新读入配置文件的时候,配置池被清空,这样 上一次读入它们所使用的内存和文件描述符就可被获得以供重用。

应该注意,池化机制的使用通常并不是强制性的,除了像日志处理器这样的情况: 你确实需要注册清扫函数来保证服务器重起时关闭日志文件(使用函数ap_pfopen 最容易做到,这个函数也安排基本文件描述符在任何子进程之前关闭,对CGI脚本, 是exec以后), 或者是使用了超时机制的情况下(这里还没有列出文档)。然而,使用它有两个好处: 分配到池中的内存永远不会泄漏(即使你分配了一个临时串又忘了释放它); 以及,对于内存分配,ap_palloc通常比malloc快。

我们这里从内存如何分配到池中开始,再讨论资源池机制如何跟踪其他资源。

池中的内存分配

调用ap_palloc把内存分配到池中,它有两个参数, 一个是指向资源池数据结构的指针,另一个是要分配的内存数量(按char计算)。 在请求处理器内部,得到资源池指针的最普通的方式是察看相关的 request_rec结构的pool槽; 因此模块代码中常见下列程序段重复出现:

int my_handler(request_rec *r)
{
struct my_structure *foo;
...

foo = (foo *)ap_palloc (r->pool, sizeof(my_structure));
}

注意没有ap_pfree函数 --- ap_palloc分配的内存只在相关联的资源池清空时被释放。 这意味着ap_palloc不必做像malloc()那样多的计算; 典型情况下它要做的就是对齐分配大小,生成块指针,再做一个范围检查。

(也存在ap_palloc的重负载使用导致一个服务进程变得过份庞大的可能性。 有两个解决办法;简单地,你可以用malloc并确信所有分配的内存都显式地用 free释放了,或者你可以在主资源池里分配一个子资源池, 在子资源池里分配内存并周期性地清空它。后一个技术在下面关于子资源池的章节里有讨论, ,在列目录的代码中有实际使用,主要是为了对数以千计的文件列目录时避免过多的存储分配。)。

分配初始化的内存

有几个函数用于分配初始化的内存,使用很频繁。 函数ap_pcallocap_palloc有同样的界面,但是会在返回之前清零所分配内存。 函数ap_pstrdup以一个资源池和一个char *为参数, 为指针指向的字符串的拷贝分配内存并返回拷贝的指针。 最后ap_pstrcat是个参数表可变的函数,它的参数有一个资源池指针和至少两个 以NULL结尾的char *。它要为每个字符串的拷贝的合并分配足够的内存。 例如:

ap_pstrcat (r->pool, "foo", "/", "bar", NULL);

返回一个指向包含8个字节、已被初始化为"foo/bar"的一块内存的指针。

Apache Web服务器中常用的池

说真的,一个池主要由它的生命期而不是别的来定义。 http main函数中存在一些静态池,在适当的时候会作为参数传给各种非main函数。它们是:

permanent_pool(译注:这里我觉得应该是一个定义的标示符, 但是在源程序里没找到它,而以下提到的其他几种在main()里都有定义)
  • 从不传给别的任何函数,它是所有池的原型
pconf
  • permanent_pool的子池
  • 在一个“配置循环”开始的时候被创建;一直存在直到服务器停止或者重起; 被传递给所有的配置时刻函数,或者通过cmd->pool调用, 或者作为"pool *p"参数传给那些不取得池的函数。
  • 传给模块的init()函数
ptemp
  • 不好意思我说了假话,这个池目前在1.3版中没有使用, 在开发pthreads中我把它改成这个名字。 我涉及了上层的ptrans的用法...把它与后面子进程的ptrans的定义对比一下。
  • permanent_pool的子池
  • 在一个“配置循环”开始的时候被创建;一直存在直到配置分析工作完成; 通过 cmd->temp_pool传递给配置时刻函数。 有点像个“私生子”,因为它不是到处都可用的。 只用于那些可能会有些函数需要使用而且在配置结束后要删除的临时空间。
pchild
  • permanent_pool的子池
  • 当产生子进程时创建(或者产生一个线程);存在直到此进程(线程)被销毁。
  • 传递给模块child_init函数
  • 销毁动作发生正好child_exit函数执行之后... (这个能够解释为什么我认为child_exit是冗余而不必要的东西)
ptrans
  • 应该是pchild的子池,但目前是permanent_pool的子池,参见以上
  • 在准备进入进入accept()循环接受连结之前由子进程清除
  • 使用如connection->pool
r->pool
  • 对于主请求这个是connection->pool的子池;对于子请求它是父请求池的子池。
  • 存在直到请求结束(例如,ap_destroy_sub_req, 或者在请求处理完成之后的child_main函数)
  • 注意r自身是从r->pool中分配的;,r->pool首先被创建, 然后r是从中调用palloc()创建的第一个元素

对几乎所有的事情r->pool就是所使用的池。但是你可以看到其他的生命期组件, 比如pchild,对某些模块是非常有用的——像那些需要为每个子进程打开一个数据库连接 并且希望在子进程销毁以后清除连接的模块。

你也可以看到一些bug是如何出现的,比如把connection->user设置为来自r->pool的一个值 --在这种情况下,连接存在于比r->pool更长的ptrans的生命期 (特别是如果r->pool是子请求的情况下!)。 因此正确的做法是从connection->pool分配。

在mod_include/mod_cgi中还有一个有趣的bug。在那里你可以看到他们作这个测试来决定使用 r->pool还是r->main->pool。这种情况下,它们注册来以备安全清除的资源是一个子进程。 如果注册到r->pool,那么当子请求完成时代码会调用wait()等待此子进程。 使用mod_include时这可以是任何老的#include指令,延迟可能长达三秒... 而且会频繁发生。作为替代,子进程在r->main->pool中注册,这使得它将在整个请求完成以后被清除, ,在输出被发送给客户端且记入日志以后。

跟踪打开的文件及其他

如以上所述,资源池也被用于跟踪除内存以外的其它类型的资源。最常见的就是打开的文件。 用于这种用途的典型例程是ap_pfopen,它使用一个资源池和两个字符串作为参数; 字符串参数与标准函数fopen相同,例如,

...
FILE *f = ap_pfopen (r->pool, r->filename, "r");

if (f == NULL) { ... } else { ... }

也有一个ap_popenf例程,它与底层系统调用open相似。 它们都会安排文件在被考虑的的资源池清空时关闭。

与内存的情况不同,存在显式关闭由ap_pfopenap_popenf打开的文件的函数,即ap_pfcloseap_pclosef。(这是因为,在许多系统上单个进程能够同时打开的文件数量是相当有限的)。 由于在像linux那样的系统上多次关闭同一个FILE*指针会带来严重的反应, 使用这些函数来关闭ap_pfopenap_popenf关联的文件非常重要, 否则在那样的系统上就会引起致命的错误。

(不强求使用close函数,因为不管怎样文件最终会被关闭, 但是你应该考虑到你的模块正在或者可能打开大量文件的情况)。

其他类型的资源 --- 清扫函数

这里有更多的文字描述在执行文件操作及进程产生 (spawn_process)的阶段中的清扫指令。

池的清扫一直存在到调用clear_pool(): 调用clear_pool(a)会在a的所有子池上递归调用destroy_pool(); 然后调用a的所有清扫函数;再释放a占用的全部内存。而调用destroy_pool(a)会调用clear_pool(a) 然后释放池结构自身。, clear_pool(a)不会删除a,它只是释放所有资源你就可以立刻重新使用a。

微调控制 --- 创建和处理子池,及关于子请求的一个备忘

在很罕见的情况下,太自由地使用ap_palloc()和相关的原语 可能会导致意料外的失控的资源分配。你可以这样来处理,创建一个子池, 在子池内而不是主池内分配资源,清除或者销毁子池来释放关联的资源。 (这真的一种罕见的情况;在标准模块集中唯一出现的情况是列目录时, 而且只是对于非常大的目录。 不必要地使用这里讨论这种原语可能会把你的代码弄得一团糟而几乎没什么好处)。

创建子池的原语是ap_make_sub_pool,使用另一个池(父池)作为参数。 当主池被清空时,子池被销毁。子池也可以在任何时刻被清空或者销毁,分别通过调用函数 ap_clear_poolap_destroy_pool。 (区别是ap_clear_pool只释放池相关联的资源,而 ap_destroy_pool还释放池自身。对于前者,你可以反复在池中分配资源和清除资源; 而对于后者,简单地,池被销毁,没有了)。

最后注意 --- 子请求有它们自己的资源池,也就是主请求资源池的子池。 再次为已经分配过资源的子请求(使用ap_sub_req_...函数申请的) 请求资源的合理方式是调用ap_destroy_sub_req, 这个函数可以释放资源池。在调用它之前, 确保把你所关心的在子请求资源池中分配的东西拷贝到别的不那么容易发生变化的地方。 (例如,request_rec结构中的文件名字)。

(还有,在多数环境下,你不应该强行调用这个函数; 系统为每一个典型的子请求只分配2k左右的内存, 而且不管怎样主请求池清空的时候它们都会被释放。 只有在一个单独的主请求中需要为很多很多的子请求分配资源时 才应该考虑使用ap_destroy_... functions)。

top

配置,命令及其相关事项

Apache服务器的设计目标之一是保持与NCSA 1.3 服务器的外部兼容 ---这就是,读取同样的配置文件、正确处理其中所有的指令, 一般地说就是成为NCSA服务器的嵌入替代。另一方面, 另一个设计目标是把尽可能多的功能移到模块中去,那些模块几乎不可能对整体的服务器核心做什么。 调和这些目标的唯一办法是把对大多数命令的处理从中心服务器移到模块中去。

然而,仅仅给模块以命令表对于把它们从服务器核心中分离出来是不够的。 服务器为了以后操作命令不得不记住它们。那涉及维护许多数据:模块的私有数据; 有可能对每服务器或者每目录都有用的数据,大多数情况下是对每目录, 包括特定的访问控制和鉴别信息,还有关于如何利用后缀决定文件类型的信息, 这可以用AddTypeDefaultType指令修改,等等。 一般地,管理哲学是任何可以由目录配置的对象就应该由目录配置; 每服务器的信息通常用在处理像AliasRedirect信息的标准模块集中, 这样的指令在请求被绑定到基本文件系统的特定位置之前起作用。

模拟NCSA服务器的另一个要求是能够处理每目录的配置文件,一般是.htaccess文件, 即使对于NCSA服务器那些文件可以包含与访问控制完全无关的指令。 因此,在翻译了URI -> filename之后和执行其他任何阶段工作之前, 服务器根据翻译后的路径名称在基本文件系统的目录层次中漫游来寻找任何可能提供的 .htaccess文件。读出的信息将会被合并到从服务器的配置文件 读入的程序信息中(或者来自access.conf文件的<Directory>小节; 或者来自srm.conf文件的缺省值,这样使用的大多数目的都是使其表现像 <Directory />目录)。

最后,在完成了为一个涉及到读取.htaccess的请求的服务以后, 我们需要丢弃为处理请求分配的存储器。处理的方式与在别的出现相似问题的地方采取的方式一样, 就是通过绑定那些结构到处理每个事务的资源池。

每目录配置结构

我们来看看这一切是怎样在mod_mime.c里实现的,这段程序定义了模拟 NCSA服务器用文件后缀判断文件类型的行为的文件类型处理器。 这里,我们要关注的是实现AddTypeAddEncoding命令的代码。 这些命令可能会出现在.htaccess文件里,因此它们必须在模块的私有“每目录”数据里处理, 这些数据实际上由两个分离的组成,一个记录MIME类型,一个编码信息,声明如下:

typedef struct {
    table *forced_types;      /* Additional AddTyped stuff */
    table *encoding_types;    /* Added with AddEncoding... */
} mime_dir_config;

当服务器读入配置文件或者包含MIME模块命令的<Directory>节时, 需要创建一个mime_dir_config结构来让那些命令起作用。 通过调用在模块的`create per-dir config slot'中找到的函数来做这个工作, 由两个参数:配置信息适用的目录名字(或者是NULL,对于srm.conf), 和一个指向资源池的指针,所有资源分配应该在那个池中进行。

(如果读入的是一个.htaccess文件,则此资源池是一个针对请求的“每请求”资源池; 否则就是一个用于配置信息的资源池,重起时才会清空。这两种方式,对于创建结构当清空资源池时能够销毁都是很重要的, 必要的时候可以通过注册一个清扫函数)。

对于MIME模块,“每目录”配置的创建函数只是用ap_palloc分配上述的结构 和创建那两个来填充。看起来就像这样:

void *create_mime_dir_config (pool *p, char *dummy)
{
mime_dir_config *new =
(mime_dir_config *) ap_palloc (p, sizeof(mime_dir_config));

new->forced_types = ap_make_table (p, 4);
new->encoding_types = ap_make_table (p, 4);

return new;
}

现在,假设我们已经从一个.htaccess文件读取了信息。 我们沿着目录层次已经有了下一个目录的“每目录”配置结构。 如果读入的这个.htaccess文件中没有AddType或者 AddEncoding命令, 它的“每目录”配置结构对于MIME模块仍然是有效的,我们可以使用它。 否则,我们需要以某种方式合并这两个结构。

要那样做,服务器要调用模块的“每目录”配置合并函数, 在提供了这样一个函数的前提下。此函数有三个参数:要合并的两个结构, 和一个用来在其中分配结果的资源池。对于MIME模块, 要做的全部工作就是用父目录得到的配置结构覆盖这个“新目录”的每目录配置结构的MIME:

void *merge_mime_dir_configs (pool *p, void *parent_dirv, void *subdirv)
{
mime_dir_config *parent_dir = (mime_dir_config *)parent_dirv;
mime_dir_config *subdir = (mime_dir_config *)subdirv;
mime_dir_config *new =
(mime_dir_config *)ap_palloc (p, sizeof(mime_dir_config));

new->forced_types = ap_overlay_tables (p, subdir->forced_types,
parent_dir->forced_types);
new->encoding_types = ap_overlay_tables (p, subdir->encoding_types,
parent_dir->encoding_types);

return new;
}

注释 --- 如果没有提供“每目录”合并函数,服务器就只使用子目录的配置信息, 并忽略父目录的配置信息。对于某些模块,这样工作得很好(例如, 对于includes模块,它们的“每目录”配置信息仅仅由XBITHACK的状态组成), 对于那些模块,你不能只是声明一个函数又把模块自身的相应结构的位置放一个NULL

命令处理

现在我们有了这些结构,我们需要能够计算出如何去填充它们。 那涉及到处理实际的AddTypeAddEncoding命令。 为了找到这些命令,服务器在模块的命令表中搜寻。 该表包含着命令需要几个参数、是什么格式、在什么地方被允许等等的信息。 这些信息足够让服务器以预先分析出来的参数来调用大多数的命令处理函数。 不再罗嗦了,我们来看看AddType的命令处理器,看起来像这样 (AddEncoding命令看起来基本上也是一样的,所以这里不再列出):

char *add_type(cmd_parms *cmd, mime_dir_config *m, char *ct, char *ext)
{
if (*ext == '.') ++ext;
ap_table_set (m->forced_types, ext, ct);
return NULL;
}

这个命令处理器异乎寻常地简单。像你能看到的,它有四个参数,头两个是预先分析出的参数, 第三个是正在考虑的的模块的每目录配置结构的指针,第四个是指向 cmd_parms结构的指针。这个结构包含一批被频繁使用于部分但不是全部命令的参数; 还包括一个资源池(从中可以分配内存和应该在其中绑定清扫函数);还有正在被配置的 (虚拟)服务器信息,如果必要从中可以得到模块的“每服务器”配置数据。

这个命令处理器如此简单的另一个方面是其中没有对可能遇到的错误进行处理。 如果出现错误,它会返回一个错误的消息来代替NULL; 这将引起在服务器的stderr上打印出一个错误信息, 如果发生在主配置文件中还将紧跟着一个快速的退出;对于一个.htaccess文件, 语法错误记录在服务器的错误日志中(和一个出错位置的标示一起), 并且请求将会得到一个服务器错的响应(HTTP error status,错误代码 500)。

MIME模块的命令表对于看起来有如下样子的那些命令有条目:

command_rec mime_cmds[] = {
{ "AddType", add_type, NULL, OR_FILEINFO, TAKE2,
"a mime type followed by a file extension" },
{ "AddEncoding", add_encoding, NULL, OR_FILEINFO, TAKE2,
"an encoding (e.g., gzip), followed by a file extension" },
{ NULL }
};

这些表中的条目是:

最后,这些全部设置完成了,我们必须去使用它。在这个模块处理器中, 特别是对它的文件类型处理器,这是最后完成的,看上去多少就像这个样子; 注意“每目录”配置结构是从request_rec的“每目录”配置向量中用 ap_get_module_config函数提取出来的。

int find_ct(request_rec *r)
{
int i;
char *fn = ap_pstrdup (r->pool, r->filename);
mime_dir_config *conf = (mime_dir_config *)
ap_get_module_config(r->per_dir_config, &mime_module);
char *type;

if (S_ISDIR(r->finfo.st_mode)) {
r->content_type = DIR_MAGIC_TYPE;
return OK;
}

if((i=ap_rind(fn,'.')) < 0) return DECLINED;
++i;

if ((type = ap_table_get (conf->encoding_types, &fn[i])))
{
r->content_encoding = type;

/* go back to previous extension to try to use it as a type */
fn[i-1] = '\0';
if((i=ap_rind(fn,'.')) < 0) return OK;
++i;
}

if ((type = ap_table_get (conf->forced_types, &fn[i])))
{
r->content_type = type;
}

return OK;
}

其他方面的备忘 -- “每服务器”配置,虚拟服务器,等等

“每服务器”模块配置背后的基本想法基本上与“每目录”配置是一样的; 有一个创建函数和一个合并函数,当一个虚拟服务器部分地覆盖基服务器的配置的时候调用后者, 并计算一个复合结构。(就像“每目录”配置那样,缺省情况是, 如果没有指定合并函数而又在某个虚拟服务器中培植了一个模块,则基本的配置就被简单地忽略掉)。

唯一牢固的区别是,当一个命令需要配置“每服务器”的私有模块数据时, 它需要利用cmd_parms数据去取得。这里有一个来自alias模块的例子, 它也显示了怎样一个语法错误是怎样返回的 (注意这里传给命令处理器的“每目录”配置参数被声明为一个哑元, 这是因为这个模块实际上并不需要“每目录”配置的数据):

char *add_redirect(cmd_parms *cmd, void *dummy, char *f, char *url)
{
server_rec *s = cmd->server;
alias_server_conf *conf = (alias_server_conf *)
ap_get_module_config(s->module_config,&alias_module);
alias_entry *new = ap_push_array (conf->redirects);

if (!ap_is_url (url)) return "Redirect to non-URL";

new->fake = f; new->real = url;
return NULL;
}

 


项目维护者: kajaa [本文译者: suncjs * ]

项目说明 | 项目进度 | 项目讨论区 | Apache手册中文版