当前位置: 首页 > 工具软件 > Tiny httpd > 使用案例 >

Tiny-httpd源码解析

叶谦
2023-12-01
#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>
#include <stdint.h>

#define ISspace(x) isspace((int)(x))
#define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n"
#define STDIN 0
#define STDOUT 1
#define STDERR 2

void accept_request(void *);
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);

void accept_request(void * arg){
    /*
        arg:一个已连接的套接字描述符
        该函数接收客户端的请求,返回响应
    */
    int client=(intptr_t)arg;
    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;
    char*query_string=NULL;
    numchars=get_line(client,buf,sizeof(buf));
    i=0;j=0;
    while(!ISspace(buf[i])&&i<sizeof(method)-1){            //读取请求报文的方法后面加\0
        method[i]=buf[i];
        i++;
    }
    j=i;
    method[i]='\0';

    if(strcasecmp(method,"GET")&&strcasecmp(method,"POST")){ //如果方法不是GET和POST,发送501响应报文,返回
        unimplemented(client);
        return;
    }
    if(strcasecmp(method,"POST")==0)
        cgi=1;
    i=0;
    while(ISspace(buf[j])&&(j<numchars))
        j++;
    while(!ISspace(buf[j])&&(i<sizof(url)-1)&&(j<numchars)){ //读取url
        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 指向‘?’或‘\0'
            query_string++;
        }
        if(*query_string=='?'){                             //如果query_string所指字符为’?‘则将其设置为\0然后指向下一个字符
            cgi=1;
            *query_string='\0';
            query_string++;
        }
    }
    sprintf(path,"thdocs%s",url);                           //将url写入path,thdocs为该源文件目录下的thodocs文件夹,如path为thodocs/或thodocs/index.html
    if(path[strlen(path)-1]=='/')                           //如果path最后一个字符为/则将入后缀index.html
        strcat(path,"index.html");
    if(stat(path,&st)==-1){
        while((numchars>0)&&strcmp("\n",buf))               //获取文件的状态存于 st中,如果获取失败则向客户端发送没发现文件
            numchars=get_line(client,buf,sizeof(buf));
        not_found(client);

    }
    else{
        if((st.st_mode & s_IFMT)==S_IFDIR)                 //如果path指向的是目录则添加/index.html
            strcat(path,"/index.html");
        if((st.st_mode & S_IXUSR)||(st.st_mode& S_IXGRP)||(st.st_mode & S_IXOTH)) //查看是否具有可执行权限,如果有则设cgi为1
            cgi=1;                                         //如果方法为POST或方法为GET但是url中包含?或url所指文件有可执行权限则设置cgi为1否则设置为0
        if(!cgi)
            serve_file(client,path);                       //如果cgi为0直接将访问的文件返回
        else                                               //否则执行访问文件将文件执行结果返回给客户端
            execute_cgi(client,path,method,query_string);
    }

    close(client);
}

void serve_file(int client,const char *filename){
    /*
        client:客户套接字描述符
        filename:访问文件路径
        该函数发送响应报文,报文主体为访问文件
    */
    FILE *resource=NULL;
    int numchars=1;
    char buf[1024];

    buf[0]='A';
    buf[1]='\0';
    while((numchars>0)&&strcmp(buf,"\n"))                 //略过请求报文头部直到读到空行或读不到东西
    {
        numchars=get_line(client,buf,sizof(buf));
    }

    resource=fopen(filename,"r");
    if(resource==NULL)                                   //如果打不开资源则向客户端发送找不到文件报文
        not_found(client);
    else{
        header(client,filename);                         //发送报文首部
        cat(client,resource);                            //读文件,将文件写入到报文主体然后发送 
    }
    fclose(resource);
}

void headers(int client,const char *filename){
    /*
        client:客户端套接字描述符
        filename:访问文件路径
        该函数发送响应报文的头部
    */
    char buf[1024];
    (void)filename;

    strcpy(buf, "HTTP/1.0 200 OK\r\n");
    send(client, buf, strlen(buf), 0);
    strcpy(buf, SERVER_STRING);
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "Content-Type: text/html\r\n");
    send(client, buf, strlen(buf), 0);
    strcpy(buf, "\r\n");
    send(client, buf, strlen(buf), 0);
}

void cat(int client, FILE *resource)                
{
    /*
        client:客户端套接字描述符
        resource:访问文件指针
        该函数读取文件并作为响应报文的主体发送给客户端
    */
    char buf[1024];

    fgets(buf, sizeof(buf), resource);
    while (!feof(resource))
    {
        send(client, buf, strlen(buf), 0);
        fgets(buf, sizeof(buf), resource);
    }
}

void execute_cgi(int client,const char *path,const char* method,const char *query_string){
    /*
        client:客户套接字描述符
        path:访问文件路径
        method:请求报文方法
        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[0]='A';buf[1]='\0';
    if(strcasecmp(method,"GET")==0){                                        
        while(numchars>0&&strcmp(buf,"\n"))             //略过报文头部到达报文主体
            numchars=get_line(client,buf,sizeof(buf));
    }
    else if(strcasecmp(method,"POST")==0){
        numchars=get_line(client,buf,sizeof(buf));
        while(numchars>0&&strcmp(buf,"\n")){           //略过报文头部,并且检查有无content-length首部字段,如果有则解析出内容长度
            buf[15]='\0';
            if(strcmp(buf,"Content-Length:")==0)
                content_length=atoi(buf+16);
            numchars=getline(client,buf,sizeof(buf));
        }
        if(content_length==-1)                        //如果请求报文没有content-length首部字段则发送给客户端bad_request响应报文
            bad_request(client);
            return;
    }
    else{                                             //若果请求报文是其他方法则不处理,本服务器只能处理GET和POST两种方法

    }
    if(pipe(cgi_output)<0){connot_execute(client);return;} //生成用于进程间通信的管道用于父进程向子进程(所访问的文件)传送消息
    if(pipe(cgi_input)<0){connot_execute(client);return;}  //生成用于进程间通信的管道用于子进程向父进程传送消息
    
    sprintf(buf,"HTTP/1.0 200 OK\r\n");                    //发送响应报文首部
    send(client,buf,strlen(buf),0);

    if((pid=fork())<0){connot_execute(client);return;}    //创建子进程    
    if(pid==0){                                           //子进程

        char meth_env[255];
        char query_env[255];
        char length_env[255];

        dup2(cgi_input[0],STDIN);                        //将输入管道的读端口和标准输出绑定(重定向)
        dup2(cgi_output[1],STDOUT);                      //将输出管道的写端口和标准输出绑定
        close(cgi_input[1]);                             //关闭输入管道的写端口
        close(cgi_output[0]);                            //关闭输出管道的读端口
        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);                           //如果是GET方法,添加此环境变量
        }
        else{
            sprintf(length_env,"CONTENT_LENGTH=%d",content_length);
            putenv(length_env);                          //如果是POST方法,添加此环境变量
        }
        execl(path,NULL);                                //执行访问文件
        exit(0);
    }
    else{                                                //父进程
        close(cgi_input[0]);                             //关闭输入管道读端口
        close(cgi_output[1]);                            //关闭输入管道写端口
        if(strcasecmp(method,"POST")==0){                //如果是POST方法,则将客户端发过来的请求报文的主体传给正在执行的访问文件进程    
            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);                         //等待子进程,处理僵尸进程
    }



}

void bad_request(int client)
{   
    /*
        client:客户套接字接口
        发送400响应报文
    */
    char buf[1024];

    sprintf(buf, "HTTP/1.0 400 BAD REQUEST\r\n");
    send(client, buf, sizeof(buf), 0);
    sprintf(buf, "Content-type: text/html\r\n");
    send(client, buf, sizeof(buf), 0);
    sprintf(buf, "\r\n");
    send(client, buf, sizeof(buf), 0);
    sprintf(buf, "<P>Your browser sent a bad request, ");
    send(client, buf, sizeof(buf), 0);
    sprintf(buf, "such as a POST without a Content-Length.\r\n");
    send(client, buf, sizeof(buf), 0);
}

void unimplemented(int client){
    /*
        client:客户套接字接口
        发送501响应报文
    */
    char buf[1024];

    sprintf(buf,"HTTP/1.0 501 Method Not Impementid\r\n");
    send(client,buf,strlen(buf),0);
    sprintf(buf,SERVER_STRING);
    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, "<HTML><TITLE>Not Found</TITLE>\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "<BODY><P>The server could not fulfill\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "your request because the resource specified\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "is unavailable or nonexistent.\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "</BODY></HTML>\r\n");
    send(client, buf, strlen(buf), 0);
}

void cannot_execute(int client){
    /*
        client:客户套接字接口
        发送500响应报文
    */
    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);
}

void not_found(int client)
{
    /*
        client:客户套接字接口
        发送404响应报文
    */
    char buf[1024];

    sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, SERVER_STRING);
    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, "<HTML><TITLE>Not Found</TITLE>\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "<BODY><P>The server could not fulfill\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "your request because the resource specified\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "is unavailable or nonexistent.\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "</BODY></HTML>\r\n");
    send(client, buf, strlen(buf), 0);
}

void erro_die(const char *sc){
    /*
        sc:错误原因
        该函数打印错误原因并退出
    */
    perror(sc);
    exit(1);
}
int get_line(int sock,char *buf,int size){
    /*
        sock:客户套接字描述符
        buf:用来存放一行字符
        size:buf数组大小
        读取一行以\0结尾
    */
    int i=0;
    char c='0';
    int n;
    
    while((i<size-1)&&(c!='\n')){              //当入到\n、\r\n、\r时将其换为\n最后加上\0作为一行
        n=recv(sock,&c,1,0);
        if(n>0){
            if(c=='\r'){
                n=recv(sock,&c,1,MSG_PEEK);
                if(n>0&&c=='\n')
                    recv(sock,&c,1,0);
                else
                    c='\n';
            }
            buf[i]=c;
            i++;
        }
        else
            c='\n';
    }
    buf[i]='\0';
    return i;
}
int startup(u_short *port){
    /*
        port:服务器套接字要绑定的tcp端口
        创建套接字,并对其进行绑定和转换为监听态
    */
    int httpd=0;                             //服务器套接字描述符
    int on=1;
    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((setsockopt(httpd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)) //设置套接字选项
        error_die("setsockopt failed");
    if(bind(httpd,(struct sockaddr *) &name,sizeof(name))<0)         //绑定套接字地址
        error_die("bind");
    if(*port == 0){                          //如果为默认端口,取其端口号
        socklen_t namelen=sizeof(name);                                 
        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);
}


int main(){
    int server_sock=-1;                     //服务器套接字描述符
    u_short port=4000;
    int client_sock=-1;                     //已连接套接字
    struct sockaddr_in client_name;         //客户套接字结构
    socklen_t client_name_len=sizeof(client_name);                          

    server_sock=startup(&port);             //创建套接字并设置为监听态,返回监听套接字描述符,同时返回绑定的端口号
    printf("httpd runing on port %d\n",port);
    while(1){
        client_sock=accept(server_sock,(struct sockaddr*)&client_name,&client_name); //接收一个客户连接
        if(client_sock==-1)
            erro_die("accept");
        accept_request(&client_sock);      //对客户进行服务       
    }
    close(server_sock);                    //关闭服务器套接字   
    return 0;
}
 类似资料: