C++分析TinyHTTPd源码

2021/9/12 20:06:34

本文主要是介绍C++分析TinyHTTPd源码,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

有关于TinyHTTPd的源码解析网站已经很多,本文仅记录学习

运行环境CentOS 8,QT;代码可以运行但是有bug,但是用于理解阅读还算可以

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <strings.h>
#include <string.h>
#include <sys/stat.h>
#include <pthread.h>
#include <sys/wait.h>
#include <stdlib.h>

#define ISspace(x) isspace((int)(x))//判断是否为空格,是空格返回1

#define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n"

void accept_request(int);//
void bad_request(int);//
void cat(int, FILE *);
void cannot_execute(int);//
void error_die(const char *);//
void execute_cgi(int, const char *, const char *, const char *);//
int get_line(int, char *, int);//
void headers(int, const char *);
void not_found(int);//
void serve_file(int, const char *);//
int startup(u_short *);//
void unimplemented(int);//

int main(void)
{
    //调试测试
    printf("run 1");
    int server_sock=-1;//服务器的套接字
    int client_sock=-1;//客户端的套接字
    //struct sockaddr_in client_sock;
    //套接字本身是文件描述符,可以用int,也可以用SOCKS
    u_short port=0;//端口号
    struct sockaddr_in client_name;//用于存储客户端的端口号和网络地址
    pthread_t newthread;//定义一个线程

    //调试测试
    printf("run 1");
    //先对服务器新建套接字
    server_sock=startup(&port);//将port地址传给startup函数,此时server_sock是一个正在监听的,且有网络地址信息的服务器套接字
    //cout<<"server_sock running on port:"<<port<<endl;
    printf("server_sock running on port : %d \n",port);

    int client_name_len=sizeof(client_name);
    while(1){
       client_sock = accept(server_sock,(struct sockaddr*)&client_name,&client_name_len);

        if(client_sock==-1){
            error_die("accept");
        }
        //收到连接请求后派生线程
        if(pthread_create(&newthread,NULL,accept_request,client_sock)){
            perror("pthread_create");
        }
    }
    close(server_sock);//关闭套接字,关闭TCP连接
    return 0;
}

int startup(u_short * port)
{
    int server_sock=0;//定义局部变量,服务器套接字
    struct sockaddr_in server_name;

    //建立套接字
    server_sock=socket(PF_INET,SOCK_STREAM,0);//第一个参数代表IPV4协议族,第二个代表TCP连接,0无意义,server_sock保存套接字的文件符,即代表套接字
    if(server_sock==-1){
        error_die("socket");//建立套接字错误返回-1
    }
    memset(&server_name,0,sizeof(server_name));//把server_name结构里面的每一个成员的value设置为0,相当于初始化。
    server_name.sin_family=AF_INET;//AF_INET和PF_INET相同,都代表IPV4协议簇
    server_name.sin_port=htons(*port);//htons将机器字符序转换成网络字符序,是必要的
    server_name.sin_addr.s_addr=htonl(INADDR_ANY);//表示所有本机ip,即0.0.0.0
    if(bind(server_sock,(struct sockaddr*)& server_name,sizeof(server_name))<0){
        //将服务器端的套接字信息和服务器端的套接字绑定
        error_die("bind");
    }

    //如果端口号为0,随即分配端口号
    if(*port == 0){
        int server_name_length=sizeof(server_name);
        if(getsockname(server_sock,(struct sockaddr*)&server_name, &server_name_length)== -1){
            error_die("getsockname");
            //server_name里面保存server_sock的信息
        }
        *port=ntohs(server_name.sin_port);//将网络字符序转换成机器字符序
    }
    //开始监听
    if(listen(server_sock,5)<0){
        error_die("listen");
    }
    return server_sock;//返回服务器的套接字
}
void error_die(const char * error)
{
    perror(error);
    exit(1);
}
void accept_request(int client_sock)
{
    //int client=*(int*)client_sock;//类型转换一下,以适应pthread_create的参数
    char buf[1024];//缓冲池
    int numchars;
    char method[255];//请求方法GET POST
    char url[255];//请求的资源路径
    char path[512];//文件相对路径
    size_t i,j;
    struct stat st;//文件结构类型
    int cgi=0;//是否执行cgi脚本的参数
    char *query_string=NULL;//POST方法的参数指针;

    numchars=get_line(client_sock,buf,sizeof(buf));//从client套接字里读取一个buf大小的数据
    i=0,j=0;

    //读取完之后就开始解析http请求报文的内容
    //第一部分的每个信息都是以空格分开的, 请求模式 url http协议版本
    while(!ISspace(buf[j]) && i<sizeof(method)-1){
        method[i] = buf[j];//第一部分是请求方法,GET 或 POST
        i++;
        j++;
    }
    method[i]='\0';//c风格字符串

    //确定方法,strcasecmp匹配成功返回0
    if(strcasecmp(method,"GET") && strcasecmp(method,"POST")){
        unimplemented(client_sock);//两种方法都不是报错
        return;
    }
    if(strcasecmp(method,"POST") == 0){
        //POST 类型
        cgi=1;
    }

    i=0;

    //接着上次的j读取,过滤空格,method空格后面是url
    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){
        //GET 方法
        query_string = url;//query_string 指针指向url

        while((*query_string)!='?' && (*query_string)!= '\0'){
            //截取url ? 之前的字符,即域名,可能会包括路径,统一来将,问好前面是资源路径
            query_string++;
        }

        //如果有'?'说明有参数,因为参数放在?的后面,既然有参数,就属于有参数的GET模式,执行cgi脚本
        if(*query_string == '?'){
            cgi=1;
            *query_string='\0';
            query_string++;
        }
    }

    //下面是TinyHTTPd项目的htodcs文件下的文件
    sprintf(path,"htdocs%s",url);//打印 htdocs+url,并将url从头到'\0'的部分给path,'\0'后面还有别的字符
    if(path[strlen(path)-1] == '/'){
        //最后一个符号是'/'说明文件类型是目录,则返回index.html,即目录里面的默认文件
        strcat(path,"index.html");
    }

    //根据path路径,找文件,并获取path信息保存在 文件结构体st中
    if(stat(path,&st)==-1){
        //执行失败,文件未找到,则丢弃请求报文的所有行的字符,并报错退出
        while(numchars>0 && strcmp("\n",buf)){//不为空且不为换行符
            numchars=get_line(client_sock,buf,sizeof(buf));//把字符从客户端套接字里面全部取出来
            not_found(client_sock);
        }
    }
    else{
        //获取文件信息成功
        //如果是目录,则默认使用目录下的index.html 文件
        if((st.st_mode & S_IFMT) == S_IFDIR){
            //文件模式与S_IFMT相与,得到的结果如果等于S_IFDIR,意思就是如果文件模式是目录
            strcat(path,"/index.html");//在路径后面加上index.html,是目录就默认打开目录下的index.html文件
        }
        if((st.st_mode & S_IXUSR)||
           (st.st_mode & S_IXGRP)||
           (st.st_mode & S_IXOTH)){
            cgi=1;
        }

        if(!cgi){
            //如果不是cgi程序,则就是静态页面请求
            serve_file(client_sock,path);//直接返回文件信息给客户端,静态页面被返回
        }
        else{
            //cgi程序,执行cgi脚本
            execute_cgi(client_sock,path,method,query_string);
            //query_string此时正指向url '?'之后的第一个字符,是参数部分
            //如果是POST模式,则就需要在 请求报文的body部分提取参数
            //如果是GET模式,就要在 query_string之后提取参数
            //path是保存的资源路径
        }
    }
    close(client_sock);
    //return;
}
void serve_file(int client_sock, const char *path)
{
    //用来返回文件
    FILE *resource=NULL;
    int numchars=1;
    char buf[1024];

    //这两个是什么意思?
    buf[0]='A';
    buf[1]='\0';

    while(numchars>0 && strcmp("\n",buf)){
        numchars=get_line(client_sock,buf,sizeof (buf));//把客户端套接字读取完
    }
    resource = fopen(path,"r");//r:只读的方式打开文件
    if(resource == NULL){
        not_found(client_sock);
    }
    else{
        headers(client_sock,path);//先返回文件头部信息
        cat(client_sock,resource);//将resource描述符指定文件中的数据发送给客户端的套接字
    }
    fclose(resource);
}

void headers(int client_sock, const char *path)
{
    char buf[1024];
    (void)path;//这句是什么意思?强制类型转化一下
    strcpy(buf, "HTTP/1.0 200 OK\r\n");
        send(client_sock, buf, strlen(buf), 0);
        strcpy(buf, SERVER_STRING);
        send(client_sock, buf, strlen(buf), 0);
        sprintf(buf, "Content-Type: text/html\r\n");
        send(client_sock, buf, strlen(buf), 0);
        strcpy(buf, "\r\n");
        send(client_sock, buf, strlen(buf), 0);
}

void cat(int client_sock, FILE *resource)
{
    //将文件结构指针resourece中的数据发送给client_sock
    char buf[1024];
    fgets(buf,sizeof (buf),resource);//从文件结构指针resource中读取数据,保存在buf中
    //处理文件流剩下的字符
    while(!feof(resource)){
        //指针没有到尾部
        send(client_sock,buf,strlen(buf),0);//将文件流中的字符全部发送给client_sock
        fgets(buf,sizeof (buf),resource);
    }
}

//从客户端套接字读取一行数据,以\r\n为结束符
//http的请求报文第一行也就是第一部分 是 请求方式 url http版本,GET的参数信息在url的?之后
//第二部分是请求头部,第一行之后的部分,说明服务器要使用的附加信息
//第三部分是空行,请求头部后面的空行是必须的
//最后一部分是主体,body,可以添加数据,POST的提交数据就在body里面
int get_line(int client_sock, char *buf, int buf_size)
{
  int i=0;
  int n;
  char c='\0';

  //读取一行
  while(i<buf_size-1 && c!='\n'){
      n=recv(client_sock,&c,1,0);//每次从client_sock里面读取一个字符

      if(n>0){
          if(c=='\r'){
              //读取到回车符就再读取一个,如果没有则结束,如果是换行符则丢弃回车符,以换行符为结尾
              n=recv(client_sock,&c,1,MSG_PEEK);//MSG_PECK标志将读取的字符保留在窗口
              if(n>0 && c=='\n'){
                  recv(client_sock,&c,1,0);//将上次保留的字符读取并丢弃
              }
              else{
                  c='\n';//窗口里的字符不是回车符或没有字符,则以回车符结尾
              }
          }
          buf[i]=c;
          i++;
      }
      //没有读取到数据
      else{
          c='\n';
      }
  }
  buf[i]='\0';//c风格字符串
  return i;
  //返回结束后,buf里面存有client_sock的一行数据,包括空格,但只有一行
}
void unimplemented(int client_sock)
{
    char buf[1024];
    //http method 不被支持
    sprintf(buf,"HTTP/1.0 501 Method Not Implemented\r\n");
    send(client_sock,buf,strlen(buf),0);

    //服务器信息
    sprintf(buf,SERVER_STRING);
    send(client_sock,buf,strlen(buf),0);
    sprintf(buf,"Content-Type:text/html\r\n");
    send(client_sock,buf,strlen(buf),0);
    sprintf(buf,"\r\n");
    send(client_sock,buf,strlen(buf),0);
    sprintf(buf,"<HTML><HEAD><TITLE>Method Not Implemented\r\n");
    send(client_sock,buf,strlen(buf),0);
    sprintf(buf,"</TITLE></HEAD>\r\n");
    send(client_sock,buf,strlen(buf),0);
    sprintf(buf,"<BODY><P>HTTP request method not supported.\r\n");
    send(client_sock,buf,strlen(buf),0);
    sprintf(buf,"</BODY></HTML>\r\n");
    send(client_sock,buf,strlen(buf),0);
}
void not_found(int client_sock)
{
    char buf[1024];
    sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n");
    send(client_sock, buf, strlen(buf), 0);
    sprintf(buf, SERVER_STRING);
    send(client_sock, buf, strlen(buf), 0);
    sprintf(buf, "Content-Type: text/html\r\n");
    send(client_sock, buf, strlen(buf), 0);
    sprintf(buf, "\r\n");
    send(client_sock, buf, strlen(buf), 0);
    sprintf(buf, "<HTML><TITLE>Not Found</TITLE>\r\n");
    send(client_sock, buf, strlen(buf), 0);
    sprintf(buf, "<BODY><P>The server could not fulfill\r\n");
    send(client_sock, buf, strlen(buf), 0);
    sprintf(buf, "your request because the resource specified\r\n");
    send(client_sock, buf, strlen(buf), 0);
    sprintf(buf, "is unavailable or nonexistent.\r\n");
    send(client_sock, buf, strlen(buf), 0);
    sprintf(buf, "</BODY></HTML>\r\n");
    send(client_sock, buf, strlen(buf), 0);
}
void execute_cgi(int client_sock, 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;//处理POST模式

    //这两个是什么意思?
    buf[0]='A';
    buf[1]='\0';

    if(strcasecmp(method,"GET")==0){
        //GET方法,一般用于获取or查询信息
        while(numchars>0 && strcmp("\n",buf)){
            numchars=get_line(client_sock,buf,sizeof(buf));//把客户端套接字里面的数据全部读取并存放在buf中,其实get模式的参数在url中
        }
    }
    else{
        //POST模式
        numchars = get_line(client_sock,buf,sizeof (buf));//读取一行

        //获取http消息实体的传输长度 content_length
        while(numchars>0 && strcmp("\n",buf)){
            buf[15]='\0';//说明buf留了前14位,看看是不是content-length:这十四个字符
            if(strcasecmp(buf,"Content-Length:")==0){
                content_length = atoi(&(buf[16]));//报文格式,content-length:之后就是长度,保存下来即可
            }
            numchars = get_line(client_sock,buf,sizeof (buf));//循环
        }
        if(content_length==-1){
            bad_request(client_sock);//请求的页面数据为空,没有数据,就是我们打开网页经常出现空白页面
            return;
        }
    }

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

    //建立管道,两个通道,cgi_output[0]:读取端,cgi_output[1]:写入端
    //管道只能在具有公共祖先的进程之间进行,这里是父子进程之间
    if(pipe(cgi_output)<0){
        //建立管道失败
        cannot_execute(client_sock);
        return;
    }

    if(pipe(cgi_input)<0){
        //建立管道失败
        cannot_execute(client_sock);
        return;
    }

    //创建子进程,这样就创建了父子进程之间的IPC通道
    if((pid=fork())<0){
        //创建进程失败
        cannot_execute(client_sock);
        return;
    }

    //下面是实现进程之间的管道通信机制
    /*子进程继承了父进程的pipe,然后通过关闭子进程output管道的输出端,input管道的写入端;
    关闭父进程output管道的写入端,input管道的输出端*/

    //子进程
    if(pid == 0){
        char meth_env[255];
        char query_env[255];
        char length_env[255];

        //复制文件句柄,重定向进程的标准输入输出
        //dup2 的第一个参数描述符关闭
        dup2(cgi_output[1],1);//标准输出重定向到output的写入端
        dup2(cgi_input[0],0);//标准输入重定向到input的输入端
        close(cgi_input[1]);
        close(cgi_output[0]);
        //这样outpput就是输出,input就是输入,理论是只用一个管道也可以
        sprintf(meth_env,"REQUEST_METHOD=%s",method);
        putenv(meth_env);//该函数作用是改变或增加环境变量,现在很少有程序用这个东西了,毕竟tinyhttpd也是一个很老的程序,搞不懂这个函数是干啥用的

        if(strcasecmp(method,"GET")==0){
            //GET模式
            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);//exec函数簇,执行CGI脚本,获取cgi的标准输出作为相应内容发送给客户端
        //通过dup2重定向,标准输出内容进入管道output的输入端
        exit(0);//子进程退出
    }
    else{
        //父进程
        close(cgi_output[1]);//关闭管道的一端,这样可以建立父子进程间的管道通信
        close(cgi_input[0]);
        /*通过关闭对应管道的通道,然后重定向子进程的管道某端,这样就在父子进程之间构建一条单双工通道如果不重定向,将是一条典型的全双工管道通信机制*/
        if(strcasecmp(method,"POST")==0){
            //POST模式
            for(i=0;i<content_length;i++){
                recv(client_sock,&c,1,0);//从客户端的套接字一次接受一个字符
                write(cgi_input[1],&c,1);//写入input,重定向到标准输入,就是作为输入用
                //数据传送:input[1]父进程 -> input[0]子进程 标准输入 执行cgi程序 ->  STDIN -> STDOUT -> output[1]子进程 标准输出 -> output[0] 父进程得到子进程的输出,发送给客户端套接字

            }
            while(read(cgi_output[0],&c,1)>0){
                //读取output的管道输出到客户端,output输出端为cgi脚本执行后的内容
                //即将cgi执行结果发送给客户端,即send到浏览器,如果不是POST则只有这一处理
                send(client_sock,&c,1,0);
            }

            //关闭剩下的管道端,子进程在执行dup2之后,就已经关闭了管道的一端,这里关闭另一端
            close(cgi_output[0]);
            close(cgi_input[1]);
            waitpid(pid,&status,0);//等待子进程结束
        }
    }
}
void bad_request(int client_sock)
{
    char buf[1024];
    /*将字符串存入缓冲区,再通过send函数发送给客户端*/
    sprintf(buf, "HTTP/1.0 400 BAD REQUEST\r\n");
    send(client_sock, buf, sizeof(buf), 0);
    sprintf(buf, "Content-type: text/html\r\n");
    send(client_sock, buf, sizeof(buf), 0);
    sprintf(buf, "\r\n");
    send(client_sock, buf, sizeof(buf), 0);
    sprintf(buf, "<P>Your browser sent a bad request, ");
    send(client_sock, buf, sizeof(buf), 0);
    sprintf(buf, "such as a POST without a Content-Length.\r\n");
    send(client_sock, buf, sizeof(buf), 0);
}
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);
}



这篇关于C++分析TinyHTTPd源码的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程