开源项目Tinyhttp项目源码详解

2023-11-08

HTTP协议

http协议叫做超文本传输协议,是基于tcp/ic协议的应用层协议。
具体内容可以借鉴这一篇博客:https://blog.csdn.net/qq_36894974/article/details/103930478。
本文主要涉及Tinyhttpd开源项目,该项目就是利用C语言简单地实现了http这一协议。

项目文件结构

在这里插入图片描述
拿到开源项目的第一件事就是查看README文档。example文件夹下是简单的tcp和udp服务器、客户端程序,并不关键。主要看httpd.c

先从main函数开始看

int main(void)
{
    int server_sock = -1;
    u_short port = 0;
    int client_sock = -1;
    struct sockaddr_in client_name;
    int client_name_len = sizeof(client_name);
    //pthread_t newthread;
    
    server_sock = startup(&port);
    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");
        accept_request(client_sock);
        /*if (pthread_create(&newthread , NULL, accept_request, client_sock) != 0)
        perror("pthread_create");*/
    }
    close(server_sock);
    return(0);
  • startup(端口),创建socket绑定本地并设置监听,返回本地socket的fd。
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;
    //将*port 转换成以网络字节序表示的16位整数
    name.sin_port = htons(*port);
    name.sin_addr.s_addr = htonl(INADDR_ANY);
    
    //如果传进去的sockaddr结构中的 sin_port 指定为0,这时系统会选择一个临时的端口号
    if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
    error_die("bind");

    //如果调用 bind 后端口号仍然是0,则手动调用getsockname()获取端口号
    if (*port == 0)  /* if dynamically allocating a port */
    {
        int namelen = sizeof(name);
        //调用getsockname()获取系统给 httpd 这个 socket 随机分配的端口号
        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);
}

获得serve_sockde句柄,创建客户端套接字地址结构并accept阻塞等待有客户端连接上来。当客户端连接上来时,跳转无限循环whil(1)执行accept_request响应客户端请求。

accept_request函数

协议主要实现部分都是在这个函数里面。为了方便阅读,所有详细的我都写进代码里面。
代码如下

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;
    int cgi = 0;      /* becomes true if server decides this is a CGI
    * program */
    char *query_string = NULL;

    //numchars记录请求行的长度,buf储存请求行的内容(请求方法,method)
    numchars = get_line(client, buf, sizeof(buf));
    i = 0; j = 0;
    //检查
    while (!ISspace(buf[j]) && (i < sizeof(method) - 1))//ISspace判断是否为空字符
    {
        method[i] = buf[j];
        i++; j++;
    }
    method[i] = '\0';

    //如果请求的方法不是 GET 或 POST 任意一个的话就直接发送 response 告诉客户端没实现该方法
    if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))//strcasecmp比较相同则返回0
    {
        unimplemented(client);//里面是一行一行的send服务器响应信息,状态行、消息报头、空行、响应正文
        return;
    }

    //如果是 POST 方法就将 cgi 标志变量置一(true)
    if (strcasecmp(method, "POST") == 0)
    cgi = 1;

    i = 0;
    //跳过所有的空白字符(空格)
    while (ISspace(buf[j]) && (j < sizeof(buf))) 
    j++;

    //然后把 URL 读出来放到 url 数组中
    while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf)))
    {
        url[i] = buf[j];
        i++; j++;
    }
    url[i] = '\0';

    //如果这个请求是一个 GET 方法的话
    if (strcasecmp(method, "GET") == 0)
    {
        //用一个指针指向 url
        query_string = url;

        //去遍历这个 url,跳过字符 ?前面的所有字符,如果遍历完毕也没找到字符 ?则退出循环
        while ((*query_string != '?') && (*query_string != '\0'))
        	query_string++;

        //退出循环后检查当前的字符是 ?还是字符串(url)的结尾
        if (*query_string == '?')
        {
            //如果是 ? 的话,证明这个请求需要调用 cgi,将 cgi 标志变量置一(true)
            cgi = 1;
            //从字符 ? 处把字符串 url 给分隔会两份
            *query_string = '\0';
            //使指针指向字符 ?后面的那个字符
            query_string++;
        }
    }

 //将前面分隔两份的前面那份字符串,拼接在字符串htdocs的后面之后就输出存储到数组 path 中。相当于现在 path 中存储着一个字符串
 sprintf(path, "htdocs%s", url);

 //如果 path 数组中的这个字符串的最后一个字符是以字符 / 结尾的话,就拼接上一个"index.html"的字符串。首页的意思
 if (path[strlen(path) - 1] == '/')
 strcat(path, "index.html");

 //在系统上去查询该文件是否存在
 if (stat(path, &st) == -1) {
     //如果不存在,那把这次 http 的请求后续的内容(head 和 body)全部读完并忽略
     while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
     numchars = get_line(client, buf, sizeof(buf));
     //然后返回一个找不到文件的 response 给客户端
     not_found(client);
 }
 else
 {
     //文件存在,那去跟常量S_IFMT相与,相与之后的值可以用来判断该文件是什么类型的
     if ((st.st_mode & S_IFMT) == S_IFDIR)  
     //如果这个文件是个目录,那就需要再在 path 后面拼接一个"/index.html"的字符串
     strcat(path, "/index.html");

     //S_IXUSR, S_IXGRP, S_IXOTH三者可以参读《TLPI》P295
     if ((st.st_mode & S_IXUSR) ||       
         (st.st_mode & S_IXGRP) ||
         (st.st_mode & S_IXOTH)    )
     //如果这个文件是一个可执行文件,不论是属于用户/组/其他这三者类型的,就将 cgi 标志变量置一
     cgi = 1;

     if (!cgi)
     //如果不需要 cgi 机制的话,发送path的文件即可
     serve_file(client, path);
     else
     //如果需要则调用
     execute_cgi(client, path, method, query_string);
 }
	//断开客户端
 	close(client);
 }

serve_file函数

将filename文件通过FILE流发送到client。
代码如下

void serve_file(int client, const char *filename)
{
    FILE *resource = NULL;
    int numchars = 1;
    char buf[1024];

    //确保 buf 里面有东西,能进入下面的 while 循环
    buf[0] = 'A'; buf[1] = '\0';
    //循环作用是读取并忽略掉这个 http 请求后面的所有内容,清空缓冲区
    while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
    numchars = get_line(client, buf, sizeof(buf));

    //只读方式打开将filenameIO流赋给resource
    resource = fopen(filename, "r");
    if (resource == NULL)
    	//打开失败则发送NOT FOUND协议包
    	not_found(client);
    else
    {
        //打开成功后,发送状态行、消息报头和空行
        headers(client, filename);
        //将装有filenam内容的resource流send到client
        cat(client, resource);
    }
	//发送完毕,关闭打开的资源文件
    fclose(resource);
}

execute_cgi函数

用于响应cgi请求,流程先获取正文长度,发送状态行,创建读写两个管道,创建一个进程,子进程中创建、设置并储存环境变量,最后跳转执行path下的文件。

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 中填东西以保证能进入下面的 while
    buf[0] = 'A'; buf[1] = '\0';
    //如果是 http 请求是 GET 方法的话读取并忽略请求剩下的内容,相当于再次判断是否cgi请求
    if (strcasecmp(method, "GET") == 0)
    	while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
    		numchars = get_line(client, buf, sizeof(buf));
    else    /* POST */
    {
        //只有 POST 方法才继续读内容
        numchars = get_line(client, buf, sizeof(buf));
        //获取Content-Length:的大小
        //strcmp判断是否到尾,按道理应该是\r\n的
        while ((numchars > 0) && strcmp("\n", buf))
        {
            buf[15] = '\0';
            //当遇到Content-Length:时候,buf[16]装下后面的值
            if (strcasecmp(buf, "Content-Length:") == 0)
            	content_length = atoi(&(buf[16])); //记录 body 的长度大小
            numchars = get_line(client, buf, sizeof(buf));
        }

        //如果 http 请求的 header 没有指示 body 长度大小的参数,则报错返回
        if (content_length == -1) {
            bad_request(client);
            return;
        }
    }

    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];

     //dup2()包含<unistd.h>中,参读《TLPI》P97
     //将子进程的输出由标准输出重定向到 cgi_ouput 的管道写端上
     dup2(cgi_output[1], 1);
     //将子进程的输出由标准输入重定向到 cgi_ouput 的管道读端上
     dup2(cgi_input[0], 0);
     //关闭 cgi_ouput 管道的读端与cgi_input 管道的写端
     close(cgi_output[0]);
     close(cgi_input[1]);

     //构造一个环境变量
     sprintf(meth_env, "REQUEST_METHOD=%s", method);
     //putenv()包含于<stdlib.h>中,参读《TLPI》P128
     //将这个环境变量加进子进程的运行环境中
     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);
          }

     //execl()包含于<unistd.h>中,参读《TLPI》P567
     //最后将子进程替换成另一个进程并执行 cgi 脚本
     execl(path, path, NULL);
     exit(0);

 } else {    /* parent */
         //父进程则关闭了 cgi_output管道的写端和 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);
        }
 }

总结

代码量很小,是二十年前的写的,代码风格和现在有一定差距,不过以此作为入门了解http协议还是有一定的,麻雀小小,五脏俱全,关于进程、文件IO、标准IO、网络都涉及。
代码自行git clone https://github.com/qiushii/tinyhttp.git

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

开源项目Tinyhttp项目源码详解 的相关文章

  • memcached 使用 Django 监听 UDP

    Question 我无法获得memcached正在听UDP 上班 get set delete 与姜戈 我只让 memcached 监听UDP 11211 正如我在上一个问题 https stackoverflow com question
  • UDP 服务器套接字缓冲区溢出

    我正在 Linux 上编写 C 应用程序 我的应用程序有一个 UDP 服务器 它在某些事件上向客户端发送数据 UDP 服务器还接收来自客户端的一些反馈 确认 为了实现这个应用程序 我使用了一个 UDP 套接字 例如int fdSocket
  • 如何使用 ZeroMQ 处理原始 UDP?

    我有一个客户 我无法更改其代码 但我想使用 重新 编写ZeroMQ插座 客户使用原始TCP和原始的UDP插座 我知道我可以使用ZMQ ROUTER RAW对于生的TCP插座 但是原始的怎么样 UDP数据流 ZeroMQ 中对 UDP 的支持
  • Java UDP中如何获取实际数据包大小`byte[]`数组

    这是我上一个问题的后续问题 Java UDP发送 接收数据包一一接收 https stackoverflow com questions 21866382 java udp send receive packet one by one 正如
  • 数据报总是被完整接收吗?

    大多数数据报接收函数 例如c的recv或read java的DatagramPacket类或python的SocketServer 都包含找出接收数据量的可能性 c int amount recv sock buf n MSG WAITAL
  • NodeJS UDP 多播如何

    我正在尝试将 UDP 多播数据包发送到 230 185 192 108 以便每个订阅的人都会收到 有点卡住了 我相信它的广播正确 但似乎无法从任何客户端获取任何信息 Server var news Borussia Dortmund win
  • 将 Docker 容器连接到网络接口/设备而不是 IP 地址

    经过仔细的研究 测试和摆弄 我只能找到通过从 IP 端口转发来将 Docker 容器连接到给定接口的方法 这可以通过添加来完成 p Host IP Host Port Container Port to a docker run命令 我有一
  • UDP sendto 上的 ECONNREFUSED 错误

    我在使用正在写入的应用程序时遇到一些无法解释的行为 使用 sendto 向多个端口发送 UDP 数据 所有端口均使用套接字 PF INET SOCK DGRAM 0 为了一组客户端读取进程的利益 这些 sendto 偶尔会不可预测地触发经济
  • 如何监听任意端口的广播包?

    使用 NET 如何在任何端口上侦听发送到 255的udp广播数据包 而不需要绑定到特定端口 我自己找到了办法 它是这样工作的 mainSocket new Socket AddressFamily InterNetwork SocketTy
  • iOS 14 在进行本地网络广播时给出“操作系统错误:错误的文件描述符,errno = 9”

    做一点Jeopardy 风格问答 https stackoverflow blog 2011 07 01 its ok to ask and answer your own questions here 我正在 Flutter 中开发一个应
  • TCP 兼容性:为什么 TCP 不兼容数据包广播和组播操作?

    http en wikipedia org wiki User Datagram Protocol http en wikipedia org wiki User Datagram Protocol 与 TCP 不同 UDP 与数据包广播
  • 自 2012 年以来,WinSock 注册 IO 性能是否有所下降?

    我最近使用 MS 为该 API 提供的稍微可接受的文档编写了基于 WinSock Registered IO RIO 的 UDP 接收 最终的性能非常令人失望 单套接字性能有些稳定 约为每秒 180k 数据包 使用多个 RSS 队列 即多个
  • Rails 是否支持侦听 UDP 套接字的简洁方式?

    在 Rails 中 集成更新模型某些元素的 UDP 侦听过程的最佳方式是什么 特别是向其中一个表添加行 简单的答案似乎是在同一进程中使用 UDP 套接字对象启动一个线程 但不清楚我应该在哪里执行适合 Rails 方式的操作 有没有一种巧妙的
  • 为多线程 UDP 客户端执行“close ()”时套接字描述符未释放

    我在下面编写了 UDP 客户端 它基本上生成一个单独的线程来接收数据报 但是数据报仅在主线程中发送 现在 在 Linux 发行版上实例化 udpClient 1 UDP 客户端后按 ctrl D 实现退出循环 围绕 getline 调用 并
  • recvfrom() 中的 addrlen 字段有何用途?

    我在程序中使用 recvfrom 从我在 src addr 中指定的服务器获取 DGRAM 数据 但是 我不确定为什么需要初始化并传入addrlen 我读了手册页 但不太明白它的意思 如果src addr不为NULL 并且底层协议提供了源地
  • 从不同进程通过套接字 (UDP) 回复客户端

    我有一个服务器而不是 命令处理程序 进程 它通过 UDP 接收消息 并通过其发布的 API 无论该进程采用何种 IPC 机制 与该进程进行通信 从而将要做的工作委托给不同的进程 我们的系统有多个协作进程 然后 该 API 调用的结果会从命令
  • 用 C 语言进行非阻塞 udp 套接字编程:我能得到什么?

    我在理解从非阻塞 UDP 套接字返回什么recv recvfrom 时遇到问题 与 TCP 相比更具体一点 如果我错了 请纠正我 阻塞套接字 TCP 或 UDP 在缓冲区中有一些数据之前不会从 recv 返回 这可以是一定数量的字节 TCP
  • 使用多个 NIC 广播 UDP 数据包

    我正在 Linux 中为相机控制器构建嵌入式系统 非实时 我在让网络做我想做的事情时遇到问题 该系统有 3 个 NIC 1 个 100base T 和 2 个千兆端口 我将较慢的连接到相机 这就是它支持的全部 而较快的连接是与其他机器的点对
  • 如何从 DatagramPacket 中检索字符串[重复]

    这个问题在这里已经有答案了 下面的代码打印 B 40545a60 B 40545a60abc exp 但我想打印abc 以便我可以从接收系统检索正确的消息 public class Operation InetAddress ip Data
  • 用于实时传输协议的开源 .net C# 库

    net中有好的RTP开源库吗 我的目的是用于音频和视频同步问题并提高每秒帧数速率 我对 RTP 不太了解 但你可能想看看本文 http www codeproject com KB IP Using RTP in Multicasting

随机推荐

  • JSON与String字符串相互转换的方法

    首先创建一个简单的类 package com wei demo pojo Author wei Date 2022 6 2 9 24 Version 1 0 public class Teacher private int id priva
  • ASP精华[转]

  • Linux命令:ps

    ps命令 查看系统中的进程状态 ps命令的参数以及作用 参数 作用 a 显示所有进程 包括其他用户的进程 u 用户及其他详细信息 x 显示没有控制终端的进程 Linux系统中时刻运行许多进程 如果能够合理的管理它们 则可以优化系统性能 si
  • js中使用DES加解密解决方案总结

    js中使用DES加解密解决方案总结 1 需求背景 最近开发vue项目中 对于用户手机号码需要进行DES加解密操作 简介 DES加密 是一种比较传统的加密方式 其加密运算 解密运算使用的是同样的密钥 信息的发送者和信息的接收者在进行信息的传输
  • 【MLOps】第 1 章 : 为什么选择它以及现在面临的挑战

    大家好 我是Sonhhxg 柒 希望你看完之后 能对你有所帮助 不足请指正 共同学习交流 个人主页 Sonhhxg 柒的博客 CSDN博客 欢迎各位 点赞 收藏 留言 系列专栏 机器学习 ML 自然语言处理 NLP 深度学习 DL fore
  • 常用数组方法总结

    添加 删除元素 push items 从结尾添加元素 pop 从结尾弹出 提取元素 unshift items 从开头添加元素 shift 从开头提取元素 splice pos deleteCount items 从index开始 删除de
  • win10 安装 python报错-已安装这个产品的另一版本

    尝试清理干净电脑上关于之前安装的Python3 在 输入win R 输入cmd 回车 输入 python 回车 却看到 C Users 86136 gt python python 不是内部或外部命令 也不是可运行的程序 或批处理文件 但是
  • Python pd.merge()函数介绍(全)

    目录 1 前言 2 参数介绍 参数如下 3 基础案例 3 1on关键字演示 3 2left on 和 right on 关键字 3 3left index 和 right index 关键字 3 4数据连接的类型 3 4 1 1 前言 在数
  • 4.3 Verilog练习(2)

    目录 练习五 用always块实现较复杂的组合逻辑电路 练习六 在Verilog HDL中使用函数 练习七 在Verilog HDL中使用任务 task 练习八 利用有限状态机进行复杂时序逻辑的设计 练习五 用always块实现较复杂的组合
  • leveldb注释7–key与value

    作为一个kv的系统 key的存储至关重要 在leveldb中 主要涉及到如下几个key user key InternalKey与LookupKey memtable key 其关系构成如下图 user key就是用户输入的key 而Int
  • 华为OD机试 - 第k个排列(Java )

    题目描述 给定参数n 从1到n会有n个整数 1 2 3 n 这n个数字共有n 种排列 按大小顺序升序列出所有排列的情况 并一一标记 当n 3时 所有排列如下 123 132 213 231 312 321 给定n和k 返回第k个排列 输入描
  • 关于校园招聘的感受(汇总)

    对招聘会的法想法 今天春季招聘会在我校的西苑体育馆拉开了序幕 我作为大二的一名学生去看了此次招聘会 进到馆内 第一反应就是一个人 多 人多 单位多 感觉到以后大四毕业就业的压力 那么多学长学姐把自己简历送到各个用人单位 开始面试 考官出的题
  • java 动态代理

    动态代理 这里讲解jdk 动态代理 不讲解cglib动态代理 使用jdk 的InvocationHandler接口与Proxy类实现动态代理 自定义InvocationHandler接口与Proxy类 自定义实现 分析 我们想要实现动态代理
  • dcdc升压计算器excel_优秀DCDC升压

    Figure 1 Basic Application Circuit GENERAL DESCRIPTION The MT3608 is a constant frequency 6 pin SOT23 current mode step
  • pycharm简单使用(Mac):创建一个helloWord

    说明 VSCode是一款轻量级的开发工具 可以支持多款插件这个学习使用确实是一个好的工具 PyCharm是一款Python专门支持的IDE 为什么这里要使用PyCharm呢 PyCharm支持断点调试 1 第一步 创建一个项目 2 第二步
  • CUDA 基础 01 - 概念

    最近在GPU编译器测试方面遇到一些瓶颈 准备学习下cuda 相关的基础知识 warp sm index grid等 CPU VS GPU GPU最重要的一点是可以并行的实现数据处理 这一点在数据量大 运算复杂度不高的条件下极为适用 可以简单
  • 3 Ubuntu上使用Qt运行多线程,设置程序自启动及保护脚本

    Ubuntu上使用Qt运行多线程 设置程序自启动及保护脚本 多线程 自启动及保护脚本 自启动及保护脚本 结束自启动脚本 脚本程序简单说明 设置自启动 多线程 使用多线程时我们需要加入 include lt thread gt 这个头文件包含
  • 区块链常见的几大共识机制

    区块链技术被广泛应用于许多领域 其中共识机制是构成区块链系统的核心部分 共识机制是指用来维护区块链数据一致性 安全性和可靠性的机制 常见的区块链共识机制有以下几种 1 工作量证明 Proof of Work 是最早的共识机制 它将矿工通过解
  • 毕业设计-基于机器视觉的交通标志识别系统

    目录 前言 课题背景和意义 实现技术思路 一 交通标志识别系统 二 交通标志识别整体方案 三 实验分析 四 总结 实现效果图样例 最后 前言 大四是整个大学期间最忙碌的时光 一边要忙着备考或实习为毕业后面临的就业升学做准备 一边要为毕业设计
  • 开源项目Tinyhttp项目源码详解

    HTTP协议 http协议叫做超文本传输协议 是基于tcp ic协议的应用层协议 具体内容可以借鉴这一篇博客 https blog csdn net qq 36894974 article details 103930478 本文主要涉及T