學習一個簡易的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還是不太理解,管道的用途是什麼?