http框架的搭建

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sendfile.h>
#include <sys/wait.h>
#include <signal.h>

#define SIZE (1024 * 10)
typedef struct http_request{
    //1.創建一段緩衝區,存放首行信息
    char first_line[SIZE];
    //2.http的首行
    //  a)http的請求方法
    char* method;
    //  b)http中的url
    char* url;
    //  c)http的url_path
    char* url_path;
    //  d)http的query_string
    char* query_string;
    //3.http的header部分
    //  丟棄了大多數據,在此只保存Content_length
    int content_length;
}http_request;

int read_line(int64_t handle_fd, char temp[], ssize_t size)
{
    char c = '\0';
    ssize_t i = 0;
    //在不同的平臺上換行的表示方式也不同:\r、\n、\r\n
    //那麼我們就將這三種統一爲\n
    while (i < size - 1 && c != '\n'){
        //1.從socket中一次只讀取一個字符
        ssize_t read_size = recv(handle_fd, &c, 1, 0);
        if (read_size < 0){
            perror("recv");
            return -1;
        }
        if (read_size == 0){
            //預期得到\n才退出循環,結果現在讀取了0個字符,那麼也認爲是錯誤
            perror("recv");
            return -1;
        }
        //2.將不同平臺的換行均轉換爲\n
        if (c == '\r'){
            //當讀取的是\r的時候,還要看看下一個字符是不是\n
            //注意:recv的第四個參數是MSG_PEEK的時候,只會將緩衝區中的數據拿出來看一下再放回去
            read_size = recv(handle_fd, &c, 1, MSG_PEEK);
            if (read_size <= 0){
                perror("recv");
                return -1;
            }
            if (c == '\n'){
                //該平臺的分隔符是\r\n,現將其轉爲\n
                //下個字符是\n,那就將它讀取出來即可
                recv(handle_fd, &c, 1, 0);
                if (read_size <= 0){
                    perror("recv");
                    return -1;
                }
            }
            else{
                //該平臺的分隔符是\r,現將其轉爲\n
                c = '\n';
            }
        }
        //3.將讀取到的數據放入我們http_request的緩衝區中
        temp[i++] = c;
        //4.當前讀取的字符是\n就可以停止了
        if (c == '\n'){
            break;
        }
    }
    //這裏一定要用\0作爲讀取的結束標誌
    temp[i] = '\0';
    //返回讀取這一行所讀的字符個數
    return i;
}

int Split(char temp[], const char* split_ch, char* tok[], int tok_size)
{
    //使用strtok字符串切分函數
    //strtok一次只能切分一次,只在第一次調用的時候需傳入切分字符串,後面再切分該字符串的時候不需要在傳入切分的字符串了
    //這就說明strtok內部肯定維護了一個全局變量去記錄要切分的字符串,現在是多線程編程,是不能使用全局數據的
    //那麼,還有個和strtok一樣功能卻是線程安全的函數 --> strtok_r
    //strtok_r不再使用內部的一個靜態緩衝區,而是使用參數去記錄要切分的字符串緩衝區,這個參數是棧上開闢的就不會出現問題了
    char* point = NULL;
    int i = 0;
    char *pch;
    //1.第一次切分的時候,傳入需切分的字符串
    pch = strtok_r(temp, split_ch, &point);
    //2.循環調用strtok進行切分
    while (pch != NULL){
        if (i >= tok_size){
            return i;
        }
        tok[i++] = pch;
        //第二次以後調用的時候切分字符串的位置傳入NULL即可
        pch = strtok_r(NULL, split_ch, &point);
    }
    //3.返回切分後tok的元素個數
    return i;
}

int handle_first_line(int64_t handle_fd, http_request* req)
{
    int ret;
    //1.從socket中讀取出首行
    ret = read_line(handle_fd, req->first_line, sizeof(req->first_line));
    if (ret < 0){
        printf("read_line error!\n");
        return -1;
    }
    //2.解析首行,得到method和url
    //存放切分後的字符串 
    char* tok[10];
    //利用Split函數進行字符串切分
    //參數:按照" "(空格)切分req->temp,將結果放入能存放10個元素的數組tok中
    //返回值:切分後的tok中所含元素個數
    int tok_size = Split(req->first_line, " ", tok, 10);
    if (tok_size != 3){
        printf("Split error!\n");
        return -1;
    }
    req->method = tok[0];
    req->url = tok[1];
    //3.從url中解析出url_path和query_string
    char *p = req->url;
    req->url_path = p;
    for (; *p != '\0'; p++){
        if (*p == '?'){
            *p = '\0';
            req->query_string = p + 1;
            return 0;
        }
    }
    req->query_string = NULL;
    return 0;
}

int handle_header(int64_t handle_fd, http_request* req)
{
    char temp[SIZE] = {0};
    int ret;
    const char* str = "Content_Length: ";
    while (1){
        //1.循環從socket中讀取一行
        ret = read_line(handle_fd, temp, sizeof(temp)); 
        if (ret <= 0){
            printf("read_line error!\n");
            return -1;
        }
        //2.判定當前行是不是Content_Length
        //  a)是,將value取出
        //  b)不是直接丟棄
        if (strncmp(temp, str, strlen(str)) == 0){
            req->content_length = atoi(temp + strlen(str));
        }
        //3.注意:必須要讀到空行才能循環結束
        if (strcmp(temp, "\n") == 0){
            return 0;
        }
    }   
}

int is_dir(const char* file_path)
{
    struct stat st;
    int ret = stat(file_path, &st);
    if (ret < 0){
        perror("stat");
        return 0;
    }
    if (S_ISDIR(st.st_mode)){
        return 1;
    }
    return 0;
}

void handle_file_path(const char* url_path, char* file_path)
{
    //1.給url_path加上前綴
    sprintf(file_path, "./wwwroot%s", url_path);
    //2.如果用戶傳入的是目錄,那就默認取該路徑下的index.html文件
    //  a)判定用戶傳入的是目錄還是文件
    //  b)是目錄的話,判斷傳入的目錄是不是以'/'結尾的
    if (is_dir(file_path)){
        if (file_path[strlen(file_path) - 1] == '/'){
            strcat(file_path, "index.html");
        }
        else{
            strcat(file_path, "/index.html");
        }
    }
}

int get_file_length(const char* file_path)
{
    struct stat st;
    int ret = stat(file_path, &st);
    if (ret < 0){
        perror("stat");
        return 0;
    }
    return st.st_size;
}

int write_static_file(int64_t handle_fd, const char* file_path)
{
    //1.打開路徑下的文件
    int fd = open(file_path, O_RDONLY);
    if (fd < 0){
        printf("%s路徑打開失敗\n", file_path);
        return -1;
    }
    //2.讀取文件內容
    //3.根據文件內容構造HTTP響應
    //  sendfile可以在內核中實現從一個文件寫入到socket文件中
    //  在此相當於body部分的內容已經完成了
    int size = get_file_length(file_path);
    const char* first_line = "HTTP/1.1 200 OK\n";
    char Header[SIZE] = {0};
    sprintf(Header, "Content-Length:%d\n", size);
    const char* blank_line = "\n";
    //4.將響應內容寫入到socket中
    send(handle_fd, first_line, strlen(first_line), 0);
    send(handle_fd, Header, strlen(Header), 0);
    send(handle_fd, blank_line, strlen(blank_line), 0);
    sendfile(handle_fd, fd, NULL, size);
    close(fd);
    return 0;
}

int handle_static_file(int64_t handle_fd, const http_request* req)
{
    //1.根據url_path構造出真是文件的路徑
    char file_path[SIZE] = {0};
    handle_file_path(req->url_path, file_path);
    //2.打開文件,讀取文件內容,構造文件HTTP響應
    //  寫入成功返回0,寫入失敗返回-1
    int ret = write_static_file(handle_fd, file_path);
    return ret;
}

int child_handler_CGI(const http_request* req, int child_read, int child_write)
{
    //1.設定環境變量(REQUEST_METHOD,QUERY_STRING, CONTENT_LENGTH)
    //  這個操作必須在子進程中進行,否則由於父進程可能同時處理上百個子進程,此時數據會覆蓋
    char request_method_env[SIZE] = {0};
    sprintf(request_method_env, "REQUEST_METHOD=%s", req->method);
    putenv(request_method_env);

    if (strcmp(req->method, "GET") == 0){
        char query_string_env[SIZE] = {0};
        sprintf(query_string_env, "QUERY_STRING=%s", req->query_string);
        putenv(query_string_env);
    }
    else{
        char content_length_env[SIZE] = {0};
        sprintf(content_length_env, "CONTENT_LENGTH=%d", req->content_length);
        putenv(content_length_env);
    }
    //2.將標準輸入輸出重定向到管道上
    dup2(child_read, 0);
    dup2(child_write, 1);
    //3.根據url_path構造出CGI程序的路徑
    char file_path[SIZE] = {0};
    handle_file_path(req->url_path, file_path);
    //4.進行程序替換(會直接轉至CGI程序內部,即和業務相關的代碼中,後面的代碼就不會再執行了)
    execl(file_path, "file_path", NULL);
    return -1;
}

int father_handle_CGI(int64_t handle_fd, const http_request* req, int father_read, int father_write)
{
    //1.將HTTP請求的body部分寫入管道中
    if (strcmp(req->method, "POST") == 0){
        int content_length = req->content_length;
        char ch;
        int i, ret;
        for (i = 0; i < content_length; ++i){
            ch = 0;
            ret = recv(handle_fd, &ch, 1, 0);
            if (ret > 0){
                write(father_write, &ch, 1);
            }
            else{
                return -1;
            }
        }
    }
    //2.嘗試讀取子進程構造出的結果
    //3.父進程構造HTTP響應,寫會到客戶端
    const char* first_line = "HTTP/1.1 200 0k\n";
    const char* Header = "Content_Type:text/html;charset = utf-8\n";
    const char* blank_line = "\n";
    send(handle_fd, first_line, strlen(first_line), 0);
    send(handle_fd, Header, strlen(Header), 0);
    send(handle_fd, blank_line, strlen(blank_line), 0);
    char buff[1024] = {0};
    while (1){
        int readSize = read(father_read, buff, 1024);
        if (readSize <= 0){
            break;
        }
        send(handle_fd, buff, strlen(buff), 0);
    }
    //4.父進程回收子進程
    //  直接在服務器啓動的時候忽略SIGCHLD信號
    return 0;
}

int handle_CGI(int64_t handle_fd, const http_request* req)
{
    ////////////////////////////////////////////////////////////////
    //處理動態頁面需遵守CGI協議
    //1.HTTP服務器需創建子進程
    //2.子進程進行程序替換,替換成磁盤上的某個可執行程序
    //  a)父進程需傳遞給子進程的信息:
    //      請求方法(REQUEST_METHOD環境變量)
    //      若是GET還需獲得query_string(QUEY_STRING環境變量)
    //      若是POST還須獲得content_length(CONTENT_LENGTH)
    //      若是POST還需獲得body部分(通過匿名管道傳遞)
    //  b)子進程需傳遞給父進程的信息
    //      構造好的動態頁面(通過匿名管道傳遞)
    ////////////////////////////////////////////////////////////////

    //1.創建一對匿名管道
    int fd1[2];
    int fd2[2];
    pipe(fd1);
    pipe(fd2);
    int father_read = fd1[0];
    int child_write = fd1[1];
    int father_write = fd2[1];
    int child_read = fd2[0];
    //2.創建子進程
    int ret = fork();
    if (ret < 0){
        perror("fork");
        return -1;
    }
    //3.子進程執行子進程的相關邏輯
    else if (ret == 0){
        close(father_read);
        close(father_write);
        ret = child_handler_CGI(req, child_read, child_write);
        return ret;
    }
    //4.父進程執行父進程的相關邏輯
    else{
        close(child_read);
        close(child_write);
        ret = father_handle_CGI(handle_fd, req, father_read, father_write);
        close(father_read);
        close(father_write);
        return ret;
    }
}

void handle_404(int64_t handle_fd)
{
    //1.返回你的首行信息
    const char* first_line = "HTTP/1.1 404 Not Found\n";
    send(handle_fd, first_line, strlen(first_line), 0);
    //2.返回頭信息
    const char* Header = "Content-Type: text/html;charset = utf-8\n";
    send(handle_fd, Header, strlen(Header), 0);
    //3.返回一個空行
    const char* blank_line = "\n";
    send(handle_fd, blank_line, strlen(blank_line), 0);
    //4.返回一個錯誤的頁面信息
    const char* html = "<html><head><meta charset=\"utf-8\"></head> <body><h1>訪問有誤,頁面已丟失。。。</h1></body></html>";
    send(handle_fd, html, strlen(html), 0);
    //5.處理結束,返回
    return;
}

void* pthread_func(void* arg)
{
    int ret;
    int err_code = 200;
    //創建線程的時候,我們已經將處理數據的socket作爲參數傳過來了
    //在此只需要將參數轉化爲對應的類型就可以使用這個socket了
    //注意:64平臺下指針是佔8個字節的,而int是4個字節,因此爲了安全起見,我們使用int64_t類型
    int64_t handle_fd = (int64_t)arg;
    http_request req;
    memset(&req, 0x00, sizeof(http_request));
    //1.讀取並解析請求
    //  a)讀取並解析http的首行
    ret = handle_first_line(handle_fd, &req);
    if (ret < 0){
        printf("handle_first_line error!\n");
        err_code = 404;
        goto END;
    }
    //  b)接受並解析http的header部分
    ret = handle_header(handle_fd, &req);
    if (ret < 0){
        printf("handle_header error!\n");
        err_code = 404;
        goto END;
    }
    //2.靜態/動態方式生成頁面,把生成的結果寫回到客戶端上
    //請求方法可能會出現大小寫不同的問題,在這裏使用strcasecmp
    //strcasecmp進行字符串比較的時候不區分大小寫
    if (strcasecmp(req.method, "GET") == 0 && req.query_string == NULL){
        //a)如果請求方法是GET,並且沒有query_string,返回靜態頁面
        ret = handle_static_file(handle_fd, &req);
        if (ret < 0){
            printf("handle_static_file error!\n");
            err_code = 404;
            goto END;
        }
    }
    else if (strcasecmp(req.method, "GET") == 0 && req.query_string != NULL){
        //b)如果請求方法是GET,並且有quert_string,返回動態頁面
        ret = handle_CGI(handle_fd, &req);
        if (ret < 0){
            printf("handle_CGI error!\n");
            err_code = 404;
            goto END;
        }
    }
    else if (strcasecmp(req.method, "POST") == 0){
        //c)如果請求方法是POST,返回動態頁面
        ret = handle_CGI(handle_fd, &req);
        if (ret < 0){
            printf("handle_CGI error!\n");
            err_code = 404;
            goto END;
        }
    }
    else{
        //d)我們只解決GET、POST方法,那麼其他方法視爲錯誤
        printf("請求方法無法解決!\n");
        err_code = 404;
        goto END;
    }
    //3.錯誤處理:直接返回404頁面
END :
    //goto語句不建議使用,會導致程序出現不可預估的情況
    //爲了減少代碼的複雜性,迫不得已使用goto語句時,一定要往後跳,不能往前跳
    //若在C++中就可以使用異常來替換goto了
    if (err_code != 200){
        handle_404(handle_fd);
    }
    close(handle_fd);
    return NULL;
}

int main(int argc, char* argv[])
{
    //0.忽略SIGCHLD信號,在CGI的時候可以自動回收子進程
    signal(SIGCHLD, SIG_IGN);

    int ret;
    //1.將服務器的ip和port作爲參數傳進來
    if (argc != 3){
        printf("massage: ./http ip port\n");
        return 1;
    }
    //2.創建監聽套接字
    int lis_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (lis_fd < 0){
        perror("socket");
        return 1;
    }
    //3.進行端口綁定
    struct sockaddr_in lis_addr;
    socklen_t len = sizeof(struct sockaddr_in);
    lis_addr.sin_family = AF_INET;
    lis_addr.sin_port = htons(atoi(argv[2]));
    lis_addr.sin_addr.s_addr = inet_addr(argv[1]);
    ret = bind(lis_fd, (struct sockaddr*)&lis_addr, len);
    if (ret < 0){
        perror("bind");
        return 1;
    }
    //4.服務器開始監聽連接請求
    ret = listen(lis_fd, 5);
    if (ret < 0){
        perror("listen");
        return 1;
    }
    //5.開始循環處理客戶端請求
    while (1){
        //a)accept客戶端的連接請求
        struct sockaddr_in client;
        int64_t handle_fd = accept(lis_fd, (struct sockaddr*)&client, &len);
        if (handle_fd < 0){
            perror("accept");
            continue;
        }
        //b)爲了提高服務器處理數據的效率,在此使用線程
        pthread_t tid;
        pthread_create(&tid, NULL, pthread_func, (void*)handle_fd);
        //c)防止等待回收線程降低服務器效率,將創建的線程附上分離屬性
        pthread_detach(tid);
    }
    return 0;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章