C语言cgi编程入门

本文译自Getting Started with CGI Programming in C。因为本人水平有限,难免有误,欢迎指正。

这是一篇介绍如何使用C语言进行CGI编程的文章。假设读者了解C基础知识,并且可以写出简单的HTML表单和在Web服务器上安装CGI脚本。通过简单的范例对原理进行解释说明。

两点重要提醒:

  • 为了避免浪费时间,请检查——通过恰当的本地文档或者联系Web服务器管理员——你是否可以在服务器上安装和运行C语言书写的CGI脚本。同时,请从细节上检查应该如何做——特别是应该将CGI脚本放在何处。
  • 本文主要是面向C程序员讲解如何进行CGI脚本编程。实际上,CGI程序通常由别的语言书写,例如Perl,并且有着合理的原因:除非是非常简单的情况,否则使用C语言来编写CGI程序非常愚笨,并且容易出错。

为什么要进行CGI编程

就像我的《如何编写HTML表单》这篇文章中简单介绍的那样,你需要一种服务端脚本来使HTML的使用更加可靠。典型地,存在一些简单的服务端脚本,提供简单、通用的方式来处理表单提交。例如,通过Email发送文本格式的数据给指定的地址。

然而,对于一些高级处理方式,例如将数据收集到文件或者数据库,或者将信息取出并且发送回去,或者对提交的数据进行一些计算,你很可能需要自己写服务器端脚本。

从根本上说,CGI 就是HTML表单与服务端脚本之间的接口。它可能不是唯一存在的接口——请阅读 Lars Marius Garshol 的这篇极好的教程《Web如何工作:HTTP和CGI解析》,了解CGI的概念,并且注意一下其他可能存在的接口。

如果有人建议使用JavaScript作为CGI的选择之一,让他去读我的这篇文章《JavaScript 和 HTML:可能性和警告》。简单地说,JavaScript 天生不够可靠,至少在没有备份服务端脚本的情况下是这样的。

一个基本的例子

上面提到的《Web如何工作:HTTP和CGI解析》是个非常好的教程。我下面的介绍只不过是对基础知识展现的一次尝试。你如果感到困惑或者需要更多信息,请查阅其他资源。

让我们考虑下面这个简单表单。

XHTML

 

1

2

3

4

5

<form action="http://www.cs.tut.fi/cgi-bin/run/~jkorpela/mult.cgi">

<div><label>Multiplicand 1: <input name="m" size="5"></label></div>

<div><label>Multiplicand 2: <input name="n" size="5"></label></div>

<div><input type="submit" value="Multiply!"></div>

</form>

在你的浏览器中,它应该是下面这个样子:

你可以尝试一下如果你喜欢的话(译者注:这里的表单无法尝试,因为这是一张图片:))。假如你尝试的时候服务器恰好不可用或者无法访问,下面是正常情况下得到的结果:

XHTML

 

1

2

Multiplication results

The product of 4 and 9 is 36.

例子分析

现在我们分析一下上面的例子是如何工作的。

假设你在第一个 input 中输入了4,在另外一个中输入 9,然后执行提交——通常是通过点击提交按钮的方式。浏览器HTTP协议发送一个请求到这台服务器 www.cs.tut.fi。浏览器从 ACTION 的值处获取主机名,将它作为URL 的一部分(通常情况下,ACTION 属性使用的相对URL,相对于当前页面,但是这不是必须的。现在这个例子就不是)。

发送请求的时候,浏览器会提供一些额外的信息,指定一个相对URL,在这个例子,是:

XHTML

 

1

/cgi-bin/run/~jkorpela/mult.cgi?m=4&n=9

这个URL由ACTION的值去掉主机名后,追加上问号,然后加上特定格式的表单数据构成。

服务器,也就是请求发送的目标主机(在本例中是 www.cs.tut.fi),会根据它自己的规则处理请求。典型情况是,服务器的配置定义了如何将相对URL映射到文件,哪个目录用于解释CGI脚本。就像你可能猜到的那样,本例中 URL 地址中的 cgi-bin/ 部分就是用于解释的目录。也就是说,除了要接收和返回HTML文档或者其他文件(向发送请求的浏览器),Web服务器还要调用 URL 中指定的 cgi 脚本(本例中是mult.cgi) ,并且将一些数据传递给它(本里中的数据是m=4&n=9 )。

真实情况是怎么样的,取决于服务器。在当前这个例子里,Web服务器其实运行了 jkorpela 用户家目录中的  cgi-bin 子目录中的可执行程序 mult.cgi。根据服务器配置的不同,情况可能大不相同。

何为 CGI 编程

这个经常令人迷惑的CGI是Common Gateway Interface(通用网关接口)的简写,就是一套关于如何进行调用和参数传递细节问题的约定。

调用,在不同的情况下有不同的含义。对于 Perl 脚本而言,服务要调用 Perl 解释器并且以解释方式执行脚本。但是对于可执行程序,通常是由像C这样的语言写出来,然后由编译器和加载器生成的,将仅仅是开启一个独立的进程。

虽然脚本这个词通常意味着代码是解释型的,术语 CGI 脚本通常指代着解释型脚本和可执行程序二者。参阅 Nick Kew 写的《CGI编程问答》中的《是脚本还是可执行程序》。

使用 C 作为 CGI 脚本

为了配置 C 为 CGI 脚本,需要先将它转为二进制可执行程序。这里通常会有问题,因为人们通常工作在 Windows 系统,而服务器却运行在 UNIX 或者 Linux 系统上。你开发使用的系统和服务器安装的系统通常有着不同的系统架构,因此同一个可执行程序不能同时在这两套系统上运行。

这可能会导致一个无法解决的问题。如果不被允许登录到服务器上,并且又没有使用二进制兼容系统(或者跨平台编译器),就不是那么幸运了。很多系统,允许登录并且使用交互模式,作为一个 shell 用户,并且会提供 C 编译器。

你需要在服务器上编译并且载入C程序(或者,从原理上说,你也可以使用一个相同架构的系统,这样它编译的二进制程序可以运行在服务器上)。

通常,你应该这么做:

1. 在交互模式下编译和测试C程序

2. 对代码做一些的必要改动,以便它可以作为CGI程序来使用。程序要能从预定的表单提交方法中读取输入数据。使用默认的 GET 方法,程序从环境变量 QUERY_STRING 中读取输入数据(程序也应该能从文件中读取数据,但是这些文件必须要存在于服务端)。它应该在标准输出流(stdout)中生成输出,所以它应该以合适的HTTP header开始。通常,输出的格式为 HTML。

3. 再次编译和测试。在测试阶段,你可以设置环境变量 QUERY_STRING 使之包含测试数据,就像从表单发送过来一样。例如,如果你想要发送一个包含名称为 foo 的表单域的表单,可以在使用 tcsh shell 的时候,使用下面的命令:

XHTML

 

1

setenv QUERY_STRING "foo=42"

或者在使用 bash shell 的时候使用下面的命令:

XHTML

 

1

QUERY_STRING="foo=42"

4. 检查编译的版本能够适用在服务器。这也许需要重新编译。你应该登录到服务器上(使用 telnet、ssh 或者其他终端仿真器),以便可以使用那里的编译器。

5. 上传编译和加载的程序,例如可执行的二进制程序(连同需要的数据文件)到服务器上。

6. 编写一个简单的包含测试表单的HTML文档来测试该脚本。

你需要将可执行程序上传到服务器上并根据服务器的约定来命名它。甚至这里需要的编译指令和你以前在工作站上使用大相径庭。例如,如果服务器以UNIX风格来运行并且包含GNU C编译器,你将会使用类似这样的编译命令:gcc -o mult.cgi mult.c 然后移动(mv)mult.cgi 文件到一个名字为 cig-bin 的目录中去。但是如果使用 GCC,你可能使用 cc 命令。你确实需要针对这些问题,仔细检查本地说明。

文件拓展名 .cgi 通常没有固定含义。然而,可能会有依赖于服务器(或者依赖于操作系统)命名可执行文件的规则。典型的可执行文件拓展名是 .cgi 和 .exe。

Hello world 测试程序

按照惯例,刚开始接触一门新的编程技术,通常从一个简单的程序开始。这样可以避免一些潜在的问题而专注于特定环境的问题,这里是CGI。

你应该使用下面这段程序,它仅仅在HTTP头之后输出 Hello world,这是 CGI 接口要求的。这里的 HTTP头指定为无格式的ASCII文本。

C

 

1

2

3

4

5

6

#include <stdio.h>

int main(void) {

  printf("Content-Type: text/plain;charset=us-ascii\n\n");

  printf("Hello world\n\n");

  return 0;

}

在编译、加载、上传之后,你可任意通过在浏览器地址栏输入地址来进行简单测试。你也可以将它作为一个HTML文档的链接目标。URL 地址取决于你的配置情况。我的 Hello world 的 URL 是: http://www.cs.tut.fi/cgi-bin/run/~jkorpela/hellow.cgi

如何处理简单表单

对于使用  METHOD=”GET” 的表单(就像在上面的简单例子里使用的一样,因为这是默认的方法),CGI 说明指出,传递给脚本或者程序的数据保存在一个名为  QUERY_STRING 的环境变量中。

程序如何访问环境变量的值,依赖于使用的编程语言和脚本。在C语言中,可以使用库函数 getenv (在标准库 stdlib 定义) 去访问从而得到字符串类型的值。然后你就可以利用各种技术从字符串中获取数据,比如,将其中一些值转换为数值型。

输出由脚本或程序到“主输出流”这一过程的处理方式非常特别。实际上,它被引导使得它被发送回给浏览器。因此,它通过 C 程序,写一段 HTML 文档到标准输出,你使这段文档出现在用户屏幕,作为表单提交的响应。

在这个例子中,C 程序的源代码如下:

C

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

#include <stdio.h>

#include <stdlib.h>

 

int main(void)

{

    char *data;

    long m,n;

    printf("%s%c%c\n",

           "Content-Type:text/html;charset=iso-8859-1",13,10);

    printf("<TITLE>Multiplication results</TITLE>\n");

    printf("<H3>Multiplication results</H3>\n");

    data = getenv("QUERY_STRING");

    if(data == NULL)

        printf("<P>Error! Error in passing data from form to script.");

    else if(sscanf(data,"m=%ld&n=%ld",&m,&n)!=2)

        printf("<P>Error! Invalid data. Data must be numeric.");

    else

        printf("<P>The product of %ld and %ld is %ld.",m,n,m*n);

    return 0;

}

作为一名训练有素的程序员,你可能已经注意到了这段程序并没有对整数溢出做检查。所以对于非常大的操作数,它将会返回错误值。在现实生活中,这样的检查是必要的,但是这些考虑将会使得我们偏离主题。

注意:第一处 printf 调用输出的数据将被服务器作为HTTP header发送(给客户端)。有很多理由看,这是必需的,其中一个事实是,CGI 能给浏览器发送各种数据(例如图片和纯文本文件),而不只是HTML文档。对于HTML文档,你可以像上面那样调用printf;然而,如果你的字符编码不同于最常见的 ISO 8859-1 (ISO Latin 1) 的话,你将需要将 “iso-8859-1” 替换为你使用的字符编码

我已经编译该程序并将可执行文件命名为mult.cgi,保存在 www.cs.tut.fi 的CGI目录下。这说明任何 action=“http://www.cs.tut.fi/cgi-bin/run/~jkorpela/mult.cgi” 的表单被提交后,都会由这个程序进行处理。

因此,任何人只要写一个相同ACTION的自己的表单,都可以向我这个程序发送他自己的数据。所以,这个程序需要能处理任何数据。通常,在处理数据之前,需要对数据进行检查验证。

使用METHOD=”POST”

METHOD=”POST”的想法

让我们考虑下一个不同的数据处理问题。假设我们想要写一个表单,附带一行文本作为文本域,这样表单数据被发送给CGI程序,并追加到服务器的一个文本文件中去。(文本文件只能被表单和脚本的作者可读,或者它可以被另一个脚本设置为对任何人可读?)。

看起来这个例子和上面那个差不多。只需要一个不同的表单和不同的脚本(程序)。实际上,这里有一处不同的地方。上面那个例子,可以被认为是“纯粹的查询”而不改变“世界的状态”。尤其是,它是“幂等的”。例如,你想提交多少次数据就提交多少次数据,而不会导致任何问题(除了浪费一点资源)。然而,我们当前这个任务需要带来变化——文件内容的变化几乎是持久的。因此,需要 METHOD=”POST”。这篇文档《HTML表单中的GET和POST——有何区别?》中有详细的解释。我们这里假设需要使用METHOD=”POST”并将考虑技术问题。

对于使用METHOD=”POST”方法的表单,CGI 说明指出,发送给脚本程序的数据保存在标准输入流(STDIN)中,而发送的数据的长度(单位字节,例如字符)则保存在环境变量 CONTENT_LENGTH 中。

读取输入

听起来,从标准输入流读取数据可能比从环境变量中读取更为简单,但还是有点复杂。服务器没有被要求传递数据,因此,当CGI 脚本试图读取比实际存在更多的数据时,将会得到文件结束标识(EOF)。也就是说,如果你使用C语言中的getchar函数来读取数据,在读完全部数据之后发生的将是 undefined。无人担保函数将返回 EOF。

当读取输入的时候,程序必须要保证不去试图读取长于 CONTENT_LENGTH  的内容。

简单程序:接受和追加数据

下面是一个用于接受通过CGI和METHOD=”POST”发送的数据的相对简单的C程序

C

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

#include <stdio.h>

#include <stdlib.h>

#define MAXLEN 80

#define EXTRA 5

/* 4 for field name "data", 1 for "=" */

#define MAXINPUT MAXLEN+EXTRA+2

/* 1 for added line break, 1 for trailing NUL */

#define DATAFILE "../data/data.txt"

 

void unencode(char *src, char *last, char *dest)

{

for(; src != last; src++, dest++)

   if(*src == '+')

     *dest = ' ';

   else if(*src == '%') {

     int code;

     if(sscanf(src+1, "%2x", &code) != 1) code = '?';

     *dest = code;

     src +=2; }    

   else

     *dest = *src;

*dest = '\n';

*++dest = '\0';

}

 

int main(void)

{

char *lenstr;

char input[MAXINPUT], data[MAXINPUT];

long len;

printf("%s%c%c\n",

"Content-Type:text/html;charset=iso-8859-1",13,10);

printf("<TITLE>Response</TITLE>\n");

lenstr = getenv("CONTENT_LENGTH");

if(lenstr == NULL || sscanf(lenstr,"%ld",&len)!=1 || len > MAXLEN)

  printf("<P>Error in invocation - wrong FORM probably.");

else {

  FILE *f;

  fgets(input, len+1, stdin);

  unencode(input+EXTRA, input+len, data);

  f = fopen(DATAFILE, "a");

  if(f == NULL)

    printf("<P>Sorry, cannot store your data.");

  else

    fputs(data, f);

  fclose(f);

  printf("<P>Thank you! The following contribution of yours has \

been stored:<BR>%s",data);

  }

return 0;

}

从本质上讲,这个程序从CONTENT_LENGTH环境变量的值中获取输入的字符数量。然后,将数据解码,因为数据以一种特别编码的格式到达(服务端),这是之前提到过的。程序已经为一个表单写就,这个表单包含名称为data的文本域(实际上,这里仅仅是名称的长度比较重要)。例如,加入用户输入:

Hello there!

然后数据被发送给程序,以这样的编码形式:

data=Hello+there%21

(空格被编码成为+号,感叹号被编码为%21)。程序中的解码工作负责将这种格式转换为原始的格式。然后,数据被追加到一个文件(有着固定的文件名)的末尾。然后输出响应给用户。

我将已经编译过的程序命名为collect.cgi,保存在CGI脚本目录。现在,类似于下面这个表单可以用来提交数据:

XHTML

 

1

2

3

4

5

6

<FORM ACTION="http://www.cs.tut.fi/cgi-bin/run/~jkorpela/collect.cgi"

METHOD="POST">

<DIV>Your input (80 chars max.):<BR>

<INPUT NAME="data" SIZE="60" MAXLENGTH="80"><BR>

<INPUT TYPE="SUBMIT" VALUE="Send"></DIV>

</FORM>

范例程序:浏览保存在文件中的数据

最后,我们可以写一个简单程序浏览保存在文件中的数据。它仅仅需要将给定文件的内容发送到标准输出。

C

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

#include <stdio.h>

#include <stdlib.h>

#define DATAFILE "../data/data.txt"

int main(void)

{

FILE *f = fopen(DATAFILE,"r");

int ch;

if(f == NULL) {

  printf("%s%c%c\n",

  "Content-Type:text/html;charset=iso-8859-1",13,10);

  printf("<TITLE>Failure</TITLE>\n");

  printf("<P><EM>Unable to open data file, sorry!</EM>"); }

else {

  printf("%s%c%c\n",

  "Content-Type:text/plain;charset=iso-8859-1",13,10);

  while((ch=getc(f)) != EOF)

    putchar(ch);

  fclose(f); }

return 0;

}

注意这段程序将数据打印为普通文本(当成功时),在它之前还有一段header用来说明这个情况,例如,将 text/html 替换为text/plain。

表单调用这个程序非常简单,因为不需要任何输入:

XHTML

 

1

2

3

<form action="http://www.cs.tut.fi/cgi-bin/run/~jkorpela/viewdata.cgi">

<div><input type="submit" value="View"></div>

</form>

最终,有两个看起来像下面的表单。你可以测试它们:

用来发送数据的表单

注意你在这里的任何输入,对世界而言都是可见的:

用来检查数据提交的表单

数据保存文件的内容,将会被以普通文本的形式显示。

即使数据输出被声明为纯文本,Internet Explorer 浏览器仍会将它的部分解析为HTML标记。如果某些人的输入包含这些标记,就会发生一些奇怪的事情。viredata.c 程序考虑到这个问题,通过在每个大于符号 lt; 的后面写上一个空字符(就是’\0’字符),这样就不会被浏览器(即便是IE浏览器)认为是(HTML)标记了。

拓展阅读

你现在也许想去阅读CGI规范,它将告诉你CGI全部的基本细节。下一步,也许是了解CGI编程FAQ中包含些什么内容。但是注意,它相对有点老旧了。

这里有很多资料,包括介绍教程,在CGI资源索引中。特别注意这个章节《程序和脚本:C和C++:库和类》,这里有一些类可以轻易的处理表单数据。它对于使用自己的代码解析简单数据非常有帮助,就像上面的简单例子一样。但是在一些特殊的程序中,库也许处理的更好。

C语言本来被设计为在仅使用ASCII字符的环境。如今,它可以被用来处理8位字符,但是需要小心谨慎。有很多方法可以克服C实现的限制,字符通常都是8位的。关注我的书最后一章《Unicode讲解》。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值