您的位置:寻梦网首页编程乐园CGI编程>CGI 应用程序开发基础
CGI 应用程序开发基础

  • CGI 脚本结构
  • 计划你的CGI脚本
  • 标准CGI环境变量
  • CGI 脚本的移植性
  • CGI 库
  • CGI 的局限


     
    1.CGI 脚本结构


    当脚本被服务器引发时,服务器常常以两种途径之一向脚本传递信息:GET或POST。这两种方法被称为请求方法。所使用的请求方法是通过环境变量传给脚本,该环境变量叫作REQUEST_METHOD(还定义了另外两种请求方法一HEAD和PUT,但它们不是特别应用于CGI,并且不鼓励使用它们)。

    1)GET是对数据的一个请求——同样的方法被用于获得静态文档。GET方法以附加在URL后面的参数发送请求信息。这些参数将放在环境变量QUERY_STRING中传给CGI程序。例如,有一个叫作Myprog.exe的脚本,从如下的链接启动它:

    REQUEST_METHOD是GET,QUERY_STRING包含lname=b1ow&fname=joe。在“URL一编码”中将讨论QUERY_STRING的格式。

    问号从QUERY_STRING的起始处分隔开脚本名字。在一些服务器上,问号是强制性的,即使后面没有跟着QUERY_STRING。另一些服务器则允许用一个正斜杠代替问号或与之附加在一起。如果使用斜杠,服务器则用PATH_INFO而不是QUERY_STRING变量将信息传给脚本。(URL解码)


    2)当浏览器将数据从一个填写表单传给服务器时,发生POST操作。对于POST,QUERY一STRING可能为空或不空,这有赖于服务器。如果有信息,则其如GET的情况一样被格式化和传递。

    来自POST查询的数据使用STDIN从服务器传到脚本。由于STDIN是一个源,脚本需要知道有多少有效数据。于是服务器还提供了另一个变量,CONTENT_LENGTH,以指出到来数据的字节数。而POST的数据格式为:
    variable1=value1&variable2=value2&etc

    你的程序必须检查REQUEST_METHOD环境变量以知道是否要读取STDIN。CONTENT_LENGTH变量一般只在REOUEST_METHOD为POST时有用。


    CGI应用的基本结构既简单又直接明了:初始化、处理、输出和终止。由于讨论的是概念、数据源、编程规则,所以在例子中将使用伪码而不是使用某种特定语言。

    理想情况下,一个脚本具有如下形式(do-initialize,do-process和do-output代表恰当的子例程):

    程序开始
    调用 do-initialize
    调用 do-proces
    调用 do一output
    程序结束。
    实际情况并非这么简单。

    1.1 初始化

    脚本启动后必须做的第一件事是确定其输入、环境和状态。基本操作系统环境信息能以通常方式得到:在Windows NT或windows95中从系统注册区得到,在Unix系统中从标准环境变量得到,在别的Windows版本中从INI文件得到,等等。

    状态信息来自于输入,而不是操作环境或静态变量。记住:每当CGI脚本被引发时,它都好象此前从未被引发过。脚本不在调用之间持续运行,所有的东西都必须从头初始化,如下:

    1.确定脚本是如何被引发的
    典型情况下,这涉及读取REQUEST_METHOOD环境变量并分析其中的单词GET或POST。

    注意
    尽管当前定义应用于coi的操作只有GET和posT,你或许会时不时地遇到PUT或HEAD,假王口你的服务器支持它并且用户的剜览器或一个机器人使用它就可能发生这种情况。 PUl7k作为PosT的另选提供,但从未得多(认可的RFC资格,一般不被使用。HEAD被一些剜览器fotL器人(自动剜览器账用,仅用于提取HTML文在的头部,不适用于C6路程。此外还有一些古怪的请求方法。你的代码应该检查是否为GET和PosT,拒绝任何其他方法,不要假设请求方法如果不是GET便是PosT,或者相反。

    2.提取输入数据
    如果方法是GET,必须获得、分析、解码QUERY_STRING环境变量。如果方法是POST,必须检查QUERY_STRING并还要分析STDIN。如果CONTENT_TYPE环境变量是设为application/x-www-form-urlencoded,来自STDIN的源也需要解码。

    1.2处理

    脚本通过读取和分析其输入从而对环境初始化之后,便准备进入工作。在此阶段发生的事情则远没有初始化阶段那样确定。在初始化时,参数是知道的(或是可以被发现),所要做的任务对于各个脚本都多多少少地相同。然而,处理阶段是脚本的核心,在此时要做的事情几乎完全依赖于脚本的目标。


    1.处理输入数据
    此时做什么取决于脚本。例如,你可以忽略全部输入而仅仅输出数据,可能以有条理格式化的HTML将输入在吐出去,或许会在一个数据库中猎取信息在将其显示出来,或者是从前没有想到的任何事情。处理数据一般意味着,以某种方式对其进行转换。在传统的数据处理术语中,这叫做转换步骤,因为,在面向批作业的处理中,程序读取一个记录并对其施加一些规则(转换它),然后将其写回。CGI 程序很少被看作传统的数据处理,但思想是一样的。程序的处理数据阶段不同的CGI 程序,——在数据处理阶段,你拿到输入,并从其中做出一些新的东西来。

    2.输出结果
    在一个简单的CGI脚本中,输出常常只是一个头部和一些HTML。更复杂些的脚本可能;输出图形、图形与文本的混和,或者为了用一些附加信息再次调用脚本而必要的全部信息。一个常用并且更精巧的技术是使用GET调用脚本一次,这可以用一个标准的标记做到。脚本可以感知它是用GET调用的,并动态地创建HTML表单一一包括隐藏变量和再次用POST调用脚本所需的代码。

    兼容性问题

    在UNIX世界中,字符流是一种特殊的文件。默认地,STDIN和STDOUT是字符流。操作系统很有帮助地为你分析流,确保所通过的全是正确的7-bitASCII码,或者是认可的控制码。


    7-bit?是的。对于HTML,这没有问题。然而,如果你的脚本发送图形数据,使用面向字符的流则意味着立即失败。解决方法是将流切换到二进制模式。在C语言中,可以使用setmode函数:setmode(fileno(stdout),O_BINARY)。通过setmode(fi1eno(stdout),O_TEXT)在流当中进行切换。一个典型的图形脚本以字符模式输出头部,而后切换到二进制模式用于图形数据。

    在windows NT世界中,为了兼容性,流有着同样方式的行为。输出中的一个简单\n,当写到STDOUT时,被变换为\r\n。一般的windows NT调用,如write Fi1e(),不发生上述变换,如果同时想要一个回车和一个换行,则必须显式地指出\r\n。

    字符模式和二进制模式的另一种说法是cooked和raw,知道这两个名词的人或许会使用它们,而不是更常见的说法。不管使用什么词,在什么平台上,关于流存在着另一问题:默认情况下,它们是有缓冲区的,意思是操作系统挂起数据,直至看见一个行结束符、缓冲区满或者流被关闭。这意味着,你如果将有缓冲区的prinif()语句同无缓冲区的fwriie()或fprintf()语句混合在一起,事情可能就变得混乱了,尽管它们都会是写到STDOUT。printf()有缓冲区地将数据写到流,面向文件的例程则无缓冲区地输出数据。结果是乱序的一团糟。

    你可能将此归咎于后向兼容性。除了许多老程序之外,流实在没理由将默认定为有缓冲区和cooked。这应当是在需要时可以打开的选项,而不是在不要时关闭。幸运的是,你能够用setvbuf(stdout,NULL,_IONBF,0)解决这一困难,这个函数关闭UTDOUT流的全部缓冲区。

    另一个解决是避免混和不同类型的输出语句,即使这样,也不能使cooked输出变成raw。所以最好是关闭所有缓冲区。许多服务器和浏览器不喜欢接收单调乏味的输入。

    注意
    那些常把UNIX挂在嘴边的人可能会对名词CRLF(回车与换行)皱眉,而那些在其他平台上编程的人也许不认识\n或\r\n。CRLF等于\r\n。C编程者用\r表示一个回车(CR)符号,用\n表示一个换行(LF)符。(对于Basic编程,LF是Chr$(10,CR是Chr$(13)。)

    1.3 终止

    终止就是清理和退出。你如果对任何文件加了锁,则必须在程序结束前释放它们。你如果分配了内存、信号量或其他对象,也必须进行释放。不正确完成这些会导致脚本“昙花只能一现”。即脚本在第一次调用时能工作,而在以后的调用中就会崩溃。更有甚者,脚本由于没有正确释放资源和锁,将会妨碍甚至破坏其他脚本或服务器本身。

    在一些平台上一Windows NT最显著, UNIX次之——文件句柄和内存对象在进程终止时会被关闭和收回。即使这样,依赖操作系统为你清理垃圾也非明智之举。例如,在Windows NT上,如果一个程序对一个文件全部或部分加锁,而后不释放锁便终止,则文件系统的行为将是不确定的。

    必须确保你的出错一退出例程——如果有(也应该有)——了解脚本的资源并能象主退出例程一样彻底地对它们进行清理。

    2.计划脚本

    现在读者已经看到了一个脚本的基本结构,下面将要学习如何从头计划一个脚本。按照如下基本步骤进行:

    用一些时间定义程序的任务。整体、周到地考虑一下,把它写下来并描绘程序逻辑。当你已经很好理解了输入、输出和必须做的转换处理之后,才可以往下继续。
    预订好食物和饮料,把自己关在屋里一晚上,第二天便可以带着完成的程序出来了。如果前面第1步做得好,那么实际编写程序是算不了什么的(编写代码时不要忘记为它做文档)。
    测试、测试、测试。试一试各种知道的浏览器和各种能想到的输入。尤其检测一下诸如用户在一个10字节字段中输入32KB数据,或者在期望为简单文字的地方输入控制代码等等这些情况。
    将整个程序文档作为一个整体——不仅仅针对其中的单个步骤——以便让其他人维护或改编代码时能够理解你的意图。

    当然,本节的话题是上面的第一步,因此下面让我们更深入地看一看此过程:

    如果你的脚本处理表单变量,计划出每个变量的名字、预期长度、数据类型。
    当你从QUERY_STRING或STDIN拷贝变量时,检查类型和长度是否正确。UNIX破坏者的一个惯用技俩是蓄意让缓冲区溢出,鉴于一些脚本语言(显著的是sh和bash)为变量分配内存的方式,使得破坏者能够访问本应受到保护的内存,他们能在你脚本的堆或栈空间放置可执行指令。
    使用有意义的变量名字。一个指向环境变量QUERY_STRING的指针应该叫作类似PQueryString的名字,而不是P2。这不仅在一开始有助于调试,也能简化维护和修改工作。不管代码有多漂亮,也免不了在一年后想不起来P1是指向CONTENT_TYPE而P2指向QUERY_STRING。
    区分系统级参数和用户级参数。前者影响程序如何操作而后者提供实例特定的信息,例如,在一个发送电子邮件的脚本中,不要让用户指定SMTP主机的IP号。这个信息甚至不应该出现在隐藏变量里的表单上。它是实例无关的,因而应当是一个系统级参数。在Windows NT中,将该信息存在注册区里,在UNIX中,将它放入一个配置文件或系统环境变量。
    如果你的脚本退出外壳(shell out)而到系统去加载另一程序或脚本,不要传递未经检查的用户给出的变量——尤其在UNIX系统中,那里system()调用可包含管道或重定向符使不经检查的变量可引起灾难。聪明的用户和恶意的窃入者会用这种方式拷贝敏感信息或破坏数据。你如果不能完全避免system()调用,则要小心地计划。确切定义什么能作为一个参数传递,并且知道哪些bit来自用户。在程序中包含一个分析可疑字符串并将其排斥掉的算法。
    如果你的脚本存取外部文件,则要对如何处理并发做出计划。你可能会加锁部分或全部数据文件,建立一个信号量,或者使用一个文件作为一个信号量,决不要假想你的脚本是存取某一给定文件的唯一程序而毫不顾虑并发问题。你的脚本的5个拷贝可能会同时运行用以满足来自5个不同用户的请求。

    注意
    编程者使用信号量来同步多个程序、同一程序的多个实例,甚至是单个程序内的多个例程。一些操作系统具有对信号量的内置支持,另一些则要求编程者建立信号量策略。
    以最简单的含义,信号量象一个开关,它的状态可以被检测:开关是打开的吗?如果是,则这样做;否则那样做,文件经常被用作信号量(文件存在否?存在则这样做,否则那样做)。一个更复杂的方法是向文件加锁以实现互斥存取(如果能得到锁,这样做,否则,等待一会儿并重试)。
    在CGI编程中,信号量经常被用于同步同一CGI脚本的多个实例。举例来说,如果你的脚本必须更新一个文件,它不能假设文件随时可以得到。如果恰好该脚本的另一实例正在更新文件之中呢?第二个进程则必须等待,直至前一个完成,否则文件会被致命地破坏掉。解决的办法是使用一个信号量。要检测你的脚本以确保信号量被清掉。如果没有,它进入一个短循环,间隔地检测信号量。当信号量已被清掉,应设置信号量以避免其他程序介入,而后,便执行其临界区一一在这种情况下,写入文件一一然后再次清除信号量,使其他实例又能得到机会。信号量就是这样提供了一种管理并发安全性的方式。
    如果需要加锁文件,则应使用最小限定。当仅仅读取一个数据文件时,对写加锁,并在读完之后立即释放锁。当更新一条记录时,只对记录(或一定范围的字节加锁。理想情况下,锁逻辑应该紧紧围绕实际I/O调用。不要在程序一开头便打开一个文件并锁住它直至终止。如果必要,可以立刻打开文件但不要加锁,直到真正要用它时,这样能让其他应用或者你脚本的其他实例能工作平滑和快速。
    为意外事件准备良好的退出。举例来说,如果你的程序要求互斥地存取一个特定资源,准备好等待一段合理时间而后优雅地退出。决不要编写一个永久等待的调用。当你的程序从一个致命错误消亡时,要确保它在临终前对错误进行报告。错误报告应该使用简单明白的语言。如果可能,还应将错误写进一个日志文件,使得系统管理员能够知道它。
    你如果在使用一种GUI语言来编写CGI脚本,不要把捕获的错误表现为一个屏幕上的消息盒。这是一个服务器应用,错误将很少被人注意到并且清除,你的程序会挡在那里直至系统管理员偶尔路过。埋藏所有的错误,在能够存活的地方工作,把其他的都看成天灾吧。
    在启动代码编辑器之前,为你的例程写伪码,至少到一般逻辑结构一级。这常常有助于建立存根例程,使你能在开发中在程序时使用实际调用。存根例程(stub routine)是一个权宜之计,它并不实际做任何处理,仅仅接收最终例程期待的输入,返回结果一致的代码。
    对于复杂的项目,一个数据流图将大有稗益。数据流应该有别于逻辑源。数据在程序中按照某条路径流动,为各个程序片段所拥有,不管它是如何被子例程变换的。
    尽量封装私有数据和处理。你的例程应该有一个确定的输入和输出——一个门进,一个门出,并且要知道通过大门的是什么。你的例程如何完成其任务,这不是调用例程的事。这叫作“黑匣子方法”。从外面不应该看到箱子里面发生了什么,也不应对它产生影响。例如,一个正确封装的使用平面文件表的查找例程可以被置换为一个与后端数据库打交道的例程,而不用对程序的其余部分做何改变。
    在进行中为程序做文档。自组织文档的代码是最好的方法,带有一般的注释和用于分隔代码的额外空行,如果使用了含义明确,很有说明性的变量和函数名,则事情已经做了一半。但好的文档不仅仅指出一般代码是做什么的,还要说明为什么这样做。例如:“给REQUEST-METHOD赋值pRequestMethod”指出代码是做什么的,“确定是否由GET或POST引发”则说明为何编写该段代码,并且,更理想的是引出下一段代码和文档:“如果由 GET启动,做这个”或“如果由POST启动,做这个。”
    象计划输入那样仔细地定义输出。你给用户的消息应该是标准化的。例如,不要象这样报告一个文件加锁问题:“Couldn't obtain lock,Please try again later”,而报告一个栈溢出错误为“ERR4332”,成功消息也应该具有一致性,不要一次返回:
    you are the first visitor to this site since l/1/96
    而下次回返:
    you are visitor number 2 since 01-01-96

    如果按逻辑分类数据流和分组函数,则每种类型的消息应由相应于那种类型的例程产生。如果你把带有错误消息和ear1y-out成功消息的代码加入到程序的逻辑流中,那么终端用户来说,你的程序看上去不太一致,而对任何维护你的代码的人来说它是一团糟。

    注意
    early-out算法是一种用预先定义好的答案来检测异常和无意义情况并退出的算法,它不是以执行算法来决定答案的。例如,除法算法通常以两个操作检测一个除,并做一个移位而非除。

    3 标准CGI环境变量


    这里对常遇到的标准环境变量作一简要总结。各个服务器一致地实现了其中大部分, 但也有变化、例外和附加的情况。一般地,你更可能找到一个新的、没有归档的变量而非一个省略的归档变量。那么,唯一用来确认的办法就是检查你的服务器文献。本节内容来自于NCSA规范 ,是你所能找到的最接近“标准”的规范。NCSA CGI规范的URL如下:
    http://www.w3.org/hypertext/WWW/CGI/

    每当服务器加载脚本的一个实例时下述环境变量被设置,并且是私有和特定于该实例的:

    AUTH_TYPE
    如果服务器支持基本的认证并且如果脚本被保护,此变量提供认证类型,此信息是特定于协议和服务器的。AUTH_TYPE的一个例子是BASIC。
    CONTENT_LENGTH
    如果请求通过POST方法包括数据,此变量被设置为提供通过STDIN的字节的合法数据的长度——如,72。
    CONTENT_TYPE
    如果请求包括数据,此变量指定数据类型为一个MIME头一一例如,application/x-www-form-urlencoded
    GATEWAY_INTERFACE
    它提供被服务器支持的CGI接口的版本数,其格式为CGI/版本数:如CGI/1.1。
    HTTP_ACCEPT
    提供由逗号分开并被客户服务器可接受的MIME类型的列表,如image/gif,image/x-xbitmap,image/jpeg,image/pjpeg和*/*。此列表实际上来自浏览器本身,服务器只是将它传到CGI脚本。
    HTTP_USER_AGENT
    提供可能包含版本数或其他专有数据的客户沏览器名,如 Mozilla/2.0b3(Win NT;I)。
    PATH_INFO
    显示由客户提供并附在虚拟路径尾的任何附加的路径信息。它通常被用作脚本的参数 。例如,在URL
    http://www.yourcompany.com/cgi-bin/myscript.pl/dir1/dir2中,脚本为myscript.pl,PATH_INFO为/dirl/dlr2。
    PATH_TRANSLATED
    仅由部分服务器支持,此变量包含由虚拟路径到被执行脚本的转换(即虚拟路径到物理路径的映射)。例如,如果到你的Web服务器根的绝对路径为/usr/local/etc/httpd/htdocs,并且你的cgi-bin文件夹在Web服务器的根水平上(即,http://www.mycorp.com/cgi- bin),一个具有URL http://www.mycorp.com/cgi-bin/search.cgi的脚本将变量PATH_TRANSLATED 设置为/usr/local/etc/httpd/htdocs/cgi-bin/search.cgi。
    QUERY_STRING
    显示由客户提供的附在URL尾并用一个问号与脚本名分开的任何附加信息。例如,
    htt p://www.yourcompany.com/hello.html?name=joe&id=id=45中的name=joe&id=45为QUERY_STRING。
    10)REMOTE_ADDR
    它提供发请求客户的IP地址——如,199.1.166.171。此信息一直可用。
    11)REMOTE_HO8T
    它提供已分解的发请求客户的主机名。如dial-up102.abc.def.com。此信息通常不可用,这是由于两种原因:调用者的IP没能通过DNS正确映射到一个主机名,或是你的站点的Web管理员屏蔽了IP查找,Web管理员通常关闭查找是因为它们意味着在每次连接之后服务器要进行额外的步骤,这将降低服务器的运行效率。
    EMOTE_IDENT
    如果服务器和客户支持RFC931,此变量将包含由远程用户的计算机提供的识别信息。很少有服务器和客户还支持这种协议。这种信息也没什么价值,因为用户可把此信息设置为他们想要的任何东西。即使你的服务器支持也不要用这个变量。
    REMOTE_USER
    如果AUTH_TYPE被设置,此变量将包含用户提供并由服务器确认的用户名。
    注意
    AUTH_TYPE和REMOTE_USER仅在用户成功地使其标识在服务器上得到认证后(通常通过用户名和口令)才被设置,因此,这些变量仅在建立限定区域时并且仅在此区域中有用。
    REQUEST_METHOD
    它提供脚本被调用的方法。对于使用HTTP/1.0协议的脚本,仅GET和POST有意义。
    SCRIPT_NAME
    这是被调用脚本文件的名字,它对于自引用脚本很有用。例如,可用此变量产生一个通过GET方法被调用脚本的URL来产生并输出一个表单,这个表单被提交时通过POST法调用同样的脚本。通过使用此变量而非硬编码脚本名或位置将更容易做到维护——如,/cgi-bin/myscript.exe。当移动或更名脚本,当重新配置服务器而改变cgi-bin目录,或是在另外一台机器上安装脚本时,你不必改变代码。
    SERVER_NAME
    这是你的Web服务器的主机名、别名或IP地址。它对于在运行时产生指向服务器的URL是可靠的——如,www.ourcompany.com。
    SERVER_PORT
    这是本连接的端口号——如,80。
    SERVER_PROTOCOL
    这是本请求所用协议的名字/版本。如, HTTP/1.0。
    SERVER一S0FTWARE
    这是运行脚本的HTTP服务器的名字/版本。如,HTTPS/1.1。
    4 CGI脚本可移植性


    CGI程序员面临两种可移植性问题:平台独立性和服务器独立性。平台独立性是指代码不必修改就可以在不同于为其而写的硬件平台或操作系统上运行的能力。服务器独立性是指代码不必修改就可以在使用相同操作系统的另一台服务器上运行。


    4.1 平台独立性

    保持脚本可移植的最好办法就是要使用通用的语言,并且要避免使用平台特有的代码。听上去很简单,是吗?实际上,这就意味着要么用C语言要么用Perl语言,并且不不能做超出格式文本的事,也不能输出图形。

    这是否就意味着不必考虑使用VisualBasic,AppleScript和Unix shell等语言呢?是的,我认为目前是这样的。然而,平台独立性并非是选择一个CGI平台时所考虑的唯一准则,还要考虑如代码速度、维护的简易性和执行所选择任务的能力等因素。

    某此类型的操作是不可移植的。例如,如果你开发16位Windows程序,将很难在其他平台上找到所用函数VBX和DLL等效函数。如果开发的是32位windows,NT程序,你将会发现在UNIX环境下,所有异步Winsock调用都毫无意义。如果你的shell脚本调用一个System()来运行grep、并以管道形式将输出回送到你的程序,你将会发现,在windows NT或Windows 95环境下没有类似的东西。

    如果你的指令之一是以最少的修改在平台之间移动代码的能力,你可能会发现用C语言将会取得最大的成功。用ANSI C库中的标准函数写代码并且要避免其他的操作系统调用。不幸的是,遵循这样的规则将会限制脚本的功能。然而,如果你将依赖于例程自带的代码包含平台,你就使需要从一个平台移至到另一平台的工作最小化了。如果在前面部分“计划脚本”中所看到的一样,当谈及封装性时,一个设计良好的程序在其整体中的任何模块被替换后不影响到程序的其他部分。运用这些原则,你可能不得不替换一两个好程序,而且当然也得重新编译,但是,你的程序将是可移植的。

    Perl 脚本当然要比C程序更易维护,主要是因为没有编译这一步。在知道什么该修改时,可迅速修改程序。而事情难就难在这里:Perl今人恼火地迟钝,并且它的库比C语言的库更不一致——即使是在同平台上的不同版本之间也是如此。另外,windows NT下的perl相当新并且仍很奇特(似乎任何与Perl相关的东西都比其他部分更为奇特)。这个问题正在解决,但是不要在不理解Perl时就去使用它,几乎不可能直接从书上或联机资源上复制一个脚本而不需做任何修改就可在你的系统上运行。

    一旦识别出依赖于平台的部分并且找到(或写出)能得到标准函数的库,在平台之间 移动代码就不会有太大的麻烦了。

    4.2 服务器独立性

    比平台独立性更为重要的是服务器独立性(除非你只是因爱好而写脚本)。服务器独立性相当容易实现,但是因为某些原因,它对初写脚本的人来说也有点难缠。要做到服务器独立性,你的脚本必须不做任何修改就可在使用相同操作系统的任何服务器上运行。只有独立于服务器的程序作为共享软件或免费软件才真正有用,并且毫无疑问,服务器独立性对于商业软件是必须的。
    大多数编程人员考虑到的都是一些明显的问题,如不假定服务器有静态IP地址。接下来是服务器独立性的其他一些规则,尽管一旦指出来也很明显,但它还是一次次被忽略了:

    不要假定你的环境
    例如,不要因为你的开发环境上temp的目录为C:\TEMP就假定在脚本运行的其他地方目录也一样。不要将目录和文件名写死了,这将使Perl脚本的可读性更差,而这里对正确编程的曲解也时常发生。如果你的Perl脚本排除一定范围内IP地址的全部或部访问,就不要将此地址硬编码到你的程序中,也不要在注解中写上“改变此行”这样的话,应该用一个配置文件。
    不要假定特权
    在UNIX上,服务器(及你的脚本)可能是以用户nobody或root运行,或是以它们之间任何的特权水平运行的。在装有windows NT的机器上,CGI程序也继承了服务器的安全属性。检测访问权限并仔细检查返回代码,以便万一因不能访问其资源而使脚本执行失败是能提供给用户明了的错误信息。
    不要假定CGI变量的一致性
    一些服务器传递一些规定的CGI环境变量(如PATH和LIB变量),然而,它们传递的变量要依赖于运行时的环境。服务器配置也可影响CGI变量的数目和格式。如果有依赖于环境的输入,那么你的程序采取有相应的措施。
    不要假定特定版本的信息
    检测工作环境或检测告诉用户更改什么和为什么要更改的错误信息,服务器和操作系统版本都可影响脚本的环境。
    不要假定LAN或WAN配置
    在Windows NT环境下,服务器可能会是一个Windows NT工作站或是Windows NT服务器;它可能是独立的、工作组的一部分或是域的一部分。DNS(域名服务)可能或不能使用;查找可能会被限制在静态窗主机文件上。在UNIX环境下,不要假定任何关于如inetd、sendmail等守护程序的配置或系统环境,也不要假定目录名。对不能用系统调用找到的各项用一个配置文件,并给脚本维护人员指令以编辑脚本。
    不要假定系统目标的可用性
    用相应的特权,检测诸如数据库、消息队列、硬驱等目标的存在,并在找不到或配置错误时输出显式消息。没有什么比下载一个新脚本、安装而最后只得到“Run time error#203”的输出更令人恼火的了。
    5 CGI库


    通常谈及的CGI库有两种可能:一种是用户自己开发,并希望在其他项目中使用的代码库,另一种是公用的程序、例程和消息库。

    5.1 个人库
    如果你采纳了“计划脚本”中有关用黑匣子方式写代码的建议,你就会发现你正在创建一个将要反复使用的例程库。例如,在解决了如何解码URL编码数据这个问题后,就不必再去做这项工作。当你写好一个基本的main()函数后,该函数将可能为你所写的每一个CGI程序服务。这对一般的例程也一样,如查询数据库、解码输出、报告运行的错误。

    如何管理个人库取决于所用的编程语言。用C语言和汇编语言可以将代码预编译进实际的1ib文件,然后可用它来链接程序。尽管也有可能,但这种方法对CGI来说是不必要的,而且它对于解释性语言(如Perl和VisualBasic)是无效的(尽管Perl和VB可以调用已编译好的库,但是不能用同使用C语言一样的静态方式来链接它们)。使用已编译好的库的好处是当改变库中的代码时不必重新编译所有的程序。如果库是在运行时(一个DLL)装入,就不必修改任何东西。如果库是被静态链接,所需做的只是重新链接。

    另一种解决办法是保留独立的源文件,并在每个项目中包括这件文件。你可以将最为常用的例程放人一个非常大的文件中,而把其他不太常用的例程放到各自独立的文件中。以源文件格式保留文件会增加编译时间,但不必担心——尤其是同节省写代码的时间相比时。这种方法的不利之处是当修改库代码时,必须重新编译你的所有程序才能利用修改后的好处。

    没有什么可以阻止你把公共域例程并入你的个人库中。一旦确定了版权和特许允许使用和修改源代码而不必付费或没有其他条件,你就可以将感兴趣的例程筛选进你的库中。

    设计的归档良好的程序为新的程序提供了基础。如果仔细地将特定的程序组成部分分离成为例程,就没有什么理由不把整个程序的结构拆用到其他项目中。

    也可以开发某些例程特定于平台的版本,并且,如果编译程序允许的话,可自动为建立的每种类型包括进正确的例程。最坏的情况下就得手工指定需要的例程。

    使代码可重用的关键是尽量使代码通用,但也不是绝对通用。例如,美元纸币打印例程不需要通用到可同时处理美元和日元,但至少任何打印美元总量的程序都可调用它。在升级、增加功能甚至修改例程内容时,要保持每个函数的输入和输出不变。这就是实际上的黑匣子方法。通过保持调用约定和参数不变,就可自由升级任何代码段而不必担心破坏调用你的函数的程序。

    另外一种要考虑的技术是使用函数框架。假定你最终决定打印日元和美元两者的单个例程实际上是最有效的方法。但是你已经有了分开的例程,并且旧的程序不知道如何将增加的参数传递给新的例程。你不必回头去修改调用旧例程的每个程序,你只需使用库中例程的框架,这样程序唯一要做的只是用正确的参数调用新的组合例程。在一些语言中,可通过重新定义例程声明部分而实现这一点;在其他一些语言中,就需要编码一个调用并要付出一些额外系统开销的代价。但既使这样,这个代价也远小于所有的旧程序遭到破坏的代价。


    5.2公共库
    Internet具有丰富的公共域范例代码、库和预编译程序。尽管你所能找到的这些大部分是面向UNIX的(因为它出现得时间较长),然而并不缺乏面向Windows NT的例程。
    下面的序列是Internet上的一些最好的站点及对在每个站点上能找到什么的简要描述。这个序列中包含一部分站点。成百上千的站点致力于或是包含有关CGI编程的信息。打开你的web浏览器和最喜欢的搜索引擎并告诉它搜来“CGI”或“CGI Libraries”,你就会看到我说的那些东西了。为了使你免去那些乏味的击点,我已为你把它们都找了出来。请见免费资源—免费CGI源码


    6 CGI的局限

    CGI的最大局限是它的“无状态性”。一个HTTP服务器是不会记住两个请求组成——这些请求要么全是到同一服务器的,要么是到一些不同的服务器。每种情况下,服务器完成请求后,就挂起并忘记曾顺便访问过的用户。

    能够记住一个呼叫者上次接通时做了什么的能力叫做“记住用户的状态”。HTTP以及CGI都没有自动保留状态信息,在Web事务中与状态信息最相近的东西是用户的浏览器高速缓冲区和CGI程序的智能。例如,如果一个用户在填写一个表单时漏填了一个必须的字段,CGI程序不能弹出一个警告框也不能拒绝接收输入。那么,这个程序唯一的选择是要么输出一条警告信息,告诉用户单击浏览器的back按钮,要么再次输出整个表单,填入提供的字段值并让用户重试,或者修改错误或者提供遗漏的信息。

    有几种解决这个问题的办法,但都不是很令人满意。有一种想法是保留一个包含来自所有用户最新信息的文件,当一个新的请求传来时,在文件中找到这个用户并假定基于用户上一次所做事情的正确的程序状态。这种想法的问题是很难识别一个Web用户,即一个用户可能在今天没有完成操作而第二天又因其他目的再次访问这个站点。这种方法大量的精力都花费到了保留状态的算法上了,而这只是为了节省有限的一点时间。所以,这种解决问题的方法效率很低,并且它们忽视了其他的问题:即首先要识别用户。

    你不能依靠用户来提供他或她的标识。不仅那些想匿名的用户,而且即使那些想让你知道他们名字的用户都可能一次又一次地将名字拼写错。那么,用IP地址作为用户标识又如何呢?也不好。每个通过同一代理的用户都使用相同的IP地址。在某一时刻,是公司内哪个雇员在呼叫呢?你说不出来的。不仅如此,现在许多人在每一次拨号时都使IP地址动态分配。你当然不会因为这一次John Joe和Jane Joe的IP地址相同,就给John Joe访问Jane Joe的数据的特权。

    标识映射唯一可靠的形式是由服务器提供的,它运用名字和口令模式。即使这样,用户还是不能忍受每次请求时都需输入名字和口令,所以,服务器缓存数据并用前面提到的算法判断缓冲区何时变得非法。

    假定你们单位的CEO没有使用其名字或其他可猜得到的东西作为口令,并且也没有人洗劫他秘书的抽屉,也没看过他监视器上的黄色便笺,那么在服务器告诉你他是CEO时你就有理由肯定他就是CEO。那么接下来做什么呢?你的CGI程序仍必须通过一些环节来防止CEO在查你的数据库时重复回答相同的问题。你的CGI程序的每个响应都必须包含从那点开始程序向前或向后进行的所有信息。这很麻烦但却很有必要。

    第二个继承到CGI程序中的主要局限性是与围绕发送文档的设计HTTP规范的方式有关。HTTP不倾向于长交换和长交互性。这意味着当你的CGI程序要做一些像生成一个服务器推送的图形时,它必须保持连接打开。它是通过把各种图像都真正当成同一图像的组成部分而实现这一点的。

    可怜的用户例览器还坚持显示“连接活动”的信号,以为它正在检索单个的文档。从浏览器的观点来看,文档只是偶尔有点过长。但从脚本的观点来看,文档实际上是由几十个(也许是成百个)独立的图像组成的,每个图像都是按顺序通过管道传输的,并且它被做为一个巨大文件的部分被标记,而这个文件实际上并不存在。

    也许当下一代HTTP规范出现,并且例览器和服务器被更新以能利用保持有效的协议时,我们将会看到一些真正的革新。同时, CGI就会成为真正的CGI。尽管CGI偶尔不那么优雅,但它还是非常有用——并且很有意思。


    资料来源: 计算机世界网