CGIC编程
本系列的目的是演示如何使用C語言的CGI庫“CGIC”完成Web開發的各種要求。
? ??? ? 基礎知識
??? * 1: 使用CGIC的基本思路
??? * 2: 獲取Get請求字符串
??? * 3: 反轉義
??? * 4: 獲取請求中的參數值
? ?? ?進階訓練
??? * 用CGIC實現文件上傳
CGIC簡明教程1:使用CGIC的基本思路
C語言編程是一項復雜且容易出錯的工作,所以在完成復雜任務時,一定要選擇合適的庫。對于用C語言編寫CGI程序則更是如此。
CGIC是非常優秀的C語言CGI庫函數。 其下載地址為: www.boutell.com/cgic/#obtain ,現在的版本號是2.05。
本站從今天開始,將逐步介紹如何使用CGIC完成各種操作,也可以說是一個Tutorial。
(注:本系列涉及的編程環境都是Linux,Windows用戶需要對用到的操作系統命令稍作修改)
本文綱要 :
CGIC的安裝、測試安裝、使用CGIC的基本思路;
1) CGIC的下載安裝
從上面提供的官方網址下載了CGIC庫之后,解開壓縮包,里面有大約10個文件,有用的是:
cgic.h:頭文件;
cgic.c:CGIC的源代碼文件;
cgictest.c:CGIC庫的作者提供的一個CGI程序例子;
capture.c:用于調試CGI程序的工具;
Makefile:安裝CGIC的腳本文件;
可以看到,整個庫實際上就是cgic.c一個文件,可以說是非常的精煉。
我們可以把CGIC安裝為操作系統的一個動態鏈接庫,這樣我們每次編譯的時候,就不需要有cgic.c這個源文件了。
但是由于需要(以后將會看到),我們將修改cgic.c代碼,所以我們不把它安裝進系統。每次編譯的時候,只要把cgic.c和cgic.h放到當前文件夾就好了。
2) 測試安裝
在開始編寫你自己的CGI程序之前,一定要先走通他的例子程序,免得后來程序出錯的時候還不知道是配置有問題,還是你的程序代碼有問題。
我們用他自帶cgictest.c來實現自己的第一個C語言CGI程序。
你可以新建一個工作目錄,用于存放你的CGI程序源代碼,把cgic.h, cgic.c, cgictest.c三個文件拷貝到這個目錄,然后建立一個Makefile文件,其內容為:
?? 1. test.cgi:cgictest.c cgic.h cgic.c
?? 2. gcc -wall cgictest.c cgic.c -o test.cgi
需要提醒的是,第二行開頭一定是一個tab鍵(且僅有一個),不能使用空格。
保存好Makefile的內容之后,執行make命令:
make
我們看到,當前目錄下應該多了一個test.cgi文件。
在你的網站根目錄下建立一個cgi-bin目錄(當然名字可以任意取,但作為習慣,一般叫做cgi-bin),然后在Apache的配置文件里賦予其執行CGI代碼的權限,權限修改完之后要重啟Apache。完成之后,把剛才生成的test.cgi放到cgi-bin目錄中。此時我們可以在瀏覽器中輸入以下地址進行訪問:
http://127.0.0.1/cgi-bin/test.cgi
如果正常的話,應該看到一個網頁被展示出來。這樣,第一個C語言的CGI程序就運行起來了。
如果瀏覽器報錯,那么多半是配置Apache的時候有些操作沒有正確完成。
3) 使用CGIC的基本思路
從cgic.c的代碼可以看出,它定義了main函數,而在cgictest.c中定義了一個cgiMain函數。也就是說,對于使用CGIC編寫的CGI程序,都是從cgic.c中的代碼進入,在庫函數完成了一系列必要的操作(比如解析參數、獲取系統環境變量)之后,它才會調用你的代碼(從你定義的cgiMain進入)。
另外一點就是,cgi程序輸出HTML頁面的方式都是使用printf把頁面一行一行地打印出來,比如cgictest.c中的這一段代碼:
fprintf(cgiOut, "<textarea NAME=/"address/" ROWS=4 COLS=40>/n");
fprintf(cgiOut, "Default contents go here. /n");
fprintf(cgiOut, "</textarea>/n");
上面這段代碼的運行結果就是在頁面上輸出一個textarea。第一個參數cgiOut實際上就是stdin,所以我們可以直接使用printf,而不必使用fprintf。不過在調試的時候會用到fprintf來重定向輸出。
這種方式與 Java ?Servlet非常類似,Servlet也是通過調用打印語句System.out.println(…)來輸出一個頁面。(不過后來Java推出了JSP來克服這種不便。)
但是與Servlet不同的地方在于,使用C語言的我們還要自己輸出HTML頭部(聲明文檔類型):
cgiHeaderContentType("text/html");
這個語句的調用一定要在所有printf語句之前。而這個語句執行的任務實際上就是:
void cgiHeaderContentType(char *mimeType) {
??? fprintf(cgiOut, "Content-type: %s/r/n/r/n", mimeType);
}
這個語句告訴瀏覽器,這次傳來的數據是什么類型,是一個HTML文檔,還是一個bin文件… 如果是個HTML文檔,就通過瀏覽器窗口顯示,如果是一個bin(二進制)文件,則打開下載窗口,讓用戶選擇是否保存文件以及保存文件的路徑。
理解了這幾點之后,你就可以編寫自己的CGIC程序了。新建一個文件test.c試試:
下載: test.c
?? 1. #include <stdio.h>
?? 2. #include "cgic.h"
?? 3. #include <string.h>
?? 4. #include <stdlib.h>
?? 5. int cgiMain() {
?? 6.???? cgiHeaderContentType("text/html");
?? 7.???? fprintf(cgiOut, "<HTML><HEAD>/n");
?? 8.???? fprintf(cgiOut, "<TITLE>My First CGI</TITLE></HEAD>/n");
?? 9.???? fprintf(cgiOut, "<BODY><H1>Hello CGIC</H1></BODY>/n");
? 10.???? fprintf(cgiOut, "</HTML>/n");
? 11.???? return 0;
? 12. }
把Makefile文件中的cgitest.c全部換稱test.c,保存,再執行make命令即可。
此時通過瀏覽器訪問,會在頁面上看到一個大大的“Hello CGIC”。
CGIC簡明教程2:獲取Get請求字符串
Get請求就是我們在瀏覽器地址欄輸入URL時發送請求的方式,或者我們在HTML中定義一個表單(form)時,把action屬性設為“Get”時的工作方式;
Get請求字符串就是跟在URL后面以問號“?”開始的字符串,但不包括問號。比如這樣的一個請求:
http://127.0.0.1/cgi-bin/out.cgi?ThisIsTheGetString
在上面這個URL中,“ThisIsTheGetString”就是Get請求字符串。
在進入我們自己編寫的cgi代碼之前,CGIC庫已經事先把這個字符串取到了,我們可以在程序中直接獲得,要做的僅僅是在你編寫的cgiMain方法前面加入以下聲明:
extern char *cgiQueryString;
現在給出一個簡單的例子,這個例子跟上一篇的測試程序非常相似,只不過程序的輸出是使用者輸入的Get請求字符串。
下載: test.c
?? 1. #include <stdio.h>
?? 2. #include "cgic.h"
?? 3. #include <string.h>
?? 4. #include <stdlib.h>
?? 5.?
?? 6. extern char *cgiQueryString;
?? 7. int cgiMain() {
?? 8.???? cgiHeaderContentType("text/html");
?? 9.???? fprintf(cgiOut, "<HTML><HEAD>/n");
? 10.???? fprintf(cgiOut, "<TITLE>My CGIC</TITLE></HEAD>/n");
? 11.???? fprintf(cgiOut, "<BODY>");
? 12.???? fprintf(cgiOut, "<H1>%s</H1>",cgiQueryString);
? 13.???? fprintf(cgiOut, "</BODY>/n");
? 14.???? fprintf(cgiOut, "</HTML>/n");
? 15.???? return 0;
? 16. }
假設把這個程序編譯成out.cgi(編譯方法參見上一篇),并部署到Web服務器的cgi-bin目錄下,當用戶在瀏覽器地址欄輸入本文開頭給出的URL字符串時,瀏覽器頁面上會顯示:
ThisIsTheGetString
我們也可以編寫一個用于測試的HTML頁面:
下載: test.html
?? 1. <html>
?? 2. <head>
?? 3.???? <title>Test</title>
?? 4. </head>
?? 5. <body>
?? 6.???? <form action="cgi-bin/out.cgi" method="get">
?? 7.???????? <input type="text" name="theText">
?? 8.???????? <input type="submit" value="Continue →">
?? 9.???? </form>
? 10. </body>
? 11. </html>
文件的部署結構應該為:
|test.html
|—-cgi-bin/out.cgi
大家可以試試,通過瀏覽器訪問 http://127.0.0.1/test.html ,在文本框內輸入一些字符,并點擊提交按鈕,然后就可以看到cgi程序的執行結果:把在文本框輸入的字符原樣顯示在瀏覽器上。
CGIC簡明教程3:反轉義
瀏覽器在發送Get請求時,會把請求字符串進行轉義操作(英文術語為: escape); 比如,我們在地址欄輸入(注意最后”it’s me”中的空格):
http://localhost/~Jack/cgi-bin/out.cgi?it 's me
瀏覽器會把它轉義為:
http://localhost/~Jack/cgi-bin/out.cgi?it 's%20me
在上一篇最后給出的例子中,如果在文本框內輸入
it's me
你會發現,瀏覽器最終發送的請求為
http://localhost/~Jack/cgi-bin/out.cgi?theText=it%27s+me
通過CGIC,我們可以把這些被轉義后的字符還原為我們本來的輸入,這個過程就叫“反轉義” (Unescape)。
不過這個過程有點像hack他的代碼。
整個過程分三個步驟:
1)打開cgic.c,找到這一行語句:
static cgiUnescapeResultType cgiUnescapeChars(char **sp, char *cp, int len);
注意,我們要找的只是這個函數聲明,不是函數定義;
2)在這個函數聲明語句的上方,你會看到一個結構體定義:
?? 1. typedef enum {
?? 2.???? cgiUnescapeSuccess,
?? 3.???? cgiUnescapeMemory
?? 4. } cgiUnescapeResultType;
把這幾行語句復制到cgic.h文件中,并在這里把它注釋掉;
同時還要刪除在第一步中找到的函數聲明語句中的“static”關鍵字。
3)我們現在就可以使用反轉義函數cgiUnescapeChars了:
在你自己的代碼(按照慣例,還是test.c)中,加入以下聲明語句即可
extern cgiUnescapeResultType cgiUnescapeChars(char **sp, char *cp, int len);
接下來我們給出一段完整的test.c代碼
下載: test.c
?? 1. #include <stdio.h>
?? 2. #include "cgic.h"
?? 3. #include <string.h>
?? 4. #include <stdlib.h>
?? 5.?
?? 6. extern char *cgiQueryString;
?? 7. extern cgiUnescapeResultType cgiUnescapeChars(char **sp, char *cp, int len);
?? 8. int cgiMain() {
?? 9.???? char * buffer;
? 10.???? cgiHeaderContentType("text/html");
? 11.???? fprintf(cgiOut, "<HTML><HEAD>/n");
? 12.???? fprintf(cgiOut, "<TITLE>My CGI</TITLE></HEAD>/n");
? 13.???? fprintf(cgiOut, "<BODY>");
? 14.???? cgiUnescapeChars(&buffer, cgiQueryString, strlen(cgiQueryString));
? 15.???? fprintf(cgiOut, "<H1>%s</H1>",buffer);
? 16.???? fprintf(cgiOut, "</BODY>/n");
? 17.???? fprintf(cgiOut, "</HTML>/n");
? 18.???? free(buffer);
? 19.???? return 0;
? 20. }
值得注意的是,buffer的存儲空間是cgiUnescapeChars幫你分配的,但最后要由你自己來釋放(free),這一點千萬不可忘記。
下面你可以結合上一篇給出的測試用html代碼試試該cgi程序的運行結果,也可以直接在瀏覽器地址欄輸入一些帶有特殊符號的字符串。
最后講一下為什么不得不用這種hacker的方式來完成該任務,而CGIC不顯式提供?
CGIC的出發點是,我們平時只需要解析請求中的鍵值對,比如:”?q=nice&client=IE”,當我們在服務端查詢“q”的值時,我們就能得到“nice”。CGIC有一族函數幫助我們完成這個任務,比如cgiFormString(以后會講到)。在解析這種請求格式的時候,如果我們提供的參數值含有被轉義的字符,那么CGIC就會在內部調用cgiUnescapeChars完成反轉義。
但是,有時候我們會發送非常復雜的Get請求字符串,但并不是“鍵-值”對的格式。這就需要直接使用cgiUnescapeChars進行反轉義了。
例如:假設我們有個服務端cgi程序chat.cgi,這是個網絡聊天機器人(也許你可以開發自己的Web版MSN機器人、QQ機器人)。如果我們發送如下請求:
http://127.0.0.1/cgi-bin/chat.cgi? "this is a cgi user"
那么chat.cgi就會把“this is a cgi user”當做你對它說的話,經過處理,它會回復一段語句。為了方便,我們并沒有寫成“鍵-值”對的形式。這個時候被我們hack的cgiUnescapeChars就能派上用場了。
CGIC簡明教程4:獲取請求中的參數值
我們在提交一個表單(form)時,怎樣把表單內的值提取出來呢?
比如下面這個表單:
<form action="cgi-bin/out.cgi" method="POST">
??? <input type="text" name="name" />
??? <input type="text" name="number" />
??? <input type="submit" value="Submit" />
</form>
當out.cgi收到請求時,需要把輸入框”name”和輸入框”number”內的值提取出來。而且不管form中的action是GET還是POST,都要有效。
下面給出示例代碼:
下載: test.c
?? 1. #include <stdio.h>
?? 2. #include "cgic.h"
?? 3. #include <string.h>
?? 4. #include <stdlib.h>
?? 5.?
?? 6. int cgiMain() {
?? 7.???? char name[241];
?? 8.???? char number[241];
?? 9.???? cgiHeaderContentType("text/html");
? 10.???? fprintf(cgiOut, "<HTML><HEAD>/n");
? 11.???? fprintf(cgiOut, "<TITLE>My CGI</TITLE></HEAD>/n");
? 12.???? fprintf(cgiOut, "<BODY>");
? 13.???? cgiFormString("name", name, 241);
? 14.???? cgiFormString("number", number, 241);
? 15.???? fprintf(cgiOut, "<H1>%s</H1>",name);
? 16.???? fprintf(cgiOut, "<H1>%s</H1>",number);
? 17.???? fprintf(cgiOut, "</BODY>/n");
? 18.???? fprintf(cgiOut, "</HTML>/n");
? 19.???? return 0;
? 20. }
從上面的代碼可以看出,第13行和第14行獲取了輸入框的值。
獲取輸入參數值在CGIC中其實有一族函數,cgiFormString是其中最常用的一個。
cgiFormStringNoNewlines用來去掉換行符(如果用戶是在一個TextArea里輸入字符的話);
cgiFormStringSpaceNeeded用于測試輸入值的長度,可以以此為依據,然后按需精確分配緩沖區。
用C語言庫(CGIC)編寫CGI,實現文件上傳
用C語言編寫cgi程序的話,多半會用到CGIC。這是個非常流行的庫,遇到文件上傳之類的應用更是離不開它。官方頁面及下載地址為: www.boutell.com/cgic/#obtain
不少網站都有文件上傳的功能,本文展示如何用CGIC庫編寫文件上傳的服務端程序,最后給出一段簡單的HTML代碼,供大家測試使用。
下載: upload.c
?? 1. #include<stdio.h>
?? 2. #include<string.h>
?? 3. #include<unistd.h>
?? 4. #include<fcntl.h>
?? 5. #include<sys/stat.h>
?? 6. #include"cgic.h"
?? 7. #define BufferLen 1024
?? 8. int cgiMain(void){
?? 9.???? cgiFilePtr file;
? 10.???? int targetFile;
? 11.???? mode_t mode;
? 12.???? char name[128];
? 13.???? char fileNameOnServer[64];
? 14.???? char contentType[1024];
? 15.???? char buffer[BufferLen];
? 16.???? char *tmpStr=NULL;
? 17.???? int size;
? 18.???? int got,t;
? 19.???? cgiHeaderContentType("text/html");
? 20.???? //取得html頁面中file元素的值,應該是文件在客戶機上的路徑名
? 21.???? if (cgiFormFileName("file", name, sizeof(name)) !=cgiFormSuccess) {
? 22.???????? fprintf(stderr,"could not retrieve filename/n");
? 23.???????? goto FAIL;
? 24.???? }
? 25.???? cgiFormFileSize("file", &size);
? 26.???? //取得文件類型,不過本例中并未使用
? 27.???? cgiFormFileContentType("file", contentType, sizeof(contentType));
? 28.???? //目前文件存在于系統臨時文件夾中,通常為/tmp,通過該命令打開臨時文件。臨時文件的名字與用戶文件的名字不同,所以不能通過路徑/tmp/userfilename的方式獲得文件
? 29.???? if (cgiFormFileOpen("file", &file) != cgiFormSuccess) {
? 30.???????? fprintf(stderr,"could not open the file/n");
? 31.???????? goto FAIL;
? 32.???? }
? 33.???? t=-1;
? 34.???? //從路徑名解析出用戶文件名
? 35.???? while(1){
? 36.???????? tmpStr=strstr(name+t+1,"//");
? 37.???????? if(NULL==tmpStr)
? 38.???????? tmpStr=strstr(name+t+1,"/");//if "//" is not path separator, try "/"
? 39.???????? if(NULL!=tmpStr)
? 40.???????????? t=(int)(tmpStr-name);
? 41.???????? else
? 42.???????????? break;
? 43.???? }
? 44.???? strcpy(fileNameOnServer,name+t+1);
? 45.???? mode=S_IRWXU|S_IRGRP|S_IROTH;
? 46.???? //在當前目錄下建立新的文件,第一個參數實際上是路徑名,此處的含義是在cgi程序所在的目錄(當前目錄))建立新文件
? 47.???? targetFile=open(fileNameOnServer,O_RDWR|O_CREAT|O_TRUNC|O_APPEND,mode);
? 48.???? if(targetFile<0){
? 49.???????? fprintf(stderr,"could not create the new file,%s/n",fileNameOnServer);
? 50.???????? goto FAIL;
? 51.???? }
? 52.???? //從系統臨時文件中讀出文件內容,并放到剛創建的目標文件中
? 53.???? while (cgiFormFileRead(file, buffer, BufferLen, &got) ==cgiFormSuccess){
? 54.???????? if(got>0)
? 55.???????????? write(targetFile,buffer,got);
? 56.???? }
? 57.???? cgiFormFileClose(file);
? 58.???? close(targetFile);
? 59.???? goto END;
? 60. FAIL:
? 61.???? fprintf(stderr,"Failed to upload");
? 62.???? return 1;
? 63. END:
? 64.???? printf("File /"%s/" has been uploaded",fileNameOnServer);
? 65.???? return 0;
? 66. }
假設該文件存儲為upload.c,則使用如下命令編輯:
gcc -Wall upload.c cgic.c -o upload.cgi
編譯完成后把upload.cgi復制到你部署cgi程序的目錄(通常命名為cgi-bin)。
正式部署時,請務必修改用open創建新文件那一行代碼。把open的第一個參數設置為目標文件在服務器上存儲的絕對路徑,或者相對于cgi程序的相對路徑。本例中,出于簡單考慮,在cgi程序所在目錄下創建新文件。
測試用HTML代碼:
下載: upload.html
?? 1. <form target="_blank" method="post" action="cgi-bin/upload.cgi">
?? 2.???? <input name="file" type="file" /> <input name="submit" type="submit" />
?? 3. </form>
最后的文件目錄結構為
/MyWebRoot
|—/upload.html
|—/cgi-bin
|——/upload.cgi
當然,你必須配置能夠cgi-bin,并且程序要有權限在cgi-bin目錄下創建文件(因為此例把文件上傳到cgi-bin目錄下)。
那么如何控制上傳文件的大小呢?因為你有時會不允許用戶上傳太大的文件。
通過分析cgic.c的源代碼,我們發現它定義了一個變量cgiContentLength,表示請求的長度。但我們需要首先判斷這是一個上傳文件的請求,然后才能根據cgiContentLength來檢查用戶是否要上傳一個太大的文件。
cgic.c的main函數中進行了一系列if-else判斷來檢查請求的類型,首先確定這是一個post請求,然后確定數據的編碼方式為 “multipart/form-data”,這個判斷通過之后,就要開始準備接收數據了。所以我們要在接收數據開始之前使用 cgiContentLength判斷大小,如果超過標準,就立即返回,不允許繼續操作。
下面貼出修改后代碼片段(直接修改cgic.c的源代碼即可):
?? 1. else if (cgiStrEqNc(cgiContentType, "multipart/form-data")) {
?? 2. #ifdef CGICDEBUG
?? 3. CGICDEBUGSTART
?? 4.???? fprintf(dout, "Calling PostMultipartInput/n");
?? 5. CGICDEBUGEND
?? 6. #endif
?? 7. //我的代碼
?? 8. //UpSize:文件長度上限值,以byte為單位,UpSize是一個int變量,因為cgiContentLength的類型為int
?? 9.???? if(cgiContentLength>UpSize){
? 10.???????? cgiHeaderContentType("text/html");
? 11.???????? printf("File too large!/n");
? 12.???????? cgiFreeResources();
? 13.???????? return -1;
? 14.???? }
? 15. //我的代碼結束
? 16.???? if (cgiParsePostMultipartInput() != cgiParseSuccess) {
? 17. #ifdef CGICDEBUG
? 18. CGICDEBUGSTART
? 19.???????? fprintf(dout, "PostMultipartInput failed/n");
? 20. CGICDEBUGEND
? 21. #endif
? 22.???????? cgiFreeResources();
? 23.???????? return -1;
? 24.???? }
? 25. #ifdef CGICDEBUG
? 26. CGICDEBUGSTART
? 27.???? fprintf(dout, "PostMultipartInput succeeded/n");
? 28. CGICDEBUGEND
? 29.???? #endif
? 30. }
? 31. }
變量UpSize表示文件大小的上限。在cgic.c的main中找到相關代碼,并修改成上面這樣即可。你可以在cgic.c中定義UpSize,也可以在剛才完成的upload.c中定義,然后在cgic.c中用extern方式引用。
? ??? ? 基礎知識
??? * 1: 使用CGIC的基本思路
??? * 2: 獲取Get請求字符串
??? * 3: 反轉義
??? * 4: 獲取請求中的參數值
? ?? ?進階訓練
??? * 用CGIC實現文件上傳
CGIC簡明教程1:使用CGIC的基本思路
C語言編程是一項復雜且容易出錯的工作,所以在完成復雜任務時,一定要選擇合適的庫。對于用C語言編寫CGI程序則更是如此。
CGIC是非常優秀的C語言CGI庫函數。 其下載地址為: www.boutell.com/cgic/#obtain ,現在的版本號是2.05。
本站從今天開始,將逐步介紹如何使用CGIC完成各種操作,也可以說是一個Tutorial。
(注:本系列涉及的編程環境都是Linux,Windows用戶需要對用到的操作系統命令稍作修改)
本文綱要 :
CGIC的安裝、測試安裝、使用CGIC的基本思路;
1) CGIC的下載安裝
從上面提供的官方網址下載了CGIC庫之后,解開壓縮包,里面有大約10個文件,有用的是:
cgic.h:頭文件;
cgic.c:CGIC的源代碼文件;
cgictest.c:CGIC庫的作者提供的一個CGI程序例子;
capture.c:用于調試CGI程序的工具;
Makefile:安裝CGIC的腳本文件;
可以看到,整個庫實際上就是cgic.c一個文件,可以說是非常的精煉。
我們可以把CGIC安裝為操作系統的一個動態鏈接庫,這樣我們每次編譯的時候,就不需要有cgic.c這個源文件了。
但是由于需要(以后將會看到),我們將修改cgic.c代碼,所以我們不把它安裝進系統。每次編譯的時候,只要把cgic.c和cgic.h放到當前文件夾就好了。
2) 測試安裝
在開始編寫你自己的CGI程序之前,一定要先走通他的例子程序,免得后來程序出錯的時候還不知道是配置有問題,還是你的程序代碼有問題。
我們用他自帶cgictest.c來實現自己的第一個C語言CGI程序。
你可以新建一個工作目錄,用于存放你的CGI程序源代碼,把cgic.h, cgic.c, cgictest.c三個文件拷貝到這個目錄,然后建立一個Makefile文件,其內容為:
?? 1. test.cgi:cgictest.c cgic.h cgic.c
?? 2. gcc -wall cgictest.c cgic.c -o test.cgi
需要提醒的是,第二行開頭一定是一個tab鍵(且僅有一個),不能使用空格。
保存好Makefile的內容之后,執行make命令:
make
我們看到,當前目錄下應該多了一個test.cgi文件。
在你的網站根目錄下建立一個cgi-bin目錄(當然名字可以任意取,但作為習慣,一般叫做cgi-bin),然后在Apache的配置文件里賦予其執行CGI代碼的權限,權限修改完之后要重啟Apache。完成之后,把剛才生成的test.cgi放到cgi-bin目錄中。此時我們可以在瀏覽器中輸入以下地址進行訪問:
http://127.0.0.1/cgi-bin/test.cgi
如果正常的話,應該看到一個網頁被展示出來。這樣,第一個C語言的CGI程序就運行起來了。
如果瀏覽器報錯,那么多半是配置Apache的時候有些操作沒有正確完成。
3) 使用CGIC的基本思路
從cgic.c的代碼可以看出,它定義了main函數,而在cgictest.c中定義了一個cgiMain函數。也就是說,對于使用CGIC編寫的CGI程序,都是從cgic.c中的代碼進入,在庫函數完成了一系列必要的操作(比如解析參數、獲取系統環境變量)之后,它才會調用你的代碼(從你定義的cgiMain進入)。
另外一點就是,cgi程序輸出HTML頁面的方式都是使用printf把頁面一行一行地打印出來,比如cgictest.c中的這一段代碼:
fprintf(cgiOut, "<textarea NAME=/"address/" ROWS=4 COLS=40>/n");
fprintf(cgiOut, "Default contents go here. /n");
fprintf(cgiOut, "</textarea>/n");
上面這段代碼的運行結果就是在頁面上輸出一個textarea。第一個參數cgiOut實際上就是stdin,所以我們可以直接使用printf,而不必使用fprintf。不過在調試的時候會用到fprintf來重定向輸出。
這種方式與 Java ?Servlet非常類似,Servlet也是通過調用打印語句System.out.println(…)來輸出一個頁面。(不過后來Java推出了JSP來克服這種不便。)
但是與Servlet不同的地方在于,使用C語言的我們還要自己輸出HTML頭部(聲明文檔類型):
cgiHeaderContentType("text/html");
這個語句的調用一定要在所有printf語句之前。而這個語句執行的任務實際上就是:
void cgiHeaderContentType(char *mimeType) {
??? fprintf(cgiOut, "Content-type: %s/r/n/r/n", mimeType);
}
這個語句告訴瀏覽器,這次傳來的數據是什么類型,是一個HTML文檔,還是一個bin文件… 如果是個HTML文檔,就通過瀏覽器窗口顯示,如果是一個bin(二進制)文件,則打開下載窗口,讓用戶選擇是否保存文件以及保存文件的路徑。
理解了這幾點之后,你就可以編寫自己的CGIC程序了。新建一個文件test.c試試:
下載: test.c
?? 1. #include <stdio.h>
?? 2. #include "cgic.h"
?? 3. #include <string.h>
?? 4. #include <stdlib.h>
?? 5. int cgiMain() {
?? 6.???? cgiHeaderContentType("text/html");
?? 7.???? fprintf(cgiOut, "<HTML><HEAD>/n");
?? 8.???? fprintf(cgiOut, "<TITLE>My First CGI</TITLE></HEAD>/n");
?? 9.???? fprintf(cgiOut, "<BODY><H1>Hello CGIC</H1></BODY>/n");
? 10.???? fprintf(cgiOut, "</HTML>/n");
? 11.???? return 0;
? 12. }
把Makefile文件中的cgitest.c全部換稱test.c,保存,再執行make命令即可。
此時通過瀏覽器訪問,會在頁面上看到一個大大的“Hello CGIC”。
CGIC簡明教程2:獲取Get請求字符串
Get請求就是我們在瀏覽器地址欄輸入URL時發送請求的方式,或者我們在HTML中定義一個表單(form)時,把action屬性設為“Get”時的工作方式;
Get請求字符串就是跟在URL后面以問號“?”開始的字符串,但不包括問號。比如這樣的一個請求:
http://127.0.0.1/cgi-bin/out.cgi?ThisIsTheGetString
在上面這個URL中,“ThisIsTheGetString”就是Get請求字符串。
在進入我們自己編寫的cgi代碼之前,CGIC庫已經事先把這個字符串取到了,我們可以在程序中直接獲得,要做的僅僅是在你編寫的cgiMain方法前面加入以下聲明:
extern char *cgiQueryString;
現在給出一個簡單的例子,這個例子跟上一篇的測試程序非常相似,只不過程序的輸出是使用者輸入的Get請求字符串。
下載: test.c
?? 1. #include <stdio.h>
?? 2. #include "cgic.h"
?? 3. #include <string.h>
?? 4. #include <stdlib.h>
?? 5.?
?? 6. extern char *cgiQueryString;
?? 7. int cgiMain() {
?? 8.???? cgiHeaderContentType("text/html");
?? 9.???? fprintf(cgiOut, "<HTML><HEAD>/n");
? 10.???? fprintf(cgiOut, "<TITLE>My CGIC</TITLE></HEAD>/n");
? 11.???? fprintf(cgiOut, "<BODY>");
? 12.???? fprintf(cgiOut, "<H1>%s</H1>",cgiQueryString);
? 13.???? fprintf(cgiOut, "</BODY>/n");
? 14.???? fprintf(cgiOut, "</HTML>/n");
? 15.???? return 0;
? 16. }
假設把這個程序編譯成out.cgi(編譯方法參見上一篇),并部署到Web服務器的cgi-bin目錄下,當用戶在瀏覽器地址欄輸入本文開頭給出的URL字符串時,瀏覽器頁面上會顯示:
ThisIsTheGetString
我們也可以編寫一個用于測試的HTML頁面:
下載: test.html
?? 1. <html>
?? 2. <head>
?? 3.???? <title>Test</title>
?? 4. </head>
?? 5. <body>
?? 6.???? <form action="cgi-bin/out.cgi" method="get">
?? 7.???????? <input type="text" name="theText">
?? 8.???????? <input type="submit" value="Continue →">
?? 9.???? </form>
? 10. </body>
? 11. </html>
文件的部署結構應該為:
|test.html
|—-cgi-bin/out.cgi
大家可以試試,通過瀏覽器訪問 http://127.0.0.1/test.html ,在文本框內輸入一些字符,并點擊提交按鈕,然后就可以看到cgi程序的執行結果:把在文本框輸入的字符原樣顯示在瀏覽器上。
CGIC簡明教程3:反轉義
瀏覽器在發送Get請求時,會把請求字符串進行轉義操作(英文術語為: escape); 比如,我們在地址欄輸入(注意最后”it’s me”中的空格):
http://localhost/~Jack/cgi-bin/out.cgi?it 's me
瀏覽器會把它轉義為:
http://localhost/~Jack/cgi-bin/out.cgi?it 's%20me
在上一篇最后給出的例子中,如果在文本框內輸入
it's me
你會發現,瀏覽器最終發送的請求為
http://localhost/~Jack/cgi-bin/out.cgi?theText=it%27s+me
通過CGIC,我們可以把這些被轉義后的字符還原為我們本來的輸入,這個過程就叫“反轉義” (Unescape)。
不過這個過程有點像hack他的代碼。
整個過程分三個步驟:
1)打開cgic.c,找到這一行語句:
static cgiUnescapeResultType cgiUnescapeChars(char **sp, char *cp, int len);
注意,我們要找的只是這個函數聲明,不是函數定義;
2)在這個函數聲明語句的上方,你會看到一個結構體定義:
?? 1. typedef enum {
?? 2.???? cgiUnescapeSuccess,
?? 3.???? cgiUnescapeMemory
?? 4. } cgiUnescapeResultType;
把這幾行語句復制到cgic.h文件中,并在這里把它注釋掉;
同時還要刪除在第一步中找到的函數聲明語句中的“static”關鍵字。
3)我們現在就可以使用反轉義函數cgiUnescapeChars了:
在你自己的代碼(按照慣例,還是test.c)中,加入以下聲明語句即可
extern cgiUnescapeResultType cgiUnescapeChars(char **sp, char *cp, int len);
接下來我們給出一段完整的test.c代碼
下載: test.c
?? 1. #include <stdio.h>
?? 2. #include "cgic.h"
?? 3. #include <string.h>
?? 4. #include <stdlib.h>
?? 5.?
?? 6. extern char *cgiQueryString;
?? 7. extern cgiUnescapeResultType cgiUnescapeChars(char **sp, char *cp, int len);
?? 8. int cgiMain() {
?? 9.???? char * buffer;
? 10.???? cgiHeaderContentType("text/html");
? 11.???? fprintf(cgiOut, "<HTML><HEAD>/n");
? 12.???? fprintf(cgiOut, "<TITLE>My CGI</TITLE></HEAD>/n");
? 13.???? fprintf(cgiOut, "<BODY>");
? 14.???? cgiUnescapeChars(&buffer, cgiQueryString, strlen(cgiQueryString));
? 15.???? fprintf(cgiOut, "<H1>%s</H1>",buffer);
? 16.???? fprintf(cgiOut, "</BODY>/n");
? 17.???? fprintf(cgiOut, "</HTML>/n");
? 18.???? free(buffer);
? 19.???? return 0;
? 20. }
值得注意的是,buffer的存儲空間是cgiUnescapeChars幫你分配的,但最后要由你自己來釋放(free),這一點千萬不可忘記。
下面你可以結合上一篇給出的測試用html代碼試試該cgi程序的運行結果,也可以直接在瀏覽器地址欄輸入一些帶有特殊符號的字符串。
最后講一下為什么不得不用這種hacker的方式來完成該任務,而CGIC不顯式提供?
CGIC的出發點是,我們平時只需要解析請求中的鍵值對,比如:”?q=nice&client=IE”,當我們在服務端查詢“q”的值時,我們就能得到“nice”。CGIC有一族函數幫助我們完成這個任務,比如cgiFormString(以后會講到)。在解析這種請求格式的時候,如果我們提供的參數值含有被轉義的字符,那么CGIC就會在內部調用cgiUnescapeChars完成反轉義。
但是,有時候我們會發送非常復雜的Get請求字符串,但并不是“鍵-值”對的格式。這就需要直接使用cgiUnescapeChars進行反轉義了。
例如:假設我們有個服務端cgi程序chat.cgi,這是個網絡聊天機器人(也許你可以開發自己的Web版MSN機器人、QQ機器人)。如果我們發送如下請求:
http://127.0.0.1/cgi-bin/chat.cgi? "this is a cgi user"
那么chat.cgi就會把“this is a cgi user”當做你對它說的話,經過處理,它會回復一段語句。為了方便,我們并沒有寫成“鍵-值”對的形式。這個時候被我們hack的cgiUnescapeChars就能派上用場了。
CGIC簡明教程4:獲取請求中的參數值
我們在提交一個表單(form)時,怎樣把表單內的值提取出來呢?
比如下面這個表單:
<form action="cgi-bin/out.cgi" method="POST">
??? <input type="text" name="name" />
??? <input type="text" name="number" />
??? <input type="submit" value="Submit" />
</form>
當out.cgi收到請求時,需要把輸入框”name”和輸入框”number”內的值提取出來。而且不管form中的action是GET還是POST,都要有效。
下面給出示例代碼:
下載: test.c
?? 1. #include <stdio.h>
?? 2. #include "cgic.h"
?? 3. #include <string.h>
?? 4. #include <stdlib.h>
?? 5.?
?? 6. int cgiMain() {
?? 7.???? char name[241];
?? 8.???? char number[241];
?? 9.???? cgiHeaderContentType("text/html");
? 10.???? fprintf(cgiOut, "<HTML><HEAD>/n");
? 11.???? fprintf(cgiOut, "<TITLE>My CGI</TITLE></HEAD>/n");
? 12.???? fprintf(cgiOut, "<BODY>");
? 13.???? cgiFormString("name", name, 241);
? 14.???? cgiFormString("number", number, 241);
? 15.???? fprintf(cgiOut, "<H1>%s</H1>",name);
? 16.???? fprintf(cgiOut, "<H1>%s</H1>",number);
? 17.???? fprintf(cgiOut, "</BODY>/n");
? 18.???? fprintf(cgiOut, "</HTML>/n");
? 19.???? return 0;
? 20. }
從上面的代碼可以看出,第13行和第14行獲取了輸入框的值。
獲取輸入參數值在CGIC中其實有一族函數,cgiFormString是其中最常用的一個。
cgiFormStringNoNewlines用來去掉換行符(如果用戶是在一個TextArea里輸入字符的話);
cgiFormStringSpaceNeeded用于測試輸入值的長度,可以以此為依據,然后按需精確分配緩沖區。
用C語言庫(CGIC)編寫CGI,實現文件上傳
用C語言編寫cgi程序的話,多半會用到CGIC。這是個非常流行的庫,遇到文件上傳之類的應用更是離不開它。官方頁面及下載地址為: www.boutell.com/cgic/#obtain
不少網站都有文件上傳的功能,本文展示如何用CGIC庫編寫文件上傳的服務端程序,最后給出一段簡單的HTML代碼,供大家測試使用。
下載: upload.c
?? 1. #include<stdio.h>
?? 2. #include<string.h>
?? 3. #include<unistd.h>
?? 4. #include<fcntl.h>
?? 5. #include<sys/stat.h>
?? 6. #include"cgic.h"
?? 7. #define BufferLen 1024
?? 8. int cgiMain(void){
?? 9.???? cgiFilePtr file;
? 10.???? int targetFile;
? 11.???? mode_t mode;
? 12.???? char name[128];
? 13.???? char fileNameOnServer[64];
? 14.???? char contentType[1024];
? 15.???? char buffer[BufferLen];
? 16.???? char *tmpStr=NULL;
? 17.???? int size;
? 18.???? int got,t;
? 19.???? cgiHeaderContentType("text/html");
? 20.???? //取得html頁面中file元素的值,應該是文件在客戶機上的路徑名
? 21.???? if (cgiFormFileName("file", name, sizeof(name)) !=cgiFormSuccess) {
? 22.???????? fprintf(stderr,"could not retrieve filename/n");
? 23.???????? goto FAIL;
? 24.???? }
? 25.???? cgiFormFileSize("file", &size);
? 26.???? //取得文件類型,不過本例中并未使用
? 27.???? cgiFormFileContentType("file", contentType, sizeof(contentType));
? 28.???? //目前文件存在于系統臨時文件夾中,通常為/tmp,通過該命令打開臨時文件。臨時文件的名字與用戶文件的名字不同,所以不能通過路徑/tmp/userfilename的方式獲得文件
? 29.???? if (cgiFormFileOpen("file", &file) != cgiFormSuccess) {
? 30.???????? fprintf(stderr,"could not open the file/n");
? 31.???????? goto FAIL;
? 32.???? }
? 33.???? t=-1;
? 34.???? //從路徑名解析出用戶文件名
? 35.???? while(1){
? 36.???????? tmpStr=strstr(name+t+1,"//");
? 37.???????? if(NULL==tmpStr)
? 38.???????? tmpStr=strstr(name+t+1,"/");//if "//" is not path separator, try "/"
? 39.???????? if(NULL!=tmpStr)
? 40.???????????? t=(int)(tmpStr-name);
? 41.???????? else
? 42.???????????? break;
? 43.???? }
? 44.???? strcpy(fileNameOnServer,name+t+1);
? 45.???? mode=S_IRWXU|S_IRGRP|S_IROTH;
? 46.???? //在當前目錄下建立新的文件,第一個參數實際上是路徑名,此處的含義是在cgi程序所在的目錄(當前目錄))建立新文件
? 47.???? targetFile=open(fileNameOnServer,O_RDWR|O_CREAT|O_TRUNC|O_APPEND,mode);
? 48.???? if(targetFile<0){
? 49.???????? fprintf(stderr,"could not create the new file,%s/n",fileNameOnServer);
? 50.???????? goto FAIL;
? 51.???? }
? 52.???? //從系統臨時文件中讀出文件內容,并放到剛創建的目標文件中
? 53.???? while (cgiFormFileRead(file, buffer, BufferLen, &got) ==cgiFormSuccess){
? 54.???????? if(got>0)
? 55.???????????? write(targetFile,buffer,got);
? 56.???? }
? 57.???? cgiFormFileClose(file);
? 58.???? close(targetFile);
? 59.???? goto END;
? 60. FAIL:
? 61.???? fprintf(stderr,"Failed to upload");
? 62.???? return 1;
? 63. END:
? 64.???? printf("File /"%s/" has been uploaded",fileNameOnServer);
? 65.???? return 0;
? 66. }
假設該文件存儲為upload.c,則使用如下命令編輯:
gcc -Wall upload.c cgic.c -o upload.cgi
編譯完成后把upload.cgi復制到你部署cgi程序的目錄(通常命名為cgi-bin)。
正式部署時,請務必修改用open創建新文件那一行代碼。把open的第一個參數設置為目標文件在服務器上存儲的絕對路徑,或者相對于cgi程序的相對路徑。本例中,出于簡單考慮,在cgi程序所在目錄下創建新文件。
測試用HTML代碼:
下載: upload.html
?? 1. <form target="_blank" method="post" action="cgi-bin/upload.cgi">
?? 2.???? <input name="file" type="file" /> <input name="submit" type="submit" />
?? 3. </form>
最后的文件目錄結構為
/MyWebRoot
|—/upload.html
|—/cgi-bin
|——/upload.cgi
當然,你必須配置能夠cgi-bin,并且程序要有權限在cgi-bin目錄下創建文件(因為此例把文件上傳到cgi-bin目錄下)。
那么如何控制上傳文件的大小呢?因為你有時會不允許用戶上傳太大的文件。
通過分析cgic.c的源代碼,我們發現它定義了一個變量cgiContentLength,表示請求的長度。但我們需要首先判斷這是一個上傳文件的請求,然后才能根據cgiContentLength來檢查用戶是否要上傳一個太大的文件。
cgic.c的main函數中進行了一系列if-else判斷來檢查請求的類型,首先確定這是一個post請求,然后確定數據的編碼方式為 “multipart/form-data”,這個判斷通過之后,就要開始準備接收數據了。所以我們要在接收數據開始之前使用 cgiContentLength判斷大小,如果超過標準,就立即返回,不允許繼續操作。
下面貼出修改后代碼片段(直接修改cgic.c的源代碼即可):
?? 1. else if (cgiStrEqNc(cgiContentType, "multipart/form-data")) {
?? 2. #ifdef CGICDEBUG
?? 3. CGICDEBUGSTART
?? 4.???? fprintf(dout, "Calling PostMultipartInput/n");
?? 5. CGICDEBUGEND
?? 6. #endif
?? 7. //我的代碼
?? 8. //UpSize:文件長度上限值,以byte為單位,UpSize是一個int變量,因為cgiContentLength的類型為int
?? 9.???? if(cgiContentLength>UpSize){
? 10.???????? cgiHeaderContentType("text/html");
? 11.???????? printf("File too large!/n");
? 12.???????? cgiFreeResources();
? 13.???????? return -1;
? 14.???? }
? 15. //我的代碼結束
? 16.???? if (cgiParsePostMultipartInput() != cgiParseSuccess) {
? 17. #ifdef CGICDEBUG
? 18. CGICDEBUGSTART
? 19.???????? fprintf(dout, "PostMultipartInput failed/n");
? 20. CGICDEBUGEND
? 21. #endif
? 22.???????? cgiFreeResources();
? 23.???????? return -1;
? 24.???? }
? 25. #ifdef CGICDEBUG
? 26. CGICDEBUGSTART
? 27.???? fprintf(dout, "PostMultipartInput succeeded/n");
? 28. CGICDEBUGEND
? 29.???? #endif
? 30. }
? 31. }
變量UpSize表示文件大小的上限。在cgic.c的main中找到相關代碼,并修改成上面這樣即可。你可以在cgic.c中定義UpSize,也可以在剛才完成的upload.c中定義,然后在cgic.c中用extern方式引用。
總結
- 上一篇: gitee如何解决GitHub下载速度慢
- 下一篇: 移动4G技术和黑客行为