Skip to content

ShawnZL/TinyHttpd

Repository files navigation

Tinyhttpd

每个函数的作用:

accept_request: 处理从套接字上监听到的一个 HTTP 请求,在这里可以很大一部分地体现服务器处理请求流程。

bad_request: 返回给客户端这是个错误请求,HTTP 状态吗 400 BAD REQUEST.

cat: 读取服务器上某个文件写到 socket 套接字。

cannot_execute: 主要处理发生在执行 cgi 程序时出现的错误。

error_die: 把错误信息写到 perror 并退出。

execute_cgi: 运行 cgi 程序的处理,也是个主要函数。

get_line: 读取套接字的一行,把回车换行等情况都统一为换行符结束。

headers: 把 HTTP 响应的头部写到套接字。

not_found: 主要处理找不到请求的文件时的情况。

sever_file: 调用 cat 把服务器文件返回给浏览器。

startup: 初始化 httpd 服务,包括建立套接字,绑定端口,进行监听等。

unimplemented: 返回给浏览器表明收到的 HTTP 请求所用的 method 不被支持。

startup

int startup(u_short *port) {
    int httpd = 0;
    struct sockaddr_in name;

    httpd = socket(PF_INET, SOCK_STREAM, 0);
    if (httpd == -1)
        error_die("socket");
    memset(&name, 0, sizeof(name));
    name.sin_family = AF_INET;
    name.sin_port = htons(*port);
    name.sin_addr.s_addr = htonl(INADDR_ANY);
    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)
            // 在以端口号为0调用bind(告知内核去选择本地临时端口号)后,getsockname用于返回由内核赋予的本地端口号。
            // getsockname 用于获取与套接字相关联的本地协议地址
            error_die("getsockname");
        *port = ntohs(name.sin_port);
    }
    if (listen(httpd, 5) < 0)
        error_die("listen");
    return (httpd);
}

accept_request

取出 HTTP 请求中的 method (GET 或 POST) 和 url,。对于 GET 方法,如果有携带参数,则 query_string 指针指向 url 中 ? 后面的 GET 参数。

# HTTP请求(Request)
'''
当用户通过浏览器访问某个网站时浏览器会向网站服务器发送请求这个请求就叫做HTTP请求请求包含的内容主要有:
请求方法Request Method);
请求网址(Request URL);
请求头Request Headers);
请求体(Request Body)。
'''

# 下面来看一下浏览器向百度的网站服务器发送了哪些信息。
#1.请求方法Request Method)
'''
HTTP协议定义了许多与服务器交互的方法最常用的是GET和POST方法如果浏览器向服务器发送一个GET请求则请求的参数信息会直接包含在URL中例如在百度搜索栏中输入scrapy单击"百度一下“按钮,就形成了一个GET请求。
搜索结果页面的URL变为https://www.baidu.com/s?wd=scrapy,
URL中问号(?)后面的wd=scrapy就是请求的参数表示要搜索的关键字POST请求主要用于表单的提交表单中输入的卡号密码等隐私信息通过POST请求方式提交后数据不会暴露在URL中而是保存于请求体中避免了信息的泄露。
'''

# 2.请求网址Request URL)
'''
另外还有一个选项Remote Address: 14.215.177.38:443这是百度服务器的IP地址也可以使用IP地址来访问百度。
'''

# 3.请求头Request Headers)
'''
请求头的内容在Headers选项卡中的Request Headers目录下如下图所示请求头中包含了许多有关客户端环境和请求正文的信息比较重要的信息有Cookie和User-Agent等Accept:浏览器端可以接收的媒体类型text/html代表浏览器可以接收服务器发送的文档类型为text/html,也就是我们常说的HTML文档Accept-Encoding:浏览器接受的编码方式Accept-Language:浏览器所接受的语言种类Connection:表示是否需要持久连接keep-alive表示浏览器与网站服务器保持连接close表示一个请求结束后浏览器和网站服务器就会断开下次请求时需重新连接Cookie:有时也用复数形式Cookies指网站为了提高用户身份进行会话跟踪而存储在本地的数据通常经过加密),由网站服务器创建例如当我们登录后访问该网站的其他页面时发现都是处于登录状态这是Cookie在发挥作用因为浏览器每次在请求该站点的页面时都会在请求头上加上保存有用户名和密码等信息的Cookie并将其发送给服务器服务器识别出该用户后就将页面发送给服务器在爬虫中有时需要爬取登录后才能访问的页面通过对Cookie进行设置就可以成功访问登录后的页面了Host:指定被请求资源的Internet主机和端口号通常从URL中提取User-Agent:告诉网站服务器客户端使用的操作系统浏览器的名称和版本CPU版本以及浏览器渲染引擎浏览器语言等在爬虫中设置此项可以将爬虫伪装成浏览器。
'''
  
# 4.请求体Request Body)
'''
请求体中保存的内容一般是POST请求发送的表单数据对于GET请求请求体为空。
'''

并非所有出现在请求中的 HTTP 首部都属于请求头,例如在 POST 请求中经常出现的 Content-Length 实际上是一个代表请求主体大小的 entity header,虽然你也可以把它叫做请求头。

下面是一个 HTTP 请求的请求头:

GET / HTTP/1.1
Host: 192.168.0.23:47310
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*; q = 0.8
Accept - Encoding: gzip, deflate, sdch
Accept - Language : zh - CN, zh; q = 0.8
Cookie: __guid = 179317988.1576506943281708800.1510107225903.8862; monitor_count = 5

严格来说在这个例子中的 Content-Length 不是一个请求头

POST /myform.html HTTP/1.1
Host: developer.mozilla.org
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0
Content-Length: 128
void accept_request(int client) {
    char buf[1024];
    int numchars;
    char method[255];
    char url[255];
    char path[512];
    size_t i, j;
    struct stat st; //这个结构体是用来描述一个linux系统文件系统中的文件属性的结构。
    int cgi = 0; // becomes true if server decides this is a CGI program
    char *query_string = NULL;
    numchars = get_line(client, buf, sizeof(buf)); //获取的字节数
    i = 0; j = 0;
    while (!ISspace(buf[j]) && (i < sizeof(method) - 1)) { //检查非空
        method[i] = buf[j];
        i++;j++;
    }
    if (strcasecmp(method, "GET") && strcasecmp(method, "POST")) {
        // strcasecmp(判断函数s1与s2大小),s1=s2返回0;此时满足都不等于
        unimplemented(client);
    }
    if (strcasecmp(method, "POST") == 0)
        cgi = 0;
    i = 0;
    // 判断是否空白字符并将空白字符间隔出去
    while (ISspace(buf[j]) && j < sizeof(buf))
        j++;
    // 获取url
    while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf))) {
        url[i] = buf[j];
        i++; j++;
    }
    url[i] = '\0';
    if (strcasecmp(method, "GET") == 0) {
        query_string = url;
        while ((*query_string != '?') && (*query_string) != '\0')
            query_string++;
        if (*query_string == '?') {
            cgi = 1;
            *query_string = '\0';
            query_string++;
        }
    }

    sprintf(path, "htdocs%s", url);
    if (path[strlen(path) - 1] == '/')
        strcat(path, "index.html"); //将函数拼接起来
    if (stat(path, &st) == -1) { //获取文件信息,没有获取到
        while ((numchars > 0) && strcmp("\n", buf)) // \n > buf
            // read & discard headers
            numchars = get_line(client, buf, sizeof(buf));
        not_found(client);
    }
    else { //st.st_mode = 文件对应的模式,文件,目录等
        if ((st.st_mode & S_IFMT) == S_IFDIR)//首先S_IFMT是一个掩码,它的值是0170000(注意这里用的是八进制), 可以用来过滤出前四位表示的文件类型
            /*
             * 现在假设我们要判断一个文件是不是目录,我们怎么做呢?
             * 首先通过掩码S_IFMT把其他无关的部分置0,再与表示目录的数值比较,从而判断这是否是一个目录
             * */
            strcat(path, "/index.html");
        if ((st.st_mode & S_IXUSR) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH))
            // 此处为判断权限
            /* S_IXUSR owner has execute permission
             * S_IXGRP group has execute permission
             * S_IXOTH others have execute permission
             * */
            cgi = 1;
        if (!cgi)
            serve_file(client, path);
        else
            execute_cgi(client, path, method, query_string);
    }
    close(client);
}

get_line

读取套接字的一行,把回车换行等情况统一换为换行符结束

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); // 1为c参数指向的长度, 0表示取走数据
        // Debug pritnf("%02X\n", c);
        if (n > 0) { //有字节读出
            if (c == '\r') {
                n = recv(sock, &c, 1, MSG_PEEK); // 而当为MSG_PEEK时代表只是查看数据,而不取走数据。
                if ((n > 0) && (c == '\n'))
                    recv(sock, &c, 1, 0); //将 \n读取出去
                else
                    c = '\n'; // 只要结尾\r之后不是\n将c赋值\n
            }
            buf[i] = c; //读取数据。
            i++;
        }
        else
            c = '\n';
    }
    buf[i] = '\0'; //结束
    return i;
}

execute_cgi

void execute_cgi(int client, const char *path, const char *method, const char *query_string) {
    // 缓冲区
    char buf[1024];
    // 两根管道
    int cgi_output[2]; //[0] out [1] in
    int cgi_input[2];
    // 进程pid和状态
    pid_t pid;
    int status;
    int i;
    char c;
    // 读取字符数
    int numchars = 1;
    //http的content_length
    int content_length = -1;
    //默认字符
    buf[0] = 'A'; buf[1] = '\0';
    if (strcasecmp(method, "GET") == 0)
        //读取数据,把整个header都读掉,以为Get写死了直接读取index.html,没有必要分析余下的http信息了
        while ((numchars > 0) && strcmp("\n", buf)) // read & discard headers
            numchars = get_line(client, buf, sizeof(buf));
    else {
        //post
        numchars = get_line(client, buf, sizeof(buf));
        while ((numchars > 0) && strcmp("\n", buf)) {
            buf[15] = '\0';
            if (strcasecmp(buf, "Content_Length:") == 0)
                content_length = atoi(&buf[16]);
            numchars = get_line(client, buf, sizeof(buf));
        }
        if (content_length == -1) {
            bad_request(client);
            return;
        }
    }

    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    send(client, buf, sizeof(buf), 0);
    // 建立output管道
    if (pipe(cgi_output) < 0) { //成功返回0,失败返回-1
        cannot_execute(client);
        return;
    }
    // 建立input管道
    if (pipe(cgi_input) < 0) {
        cannot_execute(client);
        return;
    }
    // fork后管道都复制了一份,都是一样的
    //       子进程关闭2个无用的端口,避免浪费
    //       ×<------------------------->1    output
    //       0<-------------------------->×   input
    //       父进程关闭2个无用的端口,避免浪费
    //       0<-------------------------->×   output
    //       ×<------------------------->1    input
    //       此时父子进程已经可以通信
    if ( (pid = fork()) < 0 ) {
        cannot_execute(client);
        return;
    }
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    send(client, buf, strlen(buf), 0);
    //父进程用于收数据以及发送子进程处理的回复数据
    if (pid == 0) {
        // child: CGI script
        char meth_env[255];
        char query_env[255];
        char length_env[255];
        // //子进程输出重定向到output管道的1端
        dup2(cgi_output[1], 1); // 重新定向
        //子进程输入重定向到input管道的0端
        dup2(cgi_input[0], 0);
        close(cgi_output[0]); //关闭输出的出口
        close(cgi_input[1]); //关闭输入的入口
        //CGI环境变量
        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, path, NULL);//pathname: 要执行的文件的路径(推荐使用绝对路径)
        //      pathname: 要执行的文件的路径(推荐使用绝对路径)
        //      第二个参数是一个字符串,是第一个参数中指定的可执行文件的参数(可以是多个参数)
        //          第一个参数是程序名称(没啥用)
        //          第二个开始才是程序的参数
        //          最后一个参数需要null结束(用于告知execl参数列表的结束——哨兵)
        // int m = execl(path, path, NULL);
        // 如果path有问题,例如将html网页改成可执行的,但是执行后m为-1
        // 退出子进程,管道被破坏,但是父进程还在往里面写东西,触发Program received signal SIGPIPE, Broken pipe.
        exit(0);
    }
    else { //parent
        close(cgi_output[1]); //关闭输出的入口
        close(cgi_input[0]); //关闭输入的出口
        if (strcasecmp(method, "POST") == 0) {
            for (i = 0; i < content_length; ++i) {
                recv(client, &c, 1, 0);
                write(cgi_input[1], &c, 1);
            }
            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);
        //pid等待终止的目标子进程ID,如传递-1,则与wait函数相同,等待任意子进程终止。
    }
}

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published