使用 CGI_Lite 保持程序精简
本月,我们将关注 CGI_Lite,这是一个由 Shishir Gundavaram 编写的 Perl 5 模块。CGI_Lite 是 CGI 程序员可用的几个模块之一;其中最著名的是由 Lincoln Stein 编写的 CGI.pm。实际上,在过去的几年中,我几乎在每个“At the Forge”专栏以及许多网站的许多程序中都使用了 CGI.pm。
虽然 CGI.pm 非常有用且功能丰富,但它也很庞大,重达 153KB。在我的 Red Hat 4.2 系统(配备 40MB 内存)上,启动 Perl 5 并加载 CGI.pm 会占用大约 2.7% 的物理内存——超过 1MB——甚至在我分配任何数据结构之前。在流行的 Web 服务器上,很容易想象有多少 CGI 程序同时运行会导致 CPU 和服务器内存的沉重负载,从而导致明显的减速。
解决这个问题有许多方法,包括对 CGI 程序使用 Perl 以外的语言。但是,本月我们将关注另一种解决方案:CGI_Lite.pm,一个功能不如 CGI.pm 但更小更快的模块。CGI_Lite.pm 在磁盘上仅占用 17KB,当与 Perl 5 一起加载到内存中时,在我的系统上仅占用 2.0% 的物理内存,约 800KB。这仍然是相对较大的内存量,但考虑到调用 Perl 5 会占用约 560KB,我认为这是一个合理的折衷方案。
CGI_Lite.pm 不是万能药;它省略了许多年来已添加到 CGI.pm 中的有用功能。但是,如果您的 CGI 程序只需要有限的功能集,并且您希望尽可能保持程序的精简,您可能需要考虑在至少一些程序中使用 CGI_Lite。
在您可以使用 CGI_Lite 之前,您需要从 CPAN(Comprehensive Perl Archive Network,综合 Perl 存档网络)获取副本,这是一组 FTP 和 Web 服务器,可免费向公众提供 Perl 代码、文档和实用程序。截至撰写本文时,CGI_Lite 的最新版本是 1.8,这意味着您可以从 URL https://perldotcom.perl5.cn/CPAN/modules/by-module/CGI/CGI_Lite-1.8.tar.gz 获取它。
如果您阅读本文时 CGI_Lite 已更新,您可能需要更改 URL 最后部分的数字。检索到模块后,您可以使用以下命令解压缩它
tar -zxvf CGI_Lite-1.8.tar.gz
它会解压缩 (-z)、详细 (-v) 文件 (-f)。此操作在我的系统上创建 CGI_Lite-1.8。然后执行标准的 Perl 模块安装,如下所示
perl Makefile.PL make make install请注意,您可能必须以 root 用户身份登录才能在您的系统上安装 CGI_Lite。
模块安装完成后,您可以通过在程序顶部包含以下行在任何程序中使用它
use CGI_Lite;
在您的程序顶部。
当然,包含模块是容易的部分——学习如何使用它可能会更复杂一些。让我们通过创建一个简单的程序来了解如何使用 CGI_Lite.pm,该程序期望从 HTML 表单接收用户的名字。当表单提交后,程序会向用户打印简短的个性化问候语。如果您想知道为什么我们从 HTML 表单和 POST 方法开始,而不是更简单的 GET 方法,请继续关注——它比您想象的要困难。
列表 1 是一个简单的 HTML 表单,包含一个名为 firstname 的文本字段,我们可以将其用于测试。当用户单击此表单中的提交按钮时,firstname 文本字段将通过 POST 方法发送到名为 /cgi-bin/hello.pl 的程序。列表 2 展示了一种使用 CGI_Lite.pm 编写 hello.pl 的方法。
简而言之,此程序执行以下操作
导入 CGI_Lite 模块。
创建 CGI_Lite 的实例。
将 HTML 表单元素检索到哈希中,也称为“关联数组”。
使用 firstname 表单元素的值向用户返回字符串。
现在我们已经对上述程序中发生的事情有了总体了解,让我们更详细地了解一下,并稍微关注一下 CGI.pm 和 CGI_Lite.pm 之间的一些差异。
在 CGI.pm 中,我们可以使用 param 方法检索表单元素。当在标量上下文中调用时,param 允许我们检索单个 HTML 表单元素的值。例如,如果我们已将 $query 定义为 CGI 的实例,我们可以使用以下语句将 firstname 字段的值放在 $firstname 变量中
my $firstname = $query->param("firstname");
如果我们在数组上下文中调用 param,那么我们将获得提交给程序的所有表单元素的列表。例如,如果我们想将所有 HTML 表单元素的名称放入数组 @names 中,我们可以使用以下语句来实现
my @names = $query->param;然后我们可以迭代 @names 以检索并打印与每个表单元素关联的值,如下所示
my $element = ""; foreach $element (@names) { print "<P>$element = ", $query->param($element), "</P>\n"; }我们可以使用 CGI_Lite.pm 完成此操作,但方式略有不同。CGI_Lite.pm 有一个用于检索表单元素的单一方法,该方法使用哈希而不是标量和数组的混合。要检索表单元素,我们使用 parse_form_data 方法,该方法以哈希形式返回结果。因此,检索单个表单元素是一个两步过程。首先,我们将所有元素放入哈希中,然后检索我们感兴趣的元素
my %FORM = parse_form_data; my $firstname = $FORM{"firstname"};如果我们想获取已发送的表单元素的列表,我们可以使用 keys 函数。因此,要将表单元素的名称放入数组 @names 中,我们可以键入
my @names = keys %FORM;我们甚至可以通过在 keys 前面调用 sort 来按字母顺序获取它们
my @names = sort keys %FORM;我们可以通过迭代 @names 并检索我们感兴趣的值来打印所有表单元素的名称和值
my $element = ""; foreach $element (@names) { print ",<P>$element = ", $FORM{$element}, "</P>\n"; }
如果我们知道我们想将一个或多个表单元素放入标量变量中(而不是将它们保留在哈希中),我们可以通过调用 create_variables 方法来实现。例如,在上面的示例中,我们首先必须使用 parse_form_data 才能将表单元素放入哈希 %FORM 中。然后我们必须在单独的步骤中分配 $firstname。如果我们想根据表单的内容分配 10 个变量,我们将需要进行 10 个单独的分配,这相当低效。
为了解决这个问题,我们可以使用 create_variables 方法,该方法会自动为我们创建局部变量。如果我们想将每个表单元素变成它自己的变量,我们可以简单地调用
$query->create_variables(\%FORM);
当此方法返回时,我们为提交表单中的每个元素定义了一个新变量。因此,如果我们有一个名为 firstname 的表单元素,则与该元素关联的值现在可以通过变量 $firstname 获得。%FORM 前面的反斜杠为我们提供了对哈希的引用,这是 Perl 5 中的一项新功能,在 Perl 手册页中有详细记录(在大多数 Linux 系统上键入 man perlref 可用)。
create_variables 存在一个潜在问题,即您的程序可能会定义与一个或多个表单元素同名的变量。例如,列表 3 是 hello.pl 的一个版本,其中我们为变量 $firstname 赋值,并在提交的表单(包含名为 firstname 的元素)上调用 create_variables。
当 $firstname 设置为值 NOT CHANGED 时,如列表 3 所示,当我们调用 create_variables 时,HTML 表单元素 firstname 的值将被忽略,我们收到的问候语是给 NOT CHANGED 的,而不是用户的名字。如果我们注释掉将 $firstname 定义为 NOT CHANGED 的行,create_variables 就可以很好地完成其工作,创建一个名为 $firstname 的变量,并为其提供用户提供的值。就 Web 安全性而言,此行为是一个好主意,但一个或多个变量赋值的静默失败在我看来可能是一个陷阱。
CGI.pm 通过其 import_names 方法提供类似的功能。在这种情况下,作者鼓励用户将名称导入到单独的命名空间中,以确保与现有变量没有名称冲突。
请注意,在列表 3 版本的 hello.pl 中,我删除了 use strict 行。这是为了避免在注释掉定义 $firstname 默认值的行时可能发生的冲突。strict 模块要求您在使用变量之前定义变量;但是,如果我们引用由 create_variables 创建的变量,这是不可能的。
CGI_Lite.pm 非常智能,可以抓取通过以下两种方法之一传递的表单元素:GET 或 POST。POST 通常被认为是两者中较好的方法,因为它通过标准输入 (stdin) 而不是作为 URL 的一部分将表单的内容传递给 CGI 程序。但是,如果我们有兴趣将名称作为 URL 的一部分传递给 hello.pl,我们可以按如下方式操作
http://localhost/cgi-bin/hello.pl?firstname=Reuven
当然,如果您从 Web 服务器以外的计算机测试此程序,则需要将 localhost 替换为服务器的名称。例如,如果您的服务器在 www.fictional.edu 上运行,您可以使用
http://www.fictional.edu/cgi-bin/hello.pl?firstname=Reuven请注意,我们如何在问号后设置变量的值,这在 CGI 行话中称为“查询字符串”。查询字符串是 URL 的一部分,URL 可能不包含空格或其他可能被浏览器和/或服务器误解的“危险”字符。由于这些原因,某些字符必须以“百分比-十六进制”格式发送,其中字符的十六进制 ASCII 值以百分号开头。显然,百分号本身(ASCII 值 0x25)必须以这种方式编码。因此,如果我的“名字”实际上是两个名字,我可以按如下方式发送字符串
http://www.fictional.edu/cgi-bin/hello.pl?firstname=J%20Edgar由于“空格”字符的 ASCII 代码是 0x20(十进制为 32),因此我们可以通过发送 %20 在 URL 中插入空格。CGI_Lite.pm 会自动将百分比-十六进制编码转换为我们需要的 ASCII 代码。
虽然 GET 可以用于发送名称/值对,但它通常用于发送简单的文本字符串。例如,发送一个名称而不分配任何值可能很不错,如下所示
http://www.fictional.edu/cgi-bin/hello.pl?J%20Edgar
当 CGI 程序必须接收 Web 服务器上运行的关系数据库中用户的唯一 ID 时,此技术通常很有用。如果我们发送作为查询字符串一部分的标识符,程序可以抓取该值并将其用作数据库中表的索引,从而生成个性化的主页或为用户定制的其他唯一输出。
几家在线书商使用此方法。当我去 Amazon.com 查看我最新订单的状态时,我会访问一个看起来像这样的 URL
https://www.mybookstore.com/cgi-bin/order.pl?1234-5678-9012
我想要的是一种检索此字符串的简单方法。CGI.pm 允许您通过假装查询字符串的内容已分配给名为 keywords 的变量来获取字符串,因此如果我们使用 CGI.pm,我们可以键入
my $id_number = $query->param("keywords");现在,变量 $id_number 包含值“1234-5678-9012”。
如果我们使用 CGI_Lite.pm,事情会变得有点复杂,因为该模块希望查询字符串仅用于发送名称/值对,而不是单个文本字符串。因此,当我们发送上述查询字符串时,CGI_Lite.pm 假设我们实际上是将名为“1234-5678-9012”的表单元素设置为 null 值——这与我们可能期望的略有不同,但我们可以管理。
一种可能的方法是加载 parse_form_data 以将接收到的名称/值对转换为哈希。哈希包含一个键,该键对应于在查询字符串中传递的数据,CGI_Lite.pm 认为这是一个变量名。然后,我们可以通过获取哈希中的键列表来检索该键。列表 4 是实现此功能的代码。
这不是获取信息的最有效方法,但它确实有效。我们可以简单地从 QUERY_STRING 环境变量中读取信息——但这会引入另一个问题,即以百分比-十六进制编码发送的字符的转换。通过使用 CGI_Lite.pm 的内置功能,我们确保转换正确完成。
如果您觉得这有点令人困惑,那么您并不孤单。我的许多程序都利用了查询字符串,并且不得不假装我的数据实际上是一个变量名,这让我感到有点奇怪。也许未来版本的 CGI_Lite.pm 会处理这个问题,尽管添加太多功能最终会将其变成 CGI_NoLongerLite.pm。
调试 CGI 程序通常很困难,因为执行发生在幕后。与更典型的程序(允许我们在运行时与它们交互)相比,CGI 程序由 Web 服务器调用,输入来自用户的 Web 浏览器(通过 Web 服务器,Web 服务器将数据传递给程序),输出返回给用户的浏览器(再次通过 Web 服务器)。
CGI.pm 提供了两个用于调试 CGI 程序的好帮手。一个 dump 方法,用于打印接收到的所有 HTML 表单变量的内容,以及一个命令行界面,允许程序员在不从 HTML 表单调用程序的情况下输入变量赋值。
为了秉承其轻量级理念,CGI_Lite.pm 不提供命令行调试界面,这可能会使调试大型程序变得困难。但是,它确实提供了一种检查从用户 Web 浏览器接收的数据的方法。print_form_data 方法将所有已知的名称/值对发送到 stdout。如果您的程序无法正常工作并且您想检查输入数据的值,您可以将以下行添加到您的程序中
$query->print_form_data;
考虑到以上讨论,在编写 CGI 程序时您应该使用哪个模块?在大多数情况下,由于多种原因,我倾向于坚持使用 CGI.pm。
首先,我经常使用 CGI.pm 的命令行调试界面,而 CGI_Lite.pm 缺乏这种能力对我来说是一个主要的障碍。当然,解决这个问题是可能的,因为我在 CGI.pm 出现之前编写 CGI 程序有一段时间了,但是拥有另一个调试工具在您的武器库中永远不会有坏处,尤其是在编写大型、复杂的程序时。
我倾向于支持 CGI.pm 的第二个原因是,我经常需要与其他人一起进行项目,并且使用两个不同的 CGI 标准接口可能会给他们带来困难。(我们已经有足够的问题了;我们也不需要尝试记住我们应该使用 param 还是 parse_form_data 来检索信息。)
第三,我发现拥有额外的功能来处理生成 HTML 的小部分很有用。我过去经常忘记在 MIME 标头的末尾放置两个换行符 (\n);使用 CGI.pm,我不再需要记住。
与此同时,我认为编写在开始执行任何计算或分配任何数据结构之前就使用超过 1MB RAM 的小型 CGI 程序有点不负责任。对于我想使用 Perl(而不是 C 等编译语言)但又想最大限度地提高效率的小型项目,我使用 CGI_Lite.pm。我也喜欢使用哈希,这在我看来是存储和检索表单元素的自然方式。此外,CGI_Lite.pm 几乎可以完成我希望的所有事情,包括 HTTP Cookie 和文件上传等高级项目,这使其对小型项目颇具吸引力。
在软件膨胀和程序试图做越来越多的时代,找到一个试图做得更少并且做得好的模块令人耳目一新。CGI_Lite.pm 并非在所有情况下都适用,但它很有用、文档齐全且高效。如果您正试图从您的 Web 服务器中挤出最后的几盎司内存和 CPU 时间,请考虑在您的下一个程序中使用 CGI_Lite.pm——并享受其他项目的额外 RAM。
Reuven M. Lerner 是一位居住在以色列海法的互联网和 Web 顾问,自 1993 年初以来一直使用 Web。在业余时间,他做饭、阅读并参与社区的教育项目志愿者活动。您可以通过 reuven@netvision.net.il 与他联系。