1. 程式人生 > >Web基礎(二)CGI協議

Web基礎(二)CGI協議

Web基礎

1. CGI 協議

通用閘道器介面(Common Gateway Interface/CGI)是一種重要的網際網路技術,可以讓一個客戶端,從網頁瀏覽器向執行在網路伺服器上的程式請求資料。CGI描述了伺服器和請求處理程式之間傳輸資料的一種標準,即CGI是一種協議。

CGI是在1993年由美國國家超級計算機應用中心(NCSA)為NCSA HTTPd Web伺服器開發的。這個Web伺服器使用了UNIX shell 環境變數來儲存從Web伺服器傳遞出去的引數,然後生成一個執行CGI的獨立的程序。 CGI伺服器

CGI規範允許Web伺服器執行外部程式,並將它們的輸出傳送給Web瀏覽器,CGI將Web的一組簡單的靜態超媒體文件變成一個完整的新的互動式媒體。通俗的講CGI就像是一座橋,把網頁和WEB伺服器中的執行程式連線起來,它把HTML接收的指令傳遞給伺服器的執行程式,再把伺服器執行程式的結果返還給HTML頁。CGI 的跨平臺效能極佳,幾乎可以在任何作業系統上實現。

實際上有多種方式可以執行CGI程式,但對http的請求方法來說,只有get和post兩種方法允許執行CGI指令碼(實際上post方法的內部本質還是get方法,只不過在傳送http請求時,get和post方法對url中的引數處理方式不一樣而已)。

常用於編寫CGI的語言有perl、php、python等,實際上任何一種語言都能編寫CGI,java也一樣能寫,但java的servlet完全能實現CGI的功能,且更優化、更利於開發。

1.1 特點

CGI方式在遇到連線請求(使用者請求)時先要建立CGI的子程序,啟用一個CGI程序,然後處理請求,處理完後結束這個子程序。所以用CGI方式的伺服器有多少連線請求就會有多少CGI子程序,子程序反覆載入是CGI效能低下的主要原因。當用戶請求數量非常多時,會大量擠佔系統的資源如記憶體,CPU時間等,造成效能低下。

1.2 CGI指令碼工作流程

  1. 瀏覽器通過HTML表單或超連結請求指向一個CGI應用程式的URL
  2. 伺服器收發到請求
  3. 伺服器執行所指定的CGI應用程式
  4. CGI應用程式執行所需要的操作,通常是基於瀏覽者輸入的內容
  5. CGI應用程式把結果格式化為網路伺服器和瀏覽器能夠理解的文件(通常為HTML網頁)
  6. 網路伺服器把結果返回到瀏覽器中

1.3 實現原理

一般情況下,伺服器和CGI程式之間是通過標準輸入輸出來進行資料傳遞的,而這個過程需要環境變數的協作方可實現。每個CGI程式只能處理一個使用者請求,所以在啟用一個CGI程式程序時也建立了屬於該程序的環境變數。

1.伺服器將URL指向一個CGI應用程式 2.伺服器為應用程式執行做準備 3.應用程式執行,讀取標準輸入和有關環境變數 4.應用程式進行標準輸出

在這裡插入圖片描述

1.3.1 CGI 介面標準

介面標準 簡述
標準輸入 CGI程式像其他可執行程式一樣,可通過標準輸入(stdin)從Web伺服器得到輸入資訊,如Form中的資料,這就是所謂的向CGI程式傳遞資料的POST方法。這意味著在作業系統命令列狀態可執行CGI程式,對CGI程式進行除錯。POST方法是常用的方法。
環境變數 作業系統提供了許多環境變數,它們定義了程式的執行環境,應用程式可以存取它們。Web伺服器和CGI介面又另外設定了自己的一些環境變數,用來向CGI程式傳遞一些重要的引數。CGI的GET方法還通過環境變數QUERY_STRING向CGI程式傳遞Form中的資料。
標準輸出 CGI程式通過標準輸出(stdout)將輸出資訊傳送給Web伺服器。傳送給Web伺服器的資訊可以用多種格式,通常是以純文字或者HTML文字的形式,這樣我們就可以在命令列狀態除錯CGI程式,並且得到它們的輸出。

對於CGI程式來說,它繼承了系統的環境變數。CGI的環境變數在CGI程式啟動時初始化,在結束時銷燬。當一個CGI程式不是被HTTP伺服器呼叫時,它的環境變數幾乎是系統環境變數的複製,而當這個CGI程式被HTTP伺服器呼叫時,它的環境變數就會多出以下關於HTTP伺服器、客戶端、CGI傳輸過程等內容

與請求相關的環境變數
REQUEST_METHOD 伺服器與CGI程式之間的資訊傳輸方式。一般包括兩種:POST和GET,但在寫CGI程式時,最後還應考慮其他的情況
QUERY_STRING 採用GET時所傳輸的資訊,包含URL中問號後面的引數
CONTENT_LENGTH 對於用POST遞交的表單, 標準輸入口的位元組數
CONTENT_TYPE 指示所傳來的資訊的MIME型別。如表單是用POST提交為application/x-www-form-urlencoded,並且經過了URL編碼;而在上傳檔案的表單中,則為 multipart/form-data
CONTENT_FILE 使用Windows HTTPd/WinCGI標準時,用來傳送資料的檔名
PATH_INFO 路徑資訊。由瀏覽器通過GET方法發出
PATH_TRANSLATED CGI程式的完整路徑名
SCRIPT_NAME 所呼叫的CGI程式的名字。它指向這個CGI指令碼的路徑, 是在URL中顯示的(如, /cgi-bin/thescript)
與伺服器相關的環境變數
GATEWAY_INTERFACE 伺服器所實現的CGI版本。對於UNIX伺服器, 是CGI/1.1.
SERVER_NAME CGI指令碼執行時的主機名和IP地址
SERVER_PORT 伺服器執行的TCP埠,通常Web伺服器是80
SERVER_SOFTWARE 呼叫CGI程式的HTTP伺服器的名稱和版本號。如: CERN/3.0 或 NCSA/1.3.
與客戶端相關的環境變數
REMOTE_ADDR 客戶機的IP地址
REMOTE_HOST 客戶機的主機名,該值不能被設定
ACCEPT 列出能被此請求接受的應答方式。即客戶機所支援的MIME型別清單,內容如:“image/gif,image/jpeg”
ACCEPT_ENCODING 列出客戶機支援的編碼方式
ACCEPT_LANGUAGE 表明客戶機可接受語言的ISO程式碼
AUTORIZATION 表明被證實了的使用者
FORM 列出客戶機的EMAIL地址
IF_MODIFIED_SINGCE 當用get方式請求並且只有當文件比指定日期更早時才返回資料
PRAGMA 設定將來要用到的伺服器代理
REFFERER 指出連線到當前文件的文件的URL
USER_AGENT 客戶端瀏覽器的資訊

環境變數是一個儲存使用者資訊的記憶體區。當客戶端的使用者通過瀏覽器發出CGI請求時,伺服器就尋找本地的相應CGI程式並執行它。在執行CGI程式的同時,伺服器把該使用者的資訊儲存到環境變數裡。接下來,CGI程式的執行流程是這樣的:查詢與該CGI程式程序相應的環境變數:第一步是request_method,如果是POST,就從環境變數的len,然後到該程序相應的標準輸入取出len長的資料。如果是GET,則使用者資料就在環境變數的QUERY_STRING裡。

GET 通過在URL中嵌入的形式傳遞引數。對CGI程式而言,在GET請求中傳遞的引數要通過環境變數“QUERY_STRING”來接收。 1、引數的內容作為URL資訊,使用者可以看到;2、有大小的限制。
POST CGI程式從標準輸入接收引數。與GET方法不同的是,引數的內容從URL資訊中不能獲得,對於大小也沒有限制。 與GET方法問題1、2完全相反

1.POST 採用POST方法,那麼來自客戶端來的使用者資料將存放在CGI程序的標準輸入中,同時將使用者資料的長度賦予環境變數中的CONTENT_LENGTH。客戶端用POST方式傳送資料有一個相應的MIME型別(通用Internet郵件擴充服務:Multi-purpose Internet Mail Extensions)。目前,MIME型別一般為:application/x-wwww-form-urlencoded,該型別表示資料來自HTML表單,記錄在環境變數CONTENT_TYPE中,CGI程式應該檢查該變數的值。

2.GET 在該方法下,CGI程式無法直接從伺服器的標準輸入中獲取資料,因為伺服器把它從標準輸入接收到的資料編碼到環境變數QUERY_STRING(或PATH_INFO)中。

1.3.2 CGI程式實現

進入我們上一篇部落格的zjhttpstatic目錄中,可以看到一個最簡單的CGI程式sayhi.c。將該程式編譯後,命名為sayhi.cgi,執行zjhttp伺服器,在瀏覽器輸入http://localhost:7749/sayhi.cgi 即可測試

//sayhi.c
#include <stdio.h>

int main(){
	printf("Content-Type: text/html\n");
	printf("\n");
    printf("<html>");
    printf("<head>");
    printf("<title>CGI</title>");
    printf("</head>");
    printf("<body>");
	printf("I am a CGI program!\n");
    printf("</body>");
    printf("</html>\n");
	
	return 0;
}

再看一下從伺服器獲取資料示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 將使用者輸入的資料打印出來
void main(void) {
    //輸出一個CGI標題
    fprintf(stdout,"content-type:text/plain\n\n"); 

    char *pszMethod; 
    pszMethod = getenv("REQUEST_METHOD"); 
    if(strcmp(pszMethod,"GET") == 0) {         //GET method 
        //讀取環境變數來獲取資料 
        printf("This is GETMETHOD!\n");
        printf("SERVER_NAME:%s\n",getenv("SERVER_NAME")); 
        printf("REMOTE_ADDR:%s\n",getenv("REMOTE_ADDR")); 
        fprintf(stdout,"input data is:%s\n",getenv("QUERY_STRING"));
    } else {                                                   // POST method 
        //讀取STDIN來獲取資料
        int iLength = atoi(getenv("CONTENT_LENGTH"));
        printf("This is POSTMETHOD!\n");
        fprintf(stdout,"input data is:\n");
        
        for(int i=0; i < iLength; i++) {
            char cGet = fgetc(stdin);
            fputc(cGet,stdout);
        } 
    } 
}

POST 請求中獲取資料

void unencode(char *src, char *last, char *dest){
  // str = hello+there%21 此處跳過data=... 
  // last = ; 已到末尾.
  // dest= ; 空串.
   //解碼原則
   //原則1: '+'變' ';
   //原則2: '%xx'變成對應的16進位制ASCII碼值;
   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");
       printf("CONTENT_LENGTH =%s\n",lenstr);

       if(lenstr == NULL ||sscanf(lenstr,"%ld",&len)!=1 || len > MAXLEN){
              printf("<P>Error ininvocation - wrong FORM probably.");
       } else {
              FILE *f;
              fgets(input, len+1, stdin);//add by ycy從輸入流中獲取字串.
              unencode(input+EXTRA, input+len,data);

              f = fopen(DATAFILE,"a");
              if(f == NULL){
                     printf("<P>Sorry,cannot store your data.");
              }else{
                     fputs(data, f); //add byycy 將資料儲存在對對應的檔案中.
              }
              fclose(f);
              printf("<P>Thank you!Your contribution has been stored.");
       }

       return 0;
}

不管是POST還是GET方式,客戶端傳送給伺服器的資料都不是原始的使用者資料,而是經過URL編碼的。此時,CGI的環境變數Content_type將被設定,如Content_type = application/x-www-form-urlencode就表示伺服器收到的是經過URL編碼的包含有HTML表單變數資料。

編碼的基本規則是: 變數之間用“&”分開; 變數與其對應值用“=”連線; 空格用“+”代替; 保留的控制字元則用“%”連線對應的16禁止ASCII碼代替; 某些具有特殊意義的字元也用“%”接對應的16進位制ASCII碼代替; 空格是非法字元; 任意不可列印的ASCII控制字元均為非法字元

CGI 資料輸出

CGI程式如何將資訊處理結果返回給客戶端?這實際上是CGI格式化輸出。在CGI程式中的標準輸出stdout是經過重定義了的,它並沒有在伺服器上產生任何的輸出內容,而是被重定向到客戶瀏覽器,這與它是由C,還是Perl或Python實現無關。所以,我們可以用列印來實現客戶端新的HTML頁面的生成。比如,C的printf是向該程序的標準輸出傳送資料,Perl和Python用print向該程序的標準輸出傳送資料。

  • CGI標題 CGI的格式輸出內容必須組織成標題/內容的形式。CGI標準規定了CGI程式可以使用的三個HTTP標題。標題必須佔據第一行輸出!而且必須隨後帶有一個空行。
標題 描述
Content_type (內容型別) 設定隨後輸出資料所用的MIME型別
Location (地址) 設定隨後輸出資料所用的MIME型別
Status (狀態) 指定HTTP狀態碼
  • MIME 向標準輸出傳送網頁內容時要遵守MIME格式規則。任意輸出前面必須有一個用於定義MIME型別的輸出內容(Content-type)行,而且隨後還必須跟一個空行。如果遺漏了這一條,服務將會返回一個錯誤資訊。(同樣使用於其他標題)
型別/子型別 描述
Text/plain 普通文字型別
Text/html HTML格式的文字型別
Audio/basic 八位聲音檔案格式,字尾為.au
Video/mpeg MPEG檔案格式
Video/quicktime QuickTime檔案格式
Image/gif GIF圖形檔案
Image/jpeg JPEG圖形檔案
Image/x-xbitmap X bitmap圖形檔案,字尾為.xbm

1.3.3 注意事項

LibCGI 是一個易於使用且功能強大的庫,從頭開始編寫,以幫助在C中製作CGI應用程式。它支援字串操作,連結列表,cookie,會話,GET和POST方法以及更多內容。

CGI請求
  • 伺服器根據 以 / 分隔的路徑選擇直譯器
  • 如果有 AUTH 欄位,需要先執行 AUTH,再執行直譯器
  • 伺服器確認 CONTENT-LENGTH 表示的是資料解析出來的長度,如果附帶資訊體,則必須將長度欄位傳送到直譯器
  • 如果有 CONTENT-TYPE 欄位,伺服器必須將其傳給直譯器;若無此欄位,但有資訊體,則伺服器判斷此型別或拋棄資訊體
  • 伺服器必須設定 QUERY_STRING 欄位,如果客戶端沒有設定,服務端要傳一個空字串“”
  • 伺服器必須設定 REMOTE_ADDR,即客戶端請求IP
  • REQUEST_METHOD 欄位必須設定, GET 、POST 等,大小寫敏感
  • SCRIPT_NAME 表示執行的直譯器指令碼名,必須設定
  • SERVER_NAMESERVER_PORT 代表著大小寫敏感的伺服器名和伺服器受理時的TCP/IP埠
  • SERVER_PROTOCOL 欄位指示著伺服器與直譯器協商的協議型別,不一定與客戶端請求的SCHEMA 相同,如’https://’ 可能為HTTP
  • CONTENT-LENGTH 不為 NULL 時,伺服器要提供資訊體,此資訊體要嚴格與長度相符,即使有更多的可讀資訊也不能多傳
  • 伺服器必須將資料壓縮等編碼解析出來
CGI響應
  • CGI直譯器必須響應 至少一行頭 + 換行 + 響應內容
  • 直譯器在響應文件時,必須要有 CONTENT-TYPE
  • 在客戶端重定向時,直譯器除了 client-redir-response=絕對url地址,不能再有其他返回,然後伺服器返回一個 302 狀態碼
  • 直譯器響應 三位數字狀態碼,具體配置可自行搜尋
  • 伺服器必須將所有直譯器返回的資料響應給客戶端,除非需要壓縮等編碼,伺服器不能修改響應資料

2. zjhttp 程式碼詳解

充分學習了CGI協議,瞭解了CGI的相關知識,接下來則可以詳細的學習我們上一篇部落格的zjhttp程式碼了

看到zjHttp.c中的execute_cgi函式,結合上面的CGI相關知識與註釋,很容易理解CGI的原理。

/* 執行cgi動態解析 */
void execute_cgi(Client client, char *path, const char *method, const char *query_string) {
    char buf[1024];
    int numchars = 1;
    int content_length = -1;
    buf[0] = 'A'; buf[1] = '\0';
    if (StrCaseCmp(method, "GET") == 0) {                    /* 是GET請求,讀取並丟棄頭資訊 */
        while ((numchars > 0) && strcmp("\n", buf))
            numchars = get_line(client, buf, sizeof(buf));
    }else {                                                 /* POST請求 */
        numchars = get_line(client, buf, sizeof(buf));
        while ((numchars > 0) && strcmp("\n", buf)) {       /* 迴圈讀取頭資訊找到Content-Length欄位值 */
            buf[15] = '\0';                                 /* 擷取Content-Length: */

            if (StrCaseCmp(buf, "Content-Length:") == 0) content_length = atoi(&(buf[16]));/* 獲取Content-Length的值 */
            numchars = get_line(client, buf, sizeof(buf));
        }
        if (content_length == -1) {
            bad_request(client);
            return;
        }
    }
    sprintf(buf, "HTTP/1.0 200 OK\r\n");                    /* 返回正確響應碼200 */
    send(client, buf, strlen(buf), 0);
#ifdef _ZJ_WIN32
    CGI_ENV env;
    memset(&env, 0, sizeof(env));
    env.len = sizeof(env.buf);
    add_env(&env, "SYSTEMROOT", getenv("SYSTEMROOT"));
    add_env(&env, "REQUEST_METHOD", method);

    if (StrCaseCmp(method, "GET") == 0) {
        add_env(&env, "QUERY_STRING", query_string);
    }else {                        /* POST */
        add_env(&env, "CONTENT_LENGTH", content_length);
    }
    char abspath[MAX_PATH];
    GetModuleFileName(NULL, abspath, MAX_PATH);

    char *p = NULL;
    for (p = abspath + strlen(abspath); *p != '\\'; p--);
    *(++p) = '\0';

    for (p = path; *p != '\0'; p++) {
        if (*p == '/') *p = '\\';
    }

    strcat(abspath, path);
    printf("abspath=%s\n", abspath);
    createCgiProcess(client, env.buf, abspath, method, content_length);
#else
    int cgi_output[2];
    int cgi_input[2];
    pid_t pid;
    int status,i;
    char c;

    /* 必須在fork()中呼叫pipe(),否則子程序不會繼承檔案描述符
       pipe(cgi_output)執行成功後,cgi_output[0]為讀通道 cgi_output[1]為寫通道 */
    if (pipe(cgi_output) < 0) {
        cannot_execute(client);
        return;
    }
    if (pipe(cgi_input) < 0) {
        cannot_execute(client);
        return;
    }

    if ((pid = fork()) < 0) {
        cannot_execute(client);
        return;
    }
    /* fork出一個子程序執行cgi指令碼 */
    if (pid == 0)  /* 子程序 */{
        char meth_env[255];
        char query_env[255];
        char length_env[255];

        dup2(cgi_output[1], 1);                            /* 1代表著stdout,0代表著stdin,將系統標準輸出重定向為cgi_output[1] */
        dup2(cgi_input[0], 0);                             /* 將系統標準輸入重定向為cgi_input[0] */

        close(cgi_output[0]);                              /* 關閉了cgi_output中的讀通道 */
        close(cgi_input[1]);                               /* 關閉了cgi_input中的寫通道 */
                            
                            
        sprintf(meth_env, "REQUEST_METHOD=%s", method);    /* CGI標準需要將請求的方法儲存環境變數儲存REQUEST_METHOD */
        putenv(meth_env);
        if (strcasecmp(method, "GET") == 0) {
            sprintf(query_env, "QUERY_STRING=%s", query_string);
            putenv(query_env);
        }else {   /* POST */
            sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
            putenv(length_env);
        }

        execl(path, path, NULL);                           /* 執行CGI指令碼 */
        exit(0);
    }else {    /* 父程序 */
        close(cgi_output[1]);                              /* 關閉了cgi_output中的寫通道,此處是父程序中cgi_output變數*/
        close(cgi_input[0]);                               /* 關閉了cgi_input中的讀通道 */
        if (strcasecmp(method, "POST") == 0)
            for (i = 0; i < content_length; i++) {
                recv(client, &c, 1, 0);                    /* 開始讀取POST中的內容*/
                write(cgi_input[1], &c, 1);                /* 將資料傳送給cgi指令碼 */
            }
        
        while (read(cgi_output[0], &c, 1) > 0)             /* 讀取cgi指令碼返回資料 */
            send(client, &c, 1, 0);

        close(cgi_output[0]);
        close(cgi_input[1]);
        waitpid(pid, &status, 0);
    }
#endif /* _ZJ_WIN32 */
}

2.1 C 與Linux 函式

接下來詳細說明一下zjhttp伺服器裡一些函式的使用情況,在zjHttp.c中,accept_request函式裡使用了strtok函式,該函式是標準庫中的函式

標頭檔案<string.h> 函式原型char * strtok(char *s, const char *delim); 引數s 指向將要分割的字串,引數delim 為分割符,即以之做為分割的標誌。當函式在引數s 的字串中發現引數delim 符時則會將該字元改為\0 字元。在第一次呼叫時,strtok必需給予引數s 字串,往後的呼叫則將引數s 設定成NULL。每次呼叫成功則返回下一個分割後的字串指標。 返回值:返回下一個分割後的字串指標,如果已無則返回NULL

#include