Apache的工作原理

夏立果
2023-12-01

 Apache的请求处理

尽管不是全部的,但是绝大部分模块都关注处理HTTP请求的某些方面。不过,一个模块不能考虑处理HTTP的所有方面——这是httpd要做的工作。模块化方法的一个好处就是:一个模块可以只关注一个具体的任务,而不去考虑那些和它不相关的HTTP的其他方面。

2.7.1  内容生成

Web服务器最简单的形式就是一个程序,它侦听HTTP请求,在收到一个HTTP请求之后做出一个回复(见图2-2)。在Apache中,这个工作主要由内容生成器负责,它是Web服务器的核心。

图2-2  最简单的Web服务器

确切地说内容生成器必须处理每一个HTTP请求。一个处理函数可以通过在 httpd.conf文件中使用SetHandler和AddHandler指令进行配置。通常都是通过定义一个被处理函数所引用的函数,这样任何一个模 块都可以注册内容生成器。在没有任何模块指定特殊生成器时,使用默认的生成器,它只是简单地返回一个直接将请求映射到文件系统上所得到的文件。实现内容生 成器的模块有时被称作“内容生成器”模块或者“处理函数”模块。

2.7.2  请求处理阶段

原则上,一个内容生成器可以操作Web服务器的所有函数。例如,一个CGI程序获取请求并产 生回复,它可以控制获取请求和产生回复之间所有的事务。和其他的Web服务器一样,Apache把这个请求分成几个不同的阶段。例如,在内容生成器执行某 些任务之前,Apache会检查这个用户是否被授权去执行这些任务。

有些请求阶段发生在内容生成器之前(见图2-3)。这些阶段用来检查(也可能处理)请求报头,然后决定如何处理该请求。例如:

请求的URL将和配置信息做比较,决定将使用哪一个内容生成器。

请求的URL通常将被映射到文件系统。映射可能是一个静态的文件,或者一个CGI脚本,甚至是任何内容生成器所使用的方式。

如果启用了内容协商,mod_negotiation模块将查找最适合浏览器偏好的资源版本。例如,Apache的用户手册页面可以通过浏览器的语言要求进行指定。

mod_alias模块和mod_rewrite模块可能会改变请求的有效URL。

最后还有一个请求日志记录阶段,发生在内容生成器之后,用来给浏览器发送一个回复。

图2-3  Apache的请求处理机制

2.7.2.1  非标准请求处理

有时由于一些特殊原因,请求处理可能不会遵守上面描述的标准处理流程。

在应答被发送之前的任何一个点上,模块可能转向处理一个新的请求,或者处理错误文档(见第6章)。

模块可能定义了附加的阶段,并且允许其他模块增加钩子以便进行它们自己的处理流程(见第10章)。

quick_handler钩子有时会旁路通常的处理流程,它被mod_cache模块调用(本书没有就此讨论)。

2.7.3  处理钩子

在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章对这些内容进行介绍。

2.7.4  数据轴和过滤器

我们目前所讨论的和每一个通用的Web服务器架构都非常相似。虽然在细节上有些不同,但是请求处理(元数据→生成器→记录器)机制都是一样的。

Apache 2的主要创新就是过滤链,它将Apache 2从一个“基本的”Web服务器(像Apache 1.3和其他的服务器)变成了功能强大的应用平台。过滤链可以表示成为一个数据轴,和请求处理轴垂直(见图2-4)。请求数据可以在到达内容生成器之前被 输入过滤器进行处理,回复数据可以在发送到客户端之前被输出过滤器进行处理。过滤器使得数据处理的实现比以往的版本更加简洁有效,同时将内容生成器从它的 变换(transformation)和集合(aggregation)中分离出来。

2.7.4.1  处理函数(handler)还是过滤器(filter)

很多应用程序都可以作为处理函数或者过滤器进行应用。有时候可能很明确作为它们其中的一种方式比较合适,而另外一种则不太适合,但是在这两种极端情况之间存在着一个灰色地带。如何决定是一个处理函数呢,还是一个过滤器呢?

当作这个决定时,以下的几个问题需要考虑。

可行性:这两种情况都适合么?如果不是都适合,那么,马上就可以决定使用一种方式。

有效性:这两种情况中的一种比另外一种提供了更加有效的功能吗?过滤器通常比处理函数更加有用,因为它可以被不同的内容生成器重用,也可以连接生成器和其他的过滤器。不过每一个请求需要经某些处理函数进行处理,即使它们没有做任何工作。

复杂性:其中的一种方式比另外一种方式更加复杂吗?它需要消耗更多的时间和能力去开发吗?它 运行得更慢吗?过滤器模块通常比同样的处理函数更加复杂,因为处理函数通常可以完全控制它的数据,随意进行读写操作,而过滤器需要实现一个回调函数,该回 调函数伴随部分数据被调用几次,并且这些数据必须被当作非结构化的块进行处理。我们将在第8章仔细讨论这个问题。

例如,Apache 1.3的用户可以将一个XSLT变换构建在处理函数中(例如CGI或者PHP),然后就可以进行XSLT变换操作。另外一个选择就是,他们可以使用一个 XSLT模块,不过这种方式运行非常慢,而且比较麻烦(笔者曾试着为Apache 1.3编写了一个XSLT模块,不过后来发现对临时文件进行操作时,这个模块比在CGI脚本中运行XSLT要慢上几百倍)。在处理函数中运行XSLT是可 以的,不过这就破坏了代码的模块性和可重用性。任何需要XSLT处理的应用都需要重新发明这个轮子,使用任何一个提供给编程或者脚本语言的库,经常不得不 采用笨拙的方法,例如使用临时文件。

图2-4  Apache 2引入了一个新的数据轴线,让一些功能强大的新应用成为可能

和上述方法相比,Apache 2允许我们在一个过滤器中运行XSLT。内容处理函数请求XSLT可以简单的输出这个XML,并且把转换工作留给Apache。由Phillip Dunkel编写并发布的第一个针对Apache 2的XSLT模块目前正在处于Beta测试中,工作才刚刚开始,还是不完善的,不过已经比Apache 1.3中的XSLT好太多了。目前它还在进一步的完善,是XSLT模块的选择之一。本书作者还开发了另外一个XSLT模块。

通常,如果一个模块既有数据输入,又有数据输出,那么它可以被多个应用所使用,并且很大程度上会被实现为一个过滤器。

2.7.4.2  内容生成器示例

默认的处理函数从本地磁盘的DocumentRoot目录中发送一个文件。尽管处理器也可以这么做,不过这样并没有任何好处。

作为服务器端编程通用API的CGI是一个处理函数。由于CGI脚本想成为Web服务器架构的中心,它必须是一个处理函数。不过,mod_ext_filter也为外部过滤器提供了一个有些类似的框架。

Apache代理是一个从后端(back-end)服务器上取得内容的处理函数。

表单处理(form-processing)应用程序通常被实现成为处理函数——特别是那些 接收POST数据的表单处理应用程序,或者可以转换服务器自身状态的其他操作。同样的,从任何后端生成报告的应用程序也通常应用为处理函数。不过,如果处 理函数是基于嵌入编程元素的HTML或者XML页面时,它通常应用为一个过滤器。

2.7.4.3  过滤器示例

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中的一个数据压缩模块)更加简单,对于需要使用数据压缩的临时文件也更加方便。

2.7.5  处理的顺序

在转向讨论一个模块如何对自己挂钩子进入请求/数据处理的各个阶段之前,我们需要暂停一下,澄清一个经常让人迷惑的问题——也就是处理的顺序。

请求处理轴线是一个前向的轴线,每一个阶段都是严格按照顺序产生的,而数据处理轴线就存在着 一些疑惑。考虑到最大程度的有效性,这个轴线是管道状的,因此内容生成器和过滤器的运行并没有确定的顺序。举例来说,在输入过滤器进行某些设定之后,你通 常不应该期待它将被内容生成器或者输出过滤器所采用。

处理顺序的中心在内容生成器,它负责将数据从输入过滤器的堆栈中提取出来,并放入输出过滤器 中(如果可行的话,两种情形都会发生)。当一个生成器或者过滤器需要进行某些总体上影响请求的设定时,它必须在将这些数据向链中的后端(生成器和输出过滤 器)发送之前进行设定,或者在将数据返回给调用者(输入过滤器)之前进行设定。

2.7.6  处理钩子

既然我们已经对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) {

    /* 对这个请求进行处理 */

}

 

 类似资料: