C 實現一個簡易的Http服務器 (二)


正文 - 直接搞起

  C 實現一個簡易的Http服務器

  很久以前寫過一個簡易的http服務器, 后面和一個朋友交流, 反思后發現問題不少.在這里簡單搞一下.

讓其更加簡單去表現httpd本質, 弱化協議業務. 方便當httpd入手學習的demo.  ok, 那直接代碼走起 ~

Makefile - 編譯部分

all:httpd.exe client.exe

httpd.exe : httpd.c
    gcc -g -Wno-unused-result -Wno-int-to-pointer-cast -Wno-pointer-to-int-cast -Wall -O2 -o $@ $^ -lpthread

client.exe : client.c
    gcc -g -Wall -o $@ $^

clean:
    rm -rf *.exe

client.c - 簡單的測試客戶端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>

#define CERR(fmt, ...) \
    fprintf(stderr, "[%s:%s:%d][errno %d:%s]" fmt "\n",\
        __FILE__, __func__, __LINE__, errno, strerror(errno), ##__VA_ARGS__)

#define CERR_EXIT(fmt,...) \
    CERR(fmt, ##__VA_ARGS__), exit(EXIT_FAILURE)

#define CERR_IF(code) \
    if((code) < 0) \
        CERR_EXIT(#code)

//待拼接的字符串
#define _STR_HTTPBEG "GET /index.html HTTP/1.0\r\nUser-Agent: Happy is good.\r\nHost: 127.0.0.1:"
#define _STR_HTTPEND "\r\nConnection: close\r\n\r\n"

// 簡單請求一下
int main(int argc, char * argv[]) {
    int sfd;
    int len, port;
    char buf[BUFSIZ];
    struct sockaddr_in saddr = { AF_INET };

    // argc 默認為1 第一個參數 就是 執行程序串
    if((argc != 2) || (port = atoi(argv[1])) <= 0 )
        CERR_EXIT("Usage: %s [port]", argv[0]);
    
    // 開始了,就這樣了    
    CERR_IF(sfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP));
    saddr.sin_port = htons(port);
    CERR_IF(connect(sfd, (struct sockaddr *)&saddr, sizeof saddr));
    
    //開始發送請求
    strcpy(buf, _STR_HTTPBEG);
    strcat(buf, argv[1]);
    strcat(buf, _STR_HTTPEND);
    write(sfd, buf, strlen(buf));

    //讀取所喲內容
    while((len = read(sfd, buf, sizeof buf - 1)) > 0) {
        buf[len] = '\0';
        printf("%s", buf);    
    }
    putchar('\n');    

    close(sfd);
    return EXIT_SUCCESS;
}

httpd.c - 簡易http服務器主體

#include <stdio.h>
#include <errno.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <arpa/inet.h>
    
#define CERR(fmt, ...) \
    fprintf(stderr, "[%s:%s:%d][errno %d:%s]" fmt "\n",\
        __FILE__, __func__, __LINE__, errno, strerror(errno), ##__VA_ARGS__)

#define CERR_EXIT(fmt,...) \
    CERR(fmt, ##__VA_ARGS__), exit(EXIT_FAILURE)

#define CERR_IF(code) \
    if((code) < 0) \
        CERR_EXIT(#code)

//
// getfdline - 讀取文件描述符 fd 一行的內容,保存在buf中,返回讀取內容長度 
// fd        : 文件描述符
// buf        : 保存的內容
// sz        : buf 的大小
// return    : 返回讀取的長度
//
int getfdline(int fd, char buf[], int sz);

// 返回400 請求解析失敗, 客戶端代碼錯誤
void response_400(int cfd);
// 返回404 文件內容, 請求文件沒有找見
void response_404(int cfd);
// 返回501 錯誤, 不支持的請求
void response_501(int cfd);
// 服務器內部錯誤,無法處理等
void response_500(int cfd);
// 返回200 請求成功 內容, 后面可以加上其它參數,處理文件輸出
void response_200(int cfd);
// 服務器返回請求的文件內容
void response_file(int cfd, const char * path);

//
// request_start - 啟動一個httpd監聽端口, 使用隨機端口
// pport     : 輸出參數和輸出參數, 如果傳入NULL, 將不返回自動分配的端口
// return    : 返回啟動的文件描述符
//
int request_start(uint16_t * pport);

// 在客戶端鏈接過來, pthread 多線程處理的函數
void * request_accept(void * arg);

//
// request_cgi - 處理客戶端的http請求.
// cfd        : 客戶端文件描述符
// path        : 請求的文件路徑
// type        : 請求類型,默認是POST,其它是GET
// query    : 請求發送的過來的數據, url ? 后面那些數據
// return    : void
//
void request_cgi(int cfd, const char * path, const char * type, const char * query);

//
// 主邏輯,啟動服務,可以做成守護進程.
// 具體的實現邏輯, 啟動小型玩樂級別的httpd 服務
//
int main(int argc, char * argv[]) {
    uint16_t port = 0;
    pthread_attr_t attr;
    int sfd = request_start(&port);
    
    printf("httpd running on port %u.\n", port);
    // 初始化線程屬性
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    for(;;){
        pthread_t tid;
        int cfd = accept(sfd, NULL, NULL);
        if(cfd < 0){
            CERR("accept sfd = %d is error!", sfd);
            break;
        }
        if(pthread_create(&tid, &attr, request_accept, (void *)cfd) < 0)
            CERR("pthread_create run is error!");
    }
    // 銷毀吧, 一切都結束了
    pthread_attr_destroy(&attr);
    close(sfd);
    return EXIT_SUCCESS;
}

int 
getfdline(int fd, char buf[], int sz) {
    char c, * tp = buf;
    
    --sz;
    while((tp-buf)<sz){
        if(read(fd, &c, 1) <= 0) //偽造結束條件
            break;
        if(c == '\r'){ //全部以\r分割
            if(recv(fd, &c, 1, MSG_PEEK)>0 && c == '\n')
                read(fd, &c, 1);
            else //意外的結束,填充 \n 結束讀取
                *tp++ = '\n';
            break;
        }
        *tp++ = c;
    }
    *tp = '\0';
    return tp - buf;
}

inline void 
response_400(int cfd) {
    const char * estr = 
    "HTTP/1.0 400 BAD REQUEST\r\n"
    "Server: wz simple httpd 1.0\r\n"
    "Content-Type: text/html\r\n"
    "\r\n"
    "<p>你的請求有問題,請檢查語法!</p>\r\n";
    
    write(cfd, estr, strlen(estr));
}

inline void 
response_404(int cfd) {
    const char * estr = 
    "HTTP/1.0 404 NOT FOUND\r\n"
    "Server: wz simple httpd 1.0\r\n"
    "Content-Type: text/html\r\n"
    "\r\n"
    "<html>"
    "<head><title>你請求的界面被查水表了!</title></head>\r\n"
    "<body><p>404: 估計是回不來了</p></body>"
    "</html>";
    
    write(cfd, estr, strlen(estr));
}

inline void 
response_501(int cfd) {
    const char * estr = 
    "HTTP/1.0 501 Method Not Implemented\r\n"
    "Server: wz simple httpd 1.0\r\n"
    "Content-Type: text/html\r\n"
    "\r\n"
    "<html>"
    "<head><title>小伙子不要亂請求</title></head>\r\n"
    "<body><p>too young too simple, 年輕人別總想弄出個大新聞.</p></body>"
    "</html>";
    
    write(cfd, estr, strlen(estr));
}

inline void 
response_500(int cfd) {
    const char * estr = 
    "HTTP/1.0 500 Internal Server Error\r\n"
    "Server: wz simple httpd 1.0\r\n"
    "Content-Type: text/html\r\n"
    "\r\n"
    "<html>"
    "<head><title>Sorry </title></head>\r\n"
    "<body><p>最近有點方了!</p></body>"
    "</html>";
    
    write(cfd, estr, strlen(estr));
}

inline void 
response_200(int cfd) {
    const char * estr = 
    "HTTP/1.0 200 OK\r\n"
    "Server: wz simple httpd 1.0\r\n"
    "Content-Type: text/html\r\n"
    "\r\n";
    
    write(cfd, estr, strlen(estr));
}

void 
response_file(int cfd, const char * path) {
    char buf[BUFSIZ];
    FILE * txt = fopen(path, "r");
    
    // 如果文件解析錯誤, 給它個404
    if(NULL == txt) 
        response_404(cfd);
    else{
        //發送給200的報文頭過去, 並發送文件內容過去
        response_200(cfd);
        while(!feof(txt) && fgets(buf, sizeof buf, txt))
            write(cfd, buf, strlen(buf));
        fclose(txt);
    }
}

int 
request_start(uint16_t * pport) {
    int sfd;
    struct sockaddr_in saddr = { AF_INET };
    
    CERR_IF(sfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP));
    // 監測一下是否要更換端口, 並綁定一下端口信息
    saddr.sin_port = pport && *pport ? htons(*pport) : 0;
    CERR_IF(bind(sfd, (struct sockaddr *)&saddr, sizeof saddr));

    if(pport && !*pport) {
        socklen_t clen = sizeof saddr;
        CERR_IF(getsockname(sfd, (struct sockaddr*)&saddr, &clen));
        *pport = ntohs(saddr.sin_port);
    }

    // 開啟監聽任務
    CERR_IF(listen(sfd, SOMAXCONN));

    return sfd;
}

void * 
request_accept(void * arg) {
    char buf[BUFSIZ], path[BUFSIZ >> 1], type[BUFSIZ >> 2];
    char * lt, * rt, * query = NULL, * nb = buf;
    int iscgi, cfd = (int)arg;
    struct stat st;

    // 請求錯誤, 直接返回結果
    if(getfdline(cfd, buf, sizeof buf) <= 0) {
        response_501(cfd);
        close(cfd);
        return NULL;
    }

    // 合法請求處理
    for(lt = type, rt = nb; !isspace(*rt) && (lt - type) < sizeof type - 1; *lt++ = *rt++)
        ;
    *lt = '\0';

    //同樣處理合法與否判斷, 出錯了直接返回錯誤結果
    if((iscgi = strcasecmp(type, "POST")) && strcasecmp(type, "GET")) {
        response_501(cfd);
        close(cfd);
        return NULL;
    }

    // 在buf中 去掉空字符
    while(*rt && isspace(*rt))
        ++rt;

    // 這里得到路徑信息, query url路徑拼接
    *path = '.';
    for(lt = path + 1; (lt - path) < sizeof path - 1 && !isspace(*rt); *lt++ = *rt++)
        ;
    *lt = '\0';
    
    // 單獨處理 get 獲取 ? 后面數據, 不是 POST那就是 GET
    if(iscgi != 0) {
        for(query = path; *query && *query != '?'; ++query)
            ;
        if(*query == '?'){
            iscgi = 0;
            *query++ = '\0';
        }
    }
    
    // type , path 和 query 已經構建好了
    if(stat(path, &st) < 0) {
        response_404(cfd);
        close(cfd);
        return NULL;
    }

    // 合法情況, 執行, 寫入, 讀取權限. 監測是否是 CGI程序
    if ((st.st_mode & S_IXUSR) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH))
        iscgi = 0;
    if(!iscgi)
        response_file(cfd, path);
    else
        request_cgi(cfd, path, type, query);
    
    close(cfd);
    return NULL;
}

void 
request_cgi(int cfd, const char * path, const char * type, const char *  query) {
    pid_t pid;
    char c, buf[BUFSIZ];
    int pocgi[2], picgi[2];
    int i, contlen = -1; // 報文長度
    
    if(strcasecmp(type, "POST") == 0){
        while(getfdline(cfd, buf, sizeof buf) > 0 && strcmp("\n", buf)){
            buf[15] = '\0';
            if(!strcasecmp(buf, "Content-Length:"))
                contlen = atoi(buf + 16);
        }
        if(contlen == -1) { //錯誤的報文,直接返回錯誤結果
            response_400(cfd);
            return;
        }
    } 
    else { 
        // 讀取報文頭,就是過濾, 后面就假定是 GET
        while(getfdline(cfd, buf, sizeof buf) > 0 && strcmp("\n", buf))
            ;
    }
    
    //這里處理請求內容, 先處理錯誤信息
    if(pipe(pocgi) < 0) {
        response_500(cfd);
        return;
    }
    // 管道 是 0讀取, 1寫入
    if(pipe(picgi) < 0) {
        close(pocgi[0]), close(pocgi[1]);
        response_500(cfd);
        return;
    }
    if((pid = fork()) < 0){
        close(pocgi[0]), close(pocgi[1]);
        close(picgi[0]), close(picgi[1]);
        response_500(cfd);
        return;
    }

    // 這里就是多進程處理了, 先處理子進程
    if(pid == 0) {
        // dup2 讓前者共享后者同樣的文件表
        dup2(pocgi[1], STDOUT_FILENO); // 標准輸出算作 pocgi管道的寫入端
        dup2(picgi[0], STDIN_FILENO); // 標准輸入做為 picgi管道的讀取端
        close(pocgi[0]);
        close(picgi[1]);
        
        // 添加環境變量, 用於當前會話中
        sprintf(buf, "REQUEST_METHOD=%s", type);
        putenv(buf);
        // 繼續湊環境變量串,放到當前會話種
        if(strcasecmp(buf, "POST") == 0)
            sprintf(buf, "CONTENT_LENGTH=%d", contlen);
        else
            sprintf(buf, "QUERY_STRING=%s", query);
        putenv(buf);
        // 成功的話調到 新的執行體上
        execl(path, path, NULL);
        
        // 這行代碼原本是不用的, 但是防止 execl執行失敗, 子進程沒有退出.妙招
        exit(EXIT_SUCCESS);
    }
    // 父進程, 隨便搞了, 先發送個OK
    write(cfd, "HTTP/1.0 200 OK\r\n", 17);
    close(pocgi[1]);
    close(picgi[0]);
    
    if(strcasecmp(type, "POST") == 0){
        // 將數據都寫入到 picgi 管道中, 讓子進程在 picgi[0]中讀取 => STDIN_FILENO
        for(i = 0; i < contlen; ++i){
            read(cfd, &c, 1);
            write(picgi[1], &c, 1);
        }
    }
    //從子進程中 讀取數據 發送給客戶端, 多線程跨進程阻塞模型
    while(read(pocgi[0], &c, 1) > 0)
        write(cfd, &c, 1);
    
    close(pocgi[0]);
    close(picgi[1]);
    //等待子進程結束
    waitpid(pid, NULL, 0);
}

  代碼精簡部分, 方便溫故. http 是 tcp 的上層協議. 本質是HTTP文本協議加網絡IO處理. 簡單點同上面那些.

說復雜那全是沒邊. 系統開發層面 libcurl 庫可以解決http常用客戶端請求. 服務器端可以nginx加代理.

 

后記 - 未來展望

  錯誤是難免的歡迎指正, 加打臉.

  最近做了很多跨平台的工作, 線程, 管道, socket ... 哎總感覺 被風吹亂了頭發~(¬_¬)

  千默  http://music.163.com/#/song?id=35447148

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM