• 微信公众号:美女很有趣。 工作之余,放松一下,关注即送10G+美女照片!

知名C开源项目 – TinyHttpd 源码分析

开发技术 开发技术 3小时前 2次浏览

TinyHttpd 是一个

Github上好像找不到镜像了,找个别人上传的注释版恰恰够用
带注释的仓库:https://github.com/0xc9e36/TinyHTTPd
在线阅读代码: https://github.dev/0xc9e36/TinyHTTPd

代码框架:

这玩意有个 p 的框架,照着写而已

附上大佬的流程图分析,非常完整
知名C开源项目 - TinyHttpd 源码分析
图源:https://jacktang816.github.io/post/tinyhttpdread/

代码分析开始 main()

首先,阅读代码从找到 main 函数开始
知名C开源项目 - TinyHttpd 源码分析
只有这两个源文件有 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 %dn", 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] = '';

	/* 只支持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] = '';

	//printf("%sn", url);

    if (strcasecmp(method, "GET") == 0)
    {
		/* query_string 保存请求参数 index.php?r=param  问号后面的 r=param */
        query_string = url;
        while ((*query_string != '?') && (*query_string != ''))
            query_string++;
		/* 如果有?表明是动态请求, 开启cgi */
        if (*query_string == '?')
        {
            cgi = 1;
            *query_string = '';
            query_string++;
        }
    }

//	printf("%sn", query_string);

	/* 根目录在 htdocs 下, 默认访问当前请求下的index.html*/
    sprintf(path, "htdocs%s", url);
    if (path[strlen(path) - 1] == '/')
        strcat(path, "index.html");

	//printf("%sn", 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 知识

知名C开源项目 - TinyHttpd 源码分析

URL 理解为获取资源的路径
知名C开源项目 - TinyHttpd 源码分析

请求
知名C开源项目 - TinyHttpd 源码分析

响应
知名C开源项目 - TinyHttpd 源码分析

CRLF是换行回车,也就是 “/r/n”

通过抓包得到一个 http1.1 的请求报文如下
知名C开源项目 - TinyHttpd 源码分析

其中十六进制的 0x0D 0x0A 是 /r/n
文本编码之后是这样的:

GET http://127.0.0.1:12342/script HTTP/1.1
Host: 127.0.0.1:12342
Connection: keep-alive
Accept: application/json, text/plain, */*
Authorization: Bearer
User-Agent: ClashforWindows/0.18.8
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US


宏定义:

宏定义比较少,贴出来免得难找:

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

#define SERVER_STRING "Server: jdbhttpd/0.1.0rn"
#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


程序员灯塔
转载请注明原文链接:知名C开源项目 – TinyHttpd 源码分析
喜欢 (0)