Visual Prolog的CGI应用程序：基础篇

（以下内容译自Category:Tutorials中的CGI Applications in Visual Prolog: Basics. ）

CGI 是 Common Gateway Interface （通用网关接口）的缩写. 它是万维网联盟（the World Wide Web Consortium）推荐的用于网络服务器输入输出信息流的一种规范.

读者大约知道，HTML浏览器（如 Internet Explorer、 Netscape 等）要发送信息并从网络服务器接收信息. 不过，在万维网上并不只是简单地读一些超链接的HTML文件，还有交互，数据在浏览器中是有进有出的，包括用户在浏览器中填写的表格数据等. 有许多事网络服务器自己是应付不了的，它不知道如何处理类似的交互数据.

其实网络服务器也就是个简单的机器，它通常也就是发送一下HTML文件及其它一些它能认识的mime文件给浏览器. 一般它做不了再高级的事了，比如访问后台数据库、向浏览器发回数据库查询等. CGI就是用来增加网络服务器功能的，它可以让网络服务器与驻留在同一个机器上的CGI应用程序对话. 而CGI应用程序可以做那些网络服务器做不了的事，如：访问后台数据库或进行复杂搜索等. 使用CGI的网络服务器通常要比不使用的能为浏览器提供更多的信息.

“通用网关”有什么意义？
“通用”表示所有网络服务器都接受这个协议，“网关”是说好像是通过一扇门、一个关口从另一边的其它处理过程获得数据. 打个比方，网络服务器在网关的一边而CGI应用程序在网关的另一边，网络服务器和CGI应用程序都用CGI进行对话.

什么是流？
“流”这个术语，用来表示长长的信息队列是一次一个字符地处理的. 可以这样想象：一个直径是任意时刻只能有一个字符通过的管子，在流的形式下，字符是一次一个地从管子另一头出来的.

和管子里的水不能流向两个方向一样，程序中的一个流同一时刻也不能即可读又可写. 字符要么是从流中出来（也就是可读的流），要么是到流中去（可写的流），但同一个流一个程序不能用来既读又写.

对于CGI应用程序，字符是 ANSI 格式的，也就是8-bit字符. 关于这一点后面还要说，因为Visual Prolog缺省使用的是 Unicode 字符，是16-bit的. Visual Prolog中有合适的谓词采集流中的ANSI字符，所以处理流不成问题. 最后还要说一点，我们能浏览网页就是字符流通过网络管道一次一个字符地进入了浏览器，我们察觉不到是因为单个进入浏览器的字符很快就被组合在一起了.

什么是mime类型？
可以认为mime类型就是文件扩展名. 它说明网络服务器及其客户（如浏览器）所能处理的文件类型. 大多数网络服务器是通过文件扩展名确定mime类型的. 浏览器与网络服务器交互时，它在索求资源（特定的mime类型文件，等等）时也会告诉网络服务器自己能处理的mime类型. 网络服务器发送浏览器需要的文件，如.html、.jpg、.gif等等，供它显示. 不认识的mime类型，网络服务器或者就不送，或者就按普通ASCII或HTML文件送，具体做法由网络服务器确定.

因为信息是当作流送给浏览器的，网络服务器要在流的起始插入一些包含mime类型的头. 图1典型地说明了网络服务器给浏览器送了些什么. 一但这些带有mime类型的文件到了浏览器，它就要分析头，确定mime类型表示的数据，再按照自己可以处理的mime类型的内部表，显示相应的数据.



什么是头？
头，在这里指的是嵌在流里的、在实际数据之前的、带有回车符的一行文本. 图1中，可以看到，数据上面的那些行都是网络服务器传给浏览器的头. 在实际数据和头之间有一个分隔空行.

注意图中的“Content-type:...”头，这行就是数据的mime类型. 它与CGI应用程序特别相关，因为网络服务器调用CGI应用程序提供要传递给浏览器的数据时，它对CGI应用程序给出数据的mime类型是无能为力的. 因此，按约定CGI应用程序在传递其它数据之前先要传递这个特殊的头，头和数据之间用空行分隔. 在实际编写CGI应用程序时要特别注意这一点，忘了内容类型的头或是分隔的空行或是两者都忘了是常有的事，这会弄出很多麻烦.

什么是CGI应用程序？
CGI应用程序，或说网关程序（见图2）是一个可执行文件（一般是 .exe 文件），它不接收键盘或鼠标信息，也就是说它不是交互程序（而图形用户界面程序则是交互程序）. 这样的程序也叫控制台程序，它用 stdin 流接收输入而用 stdout 流输出. 其它的头是网络服务器在CGI应用程序之后添加的，整合在一起的信息再送给浏览器.



对于CGI应用程序，一经启动就会自己运行直到结束，不再需要什么输入. 从这点上看它是自动的，就如同过去DOS里的命令行程序xcopy、format等一样.

CGI应用程序又不是一般的控制台程序，它要求输入符合CGI规范，它的输出也会遵从CGI规范.

有时，CGI应用程序也可以写成 .DLL 文件，但这样的变化要依赖于网络服务器. 更特殊地，甚至批命令（.bat文件）也可以当成简单的CGI应用程序，只要网络服务器配置得能把这样的文件用做CGI应用程序. 这里我们不涉及这些变形，只说支持CGI规范的可执行的（.exe文件）控制台应用程序.

什么是 stdin 流和 stdout 流？
stdin 是各种编程语言都使用的一个名称，它指由底层操作系统接收输入的这样一种特殊的输入流. 程序（比如用Visual Prolog编写的程序）可以读 stdin 流，用流中的字符作为输入. 相似地，stdout 是由操作系统管理的流，程序可以把字符输出到这个流中去.

stdout 和 stdin 流可以从不同的程序中链接起来（也就是端到端连在一起）. 例如，A程序写 stdout 流，而后另一个B程序用 stdin 流读取A程序写的内容. 这是网络服务器使用的最基础的传输机制. 在前面的例子中，A程序可以表示网络服务器而B程序就可以是CGI应用程序. stdin 和 stdout 流也常常合起来称为控制台（console）.

使用CGI时信息流动是怎么发生的？ 信息的流动起始于客户端（如浏览器）向网络服务器发出的请求. 上面说过，客户端会告诉网络服务器自己的能力，还有需要网络服务器提供的信息. 参看图3，它说明了浏览器（客户）向网络服务器发送的内容.



第一行的内容：GET /Tests/file.html HTTP/1.0 有方法域. 它指明了期望的HTTP方法、请求的资源、协议版本，等等. 尽管 /Test/file.html 看起来像是个路径，但实际上它被称为URL（Uniform Resource Locator），它代表了一大堆东西，包括网络服务器所在机器上的实际文件名和目录等.

常用的方法还有 POST ，它用于从浏览器向网络服务器发送数据（不是URL的一部分）； HEAD ，它用于只接收资源的头信息. 其它一些域里包含有客户端的信息：Accept: 这个域告诉网络服务器客户端可以接受的各种数据类型，这些都是按MIME内容类型信息发送的. 因此 Accept: text/plain 就表示该客户端可以接收普通文本文件，其它的以此类推. 这个MIME类型是很重要的，因为这也是网络服务器告诉客户端数据按什么类型发送的方法. 其它一些头是些相关消息：User-Agent: - 产生请求的程序； From: - 谁的请求； Referer: - 这个请求需要的文件URL，等等. 服务器发回需要的数据作为响应. 它首先发送服务器响应头，这是用来通知传输状态、发送数据类型及一些其它附属信息的. 接下来是一个空行，再接着才是实际的数据，参看图1. CGI在哪儿掺和进来的呢？——读者可能会奇怪. 好，我们再来看请求的资源，在上面的例子中就是 /Tests/file.html， 如果替换掉它，换成 /cgi-bin/helloworld.exe. 很多网络服务器的配置都是让这部分URL /cgi-bin/ 指示说 helloworld.exe 是个该服务器要调用的CGI程序.

这个时候，helloworld.exe就会被网络服务器调用，而且还会通过环境变量和 stdout 流（它也是helloworld.exe的 stdin 流）传递一些信息给helloworld.exe. 接下来，网络服务器就等着CGI应用程序，helloworld.exe，结束它的工作.

helloworld.exe一停，网络服务器就收集它写出来的数据（包括内容类型头）并把它送给等着的浏览器.

这一切都是一眨眼就完成的.

所有网络服务器的设计都很细致，能很好地完成这些工作，用不着我们操心调用CGI应用程序后的整合工作. 我们重点来看看CGI应用程序要怎么从网络服务器读信息以及自己做完事情后怎么把数据返回给网络服务器.

什么是环境变量？
所有操作系统都允许编程人员在所谓环境变量中加入 name=value 的关系式，这些环境变量是放在计算机内存中的. 例如，DOS中的路径设置就是一个环境变量. 可以用SET命令来查看为DOS设置的各种环境变量（Windows XP或Windows 2000也可以查看）. 只要在命令行提示符后敲入SET命令，控制台就会显示出所有当前可用的环境变量（在列表中就可以看到路径）.

环境变量最妙的是：可以让你构造完全个性化的变量并为其赋值. 例如，想把口令保存起来，在操作系统命令行上可以这样用SET命令：

SET password=xyze

尽管这个值是串，但是不必用引号. 所有的值在内部保存时都是串，Visual Prolog程序也是按串取出它们的. CGI应用程序中环境变量的意义在于：网络服务器也用环境变量向它调用的CGI应用程序传递信息. 下面是个例子. 我们已经知道，网络服务器在调用CGI应用程序之前把许多数据放在了 stdout 流中. 那么，CGI应用程序怎么知道要从流中读取的字符数量呢？为解决这个问题，网络服务器把内容长度放在一个叫作CONTENT_LENGTH的环境变量里，用这个环境变量的值CGI应用程序就可以从流中读出正确数量的字符. 看一下cgi.pro中的Visual Prolog代码（这是Visual Prolog的CGI包里的）是怎么做的：

clauses retrieve_POST_string = Result :- % 取长度 LenStr = environment::getVariable('CONTENT_LENGTH'), Len = toTerm(LenStr), %只从流中读取出需要长度的内容 Stream = console::getConsoleInputStream, Stream:setMode(stream::ansi(ansi)), %流是ANSI模式的！ Result = Stream:readString(Len).

除了CONTENT_LENGTH，大多数网络服务器在启动CGI应用程序之前还会设置下述标准环境变量，使用的网络服务器不同可能环境变量也会有所增减. CGI应用程序可以查看这些环境变量并使用相应的值. 读者可以参看其它读物了解这些环境变量，不然本文就太长了. 特别说一下，环境变量HTTP_REFERER在实现一些简单的安全保密措施中是很有用的（参见本文的高级篇）.

PATH_INFO PATH_TRANSLATED REMOTE_HOST REMOTE_ADDR GATEWAY_INTERFACE SCRIPT_NAME REQUEST_METHOD HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING HTTP_ACCEPT_LANGUAGE HTTP_FROM HTTP_HOST HTTP_REFERER HTTP_USER_AGENT HTTP_COOKIE QUERY_STRING SERVER_SOFTWARE SERVER_NAME SERVER_PROTOCOL SERVER_PORT CONTENT_TYPE CONTENT_LENGTH USER_NAME USER_PASSWORD AUTH_TYPE

如何测试CGI应用程序？
随同CGI应用程序，还需要安装一个网络服务器，这样才能做测试. 还需要HTML支持文件，它通过网络服务器来触发CGI应用程序. 这些文件对不同的应用程序也都会不同. 在学习例子的过程中，会慢慢了解这些支持文件.

前面什么地方说过，CGI应用程序要放在网络服务器所在的同一台机器上. 从安全上考虑，网络服务器只能从特定的（web路径）主机目录中获取文件. 因此，要把CGI应用程序（CGI可执行文件和HTML文件）放在这样的目录下. 如果不这样，就会使恶意访问者获取到不该能获取的文件.

同样，网络服务器也只能调用放在正确位置的CGI应用程序. 不能到处放CGI应用程序（尽管也有网络服务器允许这样，但并不是所有的都行）. 现在，所有的网络服务器都有配置手段（独立的配置文件，或 .ini 文件，或windows注册表，等等）来配置自己的各种设置. 其中关键的一个设置就是CGI应用程序的目录. 针对某个网络服务器使用CGI应用程序时，首要的事情就是了解这个网络服务器是如何配置其CGI应用程序目录的.

使用什么网络服务器？
以前说过，网络服务器也就是个并不太复杂的软件. 有很多可以免费使用的网络服务器，在 http://www.serverwatch.com/stypes/index.php/d2Vi 有列表，表里有商用的也有免费的. 网络服务器的难点通常是它的web-path（网络路径）及CGI应用程序目录的配置.

安装网络服务器的计算机里要安装TCP-IP协议（如果计算机已经可以访问因特网则已经安装好了）. 安装好网络服务器后，或是按IP地址或是按机器的域名，就可以从网络服务器获取数据了. CGI应用程序可以与大多数网络服务器包括与Windows 2000自带的IIS5网络服务器配合工作得很好.

CGI应用程序放在哪儿？
一定要确保CGI应用程序放在网络服务器的适当目录里，这样的目录每个网络服务器各不相同. 本文中，我们假设是放在这样的目录里的，它可以用服务器上的浏览器按这个URL: http://localhost/cgi-bin/ 被调用.

例如，如果有一个名为example1.exe的CGI应用程序，它应该能用URL：http://localhost/cgi-bin/example1.exe 在服务器上被访问到.

看个很简单的用Visual Prolog创建的CGI应用程序，有吗？
有，没问题！只需要解压缩cgitutorial.zip中的example1.zip文件到合适的地方，查看里面的Visual Prolog文件. 不过，也可以按下面的步骤创建一个这样的应用程序：

打开一个新的Visual Prolog工程，UI策略是Console（控制台）：



主工程树会显示出来，里面有IDE智能创建的可能需要的所有文件.



接着，点主菜单上的 Build ，在其中再选 Build 或是按 Ctrl-Shift-B. 注意构建中的变化，工程构造时IDE会把先前工程树中看不到的相关PFC（Prolog Foundation Classes，Prolog基础类）文件拉到工程树中来. 构造过程结束后，工程树会像下面这样：



恭喜！百分之九十五的活儿完成了. 在工程的EXE文件夹里，有了一个控制台应用程序，它现在什么事情都不会做. 我们需要加点儿代码把它变成个CGI应用程序.

我们在前面说过，CGI应用程序期望来自 stdin 流（以及一些环境变量）的输入，然后它干活儿，再把输出送到 stdout 流里去. 作为一个很简单的CGI应用程序，我们略去来自 stdin 流的输入到这个CGI应用程序的所有信息（可能网络服务器送来）. 不过，我们还是要让这个应用程序稍稍有点儿输出.

双击工程树中的 main.pro 文件，往代码的尾部走：



可以看到，程序目标从PFC的mainEXE模块中调用了一个名为run的谓词，这个模块又从 main.pro 中调用了名为 run 的谓词，所以看这里：

clauses run:- console::init, succeed. % 把自己的代码放在这里

像下面这样改一下代码：

clauses run:- console::init, %Line 1 cgi::init(stream::ansi(core::ansi)), %Line 2 stdIO::write("Content-type:  text/html\n"), %Line 3 stdIO::write("\n"), %Line 4 stdIO::write(" Hello World!  "). %Line 5

逐行说明一下这些代码：


 * Line 1: 这是Visual Prolog要求的，所有用 console 干活儿的都要首先 console::init.
 * Line 2: 取 stdout 流对象做CGI的输出. 我们说过，CGI应用程序要把自己的输出送到 stdout 流去.
 * Line 3: 现在CGI应用程序准备好了送第一行数据给调用它的网络服务器. 先是头，这是强制的，要设置数据的mime类型，这里它说数据内容mime类型是text/html. 注意结尾的回车符（\n）.
 * Line 4: 分隔头与数据需要一个空行. 按CGI规范这是强制性的，所以CGI应用程序要送一个空行（\n）. 常常会忘了这个，这将引起应用程序的故障.
 * Line 5: 最后把实际数据写到 stdout . 这里做的很简单，创建一个HTML文件并发送这个文件. 文件里只有两个单词：Hello World!.

如何测试Example1.exe？
按前面说的，把它放在网络服务器的脚本目录里. 然后用浏览器在同一个机器上按URL： http://localhost/cgi-bin/example1.exe 访问.

这么简单的应用程序有什么功能？
这个 Hello World CGI应用程序并不像它看上去那么简单. 好吧，它很简单，但它可以作为很复杂工作的起点. 就看最后一行（Line 5)，它创建了一个完整的HTML文件并用 stdout 流发给了网络服务器，网络服务器又转发给了发请求的浏览器. 如果从这里引入数据库，添加一些对后台数据库数据的条件处理，很容易就可以建成一个目录管理系统.

输入呢？
前面为求简单，我们略去了网络服务器可能给这个应用程序的所有输入. 如果CGI应用程序需要输入，就要读 stdin 流并处理信息. 此外，还要查看CGI应用程序运行时环境变量的设置. 这就有些复杂了. 不过还好，用Visual Prolog开发CGI应用程序时我们并不需要做很多编程工作. 例如，要知道CGI应用程序从网络服务器接收了些什么数据，要做的只是使用 cgi::getString 谓词（要把PFC里的cgi.pack包括到工程中）. 如果需要更方便的数据形式，还可以使用 cgi::getParamList 谓词，它会把每个数据按名称和值的对应关系组合起来，像在环境变量里那样.