创建基于 Web 的 BBS,第 1 部分
在去年的一段时间里,Web 上的流行词是“社区”。每个人都想建立一个虚拟社区,让人们在线互动,就像他们在现实生活中互动一样。
虽然虚拟社区被(并且仍然是)过度炒作,但互联网确实产生了一些这样的在线群体,其中许多成员从未见过面。如果您正在阅读这本杂志,您可能至少参与了一个电子邮件列表、聊天系统或 Usenet 新闻组。事实上,如果不是开发者和用户通过互联网相互分享信息的社区,Linux 可能不会取得今天的成功。
有几种方法可以创建在线社区,最古老和最著名的方法是电子邮件列表。设置邮件列表相对容易,只需最少的资源即可保持列表运行。另一个流行的选择是 Usenet 新闻组,它使用类似的格式,但使用与电子邮件不同的分发机制。
还有一种选择是基于 Web 的公告板系统。虽然这种系统既不如 Usenet 或电子邮件列表那样灵活或强大,但它们确实提供了一些优势。它们可以抵抗垃圾邮件,可以轻松集成到网站的其他方面,并让网站访问者有机会参与讨论,而无需注册。许多商业网站现在都为其用户提供公告板,希望将其网站变成真正的互动和双向体验,而不是另一个内容分发媒介。
从本期开始,我们将用三部分来了解如何创建我们自己的简单公告板系统。这个项目是由读者 Dwight Johnson 建议的,并且也受到了我创建的“At the Forge”主页的影响,该主页将包含这些专栏中介绍的程序示例,以及供读者讨论这些程序的中心场所。
本月,我们将了解 ATF 站点上使用的公告板系统的基本内部结构。您将会看到,我已决定保持软件和 BBS 非常简单,没有某些高级功能,例如层次结构和线程。但是,将这些功能添加到软件中,或以此为基础构建更高级的系统应该不难。下个月,我们将添加足够的功能使其成为可用的 BBS。最后,在本系列的第三部分中,我们将研究可以向系统添加许多有用功能的方法。
首要考虑因素是 BBS 的外观和感觉,因为这将迫使我们在许多其他问题上做出决定。正如我在上面指出的那样,我的目标是使此软件尽可能简单。我决定以非层次结构的方式保持讨论。每条消息都属于 BBS 中的单个主题。我们不会跟踪回复或允许子主题。主题中的消息将按时间顺序排列,从最新消息到最旧消息。
因此,用户在任何给定时间点都有几个可能的选项:开始新主题、向现有主题发布新消息、列出现有主题或浏览一个主题中的消息。
虽然我曾短暂考虑将消息存储在 ASCII 文本文件中,但我很快决定使用关系数据库。数据库使处理未来的扩展变得更容易,因为可以通过向表中添加一个或多个列来提供更多功能。数据库还使我们不必担心文件格式、锁定以及在使用 ASCII 文本文件时不可避免地出现的其他问题。
我选择的数据库是“大部分免费”的 MySQL。程序将用 Perl 编写,并将使用 Perl 的数据库接口,称为 DBI。有关任何或所有这些的信息,请参阅“资源”侧边栏。
如果您在过去几个月中一直在关注本专栏,您可能会惊讶地发现我使用简单的 CGI 程序实现了它。我本可以使用 mod_perl,这是一个将 Perl 二进制文件嵌入 Apache HTTP 服务器内部的模块。我也本可以使用 HTML::Embperl,我们在本专栏的前两期中探讨过的模板语言。
但是,现实往往是令人信服的因素,而我使用的 Web 空间提供商尚未安装 mod_perl。这些程序应该可以在 Apache::Registry 下正常运行,Apache::Registry 是 mod_perl 的模块,它提供了 CGI 标准的模拟。
如果我们要将信息存储在关系数据库中,那么第一个技术决策涉及数据库本身。我们想存储什么信息,以及我们想如何存储它?
因为我们正在存储消息和主题,所以我将系统设计为两个表,ATFThreads 和 ATFMessages。每条消息,包括有关作者和发布日期的信息,都存储在 ATFMessages 中。表中的每条消息都指向 ATFThreads 中的单个主题,从而允许我们按主题对消息进行排序。
例如,这是 ATFThreads 的定义
CREATE TABLE ATFThreads ( id SMALLINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, subject VARCHAR(255) NOT NULL, author VARCHAR(60) NOT NULL, email VARCHAR(60) NOT NULL, text TEXT NOT NULL, date DATETIME NOT NULL, UNIQUE(subject) );
每个主题都存储在数据库的单行中,由其 id 列唯一标识,我们将其定义为 SMALLINT UNSIGNED。(因此我们允许 65,535 个不同的主题,这目前应该足够了。)通过将该列声明为 AUTO_INCREMENT,我们要求 MySQL 每次插入新行时都为 id 列提供一个新值。通过将其声明为 PRIMARY KEY,我们表明 id 列将唯一标识一行。
其他列是不言自明的:subject 包含主题的主题,而 author 和 email 分别包含主题创建者的姓名和电子邮件地址。
每个主题都有一个开始讨论的开头消息;它存储在类型为 TEXT 的列的 text 列中。TEXT 字段可以包含比 VARCHAR 列给我们的 255 个字符最大值更大的文本量。VARCHAR 列会去除尾随空格,从而在处理数据库时至少省去一项内务处理工作。
最后,我们为每个主题提供一个 date 列,我们在其中使用 DATETIME 元素记录创建日期和时间。我们还使用表定义末尾的 UNIQUE 关键字确保主题的人类可读主题行是唯一的。这可以防止我们有两个名为“Problems with MySQL”的主题,例如。
现在我们已经了解了如何创建 ATFThreads,我们可以定义 ATFMessages。两者非常相似,主要区别在于对主题 ID 的引用
CREATE TABLE ATFMessages ( id MEDIUMINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, thread SMALLINT UNSIGNED NOT NULL, subject VARCHAR(60) NOT NULL DEFAULT "No subject", date DATETIME NOT NULL, author VARCHAR(60) NOT NULL DEFAULT "Mr. Nobody", email VARCHAR(60) NOT NULL DEFAULT "atf@lerner.co.il", text TEXT NOT NULL );
再一次,我们创建一个带有名为 id 的自增主键的列。不同的表可以具有同名的键,就像不同的哈希可以一样。如果我们在单个查询中引用两个表,我们可以使用 table.column 语法来区分两者,如 ATFMessages.id 和 ATFThreads.id。
请注意我们如何使用 DEFAULT 关键字为每个元素分配默认值。说实话,数据库处理程序的编写方式使得我们不太可能看到这些默认值。(空字符串作为空字符串而不是 NULL 值传递给数据库。要获得真正的 NULL,我们必须传递未定义的标量。)但是,最好始终在程序中构建多重检查,以防其他级别之一未按您预期的方式工作。这也有助于我们跟踪问题;如果我们注意到许多用户被识别为“Mr. Nobody”,我们可以假设我们的发布软件出了问题。
我们可以通过在交互式 mysql 提示符下输入上述 SQL 命令来创建表。创建它们后,我们就可以开始处理程序了。
本项目中的程序共享许多元素。每个程序都以一系列 use 语句开头
use strict; use diagnostics; use CGI; use CGI::Carp qw(fatalsToBrowser); use DBI;
第一个,use strict,促使我们显式地引用变量,可以通过将它们创建为词法变量(使用 my 语句)或使用 use vars 语句。我已选择将所有变量创建为词法变量,但如果您有兴趣将通用变量定义放入外部文件中,您可能需要考虑将它们设为全局变量。
接下来,我们调用 use diagnostics,它告诉 Perl 在我们的程序出现问题时,从 perldiag 手册页中为我们提供信息。我发现 use diagnostics 在处理 Web 应用程序时是一个非常宝贵的调试工具,因为它通常会指出我犯的一个愚蠢的错误。这与 use strict 和 -w 标志一起,使 Perl 编程更不容易出错。
然后,我们加载 CGI::Carp 模块,该模块使用自己的例程覆盖内置的 Carp 模块,这些例程可以在我们的 HTTP 服务器的错误日志中生成更准确的消息。我们还导入 CGI::Carp::fatalsToBrowser,如果发生错误,它会将错误消息发送到用户的浏览器。这使我们可以使用标准的 die 语句,而不必担心我们是否已发送 HTTP “Content-type”标头。在没有此类标头的情况下向用户的浏览器发送消息几乎总是会导致错误消息显示。
BBS 中的每个程序还定义了许多变量:$database、$server、$port、$username 和 $password。这些变量用于使用 DBI 登录到数据库;通过在程序顶部设置它们,您可以根据需要修改它们,而无需更改硬编码的字符串。
每个程序还会关闭缓冲,以便在程序将其发送到相应的文件句柄后立即将信息发送到用户的浏览器。通常,说
print "<P>Hello</P>";
不会将 <P>Hello</P> 发送到用户的浏览器。相反,它将字符串放在缓冲区中。当缓冲区被填满时,其内容将被发送到用户的浏览器。这更有效率,因为计算机可以一次复制大量数据,而不是花费时间进入和退出处理文件操作的例程。但是,这也意味着用户必须等待才能看到结果。我们可以通过设置内置的 Perl 变量 $| 来关闭缓冲
$| = 1;最后,每个程序都使用标准的 DBI 例程连接到数据库
my $dbh = DBI->connect("DBI:mysql:$database:$server:$port", $username, $password);如果连接成功,我们将收到数据库句柄 (dbh) 并将其存储在 $dbh 中。但是,如果 $dbh 为 false,我们应该报告错误,因为这意味着连接不起作用
die "DBI error from connect:", $DBI::errstr unless $dbh;在准备查询时,我们可以做同样的事情。$dbh->prepare 的结果是语句句柄 (sth)。当定义时,$sth 是一个对象,它本身接受方法。当 $sth 未定义时,语句准备失败
my $sth = $dbh->prepare($sql); die "DBI error with prepare: ", $sth->errstr unless $sth;我们可以使用 $sth->execute 执行我们的语句,它的工作方式与 $dbh->prepare 非常相似。不同之处在于,结果代码是一个简单的值,而不是一个对象
my $result = $sth->execute;在某些程序中,我们测试 $result 的值并使用 die 报告错误
die "DBI error with execute: ", $sth->errstr unless $result;在其他程序中,我们使用 $result 来决定是继续执行程序还是打印更友好的错误消息
if ($result) { # do something } else { # indicate an error }最后,我们总是在程序结束时断开与数据库的连接
$dbh->disconnect;这并不是真正必要的,因为 DBI 和 Perl 会在程序退出时关闭所有此类连接。但是,如果您使用 -w 运行,则每次程序在未正常断开与数据库的连接的情况下退出时,都会在您的错误日志中插入一条消息。我们这样做是为了使我们的错误日志免受虚假细节的影响。
由于每条消息都必须属于一个主题,因此我们将首先了解如何创建主题。主题只不过是 ATFThreads 表中的单行,因此我们的主题创建程序将非常简单。
本文中引用的三个列表可在 ftp://ftp.linuxjournal.com/pub/lj/listings/issue57/3193.tgz 匿名下载。由于空间考虑,此处未打印它们。
Add-thread.pl(存档文件中的列表 1)使用 HTML 表单的内容将新行插入 ATFThreads 中。但是,它也执行一些额外的操作,以确保数据可以以有用的方式检索。
我们可以在 SQL 查询中的文本字符串周围使用单引号或双引号。DBI 使用双引号作为参数,因此排除了使用引号的可能性。因此,我们在文本字符串周围使用单引号。但是,这提出了如何将单引号传递给程序的问题。一个简单的解决方案是对用户生成的每个文本字符串执行替换。例如
$value{"subject"} = $query->param("subject"); $value{"subject"} =~ s/\'/\'\'/g;
我们可以通过使用内置的 $dbh->quote 方法做得更好,该方法为我们引用文本字符串。$dbh->quote 决定是使用单引号还是双引号,并且还可以轻松处理特殊字符,例如引号和问号。我们使用 foreach 循环来引用每个元素
# Get the form parameters foreach my $element (qw(subject text author email)) { $value{$element} = $dbh->quote($query->param($element)); }完成此操作后,我们可以确保 $value{$element} 适合插入数据库。
我们还对包含启动主题的文本的“text”HTML 元素执行了几次替换。首先,我们删除所有 HTML 标记,以防止人们链接到各种疯狂的网站。虽然可能希望允许人们在其帖子中包含 HTML,但如果插入格式命令,也可能导致混乱。我决定稍微严厉一点,禁止所有 HTML。我们通过删除 < 和 > 之间的所有内容来做到这一点
$text =~ s/<.*?>//sg;
请注意我们如何使用 Perl 的非贪婪运算符 *? 而不是 * 来删除 HTML 标记。如果我们使用 * 并且该行有两个 HTML 标记,Perl 将删除从第一个 < 到最后一个 > 的所有内容。我们使用 /s 修饰符告诉 Perl . 包括所有字符,包括换行符。如果没有 /s,\n 将不包含在 . 中,这意味着像这样的两行标记
<a href="http://www.cnn.com/">将被忽略。
然后,我们确保正确处理换行符,首先删除多个换行符,然后用 HTML 段落标记替换它们
$text =~ s/\r\n/\n/g; $text =~ s/\r/\n/g; $text =~ s|\n\n|</P>\n<\P>|gi;
执行完所有这些任务后,add-thread.pl 创建将新主题插入 ATFThreads 的 SQL 查询
my $sql = "INSERT INTO ATFThreads "; $sql .= " (subject, text, author, email, date) "; $sql .= "VALUES ($values, NOW())";我们插入主题的日期以供将来使用,但也为了我们可以按创建顺序对主题进行排序。
列出主题的程序,适当地命名为 list-threads.pl(存档文件中的列表 2),使用 SELECT 查询来检索 ATFThreads 中的所有行
my $sql = "SELECT id,subject FROM ATFThreads ORDER BY subject";
在执行 $sth->execute 后,它会检查返回了多少行。如果没有返回任何行,我们指示尚未创建任何主题。如果主题存在,我们使用 $sth->fetchrow 迭代结果,这会将查询结果放入 @row 中。我们可以拉出 @row 的元素并打印列表
if ($sth->rows) { print "<ul>\n"; while (my @row = $sth->fetchrow) { print "<li> "; print "<a href=\"/cgi-bin/view-thread.pl?"; print "$row[0]\">$row[1]</a>\n"; } print "</ul>\n"; $sth->finish; }用户将看到按字母顺序排列的主题标题列表,每个标题都是指向 view-thread.pl(存档文件中的列表 3)的超链接,如下所述。如您所见,view-thread.pl 的参数是主题的 id 值,即定义的 primary key。
下个月,我们将完成基本 BBS 的设计和实现,添加创建消息和搜索系统的能力。在此之前,请考虑访问 http://www.lerner.co.il/atf/ 上此软件的工作实现,您可以在那里与其他专栏读者交流想法。
