应用所学知识

作者:Reuven M. Lerner

在过去的一年中,我们研究了如何使用 Perl 编写的 CGI 程序来创建复杂的网站。 其中,我们探讨了 HTML/Perl 模板(用于将设计与程序分离)、HTTP Cookie(用于识别回访用户)和关系数据库(用于以易于检索的格式存储信息,同时提高稳健性和安全性)。

本月,我们将了解如何将所有这些技术结合使用。 这种技术的组合在许多商业网站上都有使用,并且是包括 Microsoft 的 Active Server Pages (ASP)、Vignette 的 StoryServer、AOL 免费发布的 AOLServer 和免费分发的 PHP/FI 等多个 Web 服务器项目背后的基本思想。

请注意,本月专栏中的许多示例都参考了先前《Linux Journal》期刊中讨论的技术和思想。 如果您是本杂志或这些想法的新手,请参阅“资源”侧边栏,它应该为您学习这些主题提供一些良好的起点。

我们的示例网站将围绕一个跟踪用户生日的单表展开。 一旦我们创建了生日表,我们将编写一个 CGI 程序,允许用户将其生日输入到表中。 最后,我们将结合使用 Cookie 和 Perl/HTML 模板来创建个性化的主页,从中提取数据库中存储的信息。

假设 MySQL 是我们的关系数据库服务器,并且我们的表将在“test”数据库中,请在提示符下键入命令

mysql test

这将启动 MySQL 客户端,允许我们以交互方式输入 SQL 查询。 这些示例中使用的“test”数据库随 MySQL 一起提供,并且完全不安全——它减少了解释如何保护使用 MySQL 创建的新数据库所需的空间。 因此,您应该认真考虑为每个 Web 应用程序创建单独的数据库和用户,以降低未经授权的用户修改或查看系统数据的风险。

首先,我们必须创建一个包含有关用户及其生日信息的表。 以下是一些创建此类表的简单 SQL 命令

mysql> create table birthdays (person_id int unsigned
auto_increment,
        ->   firstname varchar(15) not null,
        ->   lastname varchar(15) not null,
        ->   email varchar(50) not null primary key,
        ->   birthdate date not null,
        ->   key id (person_id));

这将在“test”数据库中创建“birthdays”表。 该表有五列(person_id、firstname、lastname、email 和 birthdate),所有列均不能为空。 表中的每一行代表一个不同的用户,我们希望跟踪其出生日期; 我们通过将“email”列设置为主键来确保不重复输入用户,这是一种花哨的说法,表示“email”的任何值都不能重复。 由于用户可能拥有多个电子邮件地址,因此我们无法确保用户不会两次输入其生日。 但是,这很可能会减少这种重复,并且比使用很少唯一的姓名要好。

第一列 person_id 将在每次我们向数据库添加用户时由 MySQL 自动设置。 系统中的第一个条目的 person_id 将设置为 1,第二个条目将设置为 2,依此类推。 由于 person_id 的类型为 int unsigned,因此我们的系统最多可以接受 4,294,967,295 个唯一条目——小于您的特定数据库可能需要的数量,但对于我的大多数用途来说已经足够大了。

我们可以在“mysql”提示符下使用“describe”命令来了解数据库的良好概况,如下所示

mysql> describe birthdays;
Field       Type    Null        Key     Default Extra
person_id   int(10) unsigned    MUL     0       auto-increment
firstname   varchar(15)
lastname    varchar(15)
email       varchar(50)         PRI
birthdate   date                        0000-00-00
将数据输入到表中

现在我们已经创建了基础架构,我们需要应用程序来允许人们输入他们的生日。 最简单的方法是创建一个 HTML 表单,其内容提交给 CGI 程序,该程序从表单中获取信息并将其保存到数据库中。 清单 1 中显示了这样一个表单。

该表单相对简单,尽管由于使用“select”和“option”标记定义的长选择列表,它可能看起来有点令人生畏。 使用此类列表,而不是允许用户在文本字段中输入生日,可以减少用户可能输入的错误数量。 当然,有人可能会创建一个具有相同字段名称的 HTML 表单,使用文本字段而不是选择列表,从而绕过我们的系统。 因此,寓意是您应该始终尝试减少用户可能输入的错误数量,但始终检查以确保数据输入正确。

正如您从查看清单 1 中看到的那样,我们的表单收集六条信息:名字、姓氏、电子邮件地址以及用户的出生月份、日期和年份。 后三条信息是分开的,以便我们可以简化用户界面; 正如我们很快将看到的,将这些信息组合起来以创建有效的 MySQL “date”类型非常简单。

我们表单的“form”标记指示应使用 POST 方法将数据提交到名为“enter-birthday-info.pl”的 CGI 程序。

我们获取用户的输入并创建一个 SQL 查询,该查询在表中创建一个新条目

# Now that we have the basic information, create
# an SQL query
my $command = "insert into birthdays ";
$command .= "(firstname, lastname, email, birthdate) ";
$command .= "values ";
$command .= "(\"$firstname\", \"$lastname\",
\"$email\", \"$birthdate\")";

当然,您不需要将命令存储在变量 $command 中。 实际上,您可以在使用 $dbh->query 时直接创建命令,而不是将其放在一起然后将 $command 作为参数传递给 $dbh->query。 以这种方式将查询放在一起使得在编程时更易于阅读,并且在出现错误时更易于将 SQL 查询发送到屏幕。

在我们向数据库服务器发送查询后,行可能已添加。 但是,我们不想仅仅假设它已添加,因为可能发生了一些严重的事情,并且我们希望向用户正确指示结果。

首先,我们检查 $dbh->errno(MySQL 返回的错误值)是否设置为 2000。 这是尝试插入与另一行冲突的行时返回的特定错误代码。 由于我们将“email”定义为主键,因此如果 errno 设置为 2000,则我们尝试输入重复的电子邮件地址的可能性相当高

if ($dbh->errno == 2000)
   {
        &log_and_die(
"There is already an entry in\ the database for \"
$email\". Try another\ e-mail address!");
   }

如果不是这种情况,那么我们应该检查是否有其他错误。 检测错误的最简单方法是查看 $sth 是否未定义; 如果它没有被赋予任何值,则发生了错误,我们会在错误日志中为用户标识该错误。 请注意,我们的一般错误捕获机制需要在捕获错误 2000 的机制之后。

elseif (!defined $sth)
   {
     &log_and_die("MySQL error " . $dbh->errno .
"\ on command \"$command\"<P>" . $dbh->errmsg)
 unless (defined $sth);
   }
最后,如果没有发生错误,那么我们可以打印一条消息指示成功
else
   {
        # Return something to the user
        print "<P>Done!</P>\n";
   }
跟踪用户

现在用户可以将有关他们生日的信息输入到系统中。 但是请稍等——在本专栏的开头,我说我们将使其能够跟踪用户的来来去去。 一种简单的方法是使用 HTTP Cookie,它是存储在用户计算机上的小数据片段,发送到设置它们的站点。 CGI 程序可以在每次向用户的浏览器返回 HTTP 响应时设置 Cookie。 然后,无论设置了哪些 Cookie,都会在后续每次访问该 Web 服务器时返回。 这样,它们可以用作变量,尽管这些变量可能会被担心隐私的用户修改或删除,或者这些用户可能会从朋友的计算机访问我们的站点。

我们的 Cookie 必须是一个唯一的标识符,可用于从“birthdays”表中调出用户的条目。 因此,我们有两种选择——用户的电子邮件地址,它保证是唯一的,因为它是一个主键,以及 person_id,它在每次向表中添加新条目时自动递增。 我们将使用 person_id,但是没有理由不能使用“email”列。 实际上,考虑到“email”是主键,您甚至可以取消 person_id 列——除非您要创建引用单个用户的其他表,否则跟踪整数(例如 person_id)比使用其完整的电子邮件地址高效得多。

我们如何检索添加到我们可能添加的行中的 ID? 最明显的方法是检索我们刚刚输入的行,创建并向 MySQL 服务器发送 SQL 查询。 但是 MySQL 有一种更简单的方法来做到这一点——在发送我们的查询后,我们可以请求 $sth->insertid$sth 是“语句句柄”,它是一个允许我们发送和检索有关单个 SQL 查询和语句的信息的对象,而 insertid$sth 提供的方法之一。

一旦我们知道 person_id 的值,我们就可以使用以下两行 Perl 代码创建一个 Cookie

my $cookie = $query->cookie(-name => "person_id",
        -value => $sth->insertid);

Cookie(现在存储在 $cookie 中)作为 CGI 程序返回给用户浏览器的 HTTP 标头的一部分发送。 因此,我们程序的原始版本(请参阅 清单 2)在程序执行开始时使用命令发送了一个基本的 MIME 标头

print $query->header("text/html");
但是我们程序的新版本必须将其移动到程序的稍后部分,在我们已经将查询发送到数据库之后。 此外,我们将不得不修改我们的语句,以便它将 Cookie 与标头中的 MIME 信息一起发送。 这可以通过以下代码实现
print $query->header(-type => "text/html",
                         -cookie => $cookie);
当上述代码运行时,它会发送 MIME 标头(描述我们正在发送给客户端的输出类型)和描述我们要设置的“person_id”Cookie 的信息。 今后,每当此用户访问我们的站点时,我们将能够检索“person_id”的值,从而在数据库中的表中查找该用户。

您可以在 清单 3 中看到更改我们程序的结果。 修改非常小,但它们确保每当我们成功向数据库添加一行时,都会将 Cookie 返回到用户的浏览器。 (当没有行添加到数据库时,标头保持不变,发送 MIME 标头,但没有其他内容。)

检索数据

我们系统的最后一部分将 Cookie 和数据库与我们几个月前讨论的另一项技术 Perl/HTML 模板结合在一起。 模板是 HTML 页面,在 HTML 标记之间散布着 Perl 的小片段,通过一个简短的程序访问,该程序使用 Text::Template 模块将 Perl 转换为文本。 这允许 HTML 页面执行数据库查找、计算和其他需要计算的元素,这些元素太难通过服务器端包含来完成。 与直接 CGI 程序不同,模板可以由您网站的设计人员和 HTML 编码人员编辑,从而消除了程序员成为更改程序生成的 HTML 的瓶颈。

清单 4 中,您可以看到 wrapper.pl,这是一个将模板转换为 HTML 的 CGI 程序。 (先前一期 LJ 中发布的 wrapper.pl 版本未经彻底测试,并且包含在此版本中修复的几个错误。)假设 wrapper.pl 已安装在您的系统上,并且文件 /home/httpd/html/birthdayhp.tmpl 是一个有效的模板,您应该能够请求

/cgi-bin/wrapper.pl?/home/httpd/html/birthdayhp.tmpl

换句话说,应该使用单个参数调用 wrapper.pl,即应该评估然后返回的模板的名称。

此版本的 wrapper.pl 允许我们使用 LJ 包(如 Text::Template 的手册页中所述)将变量传递给模板。 这允许我们将变量值从 wrapper.pl 传递到模板。 通常,我们不希望将变量值从 wrapper.pl 传递到模板,因为模板应该在某种程度上与其周围环境隔离,并且应该允许分配自己的变量。 但在这种情况下,我们确实想传递一个值,即 $query(CGI 实例),这使我们能够根据 CGI 规范访问传递给 wrapper.pl 的信息。 这包括“cookie”方法,它允许我们检索服务器过去设置的 Cookie。

我在 清单 5 中包含了一个 birthdayhp.tmpl 的简单版本,以便您可以看到将 Perl 包含在 HTML 中有多么容易。 以这种方式提供文档会带来性能损失,因为每次在您的系统上查看模板时,您都会强制调用 Perl。 但模板的多功能性和易于包含在站点中通常会抵消这种缺点。 大型网站的编辑和制作人员可以修改网站的内容和设计,而不会干扰网站运行所需的程序。 程序被花括号包围,使其易于在 HTML 文件中找到。

在处理模板时要记住的一件事是,每个 Perl 片段的输出都会插入到生成的 HTML 中。 如果您希望从 Perl 片段中向用户的浏览器发送文本“Hello, there”,为了使事情正常工作,您必须使用

{
    "Hello, there"
  }

或稍微更正式的版本

{
    my $outputstring = "";
    $outputstring .= "Hello, there";
    $outputstring;
  }
不要犯在模板中尝试使用 print 的错误,就像在这种情况下一样
{
    my $outputstring = "";
    $outputstring .= "Hello, there";
    print $outputstring;
  }
由于 print,$outputstring 的内容确实会发送到用户的浏览器,但文本将在模板的其余部分之前发送。 在包含此代码的 Perl 代码块中,Text::Template 模块将插入 print 的结果——打印成功时,结果的值将为 1。 在模板的情况下,设置字符串而不是直接打印是常态,但如果您是一位经验丰富的 Perl 程序员,则需要一些时间来适应。
将所有内容整合在一起

现在的诀窍是创建一个执行以下操作的模板

  • 从用户浏览器可能发送的任何 Cookie 中获取 person_id 的值。

  • 使用 person_id 的值从数据库中检索有关用户的信息。

  • 使用数据库中的信息创建一个个性化的 HTML 页面,向用户问候。

获取用户 Cookie 的值几乎与设置它一样简单。 正如我们的 CGI 程序可以在向用户的浏览器返回 HTTP 标头时设置一个或多个 Cookie 一样,Cookie 的值会在我们可能收到的任何请求随附的 HTTP 标头中发送到我们的服务器。 一旦我们的 person_id Cookie 在用户的计算机上设置,每次访问我们的站点都会以 person_id 的名称和值作为前缀。 我们可以通过调用“cookie”方法来检索我们系统上所有 Cookie 的值,就像我们使用它来创建 Cookie 一样。

清单 6 包含一个非常短的 CGI 程序 (show-cookies.pl),它打印发送到特定服务器的所有 Cookie 的名称和值。 请记住,Cookie 仅发送到最初设置它们的服务器——因此,虽然您的浏览器可能包含大量 Cookie 名称-值对,但 show-cookies.pl 的输出将仅显示您的服务器创建的 Cookie。

在 show-cookies.pl 中,我们迭代发送到 Web 服务器的每个 Cookie。 但是对于我们的特定目的,我们只对单个 Cookie “person_id”感兴趣。 我们可以通过以下方式使用“cookie”方法来检索它

my $person_id = $query->cookie("person_id");

假设名为“person_id”的 Cookie 已发送到 Web 服务器(意味着“person_id”Cookie 过去已由该服务器上的程序设置),则其值现在将在变量 $person_id 中可用。 然后,我们可以将 $person_id 用作我们“birthdays”表中的唯一键,从而使我们能够检索有关回访用户的信息。

除其他外,我们必须确保模板处理具有 Cookie 的用户可能不会出现在数据库中,或者用户可能在没有 Cookie 的情况下转到自定义主页的可能性。 在测试模板 (cookie.tmpl) 中,这是以一种相当粗糙的方式处理的,即打印出用户的“person_id”Cookie 的值以及数据库中与 person_id 的此值匹配的行数。 在 清单 7 中显示的示例模板中,我们包含以下代码

if (($person_id == 0) || ($sth->numrows == 0))
  {
    $outputstring .=
        "<P>Error retrieving information.</P>\n";
    $outputstring .= "<P>person_id (cookie) = \"
        $person_id\".</P>\n";
    my $numrows = $sth->numrows;
    $outputstring .=
      "<P>Rows returned from table = \"
      $numrows\"</P>\n";
    $outputstring .=
        "<P><a href=\"/birthday.html\">";
    $outputstring .=
        "Enter your birthday</a></P>\n";
  }

此代码检查 $person_id(从用户的浏览器作为 Cookie 发送,并且应对应于“birthdays”表中的单行)是否等于 0,这也可能意味着它未设置。 如果 $person_id 为 0,那么我们没有此用户的 Cookie 记录。 这并不一定意味着用户以前从未访问过我们的网站——有些用户出于隐私考虑拒绝 Cookie,其他用户使用多个浏览器(每个浏览器都保留自己的 Cookie 列表),还有一些用户可能正在从多台计算机访问 Web。 但是我们的系统确实确保从同一台计算机(和同一浏览器)访问我们网站的用户每次访问我们的系统时都会看到他们的生日显示。

我们还将 $sth->numrows 与 0 进行比较,以查看是否没有从数据库返回行。 用户很可能很久以前访问过我们的站点,并且来自该访问的 Cookie 仍然保留在该用户的计算机上——但是数据库中所有早期访问者的条目都以某种方式被删除。 在这种情况下,$sth->numrows 将返回 0(这意味着没有行的 person_id 列与用户 Cookie 中的 $person_id 匹配),并且我们必须请求用户输入新的生日条目。

如果查询确实返回了一行(并且我们知道它最多会返回一行,因为 person_id 必须是唯一的),那么我们必须使用 $sth->fetchrow 抓取该行,然后将结果数组的值读入我们的变量中。 在这种特殊情况下,我们所做的只是打印它们

{
        while (my @arr = $sth->fetchrow)
        {
          my ($firstname, $lastname, $email,
                $birthdate) = @arr;
        $outputstring .=
                "<P>firstname =
\"$firstname\"</P>\n";
        $outputstring .=
                "<P>lastname =
\"$lastname<"</P>\n";
        $outputstring .=
                "<P>email = \"$email\"</P>\n";
        $outputstring .=
                "<P>birthdate =
\"$birthdate\"</P>\n";
        }
}

当然,如果我们有兴趣做一些更有趣的事情,我们可以通过获取 $sth->fetchrow 返回的值,并在 HTML 页面的标题或今天与用户的出生日期的比较中使用结果变量来做到这一点。 关键是数据库是一种在 CGI 程序调用之间存储信息的方法。 一旦从数据库中将信息读入 CGI 程序,我们就可以像在调用开始时分配变量一样轻松地使用该信息。

总结

大多数人不需要被提醒他们的生日。 实际上,在本示例中使用生日只是为了演示目的。 即使使用我们存储在数据库中的有限信息,我们也可以创建一个最基本的个性化主页,在标题中显示用户的姓名。 稍加努力,我们就可以在该用户的生日时打印一条特殊消息,或者指示距用户下一个生日还有多少天。

并且由于我们将所有用户的生日都存储在数据库中,因此我们可以创建访问系统中其他生日的应用程序。 例如,我们可以创建一个 CGI 程序(或 Perl/HTML 模板),用于查找系统中与您生日相同的其他用户。 可能性是无限的,将信息放入模板意味着您(作为程序员或网站管理员)可以专注于编写使事情运行所需的代码,而网站的编辑和制作人员可以使事情看起来漂亮并确保它们在语法上也是正确的。

至此,我们结束了我们将多种技术集成到单个网站的旋风式(尽管比平时更长)之旅。 基于数据库的网站越来越受欢迎,这是有充分理由的。 最大和最著名的网站将后端数据库与模板和 Cookie 结合起来,为每个用户提供个性化的体验; 既然您已经了解了如何做到这一点,请在您自己的网站上创建一些。

资源

Using What We've Learned
Reuven M. Lerner 是一位居住在以色列海法的互联网和 Web 顾问,自 1993 年初以来一直使用 Web。 在业余时间,他烹饪、阅读并为社区的教育项目做志愿者。 您可以通过 reuven@netvision.net.il 与他联系。
加载 Disqus 评论