使用 C 语言进行 CGI 编程
Perl、Python 和 PHP 是 CGI 应用程序编程的圣三位一体。书店里摆满了关于这些语言的书籍,计算机媒体对它们进行了充分报道,互联网上也有大量关于它们的信息。然而,关于使用 C 语言编写 CGI 应用程序的信息却明显缺乏。在本文中,我将展示如何使用 C 语言进行 CGI 编程,并列出一些它提供显著优势的情况。
我在我的应用程序中使用 C 语言有三个原因:速度、功能和稳定性。尽管传统观点认为情况并非如此,但我自己的基准测试发现,当要完成的处理很简单时,C 语言和 PHP 的速度相当。当处理有任何复杂性时,C 语言明显胜出。
此外,C 语言提供了出色的功能集。该语言本身带有一组最基本的功能,但有大量的库可用于计算机所使用的几乎任何工作。当然,Perl 在这方面也不逊色,我并不认为 C 语言提供了更多的可扩展性,但两者都可以满足几乎任何需求。
此外,用 C 语言编写的 CGI 程序是稳定的。由于程序是编译的,因此它不像 PHP 那样容易受到操作环境变化的影响。此外,由于该语言是稳定的,因此它不会经历 PHP 用户在过去几年中经历的剧烈变化。
我的应用程序是一个简单的事件列表,适用于企业列出即将发生的事件,例如,一天的会议日程或教堂的活动。它提供了一个旨在受密码保护的管理界面和一个公共界面,该界面列出了所有即将发生的事件(但仅限即将发生的事件)。此应用程序还提供运行时配置和界面独立性。
我使用数据库,而不是编写自己的数据存储,并且配置文件包含数据库连接信息。一组文件用于提供界面/代码分离。
管理界面允许列出、编辑、保存和删除事件。如果没有提供其他操作,则列出事件是默认操作。可以保存新的和现有的事件。该界面由一个网格屏幕组成,该屏幕显示事件列表,以及一个详细屏幕,该屏幕包含单个事件的完整记录。
此应用程序的数据库模式由单个表组成,如清单 1 中定义。此模式是 MySQL 特定的,但可以为任何数据库引擎创建等效模式。
清单 1. MySQL 模式
CREATE TABLE event ( event_no int(11) NOT NULL auto_increment, event_begin date NOT NULL default '0000-00-00', name varchar(80) NOT NULL default '', location varchar(80) NOT NULL default '', begin_hour varchar(10) default NULL, end_hour varchar(10) default NULL, event_end date NOT NULL default '0000-00-00', PRIMARY KEY (event_no), KEY event_date (event_begin) )
以下函数是实现管理界面功能所需的最低限度函数:list_events()、show_event()、save_event() 和 delete_event()。我还将把数据库数据的读取和写入抽象到它们自己的函数组中。这使每个函数更简单,从而使调试更容易。我需要用于数据存储界面的函数是 event_create()、event_destroy()、event_read()、event_write 和 event_delete。为了让我的生活更轻松,我还将添加 event_fetch_range(),以便我可以选择一系列事件——这是我在至少两个地方需要做的事情。
接下来,我需要将我的记录抽象为 C 结构,并将数据库结果集抽象为链表。抽象使我可以相对轻松地更改数据库引擎或数据表示形式,因为只有一小部分代码直接处理数据存储。
这里没有足够的空间来打印我的所有源代码。完整的源代码和我的 Makefile 可以从我的网站下载(请参阅在线资源)。
使用 C 语言时要克服的第一个障碍是获取您需要的工具集。至少,您需要一个 CGI 解析器来为您解析 CGI 信息。您很可能还在寻找一些数据库连接。一点逻辑/界面独立性也很好,这样您就不必在每次站点需要改版时都重写代码。
对于 CGI 解析,我推荐 Thomas Boutell 的 cgic 库(请参阅资源)。它非常易于使用,并提供对 CGI 接口所有部分的访问。如果您是 C++ 人员,cgicc 库也适用(请参阅资源),尽管我发现 Boutell 库更容易使用。
MySQL 几乎是 UNIX Web 开发的标准,因此我的示例应用程序坚持使用它。但是,每个重要的数据库引擎都有一个功能性的 C 接口库,因此您可以使用您喜欢的任何数据库。
我将提供我自己的界面独立性例程,但您可以使用 libxml 和 libxslt 来做同样的事情,并且更加成熟。
在运行时,我需要能够配置数据库连接。给定文件名和用于配置键的字符字符串数组,我的配置函数会填充相应的配置值数组,如清单 2 所示。现在,我可以用我选择使用的任何键填充字符串数组,并在值数组中获取结果。
清单 2. 运行时配置函数
void config_read(char* filename, char** key, char** value) { FILE* cfile; char tok[80]; char line[2048]; char* target; int i; int length; cfile = fopen(filename, "r"); if (!cfile) { perror("config_read"); return; } while(fgets(line, 2048, cfile)) { if ((target = strchr(line, '='))) { sscanf(line, "%80s", tok); for(i=0; key[i]; i++) { if (strcmp(key[i], tok) == 0) { target++; while(isspace(*target)) target++; length = strlen(target); value[i] = (char*)calloc(1, length + 1); strcpy(value[i], target); target = &value[i][length - 1]; while(isspace(*target)) *target-- = 0; } } } } fclose(cfile); }
用户界面有两个部分。作为程序员,我主要关心输入表单和 URL 字符串。其他人则关心我的表单周围的页面外观,并将表单本身视为理所当然。让双方都满意的解决方案是将页面与表单和我的程序分开存在。
模板库在 PHP 和 Perl 中比比皆是,但在 C 语言中没有常见的 HTML 模板库。最简单的解决方案是在我的 C 代码中仅包含最少的输出,并将其余部分保留在 HTML 文件中,这些文件在适当的时候输出。清单 3 中找到了可以执行此操作的函数。
清单 3. HTML 模板函数
void html_get(char* path, char* file) { struct stat sb; FILE* html; char* buffer; char fullpath[1024]; /* File & path name exceed system limits */ if (strlen(path) + strlen(file) > 1024) return; sprintf(fullpath, "%s/%s", path, file); if (stat(fullpath, &sb)) return; buffer = (char*)calloc(1, sb.st_size + 1); if (!buffer) return; html = fopen(fullpath, "r"); fread((void*)buffer, 1, sb.st_size, html); fclose(html); puts(buffer); free(buffer); }
在生成输出之前,我需要告诉 Web 服务器和浏览器我要发送的内容;cgiHeaderContentType() 完成此任务。我想要 text/html 的内容类型,所以我将其作为参数传递。对于我要显示的任何页面,要遵循的一般步骤是
cgiHeaderContentType("text/html");
html_get(path, pagetop.html);
生成程序内容。
html_get(path, pagebottom.html);
现在我可以生成页面并打印表单,我需要能够处理该表单。我需要读取数字和文本元素,因此我使用了 cgic 库中的几个函数:cgiFormStringNoNewlines() 和 cgiFormInteger()。cgic 库实现了 main 函数,并要求我实现 int cgiMain(void)。cgiMain() 是我放置大部分表单处理代码的地方。
为了在我的 show_event 函数中显示单个记录,我从 CGI eventno 参数中获取 event_no(我的主键)。cgiFormInteger() 检索整数值,如果未提供 CGI 参数,则设置默认值。
我还需要在 save_event 中从表单中获取大量数据。日期是棘手的输入,因为它们由三个数据组成:年、月和日。我需要开始日期和结束日期,这给了我六个字段来解释。我还需要输入事件的名称、开始和结束时间(它们是字符串,因为它们本身可能是事件,例如日出或日落)和地点。清单 4 显示了这在代码中是如何工作的。
清单 4 还演示了 cgiHeaderLocation(),该函数将用户重定向到新页面。在我保存提交的数据后,我想显示事件列表页面。我没有使用文字字符串,而是使用了 libcgic 提供的一个变量 cgiScriptName。使用此变量而不是文字变量意味着可以更改程序名称而不会破坏程序。
清单 4. save_event(),解析 CGI 数据
struct event* e; e = event_create(); cgiFormInteger("eventno", &e->event_no, 0); cgiFormStringNoNewlines("name", e->name, 80); cgiFormStringNoNewlines("location", e->location, 80); /* Processing date fields */ cgiFormInteger("beginyear", &e->event_begin->year, 0); cgiFormInteger("beginmonth", &e->event_begin->month, 0); cgiFormInteger("beginday", &e->event_begin->day, 0); cgiFormInteger("endyear", &e->event_end->year, 0); cgiFormInteger("endmonth", &e->event_end->month, 0); cgiFormInteger("endday", &e->event_end->day, 0); /* Process begin & end times separately */ cgiFormStringNoNewlines("beginhour", e->event_begin->hour, 10); cgiFormStringNoNewlines("endhour", e->event_end->hour, 10); event_write(e); cgiHeaderLocation(cgiScriptName);
最后,我需要一种处理提交按钮的方法。它们是最复杂的输入,因为我需要根据它们的值启动一个函数并选择一个默认值,以防万一。cgic 库有一个函数 cgiFormSelectSingle(),它完全模拟了这种行为。它要求可能的值列表位于字符串数组中。它用数组中参数的索引填充一个整数变量,如果没有任何匹配项,则使用默认值。
清单 5. 处理提交按钮
char* command[5] = {"List", "Show", "Save", "Delete", 0}; void (*action)(void)[5] = {list_events, show_event, save_event, delete_event, 0}; int result; cgiFormSelectSingle("do", command, 4, &result, 0); action[result]();
有关函数指针的信息,请参阅资源。如果函数指针仍然让您感到困惑,您可以在 switch 语句中选择要运行的函数。我更喜欢函数指针数组,因为它更紧凑,但我的旧代码仍然使用 switch 语句。
来自 C 语言的 MySQL 在很大程度上与 PHP 相同,如果您习惯了该界面。您必须使用 MySQL 的字符串转义函数来转义字符串中的问题字符,例如引号字符或反斜杠字符,但除此之外,它基本上是相同的。show_event() 函数要求我从主键中获取单个记录。所有错误检查都使代码变得臃肿,但它实际上是三个基本语句。调用 mysql_query() 执行 MySQL 语句并生成结果集。调用 mysql_store_result() 从服务器检索结果集。最后,调用 mysql_fetch_row() 从结果集中提取单个 MYSQL_ROW 变量。
MYSQL_ROW 变量可以像字符串数组 (char**) 一样处理。如果任何数据是数字的,并且您想将其视为数字数据,则需要转换它。例如,在我的应用程序中,希望日期具有三个独立的数字组件。由于此数据结构为 YYYY-MM-DD,因此我使用 sscanf() 获取组件(清单 6)。
清单 6. 从 MySQL 检索数据
MYSQL_RES* res; MYSQL_ROW row; int beginyear; int beginmonth; int beginday; if (mysql_query(db, sql)) { print_error(mysql_error(db)); return; } if((res = mysql_store_result(db)) == 0) { print_error(mysql_error(db)); return; } if ((row = mysql_fetch_row(res)) == 0) { print_error("No event found by that number"); return; } sscanf(row[0], "%d-%d-%d", &beginyear, &beginmonth, &beginday);
由于需要转义数据,因此将数据写入数据库更有趣。清单 7 显示了它是如何完成的。
清单 7. 在 MySQL 中使用用户提供的数据
char name[11]; char escapedname[21]; cgiFormStringNoNewlines("name", name, 10); mysql_real_escape_string(db, escapedname, name, strlen(name));
escapedname 包含与 name 相同的字符串,其中 MySQL 特殊字符被转义,因此我可以将它们插入到 SQL 语句中而无需担心。您必须转义从用户输入读取的所有字符串;否则,奸诈的人可能会利用您的疏忽并对您的数据库做出令人不愉快的事情。
调试 C 语言的一个明显缺点是,错误往往会导致段错误,而没有关于错误来源的诊断消息。调试器对于大多数其他类型的程序来说都很好,但 CGI 程序由于其获取输入的方式而提出了特殊的挑战。
为了帮助应对这一挑战,cgic 库包含一个名为 capture 的 CGI 程序。此程序将发送给它的任何 CGI 输入保存到文件中。您需要在 capture 的源代码中设置此文件名。当您的 CGI 程序需要调试时,在您的 cgiMain() 函数顶部添加对 cgiReadEnvironment(char*) 的调用。请务必设置文件名参数以匹配 capture 中设置的文件名。然后,将有问题的数据发送到 capture,使其成为表单的操作或请求中的脚本。现在,您可以使用 GDB 或您喜欢的调试器来查看您的代码产生了什么样的麻烦。
您可以采取一些步骤来简化以后的调试和开发。尽管这些步骤适用于所有编程,但它们在 CGI 编程中尤其有效。请记住,一个函数应该只做一件事,并且只做一件事,并且要尽早测试和经常测试。
最好尽快测试您编写的每个函数,以确保它按预期执行。而且,看看它对错误数据的反应如何也不是一个坏主意。在某些时候,该函数很可能会收到错误的数据。提前发现此行为可以避免在您的非工作时间接到令人不快的电话。
在大多数情况下,您的开发机器和您的部署机器不会是同一台机器。尽可能使您的开发系统与生产系统匹配。例如,我的软件倾向于在 Linux 或 OpenBSD 上开发,并且几乎总是部署在 FreeBSD 上。
当您准备在部署机器上构建或安装时,特别重要的是要注意库版本中的差异。您可以使用以下命令查看您的代码使用了哪些动态库ldd。检查此信息是个好主意,因为您通常可能会对您的库带来的其他依赖项感到惊讶。
如果库版本接近,通常反映在相同的主版本号中,则可能没有大问题。如果您要部署到外部托管的网站,则部署和开发机器具有不兼容的版本的情况并不少见。
我使用的解决方案是编译我自己的本地版本的库。删除库的共享版本,并链接到此本地版本而不是系统版本。它会增加您的二进制文件的大小,但它消除了您对您无法控制的库的依赖性。
在部署系统上构建二进制文件后,运行ldd再次确保已找到所有动态库。特别是当您链接到库的本地副本时,很容易忘记删除动态版本,该版本在运行时(或通过ldd)找不到。不断调整构建过程;构建并重新检查,直到没有找不到的库为止。
传统观点认为,使用 CGI 接口的程序比使用服务器模块(例如 mod_php 或 mod_perl)提供的语言的程序慢。由于我开始使用 PHP 编写 Web 应用程序,因此我在此处将其用作与用 C 语言编写的 CGI 程序进行比较的基础。我对 C 与 Perl 的相对速度不做任何断言。
我使用的比较是数据库的外部接口(events.cgi 和 events.php),因为两者都使用了相同的方法来提供界面分离。内部接口未经过测试,因为对外部接口的调用应该使对内部接口的调用相形见绌。
Apache Benchmark 用于以服务器可以承受的最快速度对每个版本进行 10,000 次查询。C 版本的平均事务时间为 581 毫秒,PHP 版本的平均事务时间为 601 毫秒。由于时间如此接近,我怀疑如果重复测试,将会看到一些时间变化。这证明是正确的,尽管 C 版本比 PHP 版本更快的情况更多。
我的正常开发使用更复杂的界面分离库 libtemplate(请参阅资源)。我有该库的 PHP 和 C 版本。当我比较使用 libtemplate 的事件调度程序的版本时,我发现 C 语言具有更快的响应时间。C 版本的平均事务时间为 625 毫秒,与更简单的版本相比并没有增加多少。PHP 版本的平均事务时间为 1,957 毫秒。同样值得注意的是,当 PHP 版本运行时,负载数通常是 C 版本运行时看到的负载数的两倍。在进行此测试时,系统上没有用户,也没有其他重要的应用程序在运行。
两个 C 版本的相当接近的时间告诉我们,大部分执行时间都花在了加载程序上。一旦程序加载完成,程序执行得非常快。另一方面,PHP 执行速度相对较慢。当然,PHP 也无法避免必须加载到内存中的问题。它也必须编译,这是 C 程序已经完成的步骤。
有了正确的工具和一点经验,使用 C 语言开发 CGI 应用程序并不比使用 Perl 或 PHP 更困难。现在我有了经验和工具,C 语言是我 CGI 应用程序的首选语言。
当应用程序需要更高级的处理和长期稳定性时,C 语言表现出色。与 PHP 不同,当服务器更改超出您的控制范围时,它不容易发生故障。除非删除共享库(例如 libc 或 libmysqlclient),否则我们的应用程序的 C 版本很难被破坏。对于需要更复杂的数据处理的应用程序,C 程序的执行速度使其成为明确的选择。
本文资源: /article/8058。
Clay Dowling 是 Lazarus Internet Development (www.lazarusid.com) 的总裁。除了编程之外,他还喜欢酿造啤酒和葡萄酒。可以通过电子邮件 clay@lazarusid.com 与他联系。