TinyHttpd 是一個
Github上好像找不到鏡像了,找個別人上傳的注釋版恰恰夠用
帶注釋的倉庫:https://github.com/0xc9e36/TinyHTTPd
在線閱讀代碼: https://github.dev/0xc9e36/TinyHTTPd
代碼框架:
這玩意有個 p 的框架,照着寫而已
附上大佬的流程圖分析,非常完整
圖源:https://jacktang816.github.io/post/tinyhttpdread/
代碼分析開始 main()
首先,閱讀代碼從找到 main
函數開始
只有這兩個源文件有 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);
}
- 這里是先調用
startup(&port);
得到一個 socket的文件描述符fd號, - 然后再在死循環里遍歷用 系統頭文件<sys/socket.h>里的 accept 函數 接收所有客戶端的 TCP 請求
- 新建一個線程,用於調用
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 知識
URL 理解為獲取資源的路徑
請求
響應
CRLF是換行回車,也就是 "/r/n"
打開抓包軟件 fiddler 4 開啟抓包,然后瀏覽器打開 http://i.baidu.com
(如果你抓不到,那就是瀏覽器有上輩子的記憶所以強制 https 了,自己解決。
通過抓包得到一個 http1.1 的請求報文如下
十六進制 0x0D 0x0A 是文本編碼符 CRLF 也稱作 /r/n,只是表達形式不同
文本編碼之后如下:
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報文的數據部分,完整的TCP報文由頭部和數據部分組成。
而 Socket/網絡套接字 這個方案就是操作系統用來承載和處理 TCP 報文的一種機制。
Linux系統庫函數里的建立socket,綁定端口和接收報文,就是處理 TCP 三次握手鏈接及其通訊的過程。(因為TCP是面向連接的一種傳輸層網絡報文
也就是說httpd作為服務器,
- 其中的
startup(&port);
函數完成了如圖的 =>Socket => bind => listen - main函數里調用系統庫的accept函數,完成了 =>accept
- 多線程的
accept_request(&client_sock);
完成了 =>read 和 =>write - main函數里的最后,調用了
=>close
虛/實線 分別代表 客戶端/服務端 的狀態變遷
宏定義:
宏定義比較少,貼出來免得難找:
#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