CGI 编程
您是否经常遇到这种情况:您完成了一个出色的 CGI 程序,将其放置在您的服务器上,启动它,然后……令您懊恼的是,您发现浏览器告诉您发生了“服务器错误”,而不是程序预期的输出?
遇到这种情况并不丢人;经验丰富的 CGI 程序员每周都会多次看到此消息。虽然经验可能会减少程序员犯错误的次数,但更多时候,它教会了他或她如何处理此类问题。
本月,我们将研究使 Web 服务器能够运行您的程序所需的所有条件。这些问题往往会给程序员带来很多麻烦,部分原因是它们改变了我们从其他编程经验中习惯的编程范例。
大多数程序都是直接运行的,从命令行(或图形用户界面)调用,然后在您的屏幕上显示。但是 CGI 程序是不同的;它们几乎总是在与用户所坐的计算机不同的计算机上运行。更糟糕的是,CGI 程序与用户之间隔了两层——用户告诉浏览器他或她想做什么;然后浏览器必须将该数据传递给服务器,服务器再将请求(和任何参数)传递给程序。因此,调试 CGI 程序的秘诀之一只是理解服务器正在调用该程序。
就像您系统上的所有其他程序一样,CGI 程序必须是可执行的。当您使用 C 和 C++ 进行编码时,记住这一点并不难,因为 gcc 和其他编译器会生成一个可执行文件,该文件具有正确的执行位。但是对于我们这些主要用 Perl(以及类似的语言,例如 Tcl 和 Python,它们使用 Unix shell 的解释器)编写程序的人来说,我们需要确保程序在运行之前是可执行的。这听起来可能很明显,但请相信我——从我收到的电子邮件来看,我可以向您保证,许多编写和安装 CGI 程序的人忘记确保他们的程序不仅对自己可读和可执行,而且对 Web 服务器运行的用户 ID 也可读和可执行。
因此,如果您在运行 CGI 程序时收到错误消息,请转到程序所在的目录,并使用 Unix “chmod”命令使其可执行。由于您希望使其对系统上的所有用户都可执行——考虑到 Web 服务器通常在“nobody” ID 下运行,这是一个好主意——我们可以发出以下命令
chmod a+x progname
因此,如果我的程序名为“progname”,我们可以双重检查以确保该程序是可执行的,方法是发出带有 -l 选项的 ls 命令,表明它应该以“长列表格式”提供信息。这意味着我们希望查看有关文件的权限、所有权和修改日期以及名称的信息。如果我们列出我们的程序,我们应该看到类似这样的内容
-rwxrwxrwx 1 reuven reuven 12779 Sep 5 15:28 progname
以上意味着“progname”由名为“reuven”的用户和名为“reuven”的组拥有(在我的个人 Linux 机器上并不罕见!),它最后一次修改是在 9 月 5 日下午 3:28,长度为 12779 字节。但最重要的是,我们看到字符串“rwx”重复了三次——首先是针对用户(在本例中为“reuven”),然后是针对用户的组(在本例中再次为“reuven”),然后是针对系统上的所有人。因此,“rwx”的三个实例意味着系统上的任何人都可以读取、写入和执行 progname,这正是我们希望的情况。
如果程序是可执行的但仍然给您带来问题,那么您的下一步应该是从命令行运行该程序。正如我之前所说,我们始终必须记住,Web 服务器(很可能与我们的机器不同)代表我们执行程序,这意味着我们通常看到的不是程序的输出,而是我们的浏览器对该输出的处理结果!当我们确切知道程序在做什么时,通常最容易进行调试,因此在 Web 服务器本身上执行程序只能有所帮助。
因此,如果程序名为“progname”,请更改到其目录(使用 cd 命令)并键入
./progname
为什么要用 ./ 前缀程序的名称?因为您的 shell 的 PATH 变量通常不会——并且可能不应该——包含您系统的 CGI 目录,这意味着如果您只是键入程序的名称,您的 PATH 可能无法工作。除非您已经在 PATH 中包含了 .,也称为当前目录,这非常方便,但也存在潜在的安全风险。我只想说,上述方法始终有效,而省略 ./ 并不总是奏效。
当您执行程序时,您看到了什么?最重要的是,MIME 标头是程序发送到其输出的第一件事。浏览器依赖 MIME 标头来了解程序将要发送的数据类型,如果没有这样的标头,您几乎总是会从浏览器收到错误。为什么?因为如果浏览器期望收到 MIME 标头,但实际上收到了来自您的程序的 HTML 格式的响应,它通常不知道该怎么办。您的浏览器不会不正确地显示数据,而是简单地(以其简洁的方式)表示它不理解应该期望哪种数据。您有责任确保在任何情况下,发送到标准输出的任何内容都以 MIME 标头开头。
虽然没有足够的空间在本月深入探讨 MIME 标头的复杂性,但我想说,除非您的程序要输出 HTML 以外的内容,否则您应始终确保它打印的第一件事是
Content-type: text/html
另请注意,此字符串后应跟两个换行符(C 和 Perl 中的 \n),而不是我们习惯于在文本字符串后放置的一个换行符。也就是说,Perl 程序员应该写
print "Content-type: text/html\n\n";
而那些(明智地)在 Perl 中使用 CGI.pm 模块的人应该能够执行以下操作
use CGI; # Bring in the CGI module my $query = new CGI; # Create an instance of CGI print $query->header("text/html"); # Output the header
为什么要使用两个换行符?简而言之,这两个换行符将程序的标头(即关于响应的信息)与其返回的内容(即响应本身)分隔开。您可能一直使用的这个系统至少有一个主要的先例——电子邮件!如果您曾经查看通过 SMTP(Internet 的简单邮件传输协议)传输的电子邮件消息,您会看到消息的标头和内容之间用一个空行分隔。
从命令行运行程序时,您还应该注意什么?如果您的程序使用 GET 方法调用(即,如果它被调用,就好像它是一个文档,除了命令行上的一些参数之外没有任何参数),那么从命令行调用它应该会产生一些输出,即使由于缺少任何输入参数,输出会有点奇怪。但是,如果您的程序将环境变量 QUERY_STRING 作为输入源,那么您可以轻松地从 Unix shell 设置该值。如果您在系统上使用 bash 或某些类似的 sh/ksh 变体,那么您应该能够键入
export QUERY_STRING="hello+there" ./program-name
请注意,虽然您不必以 Web 的“百分号十六进制”编码方案(其中奇怪的字符被百分号替换,后跟表示其 ASCII 代码的十六进制数)在 QUERY_STRING 中编写字符,但我建议您这样做,只是为了在尽可能接近程序将被调用的环境中测试您的程序。请记住,如果我们想知道事情为什么出错,我们需要假装是 Web 服务器调用程序——否则我们可能会错过与编码(或缺乏编码)相关的细微错误。
测试使用 POST 的程序有点棘手,因为当您自己调用软件时,名称/值对很难模拟。但幸运的是,对于我们这些使用 Perl 的人来说,CGI.pm 模块允许我们从命令行运行我们的程序,而不会遇到太多麻烦。如果您调用一个使用 CGI.pm 的程序,您将看到
./program-name (offline mode: enter name=value pairs on standard input)
此时,您可以输入 HTML 表单中应该调用 CGI 程序的所有元素的名称和值,然后按 Control-D。当然,您不一定需要输入所有表单元素,特别是如果您正在测试大型表单——毕竟,调试应该节省您的时间!但是,能够键入
name=Reuven email=reuven@NetVision.net.il address=17+Disraeli+Street equation=2%2B2%3D4 <control-D>
并查看我的程序使用该输入的結果,而不是必须向我的程序添加大量打印语句——这是一个经过实践检验的系统,我当然也使用它,但这通常要容易得多。请注意,在上面的示例中,我的地址在单词之间用加号输入(因为空格字符不能作为 URL 的一部分传输),并且加号 (+) 和等号 (=) 以百分号十六进制格式编码。
既然我已经稍微谈到了 Perl,让我提醒所有 Perl 程序员在运行程序时使用 -w 标志!它产生的警告非常宝贵,尤其是与更详细地描述问题的“diagnostics”包结合使用时。您还应该认真考虑使用“strict”包,该包强制您比许多 Perl 程序员习惯的方式更正式地声明变量——但权衡是 Perl 编译器随后能够在查看程序时识别潜在问题。因此,我的大多数 CGI 程序的前几行都有以下内容
/usr/local/bin/perl5 -w use strict; # Interpret variable and # subroutine names strictly use diagnostics; # Display documentation # regarding each warning # and error. use CGI; # Bring in the CGI module
至此,我们知道程序是可执行的,它在任何其他输出之前发送正确的 MIME 标头,并且如果从命令行调用,它似乎至少以基本方式工作。
因此,假设没有任何逻辑错误——即使所有这些后勤错误都已消除,逻辑错误仍然是一个重要的障碍需要克服——我们应该确保程序正在已配置为运行 CGI 程序的目录中运行!
首先,让我们考虑一下大多数程序的工作方式:当您从服务器请求文档时,它通常会确保该文件存在。如果文件存在,服务器会将文档返回给浏览器;如果不存在,服务器将返回一条错误消息,指示未找到该文件。
CGI 程序的工作方式略有不同——在这种情况下,服务器返回程序返回的任何输出,而不是程序本身。因此,服务器尝试执行该文件,就好像它是一个程序一样,然后获取该程序返回的任何内容并将其传递给用户。
现在的问题是:服务器如何知道应该简单地返回哪些文件,以及应该首先执行哪些文件?
在大多数系统中,这种区分是通过分配多个目录来作为 CGI 程序目录来实现的。这些目录传统上称为“cgi-bin”,包含我们 CGI 程序的二进制文件(即可执行文件)。这些目录中包含的任何内容都被认为是程序,并被执行;其他任何位置的内容都被认为是文本文件。因此,如果您的程序位于非 CGI 目录中,则其内容将作为文本文件发送给用户,显示的内容大致与您使用 Unix more 命令看到的内容相同。同样,如果您将 HTML 文档之一放在 CGI 目录中,请求它几乎肯定会导致错误,因为服务器会尝试执行它!
另一种方法不太常见,它允许用户将 CGI 程序放置在系统上的任何位置,只要它们的文件名具有某些后缀即可——例如,.cgi 或 .pl。好消息是,这允许用户拥有自己的 CGI 程序,而无需系统管理员在每次用户想要在其主页上拥有计数器或其他常见 CGI 程序时都配置新的 CGI 目录。但是,CGI 程序可能存在安全风险,尤其是当系统上的任何用户都可以编写或安装他或她可能想要的任何程序时。大多数系统管理员宁愿被请求创建新的 CGI 目录,也不愿冒另一种方法可能存在的潜在安全漏洞的风险。
在上述任何一种情况下——cgi-bin 目录方法或“任何目录”方法——遇到 CGI 程序问题的用户都应确保他们的程序位于正确的目录中,CGI 目录已设置为 CGI 程序,并且它们具有正确的后缀。我经常收到人们的电子邮件,他们想知道为什么每次尝试执行 CGI 程序时都会收到错误cannot perform POST method to the non-script。这条公认的简洁而神秘的错误消息试图表达的是,“您尝试从 HTML 表单向一个看起来不像程序的文件发送数据。如果您想将其设置为程序,请确保通过使用适当的配置在您的系统上将其定义为程序!”
那么,解决方案是确保您的程序位于声明为 CGI 程序的目录之一中。您的系统管理员应该能够通过使用 ScriptAlias 指令(对于 NCSA httpd 和 Apache)或 Exec 指令(对于 CERN httpd)来做到这一点。完成此操作后,您的程序应该可以运行——但我再次强调,仅仅因为上述所有步骤都井然有序,并不意味着您的程序就能工作!这只是意味着您的服务器将能够运行它,并且该程序将至少向您的浏览器返回一些结果。
Reuven M. Lerner 自 1993 年初以来一直在玩 Web,当时它看起来更像是一个有趣的玩具,而不是世界的下一个伟大媒介。他目前在以色列海法的公寓里担任独立的互联网和 Web 顾问。当不从事 Web 工作或非正式地为学龄儿童做志愿者时,他喜欢阅读(几乎任何主题,但尤其是计算机、政治和哲学——单独和一起)、烹饪、解决纵横字谜和远足。您可以通过 reuven@the-tech.mit.edu 或 reuven@netvision.net.il 与他联系。