CGI 应用程序开发基础
1.CGI 脚本结构
当脚本被服务器引发时,服务器常常以两种途径之一向脚本传递信息:GET或POST。这两种方法被称为请求方法。所使用的请求方法是通过环境变量传给脚本,该环境变量叫作REQUEST_METHOD(还定义了另外两种请求方法一HEAD和PUT,但它们不是特别应用于CGI,并且不鼓励使用它们)。
1)GET是对数据的一个请求——同样的方法被用于获得静态文档。GET方法以附加在URL后面的参数发送请求信息。这些参数将放在环境变量QUERY_STRING中传给CGI程序。例如,有一个叫作Myprog.exe的脚本,从如下的链接启动它:
<a href="cgi-bin/myprog.exe?lname=blow&fname=joe">
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代表恰当的子例程):
实际情况并非这么简单。
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调用脚本一次,这可以用一个标准的<A
HREF>标记做到。脚本可以感知它是用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.计划脚本
现在读者已经看到了一个脚本的基本结构,下面将要学习如何从头计划一个脚本。按照如下基本步骤进行:
当然,本节的话题是上面的第一步,因此下面让我们更深入地看一看此过程:
注意
early-out算法是一种用预先定义好的答案来检测异常和无意义情况并退出的算法,它不是以执行算法来决定答案的。例如,除法算法通常以两个操作检测一个除,并做一个移位而非除。
3 标准CGI环境变量
这里对常遇到的标准环境变量作一简要总结。各个服务器一致地实现了其中大部分,
但也有变化、例外和附加的情况。一般地,你更可能找到一个新的、没有归档的变量而非一个省略的归档变量。那么,唯一用来确认的办法就是检查你的服务器文献。本节内容来自于NCSA规范
,是你所能找到的最接近“标准”的规范。NCSA CGI规范的URL如下:
http://www.w3.org/hypertext/WWW/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地址。接下来是服务器独立性的其他一些规则,尽管一旦指出来也很明显,但它还是一次次被忽略了:
通常谈及的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”,你就会看到我说的那些东西了。为了使你免去那些乏味的击点,我已为你把它们都找了出来。下面是一些很有用的站点:
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偶尔不那么优雅,但它还是非常有用——并且很有意思。