TinyHTTPd--超輕量型Http Server源碼分析

來源:互聯網
上載者:User

TinyHTTPd是一個超輕量型Http Server,使用C語言開發,全部代碼不到600行,附帶一個簡單的Client,可以通過閱讀這段代碼理解一個Http Server的本質。源碼下載連結http://sourceforge.net/projects/tinyhttpd/

分析這段源碼前,需要對網路通訊協定,Unix編程,以及HTTP有一定的瞭解,這裡假設大家對http有一定的瞭解,如果有時間,會額外介紹下Http。

本文先全篇分析下該開源項目的源碼,最後給出測試。

伺服器端代碼:httpd.c


建議源碼讀取順序:main —> startup —> accept_request —> excute_cgi

TinyHTTPd 項目流程圖



先介紹幾個中間輔助函數:(完整代碼參見前面源碼連結)

從用戶端讀取一行資料,以\r或\r\n為行結束符

/**********************************************************************//* Get a line from a socket, whether the line ends in a newline,* carriage return, or a CRLF combination.  Terminates the string read* with a null character.  If no newline indicator is found before the* end of the buffer, the string is terminated with a null.  If any of* the above three line terminators is read, the last character of the* string will be a linefeed and the string will be terminated with a* null character.* Parameters: the socket descriptor*             the buffer to save the data in*             the size of the buffer* Returns: the number of bytes stored (excluding null) *//**********************************************************************//**********************************************************************//* 從socket讀取一行資料。以\r或\r\n為行結束符* Parameters: the socket descriptor*             the buffer to save the data in*             the size of the buffer* Returns: the number of bytes stored (excluding null) *//**********************************************************************/int get_line(int sock, char *buf, int size){int i = 0;char c = '\0';int n;//至多讀取size-1個字元,最後一個字元置'\0'while ((i < size - 1) && (c != '\n')){n = recv(sock, &c, 1, 0);//單個字元接收if (n > 0){if (c == '\r')//如果是斷行符號符,繼續讀取{/*使用 MSG_PEEK 標誌使下一次讀取依然可以得到這次讀取的內容,可認為接收視窗不滑動*/n = recv(sock, &c, 1, MSG_PEEK);if ((n > 0) && (c == '\n'))//如果是斷行符號分行符號recv(sock, &c, 1, 0);//繼續接收單個字元,實際上和上面那個標誌位MSG_PEEK讀取同樣的字元,讀完後刪除輸入隊列的資料,即滑動視窗,c=='\n'elsec = '\n';//只是讀取到斷行符號符,則置為分行符號,也終止了讀取}buf[i] = c;//放入緩衝區i++;}else//沒有讀取到任何資料c = '\n';}buf[i] = '\0';return(i);//返回讀到的字元個數(包括'\0')}
請求出錯情況處理
/**********************************************************************//* 告知用戶端該請求有錯誤 400* Parameters: client socket *//**********************************************************************/void bad_request(int client){char buf[1024];/*將字串存入緩衝區,再通過send函數發送給用戶端*/sprintf(buf, "HTTP/1.0 400 BAD REQUEST\r\n");send(client, buf, sizeof(buf), 0);sprintf(buf, "Content-type: text/html\r\n");send(client, buf, sizeof(buf), 0);sprintf(buf, "\r\n");send(client, buf, sizeof(buf), 0);sprintf(buf, "<P>Your browser sent a bad request, ");send(client, buf, sizeof(buf), 0);sprintf(buf, "such as a POST without a Content-Length.\r\n");send(client, buf, sizeof(buf), 0);}/**********************************************************************//* 通知用戶端CGI指令碼不能被執行 500* Parameter: the client socket descriptor. *//**********************************************************************/void cannot_execute(int client){char buf[1024];/*回饋出錯資訊*/sprintf(buf, "HTTP/1.0 500 Internal Server Error\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, "Content-type: text/html\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, "\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, "<P>Error prohibited CGI execution.\r\n");send(client, buf, strlen(buf), 0);}/**********************************************************************//* 列印出錯資訊,詳見《Unix 環境進階編程》並終止*//**********************************************************************/void error_die(const char *sc){perror(sc);exit(1);}/**********************************************************************//* 返回用戶端404錯誤資訊 404(萬惡的404) *//**********************************************************************/void not_found(int client){char buf[1024];sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, SERVER_STRING);send(client, buf, strlen(buf), 0);sprintf(buf, "Content-Type: text/html\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, "\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, "<HTML><TITLE>Not Found</TITLE>\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, "<BODY><P>The server could not fulfill\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, "your request because the resource specified\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, "is unavailable or nonexistent.\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, "</BODY></HTML>\r\n");send(client, buf, strlen(buf), 0);}

讀取檔案中的資料到client

/**********************************************************************//* Put the entire contents of a file out on a socket.  This function * is named after the UNIX "cat" command, because it might have been * easier just to do something like pipe, fork, and exec("cat"). * Parameters: the client socket descriptor *             FILE pointer for the file to cat */ /*Unix shell 命令cat file 即列印檔案file中的資料*//**********************************************************************/ /*將檔案結構指標resource中的資料發送至client*/void cat(int client, FILE *resource){ char buf[1024]; fgets(buf, sizeof(buf), resource);//從檔案結構指標resource中讀取資料,儲存至buf中 //處理檔案流中剩下的字元 while (!feof(resource))//檢測流上的檔案結束符,檔案結束返回非0值,結束返回0 {  send(client, buf, strlen(buf), 0);//檔案流中的字元全部發送給client  fgets(buf, sizeof(buf), resource);/*從檔案結構體指標resource中讀取至多bufsize-1個資料                                    (第bufsize個字元賦'\0')每次讀取一行,如果不足bufsize,                                     則讀完該行結束。這裡通過feof函數來判斷fgets是否因出錯而終止                                     另外,這裡有檔案位移位置,下一輪讀取會從上一輪讀取完的位置繼續*/ }}

返迴文件資訊給client

/**********************************************************************//* Return the informational HTTP headers about a file. *//* Parameters: the socket to print the headers on*             the name of the file *//*返迴文件頭部資訊*//**********************************************************************/void headers(int client, const char *filename){char buf[1024];(void)filename;  /* could use filename to determine file type */strcpy(buf, "HTTP/1.0 200 OK\r\n");send(client, buf, strlen(buf), 0);strcpy(buf, SERVER_STRING);send(client, buf, strlen(buf), 0);sprintf(buf, "Content-Type: text/html\r\n");send(client, buf, strlen(buf), 0);strcpy(buf, "\r\n");send(client, buf, strlen(buf), 0);}/**********************************************************************//* Send a regular file to the client.  Use headers, and report* errors to client if they occur.* Parameters: a pointer to a file structure produced from the socket*              file descriptor*             the name of the file to serve *//*返迴文件資料,用於靜態頁面返回*//**********************************************************************/void serve_file(int client, const char *filename){FILE *resource = NULL;int numchars = 1;char buf[1024];buf[0] = 'A'; buf[1] = '\0';while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */numchars = get_line(client, buf, sizeof(buf));resource = fopen(filename, "r");//唯讀方式開啟檔案if (resource == NULL)not_found(client);//如果檔案不存在,返回404錯誤else{headers(client, filename);//先返迴文件頭部資訊cat(client, resource);//將resource描述符指定檔案中的資料發送給client}fclose(resource);//關閉}
下面就是tynyhttpd伺服器端的核心代碼部分。

為了更好地理解源碼,這裡提出http的請求報文格式

                                      

伺服器端通訊端初始化設定

/**********************************************************************//* This function starts the process of listening for web connections* on a specified port.  If the port is 0, then dynamically allocate a* port and modify the original port variable to reflect the actual* port.* Parameters: pointer to variable containing the port to connect on* Returns: the socket *//**********************************************************************//*伺服器端通訊端初始化設定*/int startup(u_short *port){int httpd = 0;struct sockaddr_in name;httpd = socket(PF_INET, SOCK_STREAM, 0);//建立伺服器端通訊端if (httpd == -1)error_die("socket");memset(&name, 0, sizeof(name));name.sin_family = AF_INET;//地址簇name.sin_port = htons(*port);//指定連接埠name.sin_addr.s_addr = htonl(INADDR_ANY);//通配地址if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)//綁定到指定地址和連接埠error_die("bind");if (*port == 0)  /* if dynamically allocating a port *///動態分配一個連接埠{int namelen = sizeof(name);/*在以連接埠號碼0調用bind後,getsockname用於返回由核心賦予的本地連接埠號碼*/if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)error_die("getsockname");*port = ntohs(name.sin_port);//網路位元組順序轉換為主機位元組順序,返回主機位元組順序表達的數}if (listen(httpd, 5) < 0)//伺服器監聽用戶端請求。通訊端排隊的最大串連個數5error_die("listen");return(httpd);}
接收用戶端的請求報文
/**********************************************************************//* A request has caused a call to accept() on the server port to * return.  Process the request appropriately. * Parameters: the socket connected to the client *//**********************************************************************//**********************************************************************//* HTTP協議規定,請求從用戶端發出,最後伺服器端響應該請求並返回。 * 這是目前HTTP協議的規定,伺服器不支援主動響應,所以目前的HTTP * 協議版本都是基於用戶端請求,然後響應的這種模型。 */ /*accept_request函數解析用戶端請求,判斷是請求靜態檔案還是cgi代碼 (通過請求類型以及參數來判定),如果是靜態檔案則將檔案輸出給前端, 如果是cgi則進入cgi處理函數*//**********************************************************************/ void accept_request(int client){ char buf[1024]; int numchars; char method[255];//要求方法GET or POST char url[255];//請求的檔案路徑 char path[512];//檔案相對路徑 size_t i, j; struct stat st; int cgi = 0;      /* becomes true if server decides this is a CGI                    * program */ char *query_string = NULL; numchars = get_line(client, buf, sizeof(buf));//從client中讀取指定大小資料到buf i = 0; j = 0; //解析用戶端的http請求報文 /*接收字元處理:提取空白字元前的字元,至多254個*/ while (!ISspace(buf[j]) && (i < sizeof(method) - 1)) {  method[i] = buf[j];//根據http請求報文格式,這裡得到的是要求方法  i++; j++; } method[i] = '\0'; //忽略大小寫比較字串,用於判斷是哪種類型 if (strcasecmp(method, "GET") && strcasecmp(method, "POST")) {  unimplemented(client);//兩種method都不是,告知用戶端所請求的方法未能實現  return; } if (strcasecmp(method, "POST") == 0)//POST 類型  cgi = 1;//設定標誌位 i = 0; while (ISspace(buf[j]) && (j < sizeof(buf)))//過濾空白字元,空格後面是URL  j++;/*將buf中的非空白字元轉存進url緩衝區,遇空白字元或滿退出*/ while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf))) {  url[i] = buf[j];//擷取的是URL(互連網標準資源的地址)  i++; j++; } url[i] = '\0'; if (strcasecmp(method, "GET") == 0)//GET method {  query_string = url;//請求資訊  while ((*query_string != '?') && (*query_string != '\0'))//截取'?'前的字元   query_string++;//問號前面是路徑,後面是參數  if (*query_string == '?')//有'?',表明動態請求  {   cgi = 1;   *query_string = '\0';   query_string++;  } }//下面是TinyHTTPd項目htdocs檔案下的檔案 sprintf(path, "htdocs%s", url);//擷取請求檔案路徑 if (path[strlen(path) - 1] == '/')//如果檔案類型是目錄(/),則加上index.html  strcat(path, "index.html");////根據路徑找檔案,並擷取path檔案資訊儲存到結構體st中 if (stat(path, &st) == -1) {//執行失敗,檔案未找到  /*丟棄所有 headers 的資訊*/  while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */   numchars = get_line(client, buf, sizeof(buf));//從用戶端讀取資料進buf  not_found(client);//回應用戶端找不到 } else//擷取檔案資訊,執行成功 {  /*如果是個目錄,則預設使用該目錄下 index.html 檔案*/  if ((st.st_mode & S_IFMT) == S_IFDIR)   strcat(path, "/index.html");  if ((st.st_mode & S_IXUSR) ||      (st.st_mode & S_IXGRP) ||      (st.st_mode & S_IXOTH)    )   cgi = 1;  if (!cgi)//靜態頁面請求   serve_file(client, path);//直接返迴文件資訊給用戶端,靜態頁面返回  else//動態網頁面請求   execute_cgi(client, path, method, query_string);//執行cgi指令碼 } close(client);//關閉用戶端通訊端}
執行CGI指令碼,動態網頁面申請
/**********************************************************************//* 執行CGI(公用網卡介面)指令碼,需要設定合適的環境變數* Parameters: client socket descriptor*             path to the CGI script *//*execute_cgi函數負責將請求傳遞給cgi程式處理,伺服器與cgi之間通過管道pipe通訊,首先初始化兩個管道,並建立子進程去執行cgi函數*//*子進程執行cgi程式,擷取cgi的標準輸出通過管道傳給父進程,由父進程發送給用戶端*//**********************************************************************/void execute_cgi(int client, const char *path,const char *method, const char *query_string){char buf[1024];int cgi_output[2];int cgi_input[2];pid_t pid;int status;int i;char c;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))  /* read & discard headers讀取並丟棄 HTTP 要求 */numchars = get_line(client, buf, sizeof(buf));//從用戶端讀取else    /* POST 一般用於更新資源資訊*/{numchars = get_line(client, buf, sizeof(buf));//擷取HTTP訊息實體的傳輸長度while ((numchars > 0) && strcmp("\n", buf))//不為空白且不為分行符號{buf[15] = '\0';if (strcasecmp(buf, "Content-Length:") == 0)//是否為Content-Length欄位content_length = atoi(&(buf[16]));//Content-Length用於描述HTTP訊息實體的傳輸長度numchars = get_line(client, buf, sizeof(buf));}if (content_length == -1) {bad_request(client);//請求的頁面資料為空白,沒有資料,就是我們開啟網頁經常出現空白頁面return;}}sprintf(buf, "HTTP/1.0 200 OK\r\n");//send(client, buf, strlen(buf), 0);//建立管道,兩個通道cgi_output[0]:讀取端,cgi_output[1]:寫入端if (pipe(cgi_output) < 0) {cannot_execute(client);//管道建立失敗,列印出錯資訊return;}//管道只能具有公用祖先的進程間進行,這裡是父子進程之間if (pipe(cgi_input) < 0) {cannot_execute(client);return;}//fork子進程,這樣就建立了父子進程間的IPC通道if ((pid = fork()) < 0) {cannot_execute(client);return;}//實現進程間的管道通訊機制/*子進程繼承了父進程的pipe,然後通過關閉子進程output管道的輸出端,input管道的寫入端;關閉父進程output管道的寫入端,input管道的輸出端*///子進程,if (pid == 0)  /* child: CGI script */{char meth_env[255];char query_env[255];char length_env[255];//複製檔案控制代碼,重新導向進程的標準輸入輸出//dup2的第一個參數描述符關閉dup2(cgi_output[1], 1);//標準輸出重新導向到output管道的寫入端dup2(cgi_input[0], 0);//標準輸入重新導向到input管道的讀取端close(cgi_output[0]);//關閉output管道的寫入端close(cgi_input[1]);//關閉輸出端sprintf(meth_env, "REQUEST_METHOD=%s", method);putenv(meth_env);if (strcasecmp(method, "GET") == 0) {//GET/*設定 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);//exec函數簇,執行CGI指令碼,擷取cgi的標準輸出作為相應內容發送給用戶端//通過dup2重新導向,標準輸出內容進入管道output的輸入端exit(0);//子進程退出}else {    /* parent */close(cgi_output[1]);//關閉管道的一端,這樣可以建立父子進程間的管道通訊close(cgi_input[0]);/*通過關閉對應管道的通道,然後重新導向子進程的管道某端,這樣就在父子進程之間構建一條單雙面通道如果不重新導向,將是一條典型的全雙工系統管道通訊機制*/if (strcasecmp(method, "POST") == 0)//POST方式,將指定好的傳輸長度字元發送/*接收 POST 過來的資料*/for (i = 0; i < content_length; i++) {recv(client, &c, 1, 0);//從用戶端接收單個字元write(cgi_input[1], &c, 1);//寫入input,然後重新導向到了標準輸入//資料傳送過程:input[1](父進程) ——> input[0](子進程)[執行cgi函數] ——> STDIN ——> STDOUT // ——> output[1](子進程) ——> output[0](父進程)[將結果發送給用戶端]}while (read(cgi_output[0], &c, 1) > 0)//讀取output的管道輸出到用戶端,output輸出端為cgi指令碼執行後的內容send(client, &c, 1, 0);//即將cgi執行結果發送給用戶端,即send到瀏覽器,如果不是POST則只有這一處理close(cgi_output[0]);//關閉剩下的管道端,子進程在執行dup2之後,就已經關閉了管道一端通道close(cgi_input[1]);waitpid(pid, &status, 0);//等待子進程終止}}
上面父子進程間的管道通訊可以用下圖表示:父子進程各司其職,分工合作,通過管道建立通訊通道。
                                   
上面最終完整狀態是 POST 方式,如果不是 POST 方式,則只有 output[0] ——> 用戶端。
上面即伺服器端的程式:這裡簡單羅列一下:
#define ISspace(x) isspace((int)(x))//若x為空白格字元,返回true#define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n"void accept_request(int);//用戶端向伺服器端發送請求void bad_request(int);//告訴用戶端請求出錯,400void cat(int, FILE *);//讀取檔案並發送給用戶端void cannot_execute(int);//通知用戶端不能執行CGI指令碼(perl)void error_die(const char *);//列印出錯資訊void execute_cgi(int, const char *, const char *, const char *);//執行CGI指令碼,內部調用exec函數簇int get_line(int, char *, int);//從通訊端讀取資料,返回讀取到的字元個數void headers(int, const char *);//返回HTTP標頭檔資訊void not_found(int);//通知用戶端頁面未找到,404void serve_file(int, const char *);//發送訊息給用戶端,用於靜態頁面返回int startup(u_short *);//伺服器端通訊端設定,建立,綁定,監聽(TCP協議)void unimplemented(int);//通知用戶端所請求的網路方法沒有實現(GET、POST)
下面這個就是伺服器端的main.c
int main(void){ int server_sock = -1; u_short port = 0;//傳入的連接埠為0, int client_sock = -1; struct sockaddr_in client_name; int client_name_len = sizeof(client_name); pthread_t newthread; server_sock = startup(&port);//伺服器端監聽通訊端設定 printf("httpd running on port %d\n", port); /*多線程並發伺服器模型*/ while (1) {  //主線程  client_sock = accept(server_sock,                       (struct sockaddr *)&client_name,                       &client_name_len);//阻塞等待用戶端串連請求  if (client_sock == -1)   error_die("accept"); /* accept_request(client_sock); */ if (pthread_create(&newthread , NULL, accept_request, client_sock) != 0)//建立背景工作執行緒,執行回呼函數accept_request,參數client_sock   perror("pthread_create"); } close(server_sock);//關閉通訊端,就協議棧而言,即關閉TCP串連 return(0);}

從上面我們可以的出Tinyhttp的工作流程: 伺服器啟動,指定連接埠或隨機選取連接埠綁定httpd服務,監聽用戶端的串連請求。(startup 函數) 收到用戶端的 HTTP 要求,派生一個線程去相應用戶端請求(多線程伺服器模型),即執行 accept_request 函數 伺服器端解析用戶端 HTTP 要求報文。判斷是何種 method (GET or POST)以及擷取 url。對於 GET 方法,如果攜帶參數,則 query_string 指標指向 url 中 ? 後面的 GET 參數(http 協議) 拷貝 url 資料到 path數組,表示瀏覽器請求的伺服器檔案路徑,在 tinyhttpd 中伺服器檔案是在 htdocs 檔案夾下,若 url 以 /  結尾,或 url 本身是個目錄(stat 檔案資訊),則預設在 path 中加上 index.html,表示訪問首頁。 在檔案路徑合法的前提下,如果是靜態頁面訪問 ,直接輸出伺服器檔案到瀏覽器,即用 HTTP 格式寫到用戶端通訊端上,然後跳到。如果是動態網頁面申請(帶?的GET方式,POST方式,utl 為可執行檔),則轉調用 excute_cgi 函數執行cgi指令碼。 讀取整個 HTTP 要求並丟棄,如果是 POST 則找出Content-Length。把"HTTP/1.0 200 OK\r\n" 狀態代碼寫到通訊端。 建立兩個管道,cgi_input 和 cgi_output ,並 fork 一個進程(必須 fork 子進程,pipe 管道才有意義)。建立父子進程間的通訊機制。 在子進程中,對其進程下的管道進行重新導向,並設定對應的環境變數(method、query_string、content_length),這些環境變數都是為了給 cgi 指令碼調用,接著用 execl 運行 cgi 指令碼,可以看出 cgi 指令碼的執行在子進程中進行,然後結果通過管道以及重新導向返回給父進程。 父進程中,關閉管道一端,如果是 POST 方式,則把 POST 資料寫入 cgi_intput,已被重新導向到 STDIN,讀取 cgi_output 管道輸出到用戶端(瀏覽器輸出),具體流程圖參見上面的管道最終狀態圖。接著關閉所有管道,等待子進程結束。 關閉串連,完成一次 HTTP 要求與回應。 HTTP 是不需連線的,在進行 Web 應用程式前無須建立專門的 HTTP 應用程式層會話串連,僅需要直接利用傳輸層已為它建立好的 TCP 傳輸串連即可。即雖然是不可靠的無連線協定,但使用可可靠的 TCP 傳輸層協議,所以從資料轉送角度來講,HTTP 的報文傳輸仍是可靠的。

值得說明的是,這個項目是不能直接在Linux環境下編譯啟動並執行,它本來是在Solaris上實現的,需要修改幾處地方,由於篇幅問題,下一篇TinyHTTPd 在Linux 下編譯 給出修改地方以及最後運行測試結果。

如果錯誤,歡迎指出,交流進步,謝謝。



聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.