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

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

一.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是否為空格字符,
//也就是判斷是否為空格(' ')、定位字符(' \t ')、CR(' \r ')、換行(' \n ')、垂直定位字符(' \v ')或翻頁(' \f ')的情況。
//返回值:若參數c 為空白字符,則返回非 0,否則返回 0。#define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n"//定義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還是POSTmethod[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和POSTunimplemented(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-URLwhile (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf))){url[i] = buf[j];i++; j++;}url[i] = '\0';
//如果是GET請求,url可能會帶有?,有查詢參數if (strcasecmp(method, "GET") == 0){query_string = url;while ((*query_string != '?') && (*query_string != '\0'))query_string++;if (*query_string == '?'){//如果帶有查詢參數,需要執行cgi,解析參數,設置標志位為1cgi = 1;//將解析參數截取下來*query_string = '\0';query_string++;}}
//以上已經將起始行解析完畢
//url中的路徑格式化到pathsprintf(path, "htdocs%s", url);
//學習到這里明天繼續TODO
//如果path只是一個目錄,默認設置為首頁index.htmlif (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("\n", 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];
//發送400sprintf(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);
}/**********************************************************************/
/* 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];
//發送500sprintf(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);
}/**********************************************************************/
/* 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] = '\0';if (strcasecmp(method, "GET") == 0)//如果是GET請求//讀取并且丟棄頭信息while ((numchars > 0) && strcmp("\n", buf)) numchars = get_line(client, buf, sizeof(buf));else    {//處理的請求為POSTnumchars = get_line(client, buf, sizeof(buf));while ((numchars > 0) && strcmp("\n", buf)){//循環讀取頭信息找到Content-Length字段的值buf[15] = '\0';//目的是為了截取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;}}
//返回正確響應碼200sprintf(buf, "HTTP/1.0 200 OK\r\n");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_METHODsprintf(meth_env, "REQUEST_METHOD=%s", method);putenv(meth_env);if (strcasecmp(method, "GET") == 0) {//存儲QUERY_STRINGsprintf(query_env, "QUERY_STRING=%s", query_string);putenv(query_env);}else {   /* POST *///存儲CONTENT_LENGTHsprintf(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 = '\0';int n;while ((i < size - 1) && (c != '\n')){n = recv(sock, &c, 1, 0);/* DEBUG printf("%02X\n", c); */if (n > 0){if (c == '\r'){n = recv(sock, &c, 1, MSG_PEEK);/* DEBUG printf("%02X\n", c); */if ((n > 0) && (c == '\n'))recv(sock, &c, 1, 0);elsec = '\n';}buf[i] = c;i++;}elsec = '\n';}buf[i] = '\0';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\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);
}/**********************************************************************/
/* Give a client a 404 not found status message. */
/**********************************************************************/
void not_found(int client)
{char buf[1024];//返回404sprintf(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);
}/**********************************************************************/
/* 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)) //將HTTP請求頭讀取并丟棄numchars = get_line(client, buf, sizeof(buf));
//打開文件resource = fopen(filename, "r");if (resource == NULL)//如果文件不存在,則返回not_foundnot_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 sockethttpd = 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\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><HEAD><TITLE>Method Not Implemented\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, "</TITLE></HEAD>\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, "<BODY><P>HTTP request method not supported.\r\n");send(client, buf, strlen(buf), 0);sprintf(buf, "</BODY></HTML>\r\n");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 socketserver_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");/*啟動線程處理新的連接 */if (pthread_create(&newthread , NULL, accept_request, client_sock) != 0)perror("pthread_create");}
//關閉server socketclose(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-8
import sys,os
length = os.getenv('CONTENT_LENGTH')if length:postdata = sys.stdin.read(int(length))print "Content-type:text/html\n"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\n"print 'no found'

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

?


今天的分享就到這里,下一篇繼續分析。如果大家覺得還可以呀,記得推薦呦。

參考文章:HTTP協議全覽,tinyhttpd在Linux編譯
歡迎大家支持我公眾號:

本文章屬于原創作品,歡迎大家轉載分享。尊重原創,轉載請注明來自:七夜的故事?http://www.cnblogs.com/qiyeboy/

轉載于:https://www.cnblogs.com/qiyeboy/p/6296387.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/394128.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/394128.shtml
英文地址,請注明出處:http://en.pswp.cn/news/394128.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

monthdiff oracle_Oracle計算時間差函數

1、months_between(date1,date2) 返回兩個日期之間的月份的差值(1)、如果兩個日期月份內天數相同&#xff0c;或者都是某個月的最后一天&#xff0c;返回一個整數。否則,返回數值帶小數select months_between(sysdate,addtime)as diff_month from test62、interval 時間間隔…

洛谷——P1290 歐幾里德的游戲

P1290 歐幾里德的游戲 題目描述 歐幾里德的兩個后代Stan和Ollie正在玩一種數字游戲&#xff0c;這個游戲是他們的祖先歐幾里德發明的。給定兩個正整數M和N&#xff0c;從Stan開始&#xff0c;從其中較大的一個數&#xff0c;減去較小的數的正整數倍&#xff0c;當然&#xff0c…

passport身份驗證_了解如何使用Passport.js處理Node身份驗證

passport身份驗證by Antonio Erdeljac通過安東尼奧埃爾德雅克 了解如何使用Passport.js處理Node身份驗證 (Learn how to handle authentication with Node using Passport.js) Support me by reading it from its original source: ORIGINAL SOURCE通過閱讀原始來源為我提供支…

leetcode1448. 統計二叉樹中好節點的數目(dfs)

給你一棵根為 root 的二叉樹&#xff0c;請你返回二叉樹中好節點的數目。 「好節點」X 定義為&#xff1a;從根到該節點 X 所經過的節點中&#xff0c;沒有任何節點的值大于 X 的值。 代碼 /*** Definition for a binary tree node.* public class TreeNode {* int val;…

I/O模型系列之四:兩種高性能IO設計模式 Reactor 和 Proactor

不同的操作系統實現的io策略可能不一樣&#xff0c;即使是同一個操作系統也可能存在多重io策略&#xff0c;常見如linux上的select&#xff0c;poll&#xff0c;epoll&#xff0c;面對這么多不同類型的io接口&#xff0c;這里需要一層抽象api來完成&#xff0c;所以就演變出來兩…

python中序列類型和數組之間的區別_「Python」序列構成的數組

一、Python 標準庫的序列類型分為&#xff1a;容器序列&#xff1a;能夠存放不同類型數據的序列(list、tuple、collections.deque)。扁平序列&#xff1a;只能容納一種類型的數據(str、bytes、bytearray 和 array.array)。其中&#xff0c;容器序列存放的是它們所包含的任意類型…

如何使用EF Core在Blazor中創建級聯的DropDownList

介紹 (Introduction) In this article, we are going to create a cascading dropdown list in Blazor using Entity Framework Core database first approach. We will create two dropdown lists — Country and City. Upon selecting the value from the country dropdown, …

gcc/g++命令

參考&#xff1a;http://www.cnblogs.com/cryinstall/archive/2011/09/27/2280824.html 注意&#xff1a;gcc和g是linux系統下的編程常用指令&#xff0c;C語言文件用gcc&#xff0c;cpp文件用g。 1.預處理 g -E filename.cpp > filename.i 功能&#xff1a;輸出預處理后的…

計算機存儲

位&#xff08;bit&#xff09;&#xff1a;一個數字0或一個數字1&#xff0c;代表一位 字節&#xff08;Byte&#xff09;&#xff1a;每逢8位是一個字節&#xff0c;是數據存儲的最小單位 1Byte 8 bit 平時所說的網速&#xff1a; 100Mbps實際上是以位&#xff08;b&#xf…

leetcode113. 路徑總和 II(dfs)

給定一個二叉樹和一個目標和&#xff0c;找到所有從根節點到葉子節點路徑總和等于給定目標和的路徑。說明: 葉子節點是指沒有子節點的節點。示例: 給定如下二叉樹&#xff0c;以及目標和 sum 22&#xff0c;5/ \4 8/ / \11 13 4/ \ / \7 2 5 1 返回:[[5,4,11,…

java forward 修改請求參數_聊聊springboot session timeout參數設置

序本文主要介紹下spring boot中對session timeout參數值的設置過程。ServerPropertiesspring-boot-autoconfigure-1.5.8.RELEASE-sources.jar!/org/springframework/boot/autoconfigure/web/ServerProperties.javaOverridepublic void customize(ConfigurableEmbeddedServletCo…

javascript控制臺_如何使用JavaScript控制臺改善工作流程

javascript控制臺by Riccardo Canella里卡多卡內拉(Riccardo Canella) 如何使用JavaScript控制臺改善工作流程 (How you can improve your workflow using the JavaScript console) As a web developer, you know very well the need to debug your code. We often use extern…

appium===setup/setupclass的區別,以及@classmathod的使用方法

一、裝飾器 1.用setUp與setUpClass區別 setup():每個測試case運行前運行 teardown():每個測試case運行完后執行 setUpClass():必須使用classmethod 裝飾器,所有case運行前只運行一次 tearDownClass():必須使用classmethod裝飾器,所有case運行完后只運行一次 2.是修飾符&#xf…

cache failed module status_Flutter混編之路——iOS踩坑記錄

一、運行Xcode編譯或者flutter run/build 過程中報錯&#xff1a;"x86_64" is not an allowed value for option "ios-arch".解決方案在Debug.xcconfig中指定 “FLUTTER_BUILD_MODEdebug”&#xff0c;Release.xcconfig中指定“FLUTTER_BUILD_MODErelease”…

【最短路徑Floyd算法詳解推導過程】看完這篇,你還能不懂Floyd算法?還不會?...

簡介 Floyd-Warshall算法&#xff08;Floyd-Warshall algorithm&#xff09;&#xff0c;是一種利用動態規劃的思想尋找給定的加權圖中多源點之間最短路徑的算法&#xff0c;與Dijkstra算法類似。該算法名稱以創始人之一、1978年圖靈獎獲得者、斯坦福大學計算機科學系教授羅伯特…

java object類的常用子類_Java中Object類常用的12個方法,你用過幾個?

前言Java 中的 Object 方法在面試中是一個非常高頻的點&#xff0c;畢竟 Object 是所有類的“老祖宗”。Java 中所有的類都有一個共同的祖先 Object 類&#xff0c;子類都會繼承所有 Object 類中的 public 方法。先看下 Object 的類結構(快捷鍵&#xff1a;alt7)&#xff1a;1.…

leetcode面試題 04.12. 求和路徑(dfs)

給定一棵二叉樹&#xff0c;其中每個節點都含有一個整數數值(該值或正或負)。設計一個算法&#xff0c;打印節點數值總和等于某個給定值的所有路徑的數量。注意&#xff0c;路徑不一定非得從二叉樹的根節點或葉節點開始或結束&#xff0c;但是其方向必須向下(只能從父節點指向子…

javaweb學習總結(二十二)——基于Servlet+JSP+JavaBean開發模式的用戶登錄注冊

一、ServletJSPJavaBean開發模式(MVC)介紹 ServletJSPJavaBean模式(MVC)適合開發復雜的web應用&#xff0c;在這種模式下&#xff0c;servlet負責處理用戶請求&#xff0c;jsp負責數據顯示&#xff0c;javabean負責封裝數據。 ServletJSPJavaBean模式程序各個模塊之間層次清晰&…

2018黃河獎設計大賽獲獎_宣布我們的freeCodeCamp 2018杰出貢獻者獎獲獎者

2018黃河獎設計大賽獲獎by Quincy Larson昆西拉爾森(Quincy Larson) 宣布我們的freeCodeCamp 2018杰出貢獻者獎獲獎者 (Announcing Our freeCodeCamp 2018 Top Contributor Award Winners) Over the past 3 years, freeCodeCamp.org has grown from a small open source proje…

Log4j配置詳解

來自: http://www.blogjava.net/zJun/archive/2006/06/28/55511.html Log4J的配置文件(Configuration File)就是用來設置記錄器的級別、存放器和布局的&#xff0c;它可接keyvalue格式的設置或xml格式的設置信息。通過配置&#xff0c;可以創建出Log4J的運行環境。1. 配置文件 …