知名C開源項目 - TinyHttpd 源碼分析

TinyHttpd 是一個

Github上好像找不到鏡像了,找個別人上傳的註釋版恰恰夠用
帶註釋的倉庫:https://github.com/0xc9e36/TinyHTTPd
在線閱讀代碼: https://github.dev/0xc9e36/TinyHTTPd

代碼框架:

這玩意有個 p 的框架,照着寫而已

附上大佬的流程圖分析,非常完整
image
圖源:https://jacktang816.github.io/post/tinyhttpdread/

代碼分析開始 main()

首先,閱讀代碼從找到 main 函數開始
image
只有這兩個源文件有 main

  • httpd.c 的 main 函數在文件末尾,
  • simpleclient.c 雖然有 main 函數,但內容並不是 http 服務器,而是一個用於測試的客戶端

所以我們只需要看 httpd.c 就行了,其main()函數內容如下


int main(void)
{
	/* 定義socket相關信息 */
    int server_sock = -1;
    u_short port = 4000;
    int client_sock = -1;
    struct sockaddr_in client_name;
    socklen_t  client_name_len = sizeof(client_name);
    pthread_t newthread;

    server_sock = startup(&port);
    printf("httpd running on port %d\n", port);

    while (1)
    { 
		/* 通過accept接受客戶端請求, 阻塞方式 */
        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, (void *)&client_sock) != 0)
            perror("pthread_create");
    }

    close(server_sock);

    return(0);
}

  1. 這裏是先調用 startup(&port); 得到一個 socket的文件描述符fd號,
  2. 然後再在死循環裏遍歷用 系統頭文件<sys/socket.h>裏的 accept 函數 接收所有客戶端的 TCP 請求
  3. 新建一個線程,用於調用 accept_request(&client_sock);處理 HTTP 請求

函數作用概述:

httpd.c 裏的 startup(&port);

作用:用於建立、綁定socket網絡套接字,監聽端口
源碼:

/**********************************************************************/
/* 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 
 * 建立socket, 綁定套接字, 並監聽端口
 * */
 
/**********************************************************************/
int startup(u_short *port)
{
    int httpd = 0;
    int on = 1;
    struct sockaddr_in name;

	/* 建立套接字, 一條通信的線路 */
    httpd = socket(PF_INET, SOCK_STREAM, 0);
    if (httpd == -1)
        error_die("socket");
    memset(&name, 0, sizeof(name));				//0填充, struct sockaddr_in +實際多出來sin_zero沒有用處.
    name.sin_family = AF_INET;					//IPV4協議
    name.sin_port = htons(*port);				//主機字節序轉網絡字節序
    name.sin_addr.s_addr = htonl(INADDR_ANY);	//監聽任意IP

	/* 允許本地地址與套接字重複綁定 , 也就是說在TCP關閉連接處於TIME_OUT狀態時重用socket */
    if ((setsockopt(httpd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) < 0)  
    {  
        error_die("setsockopt failed");
    }

	/* 用於socket信息與套接字綁定 */
    if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
        error_die("bind");

	/* 未設置端口則隨機生成 */
	if (*port == 0)  /* if dynamically allocating a port */
    {
        socklen_t 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);
}

系統庫 <sys/socket.h> 裏的 accept 函數

作用:用於接收 TCP 套接字內容,並返回一個fd文件描述符用於處理。
fd類似於文件的ID號,可以直接索引到文件實體。詳情搜索 Linux 文件描述符 fd
源碼:請看 sys/socket.h 及其對應的 .c 源碼文件

httpd.c 裏的accept_request(&client_sock);

作用:根據 HTTP 請求報文,返回對應的 HTTP 響應內容。也就是 [request] ==> 該函數 ==> [response]。這個就是實現核心功能的函數,下文重點分析這個函數
源碼:


/**********************************************************************/
/* 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(void *arg)
{
    int client = *(int*)arg;
    char buf[1024];
    size_t 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;
	
	/* 獲取請求行, 返回字節數  eg: GET /index.html HTTP/1.1 */
    numchars = get_line(client, buf, sizeof(buf));
	/* debug */
	//printf("%s", buf);

	/* 獲取請求方式, 保存在method中  GET或POST */
	i = 0; j = 0;
    while (!ISspace(buf[i]) && (i < sizeof(method) - 1))
    {
        method[i] = buf[i];
        i++;
    }
    j=i;
    method[i] = '\0';

	/* 只支持GET 和 POST 方法 */
    if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
    {
        unimplemented(client);
        return NULL;
    }

	/* 如果支持POST方法, 開啓cgi */
    if (strcasecmp(method, "POST") == 0)
        cgi = 1;

    i = 0;
    while (ISspace(buf[j]) && (j < numchars))
        j++;
    while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < numchars))
    {
        url[i] = buf[j];
        i++; j++;
    }
	/* 保存請求的url, url上的參數也會保存 */
    url[i] = '\0';

	//printf("%s\n", url);

    if (strcasecmp(method, "GET") == 0)
    {
		/* query_string 保存請求參數 index.php?r=param  問號後面的 r=param */
        query_string = url;
        while ((*query_string != '?') && (*query_string != '\0'))
            query_string++;
		/* 如果有?表明是動態請求, 開啓cgi */
        if (*query_string == '?')
        {
            cgi = 1;
            *query_string = '\0';
            query_string++;
        }
    }

//	printf("%s\n", query_string);

	/* 根目錄在 htdocs 下, 默認訪問當前請求下的index.html*/
    sprintf(path, "htdocs%s", url);
    if (path[strlen(path) - 1] == '/')
        strcat(path, "index.html");

	//printf("%s\n", path);
	/* 找到文件, 保存在結構體st中*/
    if (stat(path, &st) == -1) {
		/* 文件未找到, 丟棄所有http請求頭信息 */
        while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
            numchars = get_line(client, buf, sizeof(buf));
        /* 404 no found */
		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
			/* 執行cgi 程序*/
            execute_cgi(client, path, method, query_string);
    }

    close(client);
	return NULL;
}

HTTP 1.1 知識

image

URL 理解爲獲取資源的路徑
image

請求
image

響應
image

CRLF是換行回車,也就是 "/r/n"

打開抓包軟件 fiddler 4 開啓抓包,然後瀏覽器打開 http://i.baidu.com
(如果你抓不到,那就是瀏覽器有上輩子的記憶所以強制 https 了,自己解決。
通過抓包得到一個 http1.1 的請求報文如下
image
十六進制 0x0D 0x0A 是文本編碼符 CRLF 也稱作 /r/n,只是表達形式不同
image

文本編碼之後如下:

HTTP1.1 請求報文

GET http://i.baidu.com/ HTTP/1.1
Host: i.baidu.com
Connection: keep-alive
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36 Edg/96.0.1054.29
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: BIDUPSID=6B6952070E87E7C0EAB663B0163DDECA; PSTM=1601699891; BDUSS=lpV0NWZEotcE1sM25jdTdrcVI5ZENRVHZqdTI3MHFobWh6NWNXQjdHRVpzYlZnRVFBQUFBJCQAAAAAAAAAAAEAAAD7lAszcXE1NDU0NzUxOTcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkkjmAZJI5gbk; delPer=0; PSINO=7; BDRCVFR[feWj1Vr5u3D]=I67x6TjHwwYf0; BDRCVFR[w2jhEs_Zudc]=I67x6TjHwwYf0; BDRCVFR[dG2JNJb_ajR]=mk3SLVN4HKm; BDRCVFR[-pGxjrCMryR]=mk3SLVN4HKm; BDRCVFR[tox4WRQ4-Km]=mk3SLVN4HKm; BDRCVFR[CLK3Lyfkr9D]=mk3SLVN4HKm; BAIDUID=694803A885DBD2A906D317FACD3639A0:FG=1; BCLID=11160404728357039722; BDSFRCVID=KPIOJexroG01HljHuCEgboWgxUzqrMnTDYLEOwXPsp3LGJLVgn8sEG0Ptto7dU-MO2W5ogKK3gOTH4DF_2uxOjjg8UtVJeC6EG0Ptf8g0M5; H_BDCLCKID_SF=JRKtoD0MtKvDqTrP-trf5DCShUFs3lTCB2Q-XPoO3K8WDlK6bfQhhP_VhNJP3triWbRM2MbgylRM8P3y0bb2DUA1y4vpK-onLmTxoUJ25qAhj4nDqqQfXfPebPRiJ-b9Qg-JKpQ7tt5W8ncFbT7l5hKpbt-q0x-jLTnhVn0MBCK0hI_xj6K-j6vM-UIL24cObTryWjrJabC3HRO3XU6qLT5Xht77qT5y5Rn3obbaapTHsDLm548hjq0njlLHQbjMKJREQPQJLR7BMf7s2xonDh8yXH7MJUntKJciWprO5hvvhn3O3MAMQMKmDloOW-TB5bbPLUQF5l8-sq0x0bOte-bQbG_EJ50DJR4eoK-QKt8_HRjYbb__-P4DePRTBxRZ56bHWh0MtlnSjlnGLUoJ-44ibp7PhJoHBConKUT13l7boMJRK5bdQUIT3xJXqnJ43bRTLPbOJbblKq6ODlthhP-UyNbMWh37JgnlMKoaMp78jR093JO4y4Ldj4oxJpOJ5JbMopCafJOKHICGj5AhDfK; ZD_ENTRY=baidu; H_PS_PSSID=35266_35105_31253_35239_35048_34584_34518_34532_35245_34872_26350_35115_35128; BA_HECTOR=00agag8k2h20a52h5b1gpso8k0r


HTTP1.1 響應報文

HTTP/1.1 301 Moved Permanently
Location: https://www.baidu.com/my/index?f=ibaidu
Date: Wed, 24 Nov 2021 15:58:35 GMT
Content-Length: 74
Content-Type: text/html; charset=utf-8

<a href="https://www.baidu.com/my/index?f=ibaidu">Moved Permanently</a>.


也就是說,TCP報文由頭部和數據部分組成。
這些HTTP報文就是TCP報文的數據部分。而 Socket/網絡套接字 這個方案就是操作系統用來承載和處理 TCP 報文的一種機制。
Linux庫函數的建立,綁定和接收,就是處理 TCP 三次握手鍊接及其通訊的過程。(因爲TCP是面向連接的一種傳輸層網絡報文
image
image
虛/實線 分別代表 客戶端/服務端 的狀態變遷

宏定義:

宏定義比較少,貼出來免得難找:

#define ISspace(x) isspace((int)(x))

#define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n"
#define STDIN   0
#define STDOUT  1
#define STDERR  2

原版的頭部註釋

/* 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.
 */

其他資源

應用的認證和授權(基本認證、session-cookie認證、token認證及OAuth2.0授權) https://blog.csdn.net/qq_32252957/article/details/89180882

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