知名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报文的数据部分,完整的TCP报文由头部和数据部分组成。
而 Socket/网络套接字 这个方案就是操作系统用来承载和处理 TCP 报文的一种机制。
Linux系统库函数里的建立socket,绑定端口和接收报文,就是处理 TCP 三次握手链接及其通讯的过程。(因为TCP是面向连接的一种传输层网络报文
image
也就是说httpd作为服务器,

  1. 其中的 startup(&port); 函数完成了如图的 =>Socket => bind => listen
  2. main函数里调用系统库的accept函数,完成了 =>accept
  3. 多线程的accept_request(&client_sock); 完成了 =>read 和 =>write
  4. main函数里的最后,调用了 =>close

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

posted @ 2021-11-24 23:49  蓝天上的云℡  阅读(808)  评论(0编辑  收藏  举报