使用 mSQL 和 Apache 进行 Web 计数
本文并非关于网络计数器。哦,我知道标题是这么写的,而且我将向您展示如何使用 mSQL 来制作一个快速且有效的计数器。但这只是锦上添花。本文的真正目的是探索 Apache 模块和 mSQL 编程的迷人世界。
当然,计数器远非最新事物。我敢打赌您的主页上就有一个。然而,人们使用的大多数计数器都是第三方场外计数器。这些计数器通常通过在您的页面上包含一个 <IMG> 标签来调用远程站点来实现,该站点跟踪访问次数并返回包含当前计数的计数器图像。这种方法的问题是众所周知的:它可能很昂贵,它不包括您的所有点击(尤其是来自基于文本的浏览器的点击),而且它不可定制。
作为 Linux 用户,我们不满足于简单的方式——我们以正确的方式做事。每个主要的 Linux 发行版都带有 Apache Web 服务器。这使得每个拥有网络连接的 Linux 用户都能够运行自己的网站,而无需使用第三方站点进行任何操作,甚至计数器也不例外。
Apache Web 服务器是迄今为止世界上使用最广泛的 Web 服务器。使其如此受欢迎的功能之一(除了它是免费的并且永远都是免费的之外)是其模块化设计。服务器可以通过编写直接编译到主程序中的模块来轻松增强。这些模块通常用 C 语言编写,但甚至有些模块允许您用其他语言(如 Perl)编写模块。
好消息是编写 Apache 模块并不难。即使像我这样的 C 语言新手程序员也能够很快掌握它。话虽如此,让我们开始着手为 Apache 编写一个计数器模块。
将您的 Web 计数器制作成服务器的一部分模块有几个很好的理由。通过 CGI 工作的计数器(例如上面描述的那些)不记录对您页面的所有可能点击。另一方面,通过分析日志文件工作的计数器很难保持最新,并且它们也可能遗漏一些点击(例如,它可能捕获 /~myhome/index.html 但错过 /~myhome/ 或 /~myhome)。但是,直接集成到 Web 服务器中的计数器保证可以捕获所有可能的点击,因为从您的 Web 服务器请求的所有文件都必须通过服务器发送。此外,通过 Web 服务器允许您使用实际发送给用户的文件名,因此,所有点击都标记为 /~myhome/index.html,而不是 /~myhome、/~myhome/ 和 /~myhome/index.html。其他主要优点是速度和可定制性。您知道计数器将是最新的,因为每个通过服务器的文件也都会通过您的计数器。
现在,我们所要做的就是决定如何存储数据。事实上,Apache 社区的一位友好成员已经完成了一些我们的工作,他编写了一个名为 mod_cntr.c 的 Apache 模块(请参阅资源),该模块是一个计数器,它使用纯文本文件或 DBM 数据库来存储数据。
就制作计数器模块而言,那一个确实是您所需要的全部。下载 mod_cntr.c 文件,将其放入您的 Apache 源代码目录并重新编译。但是作为有辨别力的 Linux 用户,我们想要更多功能——这种功能将来自 mSQL 数据库。
mSQL 引擎速度非常快,但代价是不完整的 SQL 语言,但它足以满足我们的需求。通过 mSQL 模块运行我们的计数器模块消除了对繁琐的文本文件的需求。我们所做的只是调用最近的 mSQL 服务器(无论是在与服务器相同的机器上还是不在同一机器上),并向其发送维护数据库所需的指令。
我不会在此处详细介绍如何获取和安装 mSQL。完整的说明可在 mSQL 网页上找到(请参阅资源)。要编译我们基于 mSQL 的计数器,您需要在您的库路径中包含 libmsql.a 库,并在某个可访问的位置包含 msql.h 标头。
当然,处理此问题的最佳方法是将 mSQL 功能添加到已有的 mod_cntr.c 中。事实上,我在我的生产服务器上就是这样做的。但作为一个简单的例子,我将重写该模块,使其仅支持 mSQL。这将消除一些混乱,并且更易于理解。
列表 1 包含基于 mSQL 的计数器的代码。让我们首先从 Apache 模块的角度来看待它。Apache 模块 API 包含用于创建模块的非常简单的规则。第 403-419 行包含 Web 服务器读取以定义模块的模块定义。这里有很多选项,但对于我们的需求(以及大多数模块的需求),我们只需要定义其中四个。前两个,在第 406 行和 408 行定义,是 create_cntr_dir_config_rec 和 create_cntr_srv_config_rec。这些是初始化我们模块使用的变量的函数的名称。请注意,这里有两个,一个包含字符“dir”,另一个包含“srv”。Apache 允许用户使用 .htaccess 文件在每个目录的基础上自定义服务器的大多数方面;此配置可通过 create_cntr_dir_config_rec 访问。主服务器配置文件(access.conf 和 httpd.conf)可通过 create_cntr_srv_config_rec 访问。
create_cntr_dir_config_rec 和 create_cntr_svr_config_rec 函数的代码分别位于第 132-146 行和 148-162 行。这两部分代码几乎相同。每个代码都只是使用第 122-127 行中定义的默认值初始化计数器信息,该信息包含在结构 cntr_config_rec(第 106-112 行)中。如果您正在编写用于任何目的的模块,则应包含类似于这两个函数的函数,以初始化服务器范围和每个目录配置的任何默认变量。
我们模块的下一个定义是第 410 行的 cntr_cmds。这是 command_rec 类型数组的名称,该数组在 Apache 标头文件中定义。这些是您的模块的用户可以输入到 Apache 配置文件中的选项。对于每个 command_rec,必须填写六个字段
用户将输入的配置选项的名称。例如,第 240 行的 command_rec 的名称为 CounterType。如果它是服务器范围配置文件的选项,通常的做法是在名称前加上 Server。
调用以将用户的配置信息提供给模块的函数的名称。
用于将其他信息传递给先前给定的函数的空指针。
指示配置选项可以出现的位置的标志。在我们的模块中,我们使用 ACCESS_CONF,这意味着该选项可以出现在服务器的 access.conf 文件或 .htaccess 文件的目录部分中,而 RSRC_CONF 意味着该选项只能出现在全局 access.conf 或 httpd.conf 中。
指示配置选项需要多少个参数的标志。在我们的模块中,我们使用 TAKE1,它表示一个参数(由服务器预先解析),而 TAKE2 表示两个参数,以空格分隔。其他标志以及有关整个服务器 API 的完整文档,可以在 https://apache.ac.cn/docs/misc/API.html 上找到。
描述配置选项用法的字符串。当用户为该选项输入错误数量或类型的参数时,将显示该字符串。
我们的模块定义了六个配置选项。CounterType 定义要使用的数据库类型(在本例中仅为 mSQL,但我保留了该行,以便轻松过渡到此模块的原始版本,其中包含文本和 DBM 数据库)。CounterAutoAdd 允许用户决定是否应将首次遇到的 URI(URL 中相对于主机的部分)自动添加到数据库中。CounterDB 是使用的 mSQL 数据库的名称,后跟该数据库中的表。还有三个类似的选项对服务器范围配置执行相同的操作。
定义的每个选项都有一个关联的函数(set_cntr_type、set_cntr_autoadd 等)。这些函数检查用户是否为该选项提供了值。如果是,则该值将添加到模块的 cntr_config_rec 结构中,替换默认值。
关于我们模块的最后一点信息在第 417 行。这是执行所有工作的函数的名称。在我们的例子中,它是位于第 331-400 行的函数 cntr_update。
这就是创建 Apache 模块的全部内容。一旦您创建一个包含模块结构的 C 文件,该结构具有两个初始化默认变量的函数,一个包含用户可定义选项的 command_rec 结构(以及将这些选项插入到您的模块中的函数)和一个执行操作的函数,您就拥有了所需的一切。当然,要执行任何有用的操作,您必须将操作编码到最后一个函数中,所以让我们看一下 cntr_update。
此函数的第一个有趣部分出现在第 343 行和 344 行。结构 r 的类型为 request_rec(在 Apache 标头中定义),并从 Web 服务器传递给我们。此结构具有我们需要的所有关于服务器当前正在处理的请求的信息,包括文档的 URI 以及文档是否已完全解析。在第 343-344 行中,我们跳过服务器为了获取最终文件而经历的所有未解析步骤,以便我们收到的 URI 是实际发送的文件名。这样,输入到我们数据库中的 URI 的格式为 /directoryname/index.html,而不是 /directoryname、/directoryname/ 或客户端用户实际输入的任何名称。在第 348-351 行中,如果没有 URI(用户输入错误或文件不存在)或文件是服务器端包含(不需要为这些文件保留计数器),我们就会中止。然后,我们为服务器范围和每个目录的情况调用函数 get_module_config。这是 Web 服务器提供的函数,它收集所有用户提供的选项并调用相应的函数(我们提供的),并返回具有已初始化变量的 cntr_config_rec 结构。
接下来,我们检查以确保用户为我们提供了数据库和表以供使用。假设一切都已设置,然后我们调用 cntr_inc 函数来增加给定 URI 的计数器。之后,我们设置几个环境变量,CGI 程序可能会对计数器感兴趣(例如,制作里程表图形的图像生成器)。
cntr_inc 函数(第 310-329 行)的主要目的是对 URI 进行一些预处理,然后调用相应的数据库递增函数。在我们的例子中,我们只有一种类型的数据库,所以这是一个非常短的函数。首先,它从 URI 中剥离双斜杠 (//),然后调用 cntr_incmsql 函数来执行实际工作。
就创建 Apache 模块而言,我们已经完成了。很酷,对吧?我当然从未想到创建 Web 服务器的扩展会如此容易。现在我们所要做的就是编码 cntr_incmsql 函数来完成这项工作。
cntr_incmsql 函数(第 253-308 行)是模块的核心。在 55 行中,我们使用 SQL 数据库服务器实现了快速、有效的 Web 计数器。mSQL 提供的 C API 包括几个函数,这些函数允许轻松访问 mSQL 的所有功能。我们使用的第一个函数在第 264 行。在那里,我们调用 msqlConnect 以将我们连接到本地数据库服务器。您也可以使用非 NULL 参数调用 msqlConnect 以连接到远程数据库服务器。在接下来的两行中,我们检查以确保我们已成功连接,如果未成功连接,则返回错误。第 269 行调用 msqlSelectDB 以选择配置选项提供给我们的数据库。同样,如果该函数未成功返回,则会生成错误。
现在我们已经连接到相应的数据库,我们可以发送我们的第一个 SQL 查询,以找出给定的 URI 是否已存在于数据库中。我们查询的表由用户给出,并且必须具有一个 uri char 字段(它是非空唯一索引)、一个 cntr_count int 字段和一个 cntr_date char 字段。我们发送的查询是
select cntr_count, cntr_date from tablename where uri='uriname'
此查询返回 URI 的现有计数(如果有)及其上次重置的时间。我们使用 msqlQuery 函数发送此查询,并像往常一样检查错误。下一步是从服务器检索结果(如果有)。我们使用 msqlStoreResult 函数执行此操作。此函数返回一个结构,该结构允许我们逐行检索结果,以及有关结果的其他信息,例如检索到的行数。在第 281 行中,我们使用 msqlNumRows 函数来查看重试了多少行。由于每个 URI 都是唯一的,因此该数字应为 1 或 0。(mSQL 支持的 SQL 查询的完整列表可以在资源中列出的 mSQL 主页上找到。)
如果该 URI 的条目已存在,我们则进入第 282-293 行的块。首先,我们使用 msqlFetchRow 函数来检索 URI 的计数数。由于我们知道我们只有一行数据,因此我们只需要调用该函数一次。如果您期望有多行数据,您可以继续调用 msqlFetchRow 来检索它们,直到数据用完为止。msqlFetchRow 函数返回一个数组,其中包含您请求的每个字段。在本例中,我们请求了 URI 的计数和日期。然后,我们递增计数并将计数和日期放入我们的结果结构中,以便 CGI 或其他感兴趣的程序可以访问它们。最后,我们发送更新查询
update
(其中包含适当的计数和 uri)。在检查错误之后,我们可以自由地关闭数据库(使用 msqlClose)并使用 (msqlFreeResult) 解除分配任何数据库内存。然后我们可以返回成功,每个人都很高兴。
如果我们拥有的 URI 在数据库中不存在,我们必须输入它。首先,我们检查是否有人在服务器的配置文件中定义了 CounterAutoAdd 选项。如果他们有,那么我们只需跳过此 URI 并回家。如果未定义 CounterAutoAdd,我们必须将 URI 添加到数据库中,计数为 1,日期为当前日期。当前日期使用服务器定义的函数 ht_time 设置,该函数返回一个预格式化的字符串。我们使用以下查询执行此操作
insert into cntr_date) values ( 'myuri', 1, 'currenttime' )
(其中包含 tablename、myuri 和 currenttime 的适当值)。然后我们总是检查错误。如果没有错误,则模块完成。我们现在有一个直接构建到 Web 服务器中的工作命中计数器。
这个模块很棒,但它并不完美。如果您运行的是高流量服务器,则会出现称为竞争条件的问题。请注意,在第 275 行,我们检查数据库以查看 URI 是否已存在。如果不存在,则在第 296 行,我们将 URI 插入到数据库中。假设在第 275 行和 296 行之间的某个位置,同一 URI 又来了另一个点击。数据库在返回时指示 URI 不存在;它不知道第一个点击即将将其添加到数据库中。当第二个点击到达第 296 行时,URI 现在已被第一个点击输入到数据库中,并且数据库由于非唯一 URI 而返回错误。
在功能齐全的 SQL 服务器中,此问题通过一种称为“事务”的技术来解决,其中可以将多个命令一起输入到数据库中——在处理任何其他命令之前。希望 mSQL 在不久的将来会支持事务。在那之前,解决此问题的一种方法是使用您站点所有 URI 和计数为 0 初始化您的数据库。这样,计数器永远不会被告知 URI 不存在。
