關於 httpd 的理解和實踐

關於 httpd 的理解和實踐

之前在網上看到有人推薦新手值得學習的C語言開源項目,其中有提到 httpd,是一個超簡單的 http服務器。

自己看了幾遍,代碼不多,就500來行,主要目的是瞭解一下HTTP協議以及服務器如何處理HTTP請求,下面就自己在閱讀過程中遇到一些問題,整理出來形成此文。CSDN上早有大神貼出了自己的閱讀心得:TinyHTTPd–超輕量型Http Server源碼分析 還有 【源碼剖析】tinyhttpd —— C 語言實現最簡單的 HTTP 服務器 ,寫的都很詳細,大家感興趣的可以去看看一看。

首先編譯運行,看一下效果:
注意兩點:
(1)如果直接執行 make 的話,你會遇到這個錯誤:cannot find -lsocket。原因是在 Linux系統中沒有這個庫文件,但是這個庫在 linux 中有被實現,位於 libc 中,編譯時被默認包含,所以可以直接在 Makefile 中去掉 -lsocket。
(2)在 htdocs 文件下,有 cgi 的程序和 html 代碼,cgi 是用 perl 寫的,對於POST請求,服務器會調用cgi程序進行響應,但文件中聲明的 perl 執行程序位置與實際的操作系統有關,很可能與你電腦上的不一致,我這裏 perl 腳本位於 /usr/local/bin 中(用 which perl 可以查看對應的路徑),所以把 cgi 文件中的第一行改爲:

#!/usr/local/bin/perl -Tw

使用Charles觀察http報文(使用firebug時發現表單參數與請求頭是分開顯示的,看不到完整的HTTP原始報文)
(1)在瀏覽器中輸入 “localhost:49418”(注意此處的端口是隨機分配的,需根據服務器端打印出的實際端口號進行設置),訪問該地址,查看 http請求報文如下:

GET / HTTP/1.1
Host: 192.168.1.101:49418
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1

(2)在表單中填入 green,點擊”提交查詢“,查看對應的HTTP請求報文如下:

POST /color.cgi HTTP/1.1
Host: localhost:49418
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://localhost:49418/
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 11

color=green

注意表單參數在最後面的報文實體部分,也就是“color=green”。

關於用perl編寫的cgi腳本程序,可以參考此連接 Perl CGI編程
其中有關於環境變量的描述,正好可以對應httpd.c中設置的環境變量 QUERY_STRINGCONTENT_LENGTH

變量名 描述
QUERY_STRING 如果服務器與CGI程序信息的傳遞方式是GET,這個環境變量的值即使所傳遞的信息。這個信息經跟在CGI程序名的後面,兩者中間用一個問號’?’分隔。
CONTENT_LENGTH 如果服務器與CGI程序信息的傳遞方式是POST,這個環境變量即是從標準輸入STDIN中可以讀到的有效數據的字節數。這個環境變量在讀取所輸入的數據時必須使用。

對應於GET和POST方法,設置不同的環境變量,代碼如下:

    if (strcasecmp(method, "GET") == 0) {
        /*設置 query_string 的環境變量*/
        sprintf(query_env, "QUERY_STRING=%s", query_string);
        putenv(query_env);
    }
    else {   /* POST */
        /*設置 content_length 的環境變量*/ 
        sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
        putenv(length_env);
    }
    execl(path, path, NULL);
    exit(0);

對於execl函數,其原型爲 int execl(const char *pathname, const char *arg0, .../* (char *)0 */);
簡單說明一下參數:
第一個參數是文件路徑(pathname),這裏要着重說一下後面的參數,對execl\execlp\execle三個函數表示命令行參數的一般方法是: char *arg0, char *arg1, ..., char *argn, (char *)0

也就是說execl函數從第二個參數(arg0)開始,正好對應於命令行參數argv[0],而命令行參數arg[0]是該命令對應的字符串,並且ISO C要求argv[argc]是一個空指針,所以execl的第一個參數是文件路徑(pathname),第二個參數一般就是命令本身對應的字符串,並且最後一個參數要以 (char *)0 結尾

在APUE中的7.4節又提到,當執行一個程序時,調用exec的進程可將命令行參數傳遞給該新程序,進程自願終止的唯一方法是顯示或隱士地(通過調用exit)調用_exit或_Exit。(進程也可以非自願地由一個信號使其終止)

main函數的原型是:int main(int argc, char *argv[]); 其中argc是命令行參數的數目, argv是指向參數的各個指針所構成的數組。在內核執行C程序時,先調用一個特殊的啓動例程,該啓動例程會從內核取得命令行參數和環境變量值,然後再執行一個exec函數。可執行程序文件將此啓動例程指定爲程序的起始地址(由C編譯器調用,設置可執行文件的起始地址爲啓動例程)。

對於管道,經常看到的操作是將管道的文件描述符重定位到標準輸入\輸出上,例如: dup2(fd[0], STDIN_FILENO);,將管道的讀端重定位成標準輸入。

    if (pid == 0)  /* child: CGI script */
    {
        char meth_env[255];
        ... ...
        dup2(cgi_output[1], 1); //標準輸出重定向到output管道的寫入端 stdout:1
        dup2(cgi_input[0], 0);  //標準輸入重定向到input管道的讀取端 stdin:0
        ... ...
        /* 調用exec函數,執行CGI腳本,通過dup2重定向,CGI的標準輸出內容進入子進程管道output[1]的輸入端 (在父進程中會讀取管道output[0],然後將此內容發送給瀏覽器)  */
        execl(path, path, NULL);
        exit(0); //子進程退出
    } else {    /* parent */
        close(cgi_output[1]); //關閉管道的一端,這樣可以建立父子進程間的管道通信
        close(cgi_input[0]);
        if (strcasecmp(method, "POST") == 0)
            for (i = 0; i < content_length; i++) {
                /* 接收 POST 過來的報文實體 (表單參數在 "報文實體" 中) */ 
                recv(client, &c, 1, 0); //從客戶端接收單個字符
                /* 將報文實體內容寫入input[1],由於在子進程中input[0]被重定向到了標準輸入,此處寫入的內容
                   會被CGI程序通過標準輸入(input[0])讀取到 */
                write(cgi_input[1], &c, 1); 
            }

        /* 讀取output的管道輸出到客戶端,output輸出端爲cgi腳本執行後的內容 */
        while (read(cgi_output[0], &c, 1) > 0)
            send(client, &c, 1, 0); //將cgi執行結果發送給客戶端,即send到瀏覽器

        close(cgi_output[0]); //關閉剩下的管道端
        close(cgi_input[1]);
        waitpid(pid, &status, 0); //等待子進程終止
    }

CGI請求處理部分使用了管道來做進程間通信,下面梳理一下這個處理流程。對於管道操作不熟悉的同學,可以去參考一下APUE(2e)中程序清單15-1和15-2,其中有提到一種管道的常用方法:將管道描述符複製爲標準輸入和標準輸出,在此之後通常子進程執行另一個程序(exec),該程序從標準輸入中讀數據(讀管道)或向標準輸出中寫入數據(寫管道)。

execute_cgi() 時序圖:

Created with Raphaël 2.1.0parent processparent processchild processchild process獲取HTTP報文實體的長度發送HTTP起始行(HTTP/1.0 200 OK)關閉cgi_output[0]和cgi_input[1]重定位cgi_output[1]到STDOUT重定位cgi_input[0]到STDIN關閉cgi_output[1]和cgi_input[0]recv(socket, ...)從client(瀏覽器)接收報文實體獲取HTTP報文實體cgi_input[1](parent) --> cgi_input[0](child)HTTP報文的實體內容執行CGI程序從stdin讀取參數,輸出到stdoutstdin對應於cgi_input[0]stdout對應於cgi_output[1]cgi_output[0](parent) <-- cgi_output[1](child)CGI程序的標準輸出send(socket, ...)發送到client(瀏覽器)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章