Linux CGI编程基础【整理】
Linux CGI编程基础
1.为什么使用CGI?
如前面所见,任何的HTML均是静态网页,它无法实现一些复杂的功能,而CGI可以为我们实现。如:a.列出服务器上某个目录中的文件,对目录中的文件进行操作;b.通过CGI实现串口通讯;c.实现数据库接口;d.实现从摄像头读取一张图片显示在网页上… 等等
2. CGI是什么?
CGI全称是 Common Gate Intergace ,在物理上,CGI是一段程序,它运行在Server上,提供同客户端 Html页面的接口。
3. CGI编程语言
你可以用任何一种你熟悉的高级语言, C,C++,C shell,Perl和VB都可以。
4. CGI的安全性
实际上CGI是比较安全的,至少比 那些没有数字签名的ActiveX控件要安全的多。除非你有意在程序里加入了破坏Server的命令, 否则一般不会有什么严重的后果。
简单的说来,CGI是用来沟通HTML表单和服务器端程序的接口(interface)。说它是接口,也就是说CGI并不是一种语言,而是可以被其他语言所应用的一个规范集。理论上讲,你可以用任何的程序语言来编写CGI程序,只要在编程的时候符合CGI规范所定义的一些东西就可以了。由于C语言在平台无关性上表现不错(几乎在任何的系统平台下都有其相应编译器),而且对大多数程序员而言都算得上很熟悉(不像Perl),因此,C是CGI编程的首选语言之一。这儿我们介绍的,就是如何使用C来编写CGI程序。
作为CGI编程的最为简单的例子,就是进行表单的处理。因而在这篇文章中,我们主要介绍的就是如何用C来编写CGI程序来进行表但处理。
5.传送方法:
所谓方法是指调用CGI程序的途径。事实上,要执行程序时,你用一种方法向服务器提出请求,此请求定义了程序如何接受数据。 下面介绍常用的两种方法:GET和POST 1.GET 当使用这种方法时,CGI程序从环境变量QUERY_STRING获取数据。
QUERY_STRING 被称为环境变量,就是这种环境变量把客户端的数据传给服务器。为了解释和执行 程序,CGI必须要分析(处理)此字符串。
POST 使用POST方法时,WEB服务器通过stdin(标准输入),向CGI程序传送数据。服务器 在数据的最后没有使用EOF字符标记,因此程序为了正确的读取stdin,必须使用CONTENT_LENGTH 。当你发送的数据将改变
Web服务器端的数据或者你想给CGI程序传送的数据超过了1024 字节,这是url的极限长度,你应该使用POST方法。 实现方法:
GET实现方法
<form name=“guyi‘s form” action=“http://www.yourname.com/cgi/your.cgi” method=GET>
POST实现方法:
<form method=post>
6. 表单编码方式:
form的enctype属性为编码方式,常用有两种:application/x-www-form-urlencoded和multipart/form-data,默认为application/x-www-form-urlencoded。
当action为get时候,浏览器用x-www-form-urlencoded的编码方式把form数据转换成一个字串(name1=value1&name2=value2...),然后把这个字串append到url后面,用?分割,加载这个新的url。
当action为post时候,浏览器把form数据封装到http body中,然后发送到server。
如果没有type=file的控件,用默认的application/x-www-form-urlencoded就可以了。
但是如果有type=file的话,就要用到multipart/form-data了。浏览器会把整个表单以控件为单位分割,并为每个部分加上Content-Disposition(form-data或者file),Content-Type(默认为text/plain),name(控件name)等信息,并加上分割符(boundary)。
GET表单的处理
对于那些使用了属性“METHOD=GET”的表单(或者没有METHOD属性,这时候GET是其缺省值),CGI定义为:当表单被发送到服务器断后,表单中的数据被保存在服务器上一个叫做QUERY_STRING的环境变量中。这种表单的处理相对简单,只要读取环境变量就可以了。这一点对不同的语言有不同的做法。在C语言中,你可以用库函数getenv(定义在标准库函数stdlib中)来把环境变量的值作为一个字符串来存取。你可以在取得了字符串中的数据后,运用一些小技巧进行类型的转换,这都是比较简单的了。在CGI程序中的标准输出(output)(比如在C中的stdout文件流)也是经过重定义了的。它并没有在服务器上产生任何的输出内容,而是被重定向到客户浏览器。这样,如果编写一个C的CGI程序的时候,把一个HTML文档输出到它的 stdout上,这个HTML文档会被在客户端的浏览器中显示出来。这也是CGI程序的一个基本原理。
我们来看看具体的程序实现,下面是一段HTML表单:
<FORM ACTION="/cgi-bin/mult.cgi"> <P>请在下面填入乘数和被乘数,按下确定后可以看到结果。 <INPUT NAME="m" SIZE="5"> <INPUT NAME="n" SIZE="5"><BR> <INPUT TYPE="SUBMIT" VALUE="确定"> </FORM>
我们要实现的功能很简单,就是把表单中输入的数值乘起来,然后输出结果。其实这个功能完全可以用JavaScript来实现,但为了让程序尽量的简单易懂,我还是选择了这个小小的乘法来作为示例。
下面就是处理这个表单的CGI程序,对应于FORM标签中的ACTION属性值。
#include <stdio.h> #include <stdlib.h> int main(void) { char *data; long m,n; printf("Content-type: text/html\n\n"); printf("<TITLE>Mult Result</TITLE>"); printf("<H3>Mult Result</H3>"); data = getenv("QUERY_STRING"); if(data == NULL) printf("<P>Don't transfer data or transfer error"); else if(sscanf(data,"m=%ld&n=%ld",&m,&n)!=2) printf("<P>Error, invalid format, data have to number"); else printf("<P>%ld and %ld result: %ld", m, n, m * n); printf("<br><h>Thank you to use the boa webserver</h1>"); return 0; }
具体的C语法就不多讲了,我们来看看它作为CGI程序所特殊的地方。
前面已经提到标准输出的内容就是要被显示在浏览器中的内容。第一行的输出内容是必须的,也是一个CGI程序所特有的:printf("%s%c%c ","Content-Type:text/html",13,10),这个输出是作为HTML的文件头。因为CGI不仅可以像浏览器输出HTML文本,而且可以输出图像,声音之类的东西。这一行告诉浏览器如何处理接受到的内容。在Content-Type的定义后面跟有两行的空行,这也是不可缺少的。因为所有CGI程序的头部输出都是相近的,因而可以为其定义一个函数,来节省编程的时间。这是CGI编程常用的一个技巧。
程序在后面调用了用了库函数getevn来得到QUERY_STRING的内容,然后使用sscanf函数把每个参数值取出来,要注意的是sscanf函数的用法。其他的就没有什么了,和一般的C程序没有区别。
把程序编译后,改名为mult.cgi放在/cgi-bin/目录下面,就可以被表单调用了。这样,一个处理GET方式表单的CGI程序就大功告成了。
POST表单处理
下面我们来考虑另外一种表单传送方法:POST。假设我们要实现的任务是这样的:把表单中客户输入的一段文本内容添加到服务器上的一个文本文件的后面。这可以看作是一个留言版程序的雏形。显然,这个工作是无法用JavaScript这种客户端脚本来实现,也算得上真正意义上的CGI程序了。
看起来这个问题和上面讲的内容很相近,仅仅是用不同的表单和不同的脚本(程序)而已。但实际上,这中间是有一些区别的。在上面的例子中,GET的处理方法可以看作是“纯查询(pure query)”类型的,也就是说,它与状态无关。同样的数据可以被提交任意的次数,而不会引起任何的问题(除了服务器的一些小小的开销)。但是现在的任务就不同了,至少它要改变一个文件的内容。因而,可以说它是与状态有关的。这也算是POST和GET的区别之一。而且,GET对于表单的长度是有限制的,而 POST则不然,这也是在这个任务中选用POST方法的主要原因。但相对的,对GET的处理速度就要比POST快一些。
在CGI的定义中,对于POST类型的表单,其内容被送到CGI程序的标准输入(在C语言中是stdin),而被传送的长度被放在环境变量 CONTENT_LENGTH中。因而我们要做的就是,在标准输入中读入CONTENT_LENGTH长度的字符串。从标准输出读入数据听起来似乎要比从环境变量中读数据来的要容易一些,其实则不然,有一些细节地方要注意,这在下面的程序中可以看到。特别要注意的一点就是:CGI程序和一般的程序有所不同,一般的程序在读完了一个文件流的内容之后,会得到一个EOF的标志。但在CGI程序的表单处理过程中,EOF是永远不会出现的,所以千万不要读多于 CONTENT_LENGTH长度的字符,否这会有什么后果,谁也不知道(CGI规范中没有定义,一般根据服务器不同而有不同得处理方法)。
我们来看看到底如何从POST表单收集数据到CGI程序,下面給出了一個比较简单的C源代碼:
#include < stdio.h > #include < stdlib.h > #define MAXLEN 80 #define EXTRA 5 /* 4个字节留给字段的名字"data", 1个字节留给"=" */ #define MAXINPUT MAXLEN+EXTRA+2 /* 1个字节留给换行符,还有一个留给后面的NULL */ #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 = " "; *++dest = ""; } int main(void) { char *lenstr; char input[MAXINPUT], data[MAXINPUT]; long len; printf("%s%c%c ", "Content-Type:text/html;charset=gb2312",13,10); printf("< TITLE >Response< /TITLE > "); lenstr = getenv("CONTENT_LENGTH"); if(lenstr == NULL || sscanf(lenstr,"%ld",&len)!=1 || len > MAXLEN) { printf("< P >form submit failed"); } else { FILE *f; fgets(input, len+1, stdin); unencode(input+EXTRA, input+len, data); f = fopen(DATAFILE, "a"); if(f == NULL) printf("< P >sorry, happened error, can't save your data"); else fputs(data, f); fclose(f); printf("< P >Thanks very much, had saved your data< BR >%s",data); } return 0; }
从本质上来看,程序先从CONTENT_LENGTH环境变量中得到数据的字长,然后读取相应长度的字符串。因为数据内容在传输的过程中是经过了编码的,所以必须进行相应的解码。编码的规则很简单,主要的有这几条:
1. 表单中每个每个字段用字段名后跟等号,再接上上这个字段的值来表示,每个字段之间的内容用&连结;
2. 所有的空格符号用加号代替,所以在编码码段中出现空格是非法的;
3. 特殊的字符比如标点符号,和一些有特定意义的字符如“+”,用百分号后跟其对应的ACSII码值来表示。
例如:如果用户输入的是:
Hello there!
那么数据传送到服务器的时候经过编码,就变成了data=Hello+there%21 上面的unencode()函数就是用来把编码后的数据进行解码的。在解码完成后,数据被添加到data.txt文件的尾部,并在浏览其中回显出来。
把文件编译完成后,把它改名为collect.cgi后放在CGI目录中就可以被表单调用了。下面给出了其相应的表单:
<FORMACTION="/cgi-bin/collect.cgi" METHOD="POST">
<P>请输入您的留言(最多80个字符):<BR ><INPUT NAME="data" SIZE="60" MAXLENGTH="80"><BR>
<INPUT TYPE="SUBMIT" VALUE="确定">
</FORM>
事实上,这个程序只能作为例子,是不能够正式的使用的。它漏掉了很关键的一个问题:当有多个用户同时像文件写入数据是,肯定会有错误发生。而对于一个这样的程序而言,文件被同时写入的几率是很大的。因此,在比较正式的留言版程序中,都需要做一些更多的考虑,比如加入一个信号量,或者是借助于一个钥匙文件等。因为那只是编程的技巧问题,在这儿就不多说了。
最后,我们来写一个浏览data.txt文件的的CGI程序,这只需要把内容输出到stdout就可以了:
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 ", "Content-Type:text/html;charset=gb2312", 13, 10); printf("<TITLE>Error</TITLE> "); printf("<P><EM>have error, can't open file</EM>"); } else { printf("%s%c%c ", "Content-Type:text/plain", 13, 10); while((ch=getc(f)) != EOF) putchar(ch); fclose(f); } return 0; }
这个程序唯一要注意的是:它并没有把data.txt 包装成HTML格式后再输出,而是直接作为简单文本(plain text)输出,这只要在输出的头部用text/plain类型代替text/html就可以了,浏览器会根据Content-Type的类型自动的选择相应的处理方法。
要触发这个程序也很简单,因为没有数据要输入,所以只需一个按钮就可以搞定了:
<FORM ACTION="/cgi-bin/viewdata.cgi"> <P><INPUT TYPE="SUBMIT" VALUE="察看"> </FORM>
到这儿,一些基本的用C编写CGI程序的原理就将完了。当然,就凭讲的这些内容,还很难编写出一个好的CGI程序,这需要进一步的学习CGI的规范定义,以及一些其他的CGI编程特有的技巧。
这篇文章的目的,也就是要你了解一下CGI编程的概念。事实上,现在的一些主流的服务器端脚本编程语言如ASP,PHP,JSP等,都基本上具备了CGI 编程的大部分的功能,但他们在使用上的,确实是比无论用什么语言进行CGI编程都要容易的多。所以在进行服务器端编程的时候,一般都会首先考虑使用这些脚本编程语言。只有当他们也解决不了,比如要进行一些更为底层的编程的时候,才会用到CGI。
最后提供一个提交表单,并收到反馈的CGI实例:
<!--pass.html--> <html> <head><title>user login verify</title></head> <body> <!--下面的action是表单提交后在服务器端执行的gic程序(即c的可执行程序)--> <!--cgi可执行程序放在 /var/www/cgi-bin/目录下--> <form name="form1" action="/cgi-bin/pass.cgi" method="GET"> <table align="center"> <tr><td align="center" colspan="2"></td></tr> <tr> <td align="right">User</td> <td><input type="text" name="Username"></td> </tr> <tr> <td align="right">Passwd</td> <td><input type="password" name="Password"></td> </tr> <tr> <td><input type="submit" value="LogIn"></td> <td><input type="reset" value="Cancel"></td> </tr> </table> </form> </body> </html>
#include <stdio.h> #include <stdlib.h> #include <string.h> char* getcgidata(FILE* fp, char* requestmethod); int main() { char *input; char *req_method; char name[64]; char pass[64]; int i = 0; int j = 0; // printf("Content-type: text/plain; charset=iso-8859-1\n\n"); printf("Content-type: text/html\n\n"); printf("The following is query reuslt:<br><br>"); req_method = getenv("REQUEST_METHOD"); input = getcgidata(stdin, req_method); // 我们获取的input字符串可能像如下的形式 // Username="admin"&Password="aaaaa" // 其中"Username="和"&Password="都是固定的 // 而"admin"和"aaaaa"都是变化的,也是我们要获取的 // 前面9个字符是UserName= // 在"UserName="和"&"之间的是我们要取出来的用户名 for ( i = 9; i < (int)strlen(input); i++ ) { if ( input[i] == '&' ) { name[j] = '\0'; break; } name[j++] = input[i]; } // 前面9个字符 + "&Password="10个字符 + Username的字符数 // 是我们不要的,故省略掉,不拷贝 for ( i = 19 + strlen(name), j = 0; i < (int)strlen(input); i++ ) { pass[j++] = input[i]; } pass[j] = '\0'; printf("Your Username is %s<br>Your Password is %s<br> \n", name, pass); return 0; } char* getcgidata(FILE* fp, char* requestmethod) { char* input; int len; int size = 1024; int i = 0; if (!strcmp(requestmethod, "GET")) { //从这里可以看出来,GET在cgi中传递的Username="admin"&Password="aaaaa"被放置在环境变量QUERY_STRING中了。 input = getenv("QUERY_STRING"); return input; } else if (!strcmp(requestmethod, "POST")) { len = atoi(getenv("CONTENT_LENGTH")); input = (char*)malloc(sizeof(char)*(size + 1)); if (len == 0) { input[0] = '\0'; return input; } while(1) { //从这里可以看出来,POST在cgi中传递的Username="admin"&Password="aaaaa"被写入stdin标准输入流中了。 input[i] = (char)fgetc(fp); if (i == size) { input[i+1] = '\0'; return input; } --len; if (feof(fp) || (!(len))) { i++; input[i] = '\0'; return input; } i++; } } return NULL; }
It's over, Every Body, Come ON!