自制Web服務(wù)器(2) 建立TCP連接&解析HTTP請求

四月初就開始著手寫Web服務(wù)器蔽氨,因為一些事耽擱了一個月焰檩,最近又在提交代碼了多柑,文章列表:

這不是一個寫Web服務(wù)器的教程遥椿,只是做的過程的記錄,因為是先寫代碼后寫文章的掏父,有些過程我直接憑記憶在這里寫下來笋轨,可能會有疏漏。

建立 TCP 連接

這一部分參考 Liso Project 的 start_code赊淑、深入理解計算機系統(tǒng) 第二版 10-12 章爵政、UNIX網(wǎng)絡(luò)編程 卷1 第三版 1-4 章。

首先作為一個 Web服務(wù)器陶缺,要能夠監(jiān)聽端口钾挟、等待TCP連接、建立TCP連接饱岸,這是基本要求掺出。因為使用C語言開發(fā),所以要用 UNIX 的套接字API苫费,TCP連接建立與釋放的流程如下:


其中:

  • socket() 用于創(chuàng)建一個套接字結(jié)構(gòu)體汤锨,這里需要使用的協(xié)議族、協(xié)議類型等百框。這一步一般不會出問題泥畅。
  • bind() 用于綁定到一個本地端口。出錯的原因一般有端口已經(jīng)被占用、非 Root 用戶綁定知名端口位仁。
  • listen() 用于說明這個套接字用于被動接收連接請求。
  • accept() 會導(dǎo)致程序陷入睡眠方椎,直到系統(tǒng)中斷提醒程序有新連接建立聂抢。
  • read() 也是一個 I/O操作,會導(dǎo)致程序陷入睡眠棠众,這個時候內(nèi)核開始從網(wǎng)卡的Buffer里面拷貝數(shù)據(jù)到內(nèi)存里琳疏,一旦拷貝完了,系統(tǒng)中斷讓進程切回來繼續(xù)執(zhí)行闸拿。

這部分對應(yīng)的代碼:

#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>

#define BUFFER_SIZE 4096
#define FORK_CHILD_PID 0

typedef struct http_mod {
    int sockfd;
    struct sockaddr_in addr;
} http_mod;

http_mod* http_init(uint16_t port) {
    http_mod* m = (http_mod*) malloc(sizeof(http_mod*));
    m->sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (m->sockfd == -1) {
       fprintf(stderr, "Create socket failed.");
       free(m);
       return NULL;
    }

    m->addr.sin_family = AF_INET;
    m->addr.sin_port = htons(port);
    m->addr.sin_addr.s_addr = htonl(INADDR_ANY);

    int bind_ret = bind(m->sockfd, (struct sockaddr*) &(m->addr), sizeof(m->addr));
    if (bind_ret != 0) {
        fprintf(stderr, "bind to %d failed!", port);
        if(close(m->sockfd)) {
            fprintf(stderr, "Close socked failed!\n");
        }
        free(m);
        return NULL;
    }

    int listen_ret = listen(m->sockfd, 5);
    if (listen_ret != 0) {
        fprintf(stderr, "Failed to listen!\n");
        if(close(m->sockfd)) {
            fprintf(stderr, "Close socked failed!\n");
        }
        free(m);
        return NULL;
    }
    fprintf(stdout, "start http mod successfully! \n"); 

    return m;
}

//TODO: replace fork with IO multiplex
int start_receive_conn(http_mod *http) {
    struct sockaddr_in client_sock_addr;
    socklen_t cs_size = sizeof(client_sock_addr);
    fprintf(stdout, "Waiting for a connection, localport: %d\n", ntohs(http->addr.sin_port));

    while (1) {
        int new_sock = accept(http->sockfd, (struct sockaddr*)(&client_sock_addr), &cs_size);
        if (new_sock == -1) {
            fprintf(stderr, "Accept connections failed!\n");
            if (close(http->sockfd)) {
                fprintf(stderr, "Close socket failed!\n");
            }
            return -1;
        } else {
            fprintf(stdout, "Receive connection from %s\n", inet_ntoa(client_sock_addr.sin_addr));
        }
        if (fork() == FORK_CHILD_PID) {
            handle_request_loop(new_sock); 
            if (close(new_sock)) {
                fprintf(stderr, "Close real sock failed! \n'");
            }
            fprintf(stdout, "Close connection from %s", inet_ntoa(client_sock_addr.sin_addr));
            exit(0);
        }

        if (close(new_sock)) {
            fprintf(stderr, "Close real sock failed! \n'");
            return -1;
        }

    }

    if (close(http->sockfd)) {
        fprintf(stderr, "Close sock failed! \n'");
    }
    fprintf(stdout, "Liso has stopped.\n");
    return 0;
}

void liso_init(struct arguments* arg) {
    http_mod* hm = http_init(arg->port);
    
    if (hm == NULL) {
        fprintf(stderr, "Initialize http module failed!\n");
        return;
    } 
    start_receive_conn(hm);
}

因為程序在調(diào)用 accept() 建立了新連接后就不處于監(jiān)聽狀態(tài)了空盼,此時別的客戶端是無法和服務(wù)端建立連接的。所以必須要處理并發(fā)問題新荤,一個最簡單的處理辦法是用 fork() 的方式支持并發(fā):

每次建立了一個TCP連接后揽趾,就調(diào)用fork() 派生出一個子進程,此時子進程和父進程所有的內(nèi)存數(shù)據(jù)苛骨、文件打開列表都是一樣的篱瞎,這時可以讓子進程繼續(xù)處理數(shù)據(jù),而父進程把剛剛建立的連接關(guān)閉掉痒芝,繼續(xù)調(diào)用 accept() 等待客戶端連接就可以支持并發(fā)了俐筋。

但是這樣弊端也很大,那就是支持并發(fā)所需要的開銷太大了严衬。更好的方法是用線程和IO多路復(fù)用澄者,但是出于快速寫一個架子的考慮,我先讓這個“服務(wù)器”可以踉踉蹌蹌地跑起來再說请琳,多路復(fù)用先加入到 TODO List 里面去粱挡。

解析 HTTP 請求

這一部分參考 編譯原理(龍書) 3到4 章、Flex&Bison 開發(fā)文檔单起、RFC 2616(HTTP/1.1 標(biāo)準(zhǔn))抱怔、RFC 2396。

解決一個計算機問題嘀倒,建立一個穩(wěn)定可復(fù)現(xiàn)屈留、可調(diào)試的一個觀察點還是很必要的,尤其我在TCP協(xié)議之上做數(shù)據(jù)傳輸测蘑,如果無法觀察到實際傳輸?shù)臄?shù)據(jù)是很惱火的灌危,所以我要抓包看數(shù)據(jù)。一開始我想用 WireShark碳胳,后來發(fā)現(xiàn)那玩意從源碼編譯起來有點麻煩勇蝙,直接用 apt 安裝了一個 tcpdump。

RFC文檔看的是 2616挨约,這是 CS-15-441/641 Project1 的項目要求給的參考文檔味混,似乎這個文檔已經(jīng)被新的標(biāo)準(zhǔn)所替代产雹,但是絕大部分內(nèi)容還是可以參考的。

HTTP請求的格式如下:


仔細(xì)研究了一下格式翁锡,發(fā)現(xiàn)其實用C語言代碼解析一下就很方便了蔓挖,完全沒有必要用Flex和Bison,但是既然文檔那么要求馆衔,就試一下這兩個工具瘟判。

Flex 是用于詞法分析的工具,它把一個輸入的字符串分割成一個個詞法單元送給語法分析工具 Bison角溃。Flex 和 Bison 在 Ubuntu 中都可以通過 apt 安裝拷获。在 Flex 中,我需要定義一些正則表達式來匹配詞法單元减细,F(xiàn)lex 文件格式如下:

%{
定義詞法單元匆瓜,可以用 #define 表示
%}
聲明部分
%%
轉(zhuǎn)換規(guī)則
%%
輔助函數(shù)

Flex 和 Bison 結(jié)合起來使用的時候, %{ %} 中的詞法單元可以不定義邪财,放到 Bison 文件中去定義陕壹。轉(zhuǎn)換規(guī)則是重點,每行規(guī)則以一個正則表達式開始树埠,后面再跟一個代碼塊糠馆。代碼塊里可以執(zhí)行一些邏輯,比如調(diào)用輔助函數(shù)怎憋,返回詞法單元給 Bison又碌。

轉(zhuǎn)換規(guī)則這里有幾個坑點

  • 我一開始每行加了\t來indent一下,這里不能加绊袋,不要從行首開始寫正則表達式
  • Flex支持的正則表達式和我平時用的正則表達式不太一樣毕匀,有些符號如 \w 是不支持的
  • 正斜杠(forward slash)符號 / 會被轉(zhuǎn)義,要想不轉(zhuǎn)義前面加上 \ 是沒有用的癌别,得用雙引號括起來:"/"

這里放一部分規(guī)則:

輔助函數(shù)那里皂岔,如果不使用 Bison 的話,需要定義一個 yywrap() 函數(shù):

int yywrap() {
    return 1;
}

使用 flex xxx.l 命令就可以把 xxx.l 文件編譯為 c 文件展姐,然后調(diào)用 yylex() 函數(shù)就會開始解析輸入的數(shù)據(jù)躁垛。 默認(rèn)是從 stdin 解析的。

Bison文件的格式和 Flex 類似圾笨。需要定義產(chǎn)生式教馆。Bison 會根據(jù)產(chǎn)生式自動生成語法解析器。

未完待續(xù)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末擂达,一起剝皮案震驚了整個濱河市土铺,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖悲敷,帶你破解...
    沈念sama閱讀 216,919評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件究恤,死亡現(xiàn)場離奇詭異,居然都是意外死亡后德,警方通過查閱死者的電腦和手機丁溅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來探遵,“玉大人,你說我怎么就攤上這事妓柜∠浼荆” “怎么了?”我有些...
    開封第一講書人閱讀 163,316評論 0 353
  • 文/不壞的土叔 我叫張陵棍掐,是天一觀的道長藏雏。 經(jīng)常有香客問我,道長作煌,這世上最難降的妖魔是什么掘殴? 我笑而不...
    開封第一講書人閱讀 58,294評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮粟誓,結(jié)果婚禮上奏寨,老公的妹妹穿的比我還像新娘。我一直安慰自己鹰服,他們只是感情好病瞳,可當(dāng)我...
    茶點故事閱讀 67,318評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著悲酷,像睡著了一般套菜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上设易,一...
    開封第一講書人閱讀 51,245評論 1 299
  • 那天逗柴,我揣著相機與錄音,去河邊找鬼顿肺。 笑死戏溺,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的挟冠。 我是一名探鬼主播于购,決...
    沈念sama閱讀 40,120評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼知染!你這毒婦竟也來了肋僧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,964評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎嫌吠,沒想到半個月后止潘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,376評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡辫诅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,592評論 2 333
  • 正文 我和宋清朗相戀三年凭戴,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片炕矮。...
    茶點故事閱讀 39,764評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡么夫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出肤视,到底是詐尸還是另有隱情档痪,我是刑警寧澤,帶...
    沈念sama閱讀 35,460評論 5 344
  • 正文 年R本政府宣布邢滑,位于F島的核電站腐螟,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏困后。R本人自食惡果不足惜乐纸,卻給世界環(huán)境...
    茶點故事閱讀 41,070評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望摇予。 院中可真熱鬧汽绢,春花似錦、人聲如沸趾盐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽救鲤。三九已至久窟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間本缠,已是汗流浹背斥扛。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留丹锹,地道東北人稀颁。 一個月前我還...
    沈念sama閱讀 47,819評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像楣黍,于是被迫代替她去往敵國和親匾灶。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,665評論 2 354

推薦閱讀更多精彩內(nèi)容