Web基礎(二)CGI協議
Web基礎
1. CGI 協議
通用閘道器介面(Common Gateway Interface/CGI)是一種重要的網際網路技術,可以讓一個客戶端,從網頁瀏覽器向執行在網路伺服器上的程式請求資料。CGI描述了伺服器和請求處理程式之間傳輸資料的一種標準,即CGI是一種協議。
CGI是在1993年由美國國家超級計算機應用中心(NCSA)為NCSA HTTPd Web伺服器開發的。這個Web伺服器使用了UNIX shell 環境變數來儲存從Web伺服器傳遞出去的引數,然後生成一個執行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指令碼工作流程
- 瀏覽器通過HTML表單或超連結請求指向一個CGI應用程式的URL
- 伺服器收發到請求
- 伺服器執行所指定的CGI應用程式
- CGI應用程式執行所需要的操作,通常是基於瀏覽者輸入的內容
- CGI應用程式把結果格式化為網路伺服器和瀏覽器能夠理解的文件(通常為HTML網頁)
- 網路伺服器把結果返回到瀏覽器中
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程式實現
進入我們上一篇部落格的zjhttp 的
static
目錄中,可以看到一個最簡單的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_NAME
和SERVER_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