標籤:

HTTP伺服器的本質:tinyhttpd源碼分析及拓展

  已經有一個月沒有更新博客了,一方面是因為平時太忙了,另一方面是想積攢一些乾貨進行分享。最近主要是做了一些開源項目的源碼分析工作,有c項目也有python項目,想提升一下內功,今天分享一下tinyhttpd源碼分析的成果。tinyhttpd是一個非常輕量型的http伺服器,c代碼500行左右,可以幫助我們了解http伺服器運行的實質。在分析之前,我們先說一下http報文。

一.http請求

http請求由三部分組成,分別是:起始行、消息報頭、請求正文

Request Line<CRLF>Header-Name: header-value<CRLF>Header-Name: header-value<CRLF>//一個或多個,均以<CRLF>結尾<CRLF>body//請求正文

1、起始行以一個方法符號開頭,以空格分開,後面跟著請求的URI和協議的版本,格式如下:

Method Request-URI HTTP-Version CRLF

其中 Method表示請求方法;Request-URI是一個統一資源標識符;HTTP-Version表示請求的HTTP協議版本;CRLF表示回車和換行(除了作為結尾的CRLF外,不允許出現單獨的CR或LF字元)。

2、請求方法(所有方法全為大寫)有多種,各個方法的解釋如下:

  • GET 請求獲取Request-URI所標識的資源
  • POST 在Request-URI所標識的資源後附加新的數據
  • HEAD 請求獲取由Request-URI所標識的資源的響應消息報頭
  • PUT 請求伺服器存儲一個資源,並用Request-URI作為其標識
  • DELETE 請求伺服器刪除Request-URI所標識的資源
  • TRACE 請求伺服器回送收到的請求信息,主要用於測試或診斷
  • CONNECT 保留將來使用
  • OPTIONS 請求查詢伺服器的性能,或者查詢與資源相關的選項和需求

應用舉例:

GET方法:在瀏覽器的地址欄中輸入網址的方式訪問網頁時,瀏覽器採用GET方法向伺服器獲取資源,eg:

GET /form.html HTTP/1.1 (CRLF)

POST方法要求被請求伺服器接受附在請求後面的數據,常用於提交表單。eg:

POST /reg.jsp HTTP/ (CRLF)Accept:image/gif,image/x-xbit,... (CRLF)...HOST:www.guet.edu.cn (CRLF)Content-Length:22 (CRLF)Connection:Keep-Alive (CRLF)Cache-Control:no-cache (CRLF)(CRLF) //該CRLF表示消息報頭已經結束,在此之前為消息報頭user=jeffrey&pwd=1234 //此行以下為提交的數據

二.tinyhttpd源碼分析 

tinyhttpd總共包含以下函數:

void accept_request(int);//處理從套接字上監聽到的一個 HTTP 請求void bad_request(int);//返回給客戶端這是個錯誤請求,400響應碼void cat(int, FILE *);//讀取伺服器上某個文件寫到 socket 套接字void cannot_execute(int);//處理髮生在執行 cgi 程序時出現的錯誤void error_die(const char *);//把錯誤信息寫到 perror void execute_cgi(int, const char *, const char *, const char *);//運行cgi腳本,這個非常重要,涉及動態解析int get_line(int, char *, int);//讀取一行HTTP報文void headers(int, const char *);//返回HTTP響應頭void not_found(int);//返回找不到請求文件void serve_file(int, const char *);//調用 cat 把伺服器文件內容返回給瀏覽器。int startup(u_short *);//開啟http服務,包括綁定埠,監聽,開啟線程處理鏈接void unimplemented(int);//返回給瀏覽器表明收到的 HTTP 請求所用的 method 不被支持。

建議源碼閱讀順序: main -> startup -> accept_request -> execute_cgi 

按照以上順序,看一下瀏覽器和tinyhttpd交互的整個流程:

三.注釋版源碼

  注釋版源碼已經放到github上了,以後所有的源碼分析都會上傳github上。由於tinyhttpd源碼較少,下面將完整的代碼貼出來。

/* J. David"s webserver *//* This is a simple webserver. * Created November 1999 by J. David Blackstone. * CSE 4344 (Network concepts), Prof. Zeigler * University of Texas at Arlington *//* This program compiles for Sparc Solaris 2.6. * To compile for Linux: * 1) Comment out the #include <pthread.h> line. * 2) Comment out the line that defines the variable newthread. * 3) Comment out the two lines that run pthread_create(). * 4) Uncomment the line that runs accept_request(). * 5) Remove -lsocket from the Makefile. */#include <stdio.h>#include <sys/socket.h>#include <sys/types.h>#include <netinet/in.h>#include <arpa/inet.h>#include <unistd.h>#include <ctype.h>#include <strings.h>#include <string.h>#include <sys/stat.h>#include <pthread.h>#include <sys/wait.h>#include <stdlib.h>#define ISspace(x) isspace((int)(x))//函數說明:檢查參數c是否為空格字元,//也就是判斷是否為空格(" ")、定位字元(" ")、CR("
")、換行("
")、垂直定位字元(" v ")或翻頁(" f ")的情況。
//返回值:若參數c 為空白字元,則返回非 0,否則返回 0。#define SERVER_STRING "Server: jdbhttpd/0.1.0
"
//定義server名稱void accept_request(int);//接收請求void bad_request(int);//無效請求void cat(int, FILE *);void cannot_execute(int);void error_die(const char *);void execute_cgi(int, const char *, const char *, const char *);int get_line(int, char *, int);void headers(int, const char *);void not_found(int);void serve_file(int, const char *);int startup(u_short *);void unimplemented(int);/**********************************************************************//* 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 *//**********************************************************************///接收客戶端的連接,並讀取請求數據void accept_request(int client){ char buf[1024]; int numchars; char method[255]; 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;//獲取一行HTTP報文數據 numchars = get_line(client, buf, sizeof(buf)); // i = 0; j = 0; //對於HTTP報文來說,第一行的內容即為報文的起始行,格式為<method> <request-URL> <version>, //每個欄位用空白字元相連 while (!ISspace(buf[j]) && (i < sizeof(method) - 1)) { //提取其中的請求方式是GET還是POST method[i] = buf[j]; i++; j++; } method[i] = "";//函數說明: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; i = 0; //將method後面的後邊的空白字元略過 while (ISspace(buf[j]) && (j < sizeof(buf))) j++; //繼續讀取request-URL while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf))) { url[i] = buf[j]; i++; j++; } url[i] = "";//如果是GET請求,url可能會帶有?,有查詢參數 if (strcasecmp(method, "GET") == 0) { query_string = url; while ((*query_string != "?") && (*query_string != "")) query_string++; if (*query_string == "?") { //如果帶有查詢參數,需要執行cgi,解析參數,設置標誌位為1 cgi = 1; //將解析參數截取下來 *query_string = ""; query_string++; } }//以上已經將起始行解析完畢//url中的路徑格式化到path sprintf(path, "htdocs%s", url);//學習到這裡明天繼續TODO//如果path只是一個目錄,默認設置為首頁index.html if (path[strlen(path) - 1] == "/") strcat(path, "index.html"); //函數定義: int stat(const char *file_name, struct stat *buf); //函數說明: 通過文件名filename獲取文件信息,並保存在buf所指的結構體stat中 //返回值: 執行成功則返回0,失敗返回-1,錯誤代碼存於errno(需要include <errno.h>) if (stat(path, &st) == -1) { //假如訪問的網頁不存在,則不斷的讀取剩下的請求頭信息,並丟棄即可 while ((numchars > 0) && strcmp("
", buf)) /* read & discard headers */ numchars = get_line(client, buf, sizeof(buf)); //最後聲明網頁不存在 not_found(client); } else { //如果訪問的網頁存在則進行處理 if ((st.st_mode & S_IFMT) == S_IFDIR)//S_IFDIR代表目錄 //如果路徑是個目錄,那就將主頁進行顯示 strcat(path, "/index.html"); if ((st.st_mode & S_IXUSR) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH) ) //S_IXUSR:文件所有者具可執行許可權 //S_IXGRP:用戶組具可執行許可權 //S_IXOTH:其他用戶具可讀取許可權 cgi = 1; if (!cgi) //將靜態文件返回 serve_file(client, path); else //執行cgi動態解析 execute_cgi(client, path, method, query_string); } close(client);//因為http是面向無連接的,所以要關閉}/**********************************************************************//* Inform the client that a request it has made has a problem. * Parameters: client socket *//**********************************************************************/void bad_request(int client){ char buf[1024];//發送400 sprintf(buf, "HTTP/1.0 400 BAD REQUEST
"); send(client, buf, sizeof(buf), 0); sprintf(buf, "Content-type: text/html
"); send(client, buf, sizeof(buf), 0); sprintf(buf, "
"); 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.
"); send(client, buf, sizeof(buf), 0);}/**********************************************************************//* 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 *//**********************************************************************/void cat(int client, FILE *resource){//發送文件的內容 char buf[1024];//讀取文件到buf中 fgets(buf, sizeof(buf), resource); while (!feof(resource))//判斷文件是否讀取到末尾 { //讀取並發送文件內容 send(client, buf, strlen(buf), 0); fgets(buf, sizeof(buf), resource); }}/**********************************************************************//* Inform the client that a CGI script could not be executed. * Parameter: the client socket descriptor. *//**********************************************************************/void cannot_execute(int client){ char buf[1024];//發送500 sprintf(buf, "HTTP/1.0 500 Internal Server Error
"); send(client, buf, strlen(buf), 0); sprintf(buf, "Content-type: text/html
"); send(client, buf, strlen(buf), 0); sprintf(buf, "
"); send(client, buf, strlen(buf), 0); sprintf(buf, "<P>Error prohibited CGI execution.
"); send(client, buf, strlen(buf), 0);}/**********************************************************************//* Print out an error message with perror() (for system errors; based * on value of errno, which indicates system call errors) and exit the * program indicating an error. *//**********************************************************************/void error_die(const char *sc){ perror(sc); exit(1);}/**********************************************************************//* Execute a CGI script. Will need to set environment variables as * appropriate. * Parameters: client socket descriptor * path to the CGI script *//**********************************************************************///執行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] = ""; if (strcasecmp(method, "GET") == 0) //如果是GET請求 //讀取並且丟棄頭信息 while ((numchars > 0) && strcmp("
", buf)) numchars = get_line(client, buf, sizeof(buf)); else { //處理的請求為POST numchars = get_line(client, buf, sizeof(buf)); while ((numchars > 0) && strcmp("
", buf)) {//循環讀取頭信息找到Content-Length欄位的值 buf[15] = "";//目的是為了截取Content-Length: if (strcasecmp(buf, "Content-Length:") == 0) //"Content-Length: 15" content_length = atoi(&(buf[16]));//獲取Content-Length的值 numchars = get_line(client, buf, sizeof(buf)); } if (content_length == -1) { //錯誤請求 bad_request(client); return; } }//返回正確響應碼200 sprintf(buf, "HTTP/1.0 200 OK
"); send(client, buf, strlen(buf), 0);//#include<unistd.h>//int pipe(int filedes[2]);//返回值:成功,返回0,否則返回-1。參數數組包含pipe使用的兩個文件的描述符。fd[0]:讀管道,fd[1]:寫管道。//必須在fork()中調用pipe(),否則子進程不會繼承文件描述符。//兩個進程不共享祖先進程,就不能使用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) /* 子進程: 運行CGI 腳本 */ { 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],這一點非常關鍵, //cgi程序中用的是標準輸入輸出進行交互 close(cgi_output[0]);//關閉了cgi_output中的讀通道 close(cgi_input[1]);//關閉了cgi_input中的寫通道 //CGI標準需要將請求的方法存儲環境變數中,然後和cgi腳本進行交互 //存儲REQUEST_METHOD sprintf(meth_env, "REQUEST_METHOD=%s", method); putenv(meth_env); 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); } // 表頭文件#include<unistd.h> // 定義函數 // int execl(const char * path,const char * arg,....); // 函數說明 // execl()用來執行參數path字元串所代表的文件路徑,接下來的參數代表執行該文件時傳遞過去的argv(0)、argv[1]……,最後一個參數必須用空指針(NULL)作結束。 // 返回值 // 如果執行成功則函數不會返回,執行失敗則直接返回-1,失敗原因存於errno中。 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++) { //開始讀取POST中的內容 recv(client, &c, 1, 0); //將數據發送給cgi腳本 write(cgi_input[1], &c, 1); } //讀取cgi腳本返回數據 while (read(cgi_output[0], &c, 1) > 0) //發送給瀏覽器 send(client, &c, 1, 0);//運行結束關閉 close(cgi_output[0]); close(cgi_input[1]);//定義函數:pid_t waitpid(pid_t pid, int * status, int options);//函數說明:waitpid()會暫時停止目前進程的執行, 直到有信號來到或子進程結束. //如果在調用wait()時子進程已經結束, 則wait()會立即返回子進程結束狀態值. 子進程的結束狀態值會由參數status 返回, //而子進程的進程識別碼也會一快返回. //如果不在意結束狀態值, 則參數status 可以設成NULL. 參數pid 為欲等待的子進程識別碼, 其他數值意義如下://1、pid<-1 等待進程組識別碼為pid 絕對值的任何子進程.//2、pid=-1 等待任何子進程, 相當於wait().//3、pid=0 等待進程組識別碼與目前進程相同的任何子進程.//4、pid>0 等待任何子進程識別碼為pid 的子進程. waitpid(pid, &status, 0); }}/**********************************************************************//* 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) *//**********************************************************************///解析一行http報文int get_line(int sock, char *buf, int size){ int i = 0; char c = ""; int n; while ((i < size - 1) && (c != "
"
)) { n = recv(sock, &c, 1, 0); /* DEBUG printf("%02X
", c); */
if (n > 0) { if (c == "
"
) { n = recv(sock, &c, 1, MSG_PEEK); /* DEBUG printf("%02X
", c); */
if ((n > 0) && (c == "
"
)) recv(sock, &c, 1, 0); else c = "
"
; } buf[i] = c; i++; } else c = "
"
; } buf[i] = ""; return(i);}/**********************************************************************//* 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 *///發送HTTP頭 strcpy(buf, "HTTP/1.0 200 OK
"); send(client, buf, strlen(buf), 0); strcpy(buf, SERVER_STRING); send(client, buf, strlen(buf), 0); sprintf(buf, "Content-Type: text/html
"); send(client, buf, strlen(buf), 0); strcpy(buf, "
"); send(client, buf, strlen(buf), 0);}/**********************************************************************//* Give a client a 404 not found status message. *//**********************************************************************/void not_found(int client){ char buf[1024]; //返回404 sprintf(buf, "HTTP/1.0 404 NOT FOUND
"); send(client, buf, strlen(buf), 0); sprintf(buf, SERVER_STRING); send(client, buf, strlen(buf), 0); sprintf(buf, "Content-Type: text/html
"); send(client, buf, strlen(buf), 0); sprintf(buf, "
"); send(client, buf, strlen(buf), 0); sprintf(buf, "<HTML><TITLE>Not Found</TITLE>
"); send(client, buf, strlen(buf), 0); sprintf(buf, "<BODY><P>The server could not fulfill
"); send(client, buf, strlen(buf), 0); sprintf(buf, "your request because the resource specified
"); send(client, buf, strlen(buf), 0); sprintf(buf, "is unavailable or nonexistent.
"); send(client, buf, strlen(buf), 0); sprintf(buf, "</BODY></HTML>
"); 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] = "";//這個賦值不清楚是幹什麼的 while ((numchars > 0) && strcmp("
", buf)) //將HTTP請求頭讀取並丟棄 numchars = get_line(client, buf, sizeof(buf));//打開文件 resource = fopen(filename, "r"); if (resource == NULL) //如果文件不存在,則返回not_found not_found(client); else { //添加HTTP頭 headers(client, filename); //並發送文件內容 cat(client, resource); } fclose(resource);//關閉文件句柄}/**********************************************************************//* 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;//設置http socket 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) /*動態分配一個埠 */ { int namelen = sizeof(name); if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1) error_die("getsockname"); *port = ntohs(name.sin_port); } //監聽連接 if (listen(httpd, 5) < 0) error_die("listen"); return(httpd);}/**********************************************************************//* Inform the client that the requested web method has not been * implemented. * Parameter: the client socket *//**********************************************************************/void unimplemented(int client){ char buf[1024];//發送501說明相應方法沒有實現 sprintf(buf, "HTTP/1.0 501 Method Not Implemented
"); send(client, buf, strlen(buf), 0); sprintf(buf, SERVER_STRING); send(client, buf, strlen(buf), 0); sprintf(buf, "Content-Type: text/html
"); send(client, buf, strlen(buf), 0); sprintf(buf, "
"); send(client, buf, strlen(buf), 0); sprintf(buf, "<HTML><HEAD><TITLE>Method Not Implemented
"); send(client, buf, strlen(buf), 0); sprintf(buf, "</TITLE></HEAD>
"); send(client, buf, strlen(buf), 0); sprintf(buf, "<BODY><P>HTTP request method not supported.
"); send(client, buf, strlen(buf), 0); sprintf(buf, "</BODY></HTML>
"); send(client, buf, strlen(buf), 0);}/**********************************************************************/int main(void){ int server_sock = -1; u_short port = 0; int client_sock = -1; struct sockaddr_in client_name; int client_name_len = sizeof(client_name); pthread_t newthread;//啟動server socket server_sock = startup(&port); printf("httpd running on port %d
", port); while (1) { //接受客戶端連接 client_sock = accept(server_sock, (struct sockaddr *)&client_name, &client_name_len); if (client_sock == -1) error_die("accept"); /*啟動線程處理新的連接 */ if (pthread_create(&newthread , NULL, accept_request, client_sock) != 0) perror("pthread_create"); }//關閉server socket close(server_sock); return(0);}

不過這個項目並不能直接在Linux上編譯運行。它本來是在solaris上實現的,貌似在socket和pthread的實現上和一般的Linux還是不一樣的,需要修改一部分內容。至於如何修改大家參考這篇文章,我也將修改版上傳到github上了,名稱為tinyhttpd-0.1.0_for_linux,大家可以clone下來,直接make編譯即可。下面演示一下如何運行tinyhttpd,編譯完成的效果如下:

下面運行./httpd,並在瀏覽器中訪問。

tinyhttpd默認cgi腳本是perl腳本,比如color.cgi,位於htdocs目錄下。

#!/usr/bin/perl -Twuse strict;use CGI;my($cgi) = new CGI;print $cgi->header;my($color) = "blue";$color = $cgi->param("color") if defined $cgi->param("color");print $cgi->start_html(-title => uc($color), -BGCOLOR => $color);print $cgi->h1("This is $color");print $cgi->end_html;

下面我想用python來實現cgi腳本,添加一些頁面,為了更加了解cgi程序的運行實質,不用python封裝好的cgi模塊,完全手工打造。首先在htdocs目錄下添加一個register.html頁面,html文檔內容如下:

<html> <head> <title>註冊信息</title> <meta charset="utf-8"> </head> <body> <form action="register.cgi" method="POST"> 賬號:<input type="text" name="zhanghao" value="" size="10" maxlength="5"> <br> <br> 密碼:<input type="password" value="" name="mima" size="10"> <br> <br> <input type="hidden" value="隱藏的內容" name="mihiddenma" size="10"> 愛好:<input type="checkbox" name="tiyu" checked="checked">體育<input type="checkbox" name="changge">唱歌 <br> <br> 性別:<input type="radio" name="sex" checked="checked"><input type="radio" name="sex"><br> <br> 自我介紹:<br> <textarea cols="35" rows="10" name="ziwojieshao"> 這裡是自我介紹 </textarea> <br> <br> 地址: <select name="dizhi"> <option value="sichuan">四川</option> <option value="beijing">北京</option> <option value="shanghai">上海</option> </select> <br> <br> <input type="submit" value="提交"> <input type="reset" value="重置"> </form> </body></html>

這是一個表單,action指向register.cgi,method為post。下面看一下register.cgi,其實是個python腳本。

#!/usr/bin/python#coding:utf-8import sys,oslength = os.getenv("CONTENT_LENGTH")if length: postdata = sys.stdin.read(int(length)) print "Content-type:text/html
" print "<html>" print "<head>" print "<title>POST</title>" print "</head>" print "<body>" print "<h2> POST data </h2>" print "<ul>" for data in postdata.split("&"): print "<li>"+data+"</li>" print "</ul>" print "</body>" print "</html>"else: print "Content-type:text/html
" print "no found"

代碼的意思是從標準輸入中讀取post中的數據,並將顯示數據輸出到標準輸出中,對比一下流程圖,更好理解。下面看一下運行效果。

參考文章:HTTP協議全覽,tinyhttpd在Linux編譯

推薦閱讀:

一步一步教你 https 抓包
淺談HTTP緩存
HTTP為什麼要設計成無狀態的,這樣比有狀態有什麼好處,有哪些協議是典型的有狀態,好處又在哪裡?

TAG:HTTP | Web服务器 |