项目实战-Http服务器

C/C++学习 同时被 3 个专栏收录
74 篇文章 0 订阅
47 篇文章 0 订阅

目录

 

Tinyhttpd是一个小型的http服务器实现,代码仅仅500行,我在大致过了一下unix环境编程之后就想找一个web服务器来练练手,这个就作为俺第一个web方向的小项目吧。
看完这个代码后准备研究一下muduo库、Nginx和Redis,上天保佑我能在秋招实习之前看完吧。

官方源文件:http://tinyhttpd.sourceforge.net/
注释后代码链接:https://github.com/Supredan/DanTinyHttpd
看完有帮助github上帮我点个star哦~磕头了!
参考了:https://github.com/qiyeboy/SourceAnalysis/tree/master/TinyHttpd,和https://github.com/cbsheng/tinyhttpd
感谢!

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

从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。

测试结果

服务器功能测试

如果大家使用的是Ubuntu,建议直接clone我的工程,因为要修改的涉及很多个文件,具体使用方式见github的readme。

clone下来之后,make之后得到可执行文件,运行:
在这里插入图片描述
在浏览器输入localhost:端口号
在这里插入图片描述
在输入栏里输入blue,点击提交。
在这里插入图片描述

使用webbench测试

此处参考http://www.xumenger.com/tinyhttpd-webbench-20161012/

这里要对文件进行修改,具体参加上述链接,结果如下:

$ webbench -c 500 -t 30 http://127.0.0.1:60423/
Webbench - Simple Web Benchmark 1.5
Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.

Benchmarking: GET http://127.0.0.1:60423/
500 clients, running 30 sec.

Speed=64856 pages/min, 309484 bytes/sec.
Requests: 32428 susceed, 0 failed.

测试图如下:
在这里插入图片描述

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

百度云盘分享 简介 笔者当初为了学习JAVA,收集了很多经典源码,源码难易程度分为初级、中级、高级等,详情看源码列表,需要的可以直接下载! 这些源码反映了那时那景笔者对未来的盲目,对代码的热情、执着,对IT的憧憬、向往!此时此景,笔者只专注Android、Iphone等移动平台开发,看着这些源码心中有万分感慨,写此文章纪念那时那景! Java 源码包 Applet钢琴模拟程序java源码 2个目标文件,提供基本的音乐编辑功能。编辑音乐软件的朋友,这款实例会对你有所帮助。 Calendar万年历 1个目标文件 EJB 模拟银行ATM流程及操作源代码 6个目标文件,EJB来模拟银行ATM机的流程及操作:获取系统属性,初始化JNDI,取得Home对象的引用,创建EJB对象,并将当前的计数器初始化,调用每一个EJB对象的count()方法,保证Bean正常被激活和钝化,EJB对象是用完毕,从内存中清除,从账户中取出amt,如果amt>账户余额抛出异常,一个实体Bean可以表示不同的数据实例,我们应该通过主键来判断删除哪个数据实例&hellip;&hellip; ejbCreate函数用于初始化一个EJB实例 5个目标文件,演示Address EJB的实现,创建一个EJB测试客户端,得到名字上下文,查询jndi名,通过强制转型得到Home接口,getInitialContext()函数返回一个经过初始化的上下文,用client的getHome()函数调用Home接口函数得到远程接口的引用,用远程接口的引用访问EJB。 EJB中JNDI的使用源码例子 1个目标文件,JNDI的使用例子,有源代码,可以下载参考,JNDI的使用,初始化Context,它是连接JNDI树的起始点,查找你要的对象,打印找到的对象,关闭Context&hellip;&hellip; ftp文件传输 2个目标文件,FTP的目标是:(1)提高文件的共享性(计算机程序和/或数据),(2)鼓励间接地(通过程序)使用远程计算机,(3)保护用户因主机之间的文件存储系统导致的变化,(4)为了可靠和高效地传输,虽然用户可以在终端上直接地使用它,但是它的主要作用是供程序使用的。本规范尝试满足大型主机、微型主机、个人工作站、和TACs 的不同需求。例如,容易实现协议的设计。 Java EJB中有、无状态SessionBean的两个例子 两个例子,无状态SessionBean可会话Bean必须实现SessionBean,获取系统属性,初始化JNDI,取得Home对象的引用,创建EJB对象,计算利息等;在有状态SessionBean中,用累加器,以对话状态存储起来,创建EJB对象,并将当前的计数器初始化,调用每一个EJB对象的count()方法,保证Bean正常被激活和钝化,EJB对象是用完毕,从内存中清除&hellip;&hellip; Java Socket 聊天通信演示代码 2个目标文件,一个服务器,一个客户端。 Java Telnet客户端实例源码 一个目标文件,演示Socket的使用。 Java 组播组中发送和接受数据实例 3个目标文件。 Java读写文本文件的示例代码 1个目标文件。 java俄罗斯方块 一个目标文件。 Java非对称加密源码实例 1个目标文件 摘要:Java源码,算法相关,非对称加密   Java非对称加密源程序代码实例,本例中使用RSA加密技术,定义加密算法可用 DES,DESede,Blowfish等。   设定字符串为“张三,你好,我是李四”   产生张三的密钥对(keyPairZhang)   张三生成公钥(publicKeyZhang)并发送给李四,这里发送的是公钥的数组字节   通过网络或磁盘等方式,把公钥编码传送给李四,李四接收到张三编码后的公钥,将其解码,李四用张三的公钥加密信息,并发送给李四,张三用自己的私钥解密从李四处收到的信息&hellip;&hellip; Java利用DES私钥对称加密代码实例 同上 java聊天室 2个目标文件,简单。 java模拟掷骰子2个 1个目标文件,输出演示。 java凭图游戏 一个目标文件,简单。 java求一个整数的因子 如题。 Java生成密钥的实例 1个目标文件 摘要:Java源码,算法相关,密钥   Java生成密钥、保存密钥的实例源码,通过本源码可以了解到Java如何产生单钥加密的密钥(myKey)、产生双钥的密钥对(keyPair)、如何保存公钥的字节数组、保存私钥到文件privateKey.dat、如何用Java对象序列化保存私钥,通常应对私钥加密后再保存、如何从文件中得到公钥编码的字节数组、如何从字节数组解码公钥。 Java数据压缩与传输实例 1个目标文件 摘要:Java源码,文件操作,数据压缩,文件传输   Jav
©️2021 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值