【C语言开源库】 一个只有500行代码的开源http服务器:Tinyhttpd学习

news2024/9/21 0:42:24

项目搬运,带中文翻译:https://github.com/nengm/Tinyhttpd

在嵌入式中,我们HTTP服务器用得最多的就是boa还有就是goahead,但是这2个代码量比较大,而Tinyhttpd只有几百行,比较有助于我们学习。

一、编译及运行

直接make之后,所以假如html有执行权限先把它去除了,chmod 600 index.html color.cgi、date.cgi必须要有执行权限。这样之后还是不行,需要这样cgi脚本第一行 改为 “#!/usr/bin/perl”,就能够运行了。

运行之后http服务器的端口为41163

效果大概是这样,截图不是我本机的

不过我更改了html为这样,本来是把js单独反在另外一个文件,不知道为什么,没有反应,最后只能把js的函数,一起写在了html中。

<html>
    <head>
        <title>table</title>
        <meta charset="UTF-8">
        <!--这是描述 js中的函数来之哪个js文件-->
        <script type="text/javascript">
        function my_button(arg)
{
    if(arg == 1)//登录 admin 123456
    {
        //获取网页上输入框的用户名密码
        var usr = document.getElementById("usr").value;//重点
        var pwd = document.getElementById("pwd").value;
        if(usr=="admin" && pwd=="123456")
        {
            window.location.href="http://www.baidu.com";
        }
        else
        {
            alert("用户名或密码错误请重新输入");
            //清空用户名密码的输入框
            document.getElementById("usr").value="";
            document.getElementById("pwd").value="";
        }
    }
    else if(arg == 0)//取消
    {
        //清空用户名密码的输入框
        document.getElementById("usr").value="";
        document.getElementById("pwd").value="";
    }
}
        
        </script>
    </head>
    <body>
        <!--id是唯一 标记一个个标签-->
        用户名:<input type="text" id="usr">
        <br>
        密码:<input type="password" id="pwd">
        <br>
        <input type="button" value="登录" onclick="my_button(1);">
        &nbsp;&nbsp;&nbsp;&nbsp;
        <input type="button" value="取消" onclick="my_button(0);">

    </body>
</html>

效果是这样的,来源于我本机:

二、代码解析

阅读这个程序需要UNIX编程的基础,包括socket相关API,多线程(虽然在Linux下没有用到),多进程和进程间通信,HTTP基础知识。

首先将源码下载到本地,我们可以看到项目代码主要有主程序 httpd.c ,一个客户端 simpleclient.c ,htdocs 下则是一个界面和一些 cgi 脚本,其它则是一些编译相关的文件,项目结构如下图:

主文件开头有两段注释,一个是程序注解,包括了程序的简单描述,作者,时间,(好家伙,1999年,远古大神),还有地址,这也是国际惯例了,大家在编码的时候也要良好的习惯。

一般情况下, 在代码层面 main 函数就是程序运行的起点,因此我们阅读源码也从 main 函数入手,我们可以在 main 函数开头看到,作者创建了一些局部变量用来保存后续创建网络连接的参数与一个客户端变量,在后续的代码中,我们可以更直观的看到这些参数的作用。

从main函数开始

int main(void)
{
    //在Ubuntu 16.04下运行,进行了修改
    int server_sock = -1;//服务器端fd
    u_short port = 0;//端口号,传0则随机绑定端口
    int client_sock = -1;//客户端fd
    struct sockaddr_in client_name;
    socklen_t  client_name_len = sizeof(client_name);
    pthread_t newthread;

    server_sock = startup(&port);//返回一个服务器端socket
    printf("httpd running on port %d\n", port);

    //不断循环接收连接请求
    while (1)
    {
        client_sock = accept(server_sock,
                (struct sockaddr *)&client_name,
                &client_name_len);//阻塞等待连接
        if (client_sock == -1)
            error_die("accept");
        //本来是线程版本,按照Linux注释修改,现在同一时间只能处理一个请求
        //应该是1999年Linux还没有线程的功能吧。。。
        //accept_request(&client_sock);//http请求的具体处理函数
        if (pthread_create(&newthread , NULL, accept_request, (void *)&client_sock) != 0)
            perror("pthread_create");
    }

    //关闭服务器端socket
    close(server_sock);

    return(0);
}

可以看到整个过程非常简单,注释写的很清楚了。

startup(&port);函数初始化后,处理的逻辑由accept_request(&client_sock);实现。

初始化函数startup(&port)

这个函数开启一个socket来监听特定端口的网络请求,输入参数为0时则动态生成一个端口号,否则用输入的参数做端口号。

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

    httpd = socket(PF_INET, SOCK_STREAM, 0);//创建socket
    if (httpd == -1)//创建失败处理
        error_die("socket");
    memset(&name, 0, sizeof(name));//清空name内容
    //设置name的参数,分别代表采用IPv4、端口的主机字节序转网络字节序、地址
    name.sin_family = AF_INET;
    name.sin_port = htons(*port);
    name.sin_addr.s_addr = htonl(INADDR_ANY);
    //设置端口复用
    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");
    //如果传入参数为0,则动态分配端口,获取端口号并传出
    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);
    }
    //设置同时监听的上限数为5
    if (listen(httpd, 5) < 0)
        error_die("listen");
    return(httpd);
}

初始化函数也很基础,过一遍APUE基本都一样,唯一不同就是这个端口分配的骚操作,注意一下就行。接下来就是重头戏accept_request(&client_sock);,我这个计网0基础的不得不补习了半天HTTP才勉强整明白。

请求处理accept_request(&client_sock)

由于该函数较长,分为多个部分分析。

请求行的处理

函数一开始对socket发送过来的数据按照HTTP协议进行了处理,HTTP请求格式如下:

先对第一行请求行进行处理。

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

    numchars = get_line(client, buf, sizeof(buf));//读取一行http请求到buf中
    // 根据HTTP协议,第一行为请求行包括:
    // 方法+URI+HTTP版本 例如:GET / HTTP/1.1
    // 即目前的buf中包括以上三部分
    i = 0; j = 0;
    // ①先获取方法到method中
    // isspace判断字符是否为空字符,为空则返回true
    while (!ISspace(buf[i]) && (i < sizeof(method) - 1))
    {
        method[i] = buf[i];
        i++;j++;
    }
    method[i] = '\0';

    // 由于httpd比较简单,仅支持GET方法或POST方法
    // strcasecmp忽略大小写比较字符串是否相等,如果都不等,则返回错误信息给客户端
    if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
    {
        unimplemented(client);
        return NULL;
    }
    // 如果是POST方法则将cgi置1
    if (strcasecmp(method, "POST") == 0)
        cgi = 1;
    // ②获取url到变量url中
    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[i] = '\0';
    // 如果是GET方法,可能含有查询请求,将url问号后的内容保存到query_string中
    if (strcasecmp(method, "GET") == 0)
    {
        query_string = url;
        // 一直把url中问号之前内容遍历
        while ((*query_string != '?') && (*query_string != '\0'))
            query_string++;
        if (*query_string == '?')
        {
            // 如果有问号则表示需要执行cgi文件,将其变量置1
            cgi = 1;
            // 将url分割成两段,现在url表示问号前的部分,query_string表示问号后的部分
            *query_string = '\0';
            query_string++;
        }
    }

此时分割出了请求方法和URL,为了避免文章过长,其中用到的int get_line(int sock, char *buf, int size)等函数可以下载我注释的完整文件来看。

本地处理

    // 将url添加到htdocs后并赋值给path
    sprintf(path, "htdocs%s", url);
    // 如果是以/结尾则把主页加在后面
    if (path[strlen(path) - 1] == '/')
        strcat(path, "index.html");
    
    // 在系统中查看path路径文件是否存在
    if (stat(path, &st) == -1) {
        // 如果不存在则将本次HTTP请求的后续内容全部丢弃
        while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
            numchars = get_line(client, buf, sizeof(buf));
        not_found(client);
    }
    else
    {
        // 如果存在该文件,判断其是否为路径名,是则在后面加上/index.html
        if ((st.st_mode & S_IFMT) == S_IFDIR)
            strcat(path, "/index.html");
        // 只要该文件具有可执行权限,则将cgi置1
        if ((st.st_mode & S_IXUSR) ||
                (st.st_mode & S_IXGRP) ||
                (st.st_mode & S_IXOTH)    )
            cgi = 1;
        // 根据cgi的值执行不同的处理函数
        if (!cgi)
            serve_file(client, path);
        else
            execute_cgi(client, path, method, query_string);
    }

    close(client);

其中stat函数原型为:int stat(const char *file_name, struct stat *buf ),它通过文件名filename获取文件信息,并保存在buf所指的结构体stat中。

如果没有执行cgi请求,则执行serve_file

void serve_file(int client, const char *filename)
{
    FILE *resource = NULL;
    int numchars = 1;
    char buf[1024];
    // 保证能进入下面的while
    buf[0] = 'A'; buf[1] = '\0';
    // 将本次Http请求后续内容丢弃
    while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
        numchars = get_line(client, buf, sizeof(buf));
    // 打开文件
    resource = fopen(filename, "r");
    // 文件不存在则返回一个404状态码
    if (resource == NULL)
        not_found(client);
    else
    {
        // 返回200成功状态码
        headers(client, filename);
        // 将文件内容发送到client
        cat(client, resource);
    }
    fclose(resource);
}

如果cgi被置1,则执行execute_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;

    // 保证能进入while循环
    buf[0] = 'A'; buf[1] = '\0';
    // 如果是GET,丢弃本次HTTP请求后续内容
    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));
        //这个循环的目的是读出指示 body 长度大小的参数,并记录 body 的长度大小。其余的 header 里面的参数一律忽略
        //注意这里只读完 header 的内容,body 的内容没有读
        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));
        }
        // 如果header没有表示body的长度则返回错误
        if (content_length == -1) {
            bad_request(client);
            return;
        }
    }
    else/*HEAD or other*/
    {
    }

    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    send(client, buf, strlen(buf), 0);

    // 创建两个管道
    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;
    }
    // 子进程用于处理CGI脚本
    if (pid == 0)  /* child: CGI script */
    {
        char meth_env[255];
        char query_env[255];
        char length_env[255];

        //将子进程的输出由标准输出重定向到 cgi_output 的管道写端上
        dup2(cgi_output[1], STDOUT);
        //将子进程的输入由标准输入重定向到 cgi_input 的管道读端上
        dup2(cgi_input[0], STDIN);
        // 关闭cgi_ouput的读和cgi_input的写
        close(cgi_output[0]);
        close(cgi_input[1]);

        //构造一个环境变量
        sprintf(meth_env, "REQUEST_METHOD=%s", method);
        putenv(meth_env);//将这个环境变量加进子进程的运行环境中
        //根据http 请求的不同方法,构造并存储不同的环境变量
        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);
        }
        // 执行path处的脚本
        execl(path, path, NULL);
        exit(0);
    } else {    /* parent */
        // 关闭cgi_ouput的写和cgi_input的读
        close(cgi_output[1]);
        close(cgi_input[0]);

        //如果是 POST 方法的话就继续读 body 的内容,并写到 cgi_input 管道里让子进程去读
        if (strcasecmp(method, "POST") == 0)
            for (i = 0; i < content_length; i++) {
                recv(client, &c, 1, 0);
                write(cgi_input[1], &c, 1);
            }

        //然后从 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);
    }
}

这里创建了一个子进程用来执行cgi程序,而父进程用于和socket通信,那么子进程执行的结果就需要发送给父进程,再由父进程发给socket,这里使用的是pipe管道,过程如下图。注意:这里的cgi_input和cgi_output是两个管道的名字,其in和out是对于子进程来说的,即cgi_input管道用于向子进程写入数据、cgi_output用于由子进程向父进程发出数据:

那么数据会先由父进程从socket读入,再发送到cgi_input的写端,子进程读入后给cgi处理,然后通过cgi_output发给父进程,父进程再发给socket。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/138921.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

用Python让奇怪的想法变成现实,2023年继续创作

2023年继续写作&#xff0c;用文章记录生活 时间过得真快&#xff0c;一下就到2023年了。 由于疫情肆虐&#xff0c;在网络的游弋的实现也长了&#xff0c;写作的自然也多了。 回想一下&#xff0c;2018-2021年这三年时间里一篇文章也没写过为0&#xff0c;哈哈&#xff0c;没…

【EHub_tx1_tx2_E100】Ubuntu18.04 + ROS_ Melodic + NVISTAR VP300 激光雷达 评测

简介&#xff1a;介绍NVISTAR 的二维DTOF激光雷达 在EHub_tx1_tx2_E100载板&#xff0c;TX1核心模块环境&#xff08;Ubuntu18.04&#xff09;下测试ROS驱动&#xff0c;打开使用RVIZ 查看点云数据&#xff0c;本文的前提条件是你的TX1里已经安装了ROS版本&#xff1a;Melodic。…

滴滴前端一面经典手写面试题

实现bind 实现bind要做什么 返回一个函数&#xff0c;绑定this&#xff0c;传递预置参数bind返回的函数可以作为构造函数使用。故作为构造函数时应使得this失效&#xff0c;但是传入的参数依然有效 // mdn的实现 if (!Function.prototype.bind) {Function.prototype.bind f…

Kuberneters(2)- Pod详解

第四章 实战入门 本章节将介绍如何在kubernetes集群中部署一个nginx服务&#xff0c;并且能够对其进行访问。 Namespace ​ Namespace是kubernetes系统中的一种非常重要资源&#xff0c;它的主要作用是用来实现多套环境的资源隔离或者多租户的资源隔离。 ​ 默认情况下&…

路由跳转同一个界面,但是params不同。页面不刷新?(路由的key)

文章目录引入知识点&#xff1a;路由的key值思路&#xff1a;结论&#xff1a;解决方法&#xff1a;效果&#xff1a;应用场景:引入知识点&#xff1a;路由的key值 如果不设置路由的key值&#xff0c;默认情况下是根据路径判断的&#xff0c;就是不包括params值 例子&#xff…

MySQL5-数据类型

目录 1.数值类型&#xff08;分为整型和浮点型&#xff09; 2.字符串类型 3.日期类型 MySQL和Java编程一样&#xff0c;创建表时要考虑数据类型。 MySQL表组成&#xff1a;列名/列数据类型&#xff1b;数据。 1.数值类型&#xff08;分为整型和浮点型&#xff09; 数据类型…

天工开物 #4 构建一个受保护的网站

前段时间&#xff0c;我出于兴趣试着做了一个需要登录鉴权才能访问的个人网站&#xff0c;最终以 Docusaurus[1] 为内容框架&#xff0c;Next.js[2] 做中间件&#xff0c;Vercel[3] 托管网站&#xff0c;再加上 Auth0[4] 作为鉴权解决方案&#xff0c;实现了一个基本免费的方案…

数位DP入门笔记(1)HUD-2089

题目&#xff1a; 题目理解和思路&#xff1a; 1.此题是给一个6位车牌号&#xff0c;正着不能含有连着的62&#xff0c;不能有4。 2.判断车牌号可能会采用dfs&#xff0c;因为每增加一位数就包含带4&#xff0c;或者形成62两种不合法情况&#xff08;事实上没有用到&#xf…

java学习day67(乐友商城)商品详情及静态化

1.商品详情 当用户搜索到商品&#xff0c;肯定会点击查看&#xff0c;就会进入商品详情页&#xff0c;接下来我们完成商品详情页的展示&#xff0c; 1.1.Thymeleaf 在商品详情页中&#xff0c;我们会使用到Thymeleaf来渲染页面&#xff0c;所以需要先了解Thymeleaf的语法。 …

带你深度剖析《数据在内存中的存储》——C语言

文章目录 一、数据类型介绍 二、整型在内存中的存储方式 2、1 原码、反码、补码的讲解 2、2 大小端介绍 2、2、1 大小端的概念 2、2、2 为什么要区分大小端存储呢&#xff1f; 2、2、3 大小端判断练习 三、浮点数在内存中的存储方式 3、1 浮点数在内存中的存储例题 3、2 浮点数…

TensorFlow2.0实战:Cats vs Dogs

数据集准备 在本文中&#xff0c;我们使用“Cats vs Dogs”的数据集。这个数据集包含了23,262张猫和狗的图像 你可能注意到了&#xff0c;这些照片没有归一化&#xff0c;它们的大小是不一样的 但是非常棒的一点是&#xff0c;你可以在Tensorflow Datasets中获取这个数据集 …

梦在远方路在脚下,社科院与杜兰大学金融管理硕士项目与你一路相伴

梦想是指引我们飞翔的翅膀&#xff0c;梦想是远方的灯塔指引着我们前进的方向。梦想距离我们很远&#xff0c;但路在脚下&#xff0c;只要朝着梦想前进&#xff0c;终有一天梦想会照进现实。就像拥有读研梦想的我们&#xff0c;在社科院杜兰金融管理硕士项目汲取能量&#xff0…

【Android OpenGL开发】OpenGL ES与EGL介绍

什么是OpenGL ES OpenGL&#xff08;Open Graphics Library&#xff09;是一个跨编程语言、跨平台的编程图形程序接口&#xff0c;主要用于图像的渲染。 Android提供了简化版的OpenGL接口&#xff0c;即OpenGL ES。 早先定义 OpenGL ES 是 OpenGL 的嵌入式设备版本&#xff…

Mac上超实用的6款软件,老用户都知道!

今天为大家带来的是6款超实用的Mac软件&#xff0c;让你不再走弯路。第一款&#xff1a;Amphetamine 防休眠的利器Amphetamine for mac是应用在Mac上的一款防休眠工具&#xff0c;可以自定义哪些程序运行时不休眠&#xff0c;做到自定义Mac睡眠时间&#xff0c;可以通过超级简单…

【数据结构】链式存储:链表(无头双向链表实现)

目录 &#x1f947;一&#xff1a;无头双向链表 &#x1f392;二、无头双向链表的实现 &#x1f4d8;1.创建节点类 &#x1f4d2;2.创建链表 &#x1f4d7;3.打印链表 &#x1f4d5;4.查找是否包含关键字key是否在单链表当中 &#x1f4d9;5.得到单链表的长度 &#x1…

PCL中常用的高级采样方法

0. 简介 我们在使用PCL时候&#xff0c;常常不满足于常用的降采样方法&#xff0c;这个时候我们就想要借鉴一些比较经典的高级采样方法。这一讲我们将对常用的高级采样方法进行汇总&#xff0c;并进行整理&#xff0c;来方便读者完成使用 1. 基础下采样 1.1 点云随机下采样 …

代码随想录拓展day6 N皇后

代码随想录拓展day6 N皇后 只有这一个内容。一刷的时候也没弄太明白&#xff0c;二刷的时候补上。还有部分内容来自牛客网左老师的算法课程。 总体思路不容易想明白&#xff0c;优化也有很大难度。这要是面试能碰上基本就是故意不给过了吧。 思路 首先来看一下皇后们的约束…

Flink 容错恢复 2.0 2022 最新进展

摘要&#xff1a;本文整理自阿里云 Flink 存储引擎团队负责人&#xff0c;Apache Flink 引擎架构师 & PMC 梅源在 FFA 核心技术专场的分享。主要介绍在 2022 年度&#xff0c;Flink 容错 2.0 这个项目在社区和阿里云产品的进展&#xff0c;内容包括&#xff1a;Flink 容错恢…

基于ssm的个人健康管理系统

项目描述 临近学期结束&#xff0c;还是毕业设计&#xff0c;你还在做java程序网络编程&#xff0c;期末作业&#xff0c;老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里根据疫情当下&#xff0c;你想解决的问…

简单理解光会产生折射的原因及折射定律的推导

已知 1、光是一种波&#xff1b; 2、光在不同介质中传播速度不同。 构建模型 如下图所示&#xff0c;光是中电磁波&#xff0c;以余弦波为例&#xff0c;取余弦波的极大值点为参考&#xff0c;建立一个平面波&#xff08;波前为一个平面&#xff09;。能明显的看出光的传播方…