知名C开源项目 - TinyHttpd 源码分析
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