1. 程式人生 > >通用的HTTP服務框架

通用的HTTP服務框架

未完成測試,註釋多,供參考

//該檔案包含了整個HTTP伺服器的實現
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/select.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/wait.h>

typedef struct sockaddr sockaddr;
typedef struct socket_in socket_in;
//定義一個結構體來表示緩衝區

#define SIZE (1024*4)
typedef struct HttpRequest{
    char* first_line;
    char* method;
    char* url;
    char* url_path;
    char* query_string
    int content_length;
}HttpRequest;

//從socket中讀取一行資料
//HTTP請求中換行符\n\r \n\r都可以相容處理
//核心思路:將未知問題轉換為已知問題.將\r\n 都轉化為\n
int ReadLine(int sock, char output[],ssize_t max_size)
{
    //1 一個字元一個字元的從socket中讀取資料
    char c = '\0';
    ssize_t i = 0;//記錄了output緩衝區中當前已經寫了多少個字元
    while (i < max_size)
    {
        ssize_t read_size = recv(sock, &c, 1, 0);
        if (read_size < 0)
        {
            //此處希望讀到完整一行,如果還沒讀到換行就讀到EOF就認為出錯
            return -1;
        }

        //2 判斷當前字元是不是\r
        if (c == '\r')
        {
            recv(sock, &c, 1, MSG_PEEK);//MSG_PEEK提前看緩衝區的內容,但是不讀取
            //3 如果當前字元是\r,嘗試讀取下一個字元
            // a)如果下一個字元是\n
            if (c == '\n')
            {
                recv(sock, &c, 1, 0);
            }//讀取字元將\r\n轉化為\n
            // b)如果下一個字元不是\n
            else
            {
                c = '\n';//將\r轉化為\n
            }
            //這兩種情況都把\r轉化成\n
        }
        //此時所有型別的分割符都被轉化為\n
        //4 如果當前字元是\n,已將這行讀完,結束函式
        if (c == '\n')
        {
            break;
        }
        //5 如果當前字元是一個普通字元,直接追加到輸出結果中
        output[i++] = c;
    }
    output[i] = '\0';
    return i;
}

//解析首行,獲取到其中的method和url:字串切分ftok
//首行格式:GET /index.html?a=10&b=20 HTTP/1.1
int ParseFirstLine(char first_line[], char** p_method, char** p_url)
{
    char* tok[10] = { 0 };
    //使用Split函式對字串進行切分,n表示切分結果有幾個部分
    int n=Split(first_line, " ",tok);
    if (n != 3)
    {
        //如果不是3,不符合HTTP協議
        printf("Split failed!n=%d\n");
        return -1;
    }
    //此處可以進行更加複雜的校驗
    *p_method = tok[0];
    *p_url = tok[1];
    return 0;
}

//進行字串切分
//strtok內部使用static變數來儲存字串切分的情況,如果有大量的客戶端去建立程序,就會
//使static變為共享資源,執行緒不安全
//使用strtok_r是執行緒安全版本,內部沒有靜態變數,需要使用者在棧上手動定義變數作為緩衝區去儲存.
int Split(char input[], const char*split_char, char* output[])
{
    char* tmp = NULL;//如果在tmp前面加static,那麼執行緒就會共用tmp,就不會執行緒安全
    int output_index = 0;
    char* p = strtok_r(input,split_char,&tmp);
    while (p != NULL)
    {
        output[output_index++] = p;
        p = strtok_r(NULL, split_char,&tmp);
    }
    return output_index;
}

//url形如: /index.html?a=10&b=20
//            http:/.www.baidu.com/index.html?a=10&b=20(不考慮)
int  ParseUrl(char url[], char** p_url_path, char** p_query_string)
{
    *p_url_path = url;
    //查詢問號所在位置
    char* p = url;
    for (; *p != '\0'; ++p)
    {
        if (*p == '?')
        {
            //找到了?,將之替換成\0
            *p = '\0';
            *p_query_string = p + 1;
            return 0;
        }
    }
    *p_query_string=NULL;//沒有找到
    return -1;
}

int ParseHeader(int new_sock, int* content_length)
{
    char buf[SIZE] = { 0 };
    while (1)
    {
        ssize_t read_size = ReadLine(new_sock, buf, sizeof(buf)-1);
        if (raed_size <= 0)
        {
            return -1;
        }
        if (strcmp(buf, "\n") == 0)
        {
            //讀到空行 
            return 0;
        }
        //Content_length:100\n
        const char* key = "Conten - Length: ";
        if (strncmp(buf, key, strlen(key)) == 0)
        {
            *content_length = atoi(buf + strlen(key));
            //break;使用break會出現粘包問題
        }
    }
    return 0;
}

void Handler404(int new_sock)
{
    const char* first_line = "HTTP/1.1 404 Not Found!\n";
    //此處可以不加header
    //Content-Type可以讓瀏覽器自動識別
    //Content-Length可以通過關閉socket的 方式告知瀏覽器已經讀完
    //body部分是 html的頁面
    const char* body = "<head><meta http-equiv=\"Content - Type\" "
        "content=\"text / html; charset = utf - 8\">"
        "</head><h1>404!!!頁面被吃了</h1>";
}

int IsDir(const char* file_path)
{
    struct stat st;
    int ret = stat(file_path, &st);
    if (ret < 0)
    {
        //此處不是目錄
        return 0;
    }
    if (S_ISDIR(st.st_mode))
    {
        return 1;
    }
    return 0;
}

void HandlerFilePath(const char* url_path, char file_path[])
{
    //url_path是以/開頭的,所以不需要wwwroot之後顯示指明/
    sprintf(file_path, "./wwwroot%s", url_path);
    //如果url_path指向目錄,就在目錄後面拼裝index.html作為預設訪問的檔案
    //識別url_path指向的是普通檔案還是目錄
    // a)url_path以/結尾,例如:/image/,一定是目錄
    if (file_path[strlen(file_path) - 1] == '/')
    {
        strcat(file_path, "index.html");
    }
    else
    {
        // b)url_path沒有以/ 結尾,此時需要根據檔案屬性來判定是否是目錄:stat函式獲取檔案屬性
        if (IsDir(file_path))
        {
            strcat(file_path, "./index.html");
        }
    }
}

ssize_t GetFileSize(const char* file_path)//int=2G,太小
{
    struct stat st;
    int ret = stat(file(file_path, &st));
    if (ret < 0)
    {
        return 0;
    }
    return st.st_size;
}

int WriteStaticFile(int new_sock, const char* file_path)
{
    //1.開啟檔案,如果開啟失敗,就返回404
    int fd = open(file_path,O_RDONLY)
    if (fd < 0)
    {
        perror("open");
        return 404;
    }
    //2.構造HTTP響應報文
    const char* first_line = "HTTP/1.1 200 OK\n";
    send(new_sock, first_line, strlen(first_line), 0);
    //此處如果更嚴謹,就需要加一些header
    //因為瀏覽器能夠自動識別Content-Type,就沒寫
    //沒寫conten_length是因為後面立刻關閉了socket
    //瀏覽器能識別資料在哪裡結束
    const char* blank_line = '\n';
    send(new_sock, first_line, strlen(blank_line), 0);
    //3.讀檔案並且寫入socket中
    //更高效:sendfile把一個檔案中的資料讀出來,寫到另一箇中.可以從中間開始
    /*char c = '\0';
    while (read(new_sock, &c, 1) > 0)
    {
        send(new_sock, &c, 1, 0);
    }*/
    ssize_t file_size = GetFileSize(file_path);
    sendfile(new_sock, fd, NULL, file_size);
    //4.關閉檔案
    colse(fd);
    return 200;
}

int HandlerStaticFile()
{
    //1.根據url_path獲取到檔案的真實目錄
    //例如,hTTP伺服器根目錄叫 ./wwwroot
    //此時有一個檔案叫做 ./wwwroot/image/101.jpg
    //在url中寫一個path就叫做 /image/101.jpg
    char file_path[SIZE] = { 0 };
    //根據下面的函式將 /image/101.jpg轉化為
    //磁碟上的 ./wwwroot/image/101.jpg
    HandlerFilePath(req->url_path, file_path);
    //2.開啟檔案,讀取檔案內容,把檔案內容寫到socket中
    int err_code = WriteStaticFile(new_sock, file_path);
    return err_code;
}

int HandlerCGIFather(int new_sock,int father_read,int father_write,const HttpRequest* req)
{
    // a)如果是POST請求,把body部分的資料讀出來寫到管道里,
    //剩下的動態生成頁面的過程都交給子程序來完成
    if (strcasecmp(req->method, "POST"))
    {
        //根據body的長度決定讀取多少個位元組
        char c = '\0';
        int i = 0;
        //使用迴圈防止 read被打斷,不加迴圈即使緩衝區夠長,
        for (; i < req->content_length; ++i)
        {
            read(new_sock, &c, 1);
            write(father_write, &c, 1);
        }
    }
    // b)構造HTTP響應
    const char* first_line = "HTTP/1.1 200 OK\n";
    send(new_sock, blank_line, strlen(blank_line),0);
    // c)從管道中讀取資料(子程序動態生成的頁面),把這個頁面也寫到socket當中,
    //此處不方便用sendfile,,主要是資料的長度不容易確定
    char c = '\0';
    while (raed(father_read, &c, 1) > 0)
    {
        write(new_sock, &c, 1);
    }
    // d)程序等待,回收子程序的資源.
    //此處如果要程序等待,最好使用waitpid,保證當前回收的子程序就是當年
    waitpid(NULL);
    return 200;
}


int HandlerCGIChild(int child_read,int child_write,int new_sock, int father_read, int father_write, const HttpRequest* req)
{
    //注意:環境變數寫在父程序中,雖然子程序能夠繼承父程序的環境變數,由於同一時刻會有很多個請求,每個請求
    //都在請求修改環境變數,會產生類似於執行緒安全的問題.導致子程序不能正確的獲取到這些資訊
    // a)設定環境變數(METHOD,CONTENT_LENGTH,QUERY_STRING)
    //   如果把上面的這幾個資訊通過管道來告知替換之後的程式,把這個資料也寫到socket當中也是可行的,但是此處要遵守CGI標準,
    //所以必須使用環境變數傳遞以上資訊.
    char method_env[SIZE] = { 0 };
    //REQUERY_METHOD=GET
    sprintf(method_env, "REQUEST_METHOD=%s", req->method);
    putenv(method_env);
    if (strcasecmp(req->method,"GET")==0)
    {//設定REQUERY_STRING
        char query_string_env[SIZE] = { 0 };
        sprintf(method_env, "QUERY_STRING=%s", req->query_string);
        putenv(query_string_env);
    }
    else
    {
        //設定CONTENT_LENGTH
        char conten_length_env[SIZE] = { 0 };
        sprint(conten_length_env, "CONTENT_LENGTH=%s", req->content_length);
        putenv(conten_length_env);
    }

    // b)把標準輸入和標準輸出重定向到管道中.此時,CGI讀寫標準輸入輸出就相當於讀寫管道.
    dup2(child_read,0);//把後面重定向到前面
    dup2(child_write, 1)
        // c)子程序進行程式替換(需要先找到是哪個CGI可執行程式,然後再使用exec函式進行替換)
        //   替換成功之後,動態頁面完全交給CGI程式來計算生成.
        char file_path[SIZE] = { 0 };
    HandlerFilePath(req->url_path, file_path);
    //l lp le
    //v vp ve
    //第一個引數是可執行程式的路徑
    //第二個引數,argv[0]
    //第三個引數NULL,表示命令列引數結束了
    execl(file_path,file_path,NULL)
    // d)替換失敗的錯誤處理,子程序就是為了替換而存在的
    //如果替換失敗,就沒有存在的必要了
}

int HandlerCGI(int new_sock)
{
    //1.建立一對匿名管道
    int fd[1], fd[2];
    pipe(fd1);
    pipe(fd2);
    int father_read = fd[0];
    int child_write = fd[1];
    int father_write = fd[1];
    int child_read = fd[0];
    //2.建立子程序fork
    pid_t ret = fork();
    if (ret > 0)
    {
        //father
        close(child_read);
        close(child_write);
        HandlerCGIFather();
    }
    else if (ret == 0)
    {
        //child;
        close(father_read);
        close(father_write);
        HandlerCGIChild();
    }
    else
    {
        perror("fork");
        goto END;
    }
    //3.父程序核心流程
    //father
    //此處先把不必要的檔案描述符關閉掉
    //
    close(child_write);
    close();
    //4.子程序的核心流程
    
//收尾工作
END:
    close();
    close();
    close();
}

//完成具體的請求處理過程
void HandlerRequest(int64_t new_sock)
{
    int err_code = 200;//定義一個預設的錯誤碼200,出錯置為400
    HttpRequest req;
    memset(&req, 0, sizeof(req));
    //1.解析請求
    // a)從socket中讀取首行
    char first_line[SIZE] = { 0 };
    if (ReadLine(new_sock,req.first_line,sizeof(req.first_line )-1) < 0)
    {
        //todo 錯誤處理,此處一旦觸發邏輯,就簡單處理,無腦返回404資料報.正常應該根據不同的錯誤原因返回不同的資料報
        err_code = 404;
        goto END;
    }
    printf("first_line:%s\n", req.first_line);
    // b)解析首行,獲取到url和method
    if (ParseFirstLine(req.first_line,&req.method,&req.url)<0)
    {
        //錯誤處理
        err_code = 404;
        goto END;
    }
    // c)解析url,獲取到url_path和query_string
    if (PerseUrl(req.url, &req.url_path, &req.query_string) < 0)
    {
        //錯誤處理
        err_code = 404;
        goto END;
    }
    // d)解析header,丟棄大部分header,只保留content-Length
    if (ParseHeader(new_sock, &req.content_length))
    {
        //錯誤處理
        err_code = 404;
        goto END;
    }
    //2.根據請求計算響應並能寫回客戶端
    if (strcasecmp(req.method, "GET")==0&&req.query_string==NULL)//strcasecmp忽略大小寫的比較
    {
        // a)處理靜態頁面
        err_code=HandlerStaticFile(new_sock,&req);
    }
    else if (strcasecmp(req.method, "GET") == 0 && req.query_string != NULL)
    {
        // b)處理動態頁面
        err_code=HandlerCGI();
    }
    else if (strcasecmp(req.method, "POST") == 0) {
        // b)處理動態頁面
        err_code=HandlerCGI();
    }
    else
    {
        //錯誤處理
        err_code = 404;
        goto END;
    }

    END:
    //收尾工作,主動關閉socket,會進入TIME_WAUT
    if (err_code != 200){
        Handler404(new_sock);
    }
    close(new_sock);
}


void* ThreadEntry(void *arg)
{
    int64_t new_sock = (int64_t)arg;
    HeadlerRequest(new_sock);
    return NULL;
}

void HttpServerStart(const char* ip, short port)
{
    //1.基本的初始化,基於TCP
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_sock < 0)
    {
        perror("socket");
        return;
    }
    //設定一個選項 REUSEADDR:地址重用
    int opt = 1;
    setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(OPT))

    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(ip);
    addr.sin_port = htons(port);
    int ret = bind(listen_sock, (sockaddr*)&addr, sizeof(addr));//一個程序可以繫結多個埠號,通常一個埠號不能被多個程序繫結
    if (ret < 0)
    {
        perror("bind");
        return;
    }
    ret = listen(listen_sock, 5);
    if (ret < 0)
    {
        perror("listen");
        return ;
    }
    printf("HttpServer start OK\n");
    
    //2.進入事件迴圈
    while (1)
    {
        //此處實現一個多執行緒版本的伺服器
        //每個請求都建立一個新的執行緒處理具體請求
        sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int64-t new_sock = accept(listen_sock, (sockaddr*)&peer, &len);//不能用static,因為N個執行緒不能共用同一個new_sock
        if (new_sock < 0)
        {
            perror("accept");
            continue;
        }
        pthread_t tid;
        pthread_create(&tid, NULL, THreadEntry, (void*)new_sock);
        //new_sock怎麼傳遞給執行緒入口函式,不能寫取地址.同一時刻伺服器可能會有很多連線執行緒,每個執行緒處理一個請求,所有的
        //請求共用同一份new_sock一定會出錯.正確的處理應該把new_sock按照值的方式傳遞到執行緒入口函式中.
        pthread_detach(tid);//執行緒分離
    }
}
int main(int argc,char* argv[])
{
    if (argc != 3)
    {
        printf("Usage ./http_server [ip] [port]\n");
        return 1;
    }
    signal(SIGCHLD, SIG_IGN);
    HttpServerStart(argv[1], atoi(argv[2]));
    return 0;
}