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 &rarr;">
 
  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方式引用。

發佈了8 篇原創文章 · 獲贊 9 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章