1. 程式人生 > >http伺服器小專案

http伺服器小專案

1. http專案整體框架

1.1http協議格式

http請求由三部分組成,分別是:起始行、訊息報頭、請求正文

Request Line<CRLF>

Header-Name: header-value<CRLF>

Header-Name: header-value<CRLF>

//一個或多個,均以<CRLF>結尾

<CRLF>


body//請求正文
  1. 起始行以一個方法符號開頭,以空格分開,後面跟著請求的URI和協議的版本,格式如下:
Method Request-URI HTTP-Version CRLF
其中 Method表示請求方法;Request-URI是一個統一資源識別符號;HTTP-Version表示請求的HTTP協議版本;CRLF表示回車和換行(除了作為結尾的CRLF外,不允許出現單獨的CRLF字元)。
  1. 請求方法(所有方法全為大寫)有多種,各個方法的解釋如下:

    GET 請求獲取Request-URI所標識的資源
    POST 在Request-URI所標識的資源後附加新的資料
    HEAD 請求獲取由Request-URI所標識的資源的響應訊息報頭
    PUT 請求伺服器儲存一個資源,並用Request-URI作為其標識
    DELETE 請求伺服器刪除Request-URI所標識的資源
    TRACE 請求伺服器回送收到的請求資訊,主要用於測試或診斷
    CONNECT 保留將來使用
    OPTIONS 請求查詢伺服器的效能,或者查詢與資源相關的選項和需求
    應用舉例: 
    GET方法:在瀏覽器的位址列中輸入網址的方式訪問網頁時,瀏覽器採用GET方法向伺服器獲取資源,eg:
    
    GET /form.html HTTP/1.1
    (CRLF) POST方法要求被請求伺服器接受附在請求後面的資料,常用於提交表單。eg: POST /reg.jsp HTTP/ (CRLF) Accept:image/gif,image/x-xbit,... (CRLF) ... HOST:www.guet.edu.cn (CRLF) Content-Length:22 (CRLF) Connection:Keep-Alive (CRLF) Cache-Control:no-cache (CRLF) (CRLF) //該CRLF表示訊息報頭已經結束,在此之前為訊息報頭 user=jeffrey&pwd=1234
    //此行以下為提交的資料

    這裡寫圖片描述

1.2整體流程

  1. 伺服器啟動,等待客戶端請求到來;

  2. 客戶端請求到來,建立新執行緒處理該請求;

  3. 讀取httpHeader中的method,擷取url,其中GET方法需要記錄url問號之後的引數串;

  4. 根據url構造完整路徑,如果是/結尾,則指定為該目錄下的index.html;

  5. 獲取檔案資訊,如果找不到檔案,返回404,找到檔案則判斷檔案許可權;

  6. 如果是GET請求並且沒有引數,或者檔案不可執行,則直接將檔案內容構造http資訊返回給客戶端;

  7. 如果是GET帶引數,POST,檔案可執行,則執行CGI;

  8. GET請求略過httpHeader,POST方法需要記錄httpHeader中的Content-Length:xx;

  9. 建立管道用於父子程序通訊,fork產生子程序;

  10. 子程序設定環境變數,將標準輸入和輸出與管道相連,並且通過exec執行CGI;

  11. 如果是POST,父程序將讀到post內容傳送給子程序,並且接收子程序的輸出,輸出給客戶端

2. 部分專案原始碼

#include"thttpd.c"

static void* msg_request(void *arg)
{
    int sock=(int)arg;
    pthread_detach(pthread_self()); 
    return (void*)handler_msg(sock);
}

int main(int argc,char* argv[])
{
    //daemon(1,0);
    if(argc!=3)
    {
        printf_log("Usage: [local_ip] [local_port]\n",FATAL);
        return 1;
    }

    int lis_sock=startup(argv[1],atoi(argv[2]));

    while(1)
    {
        struct sockaddr_in peer;
        socklen_t len=sizeof(peer);

        int sock=accept(lis_sock,(struct sockaddr*)&peer,&len);

        if(sock<0)
        {
            printf_log("accept failed",WRONING);
            continue;
        }       
        pthread_t tid;
        if(pthread_create(&tid,NULL,msg_request,(void*)sock)>0)
        {
            printf_log("pthread_create failed",WRONING);
            close(sock);
        }
    }
    return 0;
}

thttpd.c

#include"thttpd.h"
//mysql 關係型資料庫
int startup(const char* _ip,int _port) //建立監聽套接字
{
    assert(_ip);

    int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock<0)
    {
        printf_log("socket failed",FATAL);
        exit(2);
    }

    int opt=1;                     
    setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));

    struct sockaddr_in local;
    local.sin_family=AF_INET;
    local.sin_port=htons(_port);
    local.sin_addr.s_addr=inet_addr(_ip);

    if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
    {
        printf_log("bind failed",FATAL);
        exit(3);
    }

    if(listen(sock,5)<0)
    {
        printf_log("listen failed",FATAL);
        exit(4);
    }

    return sock;
}

void printf_log(const char* msg,int level)     //記錄日誌
{
    openlog("thttpd",LOG_PID|LOG_CONS|LOG_NDELAY,LOG_USER);
    switch(level)
    {
    case NORMAL:
        syslog(LOG_NOTICE,msg);
        break;
    case WRONING:
        syslog(LOG_WARNING,msg);
        break;
    case FATAL:
        syslog(LOG_ERR,msg);
        break;
    }
    closelog();
}


static int get_line(int sock,char* buf)   //按行讀取請求報頭
{
    char ch='\0';
    int i=0;
    ssize_t ret=0;
    while(i<SIZE && ch!='\n')
    {
        ret=recv(sock,&ch,1,0);
        if(ret>0&&ch=='\r')
        {
            ssize_t s=recv(sock,&ch,1,MSG_PEEK);
            if(s>0&&ch=='\n')
            {
                recv(sock,&ch,1,0);
            }
            else
            {
                ch='\n';
            }
        }
        buf[i++]=ch;
    }
    buf[i]='\0';
    return i;
}


static void clear_header(int sock)    //清空訊息報頭
{
    char buf[SIZE];
    int ret=0;
    do
    {
        ret=get_line(sock,buf);
    }while(ret!=1&&(strcmp(buf,"\n")!=0));
}

static void show_404(int sock)      //404錯誤處理
{
    clear_header(sock);
    char* msg="HTTP/1.0 404 Not Found\r\n";
    send(sock,msg,strlen(msg),0);         //傳送狀態行
    send(sock,"\r\n",strlen("\r\n"),0);      //傳送空行

    struct stat st;
    stat("wwwroot/404.html",&st);
    int fd=open("wwwroot/404.html",O_RDONLY);
    sendfile(sock,fd,NULL,st.st_size);
    close(fd);
}

void echo_error(int sock,int err_code)    //錯誤處理
{
    switch(err_code)
    {
    case 403:
        break;
    case 404:
        show_404(sock);
        break;
    case 405:
        break;
    case 500:
        break;
    defaut:
        break;
    }
}


static int echo_www(int sock,const char * path,size_t s)  //處理非CGI的請求
{
    int fd=open(path,O_RDONLY);
    if(fd<0)
    {
        echo_error(sock,403);
        return 7;
    }

    char* msg="HTTP/1.0 200 OK\r\n";
    send(sock,msg,strlen(msg),0);         //傳送狀態行
    send(sock,"\r\n",strlen("\r\n"),0);      //傳送空行

    if(sendfile(sock,fd,NULL,s)<0)
    {
        echo_error(sock,500);
        return 8;   
    }
    close(fd);
    return 0;
}


static int excu_cgi(int sock,const char* method,\
        const char* path,const char* query_string) //處理CGI模式的請求
{
    char line[SIZE];
    int ret=0;
    int content_len=-1;

    if(strcasecmp(method,"GET")==0)         //如果是GET方法的CGI
    {
        //清空訊息報頭
        clear_header(sock);
    }
    else              //POST方法的CGI
    {
        //獲取post方法的引數大小
        do
        {
            ret=get_line(sock,line);
            if(strncmp(line,"Content-Length: ",16)==0)  //post的訊息體記錄正文長度的欄位
            {
                content_len=atoi(line+16);  //求出正文的長度
            }
        }while(ret!=1&&(strcmp(line,"\n")!=0));
    }

    //因為環境變數有上限,而GET方法傳遞的引數少,所以用環境變數
    //POST方法,在正文內傳參,引數多,所以用管道
    //將method、query_string、content_len匯出為環境變數
    char method_env[SIZE/8];
    char query_string_env[SIZE/8];
    char content_len_env[SIZE/8];

    sprintf(method_env,"METHOD=%s",method);
    //把是什麼方法放到環境變數裡,方便子程序執行的cGI判斷是什麼方法。
    putenv(method_env);

    sprintf(query_string_env,"QUERY_STRING=%s",query_string);
    putenv(query_string_env);

    sprintf(content_len_env,"CONTENT_LEN=%d",content_len);
    putenv(content_len_env);


    int input[2];      //站在CGI程式的角度,建立兩個管道
    int output[2];

    pipe(input);
    pipe(output);

    pid_t id=fork();

    if(id<0)
    {
        printf_log("fork failed",FATAL);
        echo_error(sock,500);
        return 9;
    }
    else if(id==0)     //子程序
    {
        close(input[1]);    //關閉input的寫端      
        close(output[0]);  //關閉output的讀端

        //將檔案描述符重定向到標準輸入標準輸出
        dup2(input[0],0);
        dup2(output[1],1);

        execl(path,path,NULL);         //替換CGI程式
        exit(1);
    }
    else         //father
    {
        close(input[0]);      //關閉input的讀端
        close(output[1]);     //關閉output的寫端

        char ch='\0';
        if(strcasecmp(method,"POST")==0) //如果是post方法,則父程序需要給子程序傳送引數
        {
            int i=0;
            for( i=0;i<content_len;i++)
            {
                recv(sock,&ch,1,0);        //從sock裡面一次讀一個字元,總共讀conten_len個字元
                write(input[1],&ch,1);
            }
        }

    //  char* msg="HTTP/1.0 200 OK\r\n\r\n";
    //  send(sock,msg,strlen(msg),0);

//  clear_header(sock);
    char* msg="HTTP/1.0 200 OK\r\n";
    send(sock,msg,strlen(msg),0);         //傳送狀態行
    send(sock,"\r\n",strlen("\r\n"),0);      //傳送空行
        //接收子程序的返回結果
        while(read(output[0],&ch,1))      //如果CGI程式結束,寫端關閉,則讀端返回0
        {
            send(sock,&ch,1,0);
//          printf("%c",ch);
        }
        waitpid(id,NULL,0);              //回收子程序
    }

    return 0;
}


int handler_msg(int sock)       //瀏覽器請求處理函式
{
    char buf[SIZE];
    int count=get_line(sock,buf);
    int ret=0;
    char method[32];
    char url[SIZE];
    char *query_string=NULL;
    int i=0;
    int j=0;
    int cgi=0;

    //獲取請求方法和請求路徑
    while(j<count)
    {
        if(isspace(buf[j]))
        {
            break;
        }
        method[i]=buf[j];   
        i++;
        j++;
    }
    method[i]='\0';
    while(isspace(buf[j])&&j<SIZE)      //過濾空格
    {
        j++;
    }
    //如果是POST方法一定要用到cgi
    //如果是GET方法帶引數也要用到cgi
    if(strcasecmp(method,"POST")&&strcasecmp(method,"GET"))
    {
        printf_log("method failed",FATAL);
        echo_error(sock,405);
        ret=5;
        goto end;
    }

    if(strcasecmp(method,"POST")==0)
    {
        cgi=1;
    }   

    i=0;
    while(j<count)
    {
        if(isspace(buf[j]))
        {
            break;
        }
        if(buf[j]=='?')
        {
            query_string=&url[i];
            query_string++;
            url[i]='\0';
        }
        else
            url[i]=buf[j];
        j++;
        i++;
    }
    url[i]='\0';

    if(strcasecmp(method,"GET")==0&&query_string!=NULL)
    {
        cgi=1;
    }

    //找到請求的資源路徑
    char path[SIZE];
    sprintf(path,"wwwroot%s",url);       
    struct stat st;
    if(stat(path,&st)<0)            //獲取客戶端請求的資源的相關屬性
    {

        printf_log("stat path faile\n",FATAL);
        echo_error(sock,404);
        ret=6;
        goto end;
    }
    else
    {
        printf("S_ISDIR::%d\n",S_ISDIR(st.st_mode));
        if(S_ISDIR(st.st_mode))
        {
            strcat(path,"/index.html");
        }
        else if(st.st_mode & (S_IXUSR | S_IXOTH | S_IXGRP))
        {
            cgi=1;
        }
    }
    printf("cgi=%d\n",cgi);
    if(cgi)       //請求的是可執行程式或者帶引數用cgi程式處理
    {
        ret=excu_cgi(sock,method,path,query_string);           //處理CGI模式的請求
    }
    else
    {
        clear_header(sock);
        ret=echo_www(sock,path,st.st_size);  //如果是GET方法,而且沒有引數,請求的也不是可執行程式,則直接返回資源  
    }   
end:
    close(sock);
    return ret;
}

3. 專案效果展示

這裡寫圖片描述