为 mod_perl 编写模块
CGI 程序是一种常见且经过时间考验的方式,可以为网站添加功能。当用户的请求是针对 CGI 程序时,Web 服务器会启动一个单独的进程并调用该程序。发送到 STDOUT 文件描述符的任何内容都会发送到用户的浏览器,而发送到 STDERR 的任何内容都会记录在 Web 服务器的错误日志中。
虽然 CGI 一直是 Web 编程的有用标准,但它仍有许多不足之处。特别是,每次调用 CGI 程序都需要其自己的进程,这最终成为一个很大的性能瓶颈。这也意味着,如果您使用像 Perl 这样的语言,其中代码在调用时编译,您的代码将在每次调用时都进行编译。
避免此类问题的一种方法是编写您自己的 Web 服务器软件。然而,这样的项目是一项重大的 undertaking。虽然我使用的第一个 Web 服务器由 20 行 Perl 代码组成,但现在大多数服务器除了处理简单的文档请求外,还必须处理大量的标准和错误情况。
Apache,一个高度可配置的开源 HTTP 服务器,可以通过编写模块来扩展其功能。实际上,现代版本的 Apache 大部分功能都依赖于模块,而不仅仅是一些附加组件。当您为计算机系统编译和安装 Apache 时,您可以选择要安装哪些模块。
这些模块之一是 mod_perl,它将整个 Perl 二进制文件放入您的 Web 服务器中。这允许您使用 Perl 而不是 C 来修改 Apache 的行为。
即使您计划将大致相同的代码与 mod_perl 和 CGI 一起使用,了解 mod_perl 具有一些内置的智能功能来缓存编译后的 Perl 代码也很有用。这在避免创建子进程来运行 CGI 程序所获得的效率之上,提供了额外的速度提升。
在过去的一年中,本专栏介绍了一些最流行的 mod_perl 使用方法,即 Apache::Registry 和 HTML::Embperl 模块。前者允许您运行几乎所有未经修改的 CGI 程序,同时利用 mod_perl 中内置的各种速度优势。HTML::Embperl 是一个模板系统,允许我们将 HTML 和 Perl 组合在一个文件中。
Apache::Registry 和 HTML::Embperl 都提供了强大的功能,并允许程序员利用 mod_perl 的一些强大功能和速度。但是,使用这些模块会阻止我们直接访问 Apache 的内部结构,使其成为一个可以比通用 Apache 服务器更好地处理我们特定需求的程序。
本月,我们将了解如何为 mod_perl 编写模块。您将看到,编写此类模块比编写 CGI 程序更复杂。但是,它并没有显着复杂多少,并且可以为您提供极大的灵活性和强大功能。
请记住,虽然 CGI 程序可以在各种 Web 服务器上使用,通常无需修改,但 mod_perl 仅适用于 Apache 服务器。这意味着为 mod_perl 编写的模块可以在其他 Apache 服务器上工作,这些服务器占世界上一半以上的 Web 服务器,但不能在其他类型的服务器上工作,无论是免费的还是专有的。
如果跨不同服务器的可移植性是您组织中的主要目标,请在使用 mod_perl 之前三思而后行。但是,如果您期望在可预见的未来使用 Apache,我强烈建议您研究 mod_perl。您的程序将运行得更快、更有效率,并且您将能够创建仅使用 CGI 难以或不可能实现的应用程序。
CGI 程序员对 HTTP(用于几乎所有 Web 通信的超文本传输协议)的了解有限。通常,服务器接收来自 HTTP 客户端(最常见的是 Web 浏览器)的请求后,会将传入的 URL 转换为本地文件系统,检查文件是否存在,并返回响应代码以及文件内容或错误消息(如果适用)。CGI 程序仅在此过程的中途调用,在转换发生、文件已找到并启动新进程后。
mod_perl 相比之下,允许您检查和修改 HTTP 事务的每个部分,从客户端的初始联系开始,一直到服务器文件系统上的事务日志记录。每个 HTTP 服务器都将 HTTP 事务划分为一系列阶段;Apache 有十几个这样的阶段。
每个阶段都称为“处理程序”,并有机会对 HTTP 事务的当前阶段执行操作。例如,TransHandler 将 URL 转换为文件系统上的文件,LogHandler 负责将事件记录到访问日志和错误日志,而 PerlTypeHandler 检查并返回与每个文档关联的 MIME 类型。当发生重要事件(例如启动、关闭和重启)时,会调用其他处理程序。
这些 Apache 处理程序中的每一个都有一个 mod_perl 对应项,统称为“Perl*Handlers”。正如您可以从这个昵称中猜到的那样,每个 Perl*Handler 都以单词“Perl”开头,以单词“Handler”结尾。
一个通用的 Perl*Handler,简称为 PerlHandler,也可用,并且与 CGI 程序非常相似。如果您想接收请求、执行一些计算并返回结果,请使用 PerlHandler。实际上,大多数对最终用户可见的应用程序都可以使用 PerlHandler 完成。其他 Perl*Handlers 更适合从 Perl 模块更改 Apache 的行为,例如当您想要添加新型访问日志、更改授权机制或在启动或关闭时添加一些代码时。
我意识到 Perl*Handlers(意味着 Perl 程序员可用的所有可能的处理程序)和 PerlHandlers(意味着利用 Apache 通用“处理程序”的模块)之间的区别可能会令人困惑。说实话,混淆两者并不是什么大问题,因为大多数程序都是为 PerlHandler 而编写的,而不是为任何其他 Perl*Handlers 编写的。
正如我上面提到的,mod_perl 缓存 Perl 代码,编译一次,然后在后续调用期间运行该编译后的代码。这意味着,与 CGI 程序相反,我们程序中所做的更改不会立即反映在服务器上。相反,我们必须以某种方式告诉 Apache 重新加载我们的程序。最简单的方法是发送 HUP 信号(在我的 Linux 机器上是 killall -1 -v httpd),但也有其他方法。另一种方法是使用 Apache::StatINC 模块,该模块跟踪模块的修改日期,并在必要时加载新版本。
众所周知,CGI 程序是从外部进程(即 Web 服务器)调用的独立程序。PerlHandler 模块实际上是 Apache 进程内的子例程;当满足一组特定条件时,Apache 会调用我们的子例程。
编写 PerlHandler 模块与编写任何 Perl 模块没有太大区别。(如果您不熟悉编写 Perl 模块,请参阅“perlmod”手册页,或任何关于该主题的书籍。)我们创建一个模块,其中定义了一个名为“handler”的子例程,如清单 1 所示。此代码具有许多 PerlHandler 模块共有的元素。
首先,整个模块包含一个子例程“handler”。如果需要,我们可以定义其他子例程,但通常最容易使用已建立的标准和默认值。
接下来,请注意,处理程序使用单个参数调用,我们将其称为 $r。它是 Apache 对象的实例,它使我们可以访问 Apache Web 服务器的内部结构。$r 是我们通往 HTTP 服务器和用户浏览器外部世界的管道。我们调用某些方法来确定服务器和浏览器的状态,并调用其他方法将输出发送到用户的浏览器。没有 $r 我们会有些迷茫,因此我们在进入“handler”时要做的第一件事就是检索 $r 是很自然的。
我们还在程序中使用了 -w 和 use strict 编程助手。虽然这些通常是编写良好、干净的 Perl 程序的好主意,但在 mod_perl 下开发时,它们是必不可少的。正如我们稍后将看到的,mod_perl 的缓存和持久性意味着我们需要格外小心地使用内存,以便使我们的 HTTP 服务器进程尽可能精简。
我们的处理程序仅使用来自 $r 的三个方法:content_type、send_http_header 和 print。
第一个方法 content_type 允许我们设置或检索将出现在响应之前的“Content-type”标头。每个 HTTP 响应都必须用这样的标头描述,该标头告诉浏览器响应是 HTML 格式的文本文件、GIF 图像还是 zip 文件。
一旦我们将“Content-type”标头设置为适当的值,我们就使用 send_http_header 方法将所有标头发送到用户的浏览器。过了这一点,发送到用户浏览器的任何内容都将被视为 HTTP 响应正文的一部分,而不是描述该正文的标头。
第三个方法 print 类似于内置的“print”函数。但是,它考虑了“print”可能没有考虑的几个因素,例如超时。$r->print 接受参数列表,就像“print”函数一样。因此,您可以使用
$r->print("a", "b", "c");
并期望将三个字符发送到用户的浏览器。
完成编写响应后,我们通过向调用者返回 OK 符号来退出模块。我们从 Apache::Constants 导入 OK,这是一个为我们提供大量有用符号的模块。为了不过多地污染我们的命名空间,我们明确要求仅导入“OK”,不导入其他符号。
如果我们正在编写更复杂的模块,我们可能会使用导出标记之一,例如 :common 和 :response,这允许我们导入一组符号,而无需显式命名它们。因此,我们可以使用语句
use Apache::Constants qw(:response);
这将导入响应所需的所有符号。
大多数 PerlHandler 模块都希望它们的“handler”子例程返回两个符号之一:OK,表示处理程序成功处理了请求,并且没有其他 PerlHandler 需要执行任何操作,或者 DECLINED 符号。如果您的模块的“handler”例程返回 DECLINED,则表示“我无法处理给我的输入,如果其他 PerlHandler 可以做些什么,我会很高兴。” 通常,返回 DECLINED 意味着将应用默认的 Apache 行为;如果我们的 PerlHandler 返回 DECLINED,Apache 将尝试读取 URL 中命名的文件并对其执行某些操作。通过返回 OK,我们表明我们的模块处理了事情,并且 Apache 可以继续执行下一个 PerlHandler。
现在我们已经了解了编写 PerlHandler 模块是多么容易,让我们看一下如何在我们的 Web 服务器上安装此模块。我们在配置文件中执行此操作,通常名为 httpd.conf。如果您的 Apache 副本使用三个 .conf 文件,请理解它们之间的划分是人为的,并且基于服务器的历史记录,而不是对三个文件的任何实际需求。Apache 开发人员认识到这种日益人为的划分,最近决定服务器的未来版本将只有一个文件 httpd.conf,而不是三个。
Apache 配置文件依赖于指令,这些指令是伪装的变量赋值。也就是说,语句
ServerName lerner.co.il
将“ServerName”变量设置为值“lerner.co.il”。
如果您希望指令影响服务器上文件或目录的子集,则可以使用“section”。例如,如果我们说
<Directory /usr/local/apache/share/cgi-bin> AllowOverride None Options ExecCGI </Directory>
那么 AllowOverride 和 Options 指令仅适用于目录 /usr/local/apache/share/cgi-bin。通过这种方式,我们可以将不同的指令应用于不同的文件。
“Directory”部分允许我们修改特定文件和目录的行为。我们还可以使用“Location”部分来修改未连接到目录的 URL 的行为。Location 部分的工作方式与 Directory 部分相同,不同之处在于 Location 的参数相对于 URL,而 Directory 的参数相对于服务器的文件系统。
例如,我们可以将上面的 Directory 部分重写为以下 Location 部分
<Location /cgi-bin> AllowOverride None Options ExecCGI </Location>
当然,这假设以 /cgi-bin 开头的 URL 指向服务器文件系统上的 /usr/local/apache/share/cgi-bin。
所有这些背景知识对于理解我们将如何安装我们的 PerlHandler 模块是必要的。毕竟,我们的 PerlHandler 将影响一个或多个 URL 将受影响的方式。如果我们(不明智地)希望我们的 PerlHandler 模块影响 /cgi-bin 中的所有文件,那么我们使用
<Location /cgi-bin> SetHandler perl-script PerlHandler Apache::TestModule </Location>
这告诉 Apache 我们将使用 Perl 处理程序处理 /cgi-bin 下的所有 URL。然后,我们告诉 Apache 使用哪个 PerlHandler,命名 Apache::TestModule。如果我们在服务器文件系统上的适当位置没有安装 Apache::TestModule,并且如果包名不正确,这将导致错误。
上面的示例在很多方面都是不明智的,包括它掩盖了我们服务器上的所有 CGI 程序这一事实。让我们尝试一个稍微更有用的 Location 部分
<Location /hello> SetHandler perl-script PerlHandler Apache::TestModule </Location>
上面的 Location 部分意味着,每次有人从我们的服务器请求 URL “/hello” 时,Apache 都会运行 Apache::TestModule 中的 “handler” 例程。因为我们使用了 Location 部分,所以我们不必担心 /hello 是否对应于我们服务器文件系统上的目录。
这就是 mod_perl 创建状态监视器的方式
<Location /perl-status> SetHandler perl-script PerlHandler Apache::Status </Location>
每次有人从我们的服务器请求 /perl-status URL 时,都会调用 Apache::Status 模块。此模块随 mod_perl 一起提供,为我们提供有关 mod_perl 子系统的状态信息。同样,因为我们使用了 Location 部分,所以我们不必担心 /perl-status 是否对应于磁盘上的目录。通过这种方式,我们可以创建独立于文件系统存在的应用程序。
在 httpd.conf 中创建此 Location 部分后,我们必须重新启动 Apache。我们可以使用以下命令向其发送 HUP 信号
killall -HUP -v httpd
或者我们甚至可以使用随现代版本的服务器一起提供的程序 apachectl 完全重新启动 Apache
apachectl restart无论哪种方式,我们的 PerlHandler 都应该在 Apache 重新启动后处于活动状态。
我们可以通过访问 URL /hello 来测试事情是否正常工作。在我的家用机器上,我将浏览器指向 http://localhost/hello,并在不久后收到了“testing”消息。如果您没有看到此消息,请检查系统上的 Apache 错误日志。如果模块中存在语法错误,您将需要修改模块并如上所述重新启动服务器。
第一次调用 PerlHandler 模块时,Apache 可能需要一些时间才能响应。这是因为在给定的 Apache 进程上第一次调用 PerlHandler 时,必须调用 Perl 系统并加载模块。您可以使用 PerlModule 指令在一定程度上避免此问题,本文稍后将对此进行描述。
我们刚刚创建的子例程可能看起来微不足道,但它证明了我们可以通过简单地编写 Perl 子例程来轻松修改 Web 服务器的行为。此外,由于子例程几乎可以包含任何类型的 Perl 代码,因此我们可以使用独立程序可用的所有 Perl 模块、运算符、函数和正则表达式。
实际上,我们的“handler”例程只是一个入口点,可以是一个具有其他子例程的大型复杂程序。由于 Perl*Handler 模块可以在操作的每个阶段访问 Apache,因此我们可以使用 Perl 修改任何内容。一个不断增长的模块库可以执行许多常见的任务,因此您可以将时间花在问题的细节上,而不是重新发明轮子。
让我们编写另一个 PerlHandler 模块,但这次让它做一些不同于返回自己的输出的事情。为了好玩,我们将让它将文件中的标题变成 Pig Latin。(在 Pig Latin 中,每个单词的第一个字母被移动到单词的末尾,并在末尾添加“ay”。)
我们将我们的 PerlHandler 模块称为 Apache::PigLatin,这意味着我们将创建一个名为 PigLatin.pm 的模块并将其放入 Apache 模块子目录中。源代码如清单 2 所示。
我们在 httpd.conf 中使用 Directory 部分安装我们的模块
<Directory /usr/local/apache/share/htdocs/stuff> SetHandler perl-script PerlHandler Apache::PigLatin </Directory>
确保该指令指向 Apache 文档树中的实际目录。
该模块引入了几个新概念,但没有什么革命性的。首先,我们导入常量 OK、DECLINED 和 NOT_FOUND。正如我们之前指出的,我们将使用 OK 来指示我们的 PerlHandler 完成了一些操作,并使用 DECLINED 来指示 Apache 应该应用一些其他行为。我们将使用 DECLINED 来确保我们的 PerlHandler 可以处理 HTML 格式的文本,方法是检查 $r->content_type。如果 MIME 类型为 “text/html”,我们将对文件进行操作。如果是 JPEG 图像,我们将避免将其翻译成 Pig Latin,并返回 DECLINED。
接下来,我们尝试从 $r->filename 打开文件。此特定模块用作简单的 PerlHandler,因此我们可以确定从 URL 到文件系统上的文件名的转换已执行。此转换发生在 TransHandler 阶段,我们可以通过编写 PerlTransHandler 而不是简单的 PerlHandler 来修改它。虽然它已将 URL 转换为我们系统上的文件名,但 Apache 尚未检查该文件是否存在——那是我们的工作。如果我们无法打开该文件,我们将假定它不存在,并返回符号 NOT_FOUND。
现在事情变得有趣了:我们获取文件的内容并在标题上执行替换——即 <H\d> 和 </H\d> 之间的任何内容,其中 \d 是匹配任何数字的内置字符类。
我们使用 .*? 来匹配所有字符,而不是简单的 .*,以便关闭 Perl 正则表达式中的“贪婪”功能。如果我们说 .* 而不是 .*?,我们将匹配第一个 <H\d> 和最后一个 </H\d> 之间的所有字符,而不是第一对、第二对等等之间。在使用正则表达式时,贪婪通常是一件好事,但在这些情况下可能会令人沮丧。
我们在替换中使用了四个选项,使用求值 (/e)、不区分大小写 (/i)、全局操作 (/g) 和 . 正则表达式字符来匹配 \n (/s)。这使我们能够一举完成替换,并捕获可能在一行开始并在下一行继续的任何标题。
在替换内部,我们调用 pl_sent,这是一个在我们模块中定义的子例程。此子例程不是直接从 mod_perl 调用的,而是为了帮助我们的 “handler” 例程完成其工作。
更重要的是,pl_sent 调用另一个子例程 piglatin_word,该子例程将单词翻译成 Pig Latin。如果我们有兴趣创建基于 mod_perl 的大型 Web 应用程序,您可以看到如何做到这一点,创建许多子例程并从 “handler” 中访问它们。C 程序员可能会将 “handler” 视为 mod_perl 等效于 “main”,即默认调用的子例程。进入该例程后,您几乎可以做任何您想做的事情。
如果您以前从未堆叠过 split、map 和 join,则 pl_sent 例程很有趣。我们通过 \s+ 将 $sentence 分割成其组成单词,这表示一个或多个空白字符。然后,我们使用 map 对结果列表的每个元素进行操作,对每个单词运行 piglatin_word。最后,我们在最后将句子拼凑在一起,使用 join 在每个单词之间添加一个空格。结果返回给调用 s/// 运算符,该运算符将翻译后的文本插入到标题标签之间。
处理段落是一个更棘手的问题,部分原因是人们经常忘记用 <P> 和 </P> 包围段落,而是依赖于浏览器会原谅他们,如果他们只是说 <P>。此外,段落包含标点符号,这使得编写好的 Pig Latin 翻译器更加困难。
您可以编写的过滤器类型没有限制。也许最有趣和最先进的是那些使用 Perl 的 eval 运算符来评估 HTML 文件中的小段 Perl 代码的过滤器。其中许多已经存在,例如 Embperl(几个月前讨论过)和 EPerl。更简单地说,您可以确保系统上的每个文件都有统一的页眉和页脚,从而消除在每个文件的顶部和底部使用服务器端包含的需要。
mod_perl 是一项令人兴奋的开发,它已经使许多新的应用程序成为可能。但是,任何事物都有权衡,mod_perl 的附加功能是以增加内存使用量为代价的。很难计算 mod_perl 所需的额外内存,但请记住,Perl 可能有点耗内存。
此外,虽然词法(“my”或“temporary”)变量在每次通过 mod_perl 调用 Perl 模块规则后都会消失,但全局变量会在调用之间保留。这可能是跟踪程序状态的一种有吸引力的方式,但也可能导致更大的内存分配。
例如,如果您的模块创建一个包含 10,000 个元素的数组,则即使在调用程序后,该数组仍将继续消耗内存。这在某些情况下可能很有用,例如,当每次调用都引用复杂的数据结构时。但是,这也意味着大型结构将不断占用内存,而不是仅在必要时占用内存。
您可以通过强制 mod_perl 在 Apache 子进程之间共享内存来减少内存使用量。当您将 Apache 作为 Web 服务器运行时,它会“预先派生”许多进程,以便传入的连接不必等待创建新的服务器进程。这些预先派生的服务器中的每一个都被 Linux 视为单独的进程,独立运行。但是,Apache 足够智能,可以在服务器兄弟进程之间共享一些内存,至少在一定程度上是这样。
mod_perl 利用此共享内存,允许各种服务器进程也共享 Perl 代码。但是,有一个问题:您必须确保在发生预先派生之前将 Perl 代码引入 mod_perl。在拆分发生后编译的 Perl 模块和代码将提高每个单独服务器进程的内存需求,而不管另一个进程是否已加载相同的代码。
为了在 Apache 派生子进程之前加载代码,请在配置文件中使用 PerlModule 指令。
例如,如果您使用语句
PerlModule Apache::DBI
在 *.conf 文件之一中,然后
use Apache::DBI;在 PerlHandler 模块中,后一个调用实际上不会加载任何新代码。相反,它使用 mod_perl 在启动时加载的 Apache::DBI 的缓存共享版本。
您可以使用 PerlModule 加载多个模块,使用以下语法
PerlModule Apache::DBI Apache::DBII Apache::DBIII
但是,您只能通过这种方式加载十个模块。如果您想加载更多模块,可以使用 PerlRequire 指令。严格来说,PerlRequire 允许您指定要在 Apache 启动时才评估的 Perl 程序的名称。例如,
PerlRequire /usr/local/apache/conf/startup.pl将在派生 Apache 子进程之前评估 startup.pl 的内容。但是,如果您在 startup.pl 中包含许多 “use” 语句,您可以有效地绕过 PerlModule 的十个模块限制。
请记住,PerlModule 或 PerlRequire 对于模块在不同的 Apache 兄弟服务器进程之间共享是必要的,但它还不够。您仍然需要在您自己的程序中导入模块才能获得好处。
当我第一次开始使用 mod_perl 时,我认为它对于加速 CGI 程序和运行像 Embperl 这样的过滤器很有用。随着我在自己的工作中越来越依赖它,我对 mod_perl 为希望利用 Apache 的强大功能而又不想承担外部程序的开销或与 C 相关的开发时间的程序员提供的强大功能感到惊讶和印象深刻。
正如您所看到的,编写 mod_perl 模块并不困难,并且仅受您的想象力限制。它确实要求您比使用 CGI 时更仔细地考虑您的程序,因为您可以以会降低 Apache 服务器速度或以其他方式损害系统性能的方式影响 Apache 服务器。
