TinyHttpd分析
2022-09-13 16:16:03

TinyHttpd分析

HTTP协议请求格式

标准POST请求格式:

GET / HTTP/1.1
Host: 192.168.0.23:47310
Connection: keep-alive
...

标准GET请求格式:

POST / color1.cgi HTTP / 1.1
Host: 192.168.0.23 : 47310
Connection : keep - alive
Content - Length : 10
...
Form Data
color=gray

TinyHttpd源码分析

源码地址:

https://github.com/EZLippi/Tinyhttpd

首先是TinyHttpd包含的函数:

void accept_request(void *);//处理从套接字上监听到的一个HTTP请求
void bad_request(int);//返回给客户端这是一个错误请求,400响应码
void cat(int, FILE *);//读取服务器上某个文件写到socket套接字
void cannot_execute(int);//处理发生在执行cgi程序时出现的错误
void error_die(const char *);//把错误信息写到 perror  perror(用来将上一个函数发生错误的原因输出到标准设备stderr)
void execute_cgi(int, const char *, const char *, const char *);//运行cgi脚本
int get_line(int, char *, int);//读取一行HTTP报文
void headers(int, const char *);//返回HTTP响应头
void not_found(int);//返回找不到请求文件
void serve_file(int, const char *);//调用cat将服务器文件内容返回给浏览器
int startup(u_short *);//开启http服务,绑定端口,监听,开启线程处理链接
void unimplemented(int);//返回给浏览器  表明收到的HTTP请求所用的method(方法)不被支持

main

套接字:socket,对网络中不同主机上的应用进程之间进行双向通信的端点的抽象

int main(void)
{
    int server_sock = -1;  //定义服务器socket的描述符
    u_short port = 4000;   //定义监听端口
    int client_sock = -1;  //定义客户端socket的描述符
    //定义了一个结构体
    //sockaddr_in是ipv4的套接字地址结构
    struct sockaddr_in client_name;   
    socklen_t  client_name_len = sizeof(client_name);//客户端地址长度
    pthread_t newthread;    //定义线程id

    server_sock = startup(&port);   //初始化服务器
    printf("httpd running on port %d\n", port);  //控制台打印端口

    //循环创建链接和子线程
    //该循环的作用就是提供服务,等待与客户端建立链接
    while (1)  
    {
        //通过accept接收客户端请求
        //阻塞,等待客户端连接
        //&client_name是客户端的地址信息
        //client_name_len是客户端地址长度
        client_sock = accept(server_sock,
                (struct sockaddr *)&client_name,
                &client_name_len);

        /*
        int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
        accept函数接收了一个套接字的文件描述符,以及ipv4地址结构,返回一个新的套接字
        通过该套接字是实现与客户端通信
        注意点:
        1、该函数若没有connect请求,函数调用会被阻塞,直到接收到connect请求
        2、在与客户端的套接字进行连接后,accept会创建一个新的套接字,并使用新的套接字与客户端进行连接
        ,原始的套接字并不会关闭,依然打开,并可以使用于监听端口
        */

        if (client_sock == -1)
            error_die("accept");

        /* accept_request(&client_sock); */
        //如果有请求,就通过pthread_create建立一个新的线程
        //在新线程中调用accept_request函数,处理请求,实现多线程同步服务(提高效率)
        //&newthread是线程ID,NULL是线程属性
        //accept_request是线程要执行的函数
        //client_sock强制转换成void *
        if (pthread_create(&newthread , NULL, (void *)accept_request, (void *)(intptr_t)client_sock) != 0)
            perror("pthread_create");
    }

    close(server_sock);

    return(0);
}

main函数处理流程:startup初始化一个和4000端口绑定的套接字==》进入循环,accept接收客户端请求==》通过accept_create创建线程,调用accept_request处理请求

startup

int startup(u_short *port)
{
    int httpd = 0;  //定义服务器的socket描述符
    int on = 1;  
    struct sockaddr_in name;  //服务器端的IP地址

    httpd = socket(PF_INET, SOCK_STREAM, 0);  //创建一个套接口(服务器端的socket)

    if (httpd == -1)
        error_die("socket");

    memset(&name, 0, sizeof(name));//结构体初始化为0
    //结构体的三个成员变量
    name.sin_family = AF_INET;  //地址类型指定为ipv4
    name.sin_port = htons(*port);  //传入的端口,转化为网络字序节(大端)
    name.sin_addr.s_addr = htonl(INADDR_ANY);  //本机可用的ip地址

    /*
    int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
    setsockopt:设置任意类型,任意状态套接口的选项值
    第一个参数为套接字描述符
    第二个参数为设置的选项的级别,若想在套接字上设置选项,则必须设置为SOL_SOCKET
    第三个参数为要设置的选项
    第四个参数为指针,指向存放选项待设置的新值的缓冲区
    第五个参数为缓冲区长度
    */
    //此处是将套接字设置为SO_REUSEADDR选项,实现允许复用本地地址和端口
    if ((setsockopt(httpd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) < 0)  
    {  
        error_die("setsockopt failed");
    }

    //bind:将本地地址与一个套接口进行绑定
    //若传入的name中的成员变量sin_port为0,此时系统会选择一个临时端口
    if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
        error_die("bind");
    //若在bind后端口依旧为0
    if (*port == 0)  /* if dynamically allocating a port */
    {
        socklen_t namelen = sizeof(name);
        //调用getsockname()获取系统给httpd随机分配端口号
        if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
            error_die("getsockname");
        *port = ntohs(name.sin_port);
    }
    //调用listen函数监听request
    //第二个参数代表请求队列的长度
    //请求队列:当套接字在处理客户端请求时,若有新的请求进来,套接字无法处理,会将它放入缓冲区
    //当当前请求处理完毕后再从缓冲区中读取并处理
    //若有不断的新的请求进来,就将按照先后顺序在缓冲区中排队,直到缓冲区满
    //该缓冲区称为请求队列,请求队列满时不再接收新的请求
    if (listen(httpd, 5) < 0)
        error_die("listen");
    return(httpd);
}

startup函数流程:调用socket创建一个套接字==》创建sockaddr_in结构体,设置对应的IP和端口==》调用bind绑定套接字的IP和端口==》调用listen监听请求

sockaddr_in结构体

struct in_addr {                /* IPv4 4-byte address */
    in_addr_t s_addr;           /* Unsigned 32-bit integer */
};

struct sockaddr_in {            /* IPv4 socket address */
    sa_family_t    sin_family;  /* Address family (AF_INET) */
    in_port_t      sin_port;    /* Port number */
    struct in_addr sin_addr;    /* IPv4 address */
    unsigned char  __pad[X];    /* Pad to size of 'sockaddr'
};                                 structure (16 bytes) */

在in_addr中保存了以32bit的ipv4地址==》使用htonl函数将ip地址从字符串转换为整型

accept_request

void accept_request(void *arg)
{
    int client = (intptr_t)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;

    //读取http请求的第一行数据
    //将请求方法放在method中
    numchars = get_line(client, buf, sizeof(buf));
    
    i = 0; j = 0;
    //循环条件:
    //判断第i个字符是否是空格
    //判断是否超过method缓冲大小
    //-1是为了在最后添加一个'\0'作为标识符
    while (!ISspace(buf[i]) && (i < sizeof(method) - 1))
    {
        method[i] = buf[i];//若不是空格,复制到method中
        i++;
    }
    j=i;
    method[i] = '\0';//添加'\0'标识符

    //判断其中是否是GET方法或者POST方法
    if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
    {
        unimplemented(client);//都不是的话调用501错误处理
        return;
    }

    //若是POST方法,cgi设置为1
    if (strcasecmp(method, "POST") == 0)
        cgi = 1;

    i = 0;
    while (ISspace(buf[j]) && (j < numchars))
        j++;
    //此处循环作用为读取URL,与读取请求方法一样
    while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < numchars))
    {
        url[i] = buf[j];
        i++; j++;
    }
    url[i] = '\0';//添加'\0'标识符

    //若是GET方法
    if (strcasecmp(method, "GET") == 0)
    {
        query_string = url;
        //GET方法在URL后有'?',若找到'?',说明是GET
        while ((*query_string != '?') && (*query_string != '\0'))
            query_string++;
        //在GET请求中,'?'后面为参数
        if (*query_string == '?')
        {
            cgi = 1;  //CGI设置为1
            *query_string = '\0';
            query_string++;
        }
    }

    //与'htdocs'(根目录)拼接
    sprintf(path, "htdocs%s", url);
    //如果最后一个字符是'/'
    if (path[strlen(path) - 1] == '/')
        strcat(path, "index.html");//在后面加上 index.html
    
    //检查拼接后的文件是否存在,-1为不存在
    //stat函数:确认路径对应的文件状态
    if (stat(path, &st) == -1) {
        //读取并丢弃headers
        while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
            numchars = get_line(client, buf, sizeof(buf));
        not_found(client); //不存在文件返回404
    }
    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
            cgi = 1;
        if (!cgi)
            serve_file(client, path); //cgi为0,不需要调用cgi,相当于请求页面
        else
            //cgi为1,调用cgi,执行cgi程序
            //参数:
            //client:描述符
            //path:路径
            //method:请求方法
            //query_string:判断是否有?以使用GET请求发送数据
            execute_cgi(client, path, method, query_string);
    }

    close(client);//断开与客户端连接
}

accept_request函数流程:提取request类型(GET或者是POST)==》

如果是POST==》开启cgi

如果是GET==》提取URL以及URL中的参数

==》如果不是CGI,调用serve_file,返回地址中内容(页面)

==》如果是CGI,调用execute_cgi,执行CGI脚本程序

execute_cgi

CGI执行函数

void execute_cgi(int client, const char *path,
        const char *method, const char *query_string)
{
    char buf[1024];
    int cgi_output[2];
    int cgi_input[2];
    pid_t pid;
    int status;
    int i;
    char c;
    int numchars = 1;
    int content_length = -1;

    buf[0] = 'A'; buf[1] = '\0';
    //判断是不是GET,若是,丢弃headers
    if (strcasecmp(method, "GET") == 0)
        while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
            //丢弃
            numchars = get_line(client, buf, sizeof(buf));
    else if (strcasecmp(method, "POST") == 0) /*POST*/
    {
        numchars = get_line(client, buf, sizeof(buf));
        while ((numchars > 0) && strcmp("\n", buf))
        {
            buf[15] = '\0';
            //比较前15个字符是否为"Content-Length:"
            if (strcasecmp(buf, "Content-Length:") == 0)
                content_length = atoi(&(buf[16])); //记录Content-Length,int型
            numchars = get_line(client, buf, sizeof(buf));
        }
        if (content_length == -1) {  //如果不等于
            bad_request(client);  //错误处理
            return;
        }
    }
    else/*HEAD or other*/
    {
    }

    //管道:为了在子线程中的cgi和服务器调用cgi程序进程间通信
    //创建管道:子线程向服务器端写
    if (pipe(cgi_output) < 0) {
        cannot_execute(client);//创建失败
        return;
    }
    //创建管道:子线程向服务器端读
    if (pipe(cgi_input) < 0) {
        cannot_execute(client);//创建失败
        return;
    }
    //管道创建成功后创建子线程
    if ( (pid = fork()) < 0 ) {
        cannot_execute(client);//创建失败
        return;
    }
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    //发送200状态码
    send(client, buf, strlen(buf), 0);
    //PID为0,说明fork 
    //子进程用于执行CGI
    if (pid == 0)  /* child: CGI script */
    {
        char meth_env[255];
        char query_env[255];
        char length_env[255];

        //将子线程的stdout重定向至cgi_output管道,1:写端
        dup2(cgi_output[1], STDOUT);
        //将子线程的stdin重定向至cgi_input管道,0:读端
        dup2(cgi_input[0], STDIN);
        //关闭不需要的另一端
        close(cgi_output[0]);
        close(cgi_input[1]);
        //拼接
        sprintf(meth_env, "REQUEST_METHOD=%s", method);
        //设置环境变量
        putenv(meth_env);
        if (strcasecmp(method, "GET") == 0) {
            sprintf(query_env, "QUERY_STRING=%s", query_string);
            //设置环境变量
            putenv(query_env);
        }
        else {   /* POST */
            sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
            //设置环境变量
            putenv(length_env);
        }
        execl(path, NULL);//执行可执行程序
        exit(0);
    
    //父进程
    } else {    /* parent */
        //关闭不需要的管道端口
        close(cgi_output[1]);//关闭管道写端
        close(cgi_input[0]);//关闭管道读端
        //通过管道与子进程进行通信
        if (strcasecmp(method, "POST") == 0)
            //根据content_length信息读取客户端信息
            for (i = 0; i < content_length; i++) {
                recv(client, &c, 1, 0);//读取
                write(cgi_input[1], &c, 1);//通过cgi_input传入子进程的标准输出
            }
        //通过cgi_output,获取子进程的标准输出
        while (read(cgi_output[0], &c, 1) > 0)
            send(client, &c, 1, 0);//将从输出管道中读入执行结果发送给客户端
        //关闭管道端口
        close(cgi_output[0]);
        close(cgi_input[1]);
        //等待子进程结束,退出程序
        waitpid(pid, &status, 0);
    }
}

execute_cgi函数执行流程:

对于GET请求==》丢弃

对于POST请求==》提取content_length信息==》创建两个管道用以通信,创建子进程==》

==》子进程==》调用dup2将标准输入与标准输出分别重定向到对应管道的读端和写端==》设置环境变量,调用execl,执行CGI脚本

==》父进程==》通过管道向CGI脚本传入参数,获取脚本返回结果==》将结果返回给客户端

管道通信原理

使用pipe函数得到两个文件描述符,分别对应管道的读端(filedes[0])和写端(filedes[1]),当程序在写端写入数据时,在读端可以读取到写入的数据

==》通过fork创建子进程==》父进程与子进程拥有相同的变量==》

子进程也有对应管道的读端和写端两个文件描述符==》

再关闭一侧的读端和另外一侧的写端==》实现进程间的通信

剩余函数

//400错误
void bad_request(int client)
{
    char buf[1024];

    sprintf(buf, "HTTP/1.0 400 BAD REQUEST\r\n");
    send(client, buf, sizeof(buf), 0);
    sprintf(buf, "Content-type: text/html\r\n");
    send(client, buf, sizeof(buf), 0);
    sprintf(buf, "\r\n");
    send(client, buf, sizeof(buf), 0);
    sprintf(buf, "<P>Your browser sent a bad request, ");
    send(client, buf, sizeof(buf), 0);
    sprintf(buf, "such as a POST without a Content-Length.\r\n");
    send(client, buf, sizeof(buf), 0);
}

//读取文件内容,发送客户端
void cat(int client, FILE *resource)
{
    char buf[1024];//缓冲区
    //逐行读取,遇到换行符eof就停止
    fgets(buf, sizeof(buf), resource);
    while (!feof(resource)) //是否已经读取至文件结尾
    {
        send(client, buf, strlen(buf), 0);//发送给客户端
        fgets(buf, sizeof(buf), resource);
    }
}

//500错误
void cannot_execute(int client)
{
    char buf[1024];

    sprintf(buf, "HTTP/1.0 500 Internal Server Error\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "Content-type: text/html\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "<P>Error prohibited CGI execution.\r\n");
    send(client, buf, strlen(buf), 0);
}

//错误处理
void error_die(const char *sc)
{
    perror(sc);
    exit(1);
}

//从缓冲区读取一行
int get_line(int sock, char *buf, int size)
{
    int i = 0;
    char c = '\0';
    int n;

    while ((i < size - 1) && (c != '\n'))
    {
        n = recv(sock, &c, 1, 0);
        /* DEBUG printf("%02X\n", c); */
        if (n > 0)
        {
            if (c == '\r')
            {
                n = recv(sock, &c, 1, MSG_PEEK);
                /* DEBUG printf("%02X\n", c); */
                if ((n > 0) && (c == '\n'))
                    recv(sock, &c, 1, 0);
                else
                    c = '\n';
            }
            buf[i] = c;
            i++;
        }
        else
            c = '\n';
    }
    buf[i] = '\0';

    return(i);
}

//处理头部信息
void headers(int client, const char *filename)
{
    char buf[1024];
    (void)filename;  /* could use filename to determine file type */

    strcpy(buf, "HTTP/1.0 200 OK\r\n");
    send(client, buf, strlen(buf), 0);
    strcpy(buf, SERVER_STRING);
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "Content-Type: text/html\r\n");
    send(client, buf, strlen(buf), 0);
    strcpy(buf, "\r\n");
    send(client, buf, strlen(buf), 0);
}

//404处理
void not_found(int client)
{
    char buf[1024];

    sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, SERVER_STRING);
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "Content-Type: text/html\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "<HTML><TITLE>Not Found</TITLE>\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "<BODY><P>The server could not fulfill\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "your request because the resource specified\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "is unavailable or nonexistent.\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "</BODY></HTML>\r\n");
    send(client, buf, strlen(buf), 0);
}

//静态文件请求
void serve_file(int client, const char *filename)
{
    FILE *resource = NULL;
    int numchars = 1;
    char buf[1024];

    buf[0] = 'A'; buf[1] = '\0';
    while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
        numchars = get_line(client, buf, sizeof(buf));

    resource = fopen(filename, "r");
    if (resource == NULL)
        not_found(client);
    else
    {
        headers(client, filename);
        cat(client, resource);
    }
    fclose(resource);
}

//返回给浏览器  表明收到的HTTP请求所用的method(方法)不被支持
void unimplemented(int client)
{
    char buf[1024];

    sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, SERVER_STRING);
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "Content-Type: text/html\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "<HTML><HEAD><TITLE>Method Not Implemented\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "</TITLE></HEAD>\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "<BODY><P>HTTP request method not supported.\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "</BODY></HTML>\r\n");
    send(client, buf, strlen(buf), 0);
}

总结

整体工作流程:

服务器启动==》在指定端口或随机选取端口绑定httpd服务==》

在收到一个http请求时(listen的端口accept),派生一个线程运行accept_request函数==》

读取HTTP中的相关信息:method(请求方法),url==》

如果是POST==》开启cgi

如果是GET==》提取URL以及URL中的参数

==》如果不是CGI,调用serve_file,返回地址中内容(页面)

==》如果是CGI,调用execute_cgi,执行CGI脚本程序

==》调用cgi执行函数

创建两个管道与一个子进程==》cgi_input绑定stdin,cgi_output绑定stdout==》

设置环境变量==》

最后,关闭与浏览器的连接,完成一次HTTP请求和回应