Tinyhttpd學習

學習一個簡易的http服務器開源代碼,源碼:https://github.com/EZLippi/Tinyhttpd

由於該代碼不能直接在linux上運行,需要進行一些修改,項目部署參考:tinyhttpd在Linux編譯以及HTTP服務器的本質:tinyhttpd源碼分析及拓展

啓動後可直接通過瀏覽器訪問IP和端口號進行測試。

經典圖摘自:Tinyhttpd精讀解析

具體的代碼也有很多人寫過了,這裏總結一下自己學到的東西。

startup函數綁定監聽套接字等操作非常常規,然後在accept接收客戶端連接後通過pthread_create函數創建線程執行代碼,這裏進入線程入口函數accept_request,第一步就是讀取http請求並解析。

代碼首先獲取一行HTTP報文數據,代碼如下:

int get_line(int sock, char *buf, int size)
{
    int i = 0;			//遊標
    char c = '\0';		//當前讀取到的字符
    int n;			//臨時變量				
 
    while ((i < size - 1) && (c != '\n'))		//沒超過1024或者c不等於\n就一直讀
    {
        n = recv(sock, &c, 1, 0);
        /* DEBUG printf("%02X\n", c); */
        if (n > 0)
        {
            if (c == '\r')		//如果讀到了\r就證明讀到回車了
            {
                n = recv(sock, &c, 1, MSG_PEEK);	
                //先讀一個字符,最後一項是0就是正常讀完了清TCP緩衝區
                //但是MSG_PEEK不清緩衝區,這樣下一次recv的時候還是讀的它
                /* DEBUG printf("%02X\n", c); */
                if ((n > 0) && (c == '\n'))			//如果讀到了\n證明該結束了
                    recv(sock, &c, 1, 0);			//如上,這樣只是爲了清緩衝區
                else
                    c = '\n';	        //如果沒讀到其實也要換行了,所以讓c等於\n
            }
            buf[i] = c;			//每次讀取完的賦值
            i++;			//遊標+1
        }
        else
            c = '\n';			//如果一上來什麼都沒讀到,直接退出循環
    }
    buf[i] = '\0';			//字符數組最後補\0
 
    return(i);
}

然後程序通過對字符數組的操作提取出請求方法放入method數組,若爲不是GET和POST請求,返回不支持;若爲POST,CGI置爲1。

#define ISspace(x) isspace((int)(x))
 
while (!ISspace(buf[j]) && (i < sizeof(method) - 1))
{
    //提取其中的請求方式是GET還是POST
    method[i] = buf[j];
    i++; 
    j++;
}
method[i] = '\0';
//函數說明:strcasecmp()用來比較參數s1和s2字符串,比較時會自動忽略大小寫的差異。
//返回值:若參數s1和s2字符串相同則返回0
//s1長度大於s2長度則返回大於0的值,s1長度若小於s2長度則返回小於0的值。
if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
{
    //tinyhttp僅僅實現了GET和POST
    unimplemented(client);
    return;
}
//cgi爲標誌位,置1說明開啓cgi解析
if (strcasecmp(method, "POST") == 0)
//如果請求方法爲POST,需要cgi解析
    cgi = 1;

跳過空格,從BUF中並將URL存入數組,讀URL的時候是讀到?或者讀完才停,?後面就是查詢參數,此時需執行CGI解析參數,標誌位置1並截取參數,最後和自帶的htdocs文件夾組成查詢路徑。

如果路徑只是一個目錄 / ,默認設置爲首頁index.html(測試的時候權限要開啓)。

if (path[strlen(path) - 1] == '/')
    strcat(path, "index.html");

此時利用stat函數通過文件名獲取文件信息並保存在所指的stat結構體中,若頁面不存在,一直讀完剩餘的請求頭信息丟棄即可,聲明網頁不存在;若存在,看CGI,爲1進行動態解析,爲0靜態返回文件。

若返回靜態文件,調用serve_file函數,首先丟棄HTTP請求頭其他信息,然後根據filename打開文件讀取內容,調用headers函數添加HTTP頭並調用cat函數發送文件內容。

若爲動態解析,首先需要對POST請求取出Content-Length,GET無所謂。

建立兩個管道,cgi_output以及cgi_input,fork一個子進程。

int cgi_output[2];
int cgi_input[2];

//#include<unistd.h>
//int pipe(int filedes[2]);
//返回值:成功,返回0,否則返回-1。
//參數數組包含pipe使用的兩個文件的描述符。fd[0]:讀管道,fd[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;
}

在子進程中,把標準輸出重定向到cgi_output的寫入端,把標準輸入重定向到cgi_input的讀取端,關閉 cgi_input 的寫入端 和 cgi_output 的讀取端,同理在父進程中要關閉 cgi_input 的讀取端和 cgi_output的寫入端。

如果把管道和進程之間的連接關係連起來看可畫圖:

然後子進程調用excel函數執行CGI腳本,父進程則讀取POST內容,並將數據發送給CGI腳本(往cgi_input[1]裏寫),再從 cgi_output[0] 中讀取內容返回給瀏覽器。這裏可以理解爲子進程用於處理CGI文件,父進程負責socket的讀寫操作。

父進程調用waitpid等待子進程結束即可。

整體的流程圖參考:tinyhttpd 剖析

TODOLIST:CGI還是不太理解,管道的用途是什麼?

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章