tinyhttp源碼分析

tinyhttp是一個用c寫的輕量級的http server陡舅,相比較nginx贞瞒,apache這類的server徐鹤,它完全和他們不是一個量級的東西,像是小山包和喜馬拉雅山的區(qū)別颂碘,但是看看這個源碼族淮,對了解http協(xié)議和 http server的原理是超級有幫助的!F就俊祝辣!

tinyhttp是用c寫的,相比較用python或則是其他高級語言實現(xiàn)的http server切油,c語言可以更加從底層的讓讀者明白一個瀏覽器的請求是如何被server相應(yīng)蝙斜,然后server是如何response。大概用一天的時間讀完澎胡,真的是把我爽到了孕荠,強(qiáng)烈推薦!9ニ稚伍!

另外,如果你有空戚宦,可以看一下web bench的源碼个曙,這個是http壓測工具,模擬并發(fā)的生成http請求受楼】寻幔可以結(jié)合起來看一下呼寸,還是可以學(xué)到很多東西的。

源碼:https://github.com/zhaozhengcoder/rebuild-the-wheel/tree/master/tinyhttpd

tinyhttp的框架

下圖是看源碼的時候猴贰,手畫的一個流程圖对雪。


微信圖片_20171108143150.jpg

我把一些核心代碼和相應(yīng)的注釋貼在這里,如果你感興趣全部米绕,可以移步我的github瑟捣。
https://github.com/zhaozhengcoder/rebuild-the-wheel/tree/master/tinyhttpd

main函數(shù):

int main(void)
{
    int server_sock = -1;
    //監(jiān)聽的端口
    u_short port = 4000;
    int client_sock = -1;
    struct sockaddr_in client_name;
    socklen_t  client_name_len = sizeof(client_name);
    pthread_t newthread;

    server_sock = startup(&port);
    printf("httpd running on port %d\n", port);
    while (1)
    {
        //等待socket建立連接
        client_sock = accept(server_sock,(struct sockaddr *)&client_name,&client_name_len);
        if (client_sock == -1)
            error_die("accept");
        //對于一個socket連接(即一個http請求),創(chuàng)建一個線程去處理
        if (pthread_create(&newthread , NULL, (void *)accept_request, (void *)(intptr_t)client_sock) != 0)
            perror("pthread_create");
    }
    close(server_sock);
    return(0);
}

accept_request函數(shù):

//對于每一個http請求栅干,都會創(chuàng)建一個線程迈套,線程去執(zhí)行這個函數(shù)去處理請求
//注意:client是一個文件句柄,在accept_request函數(shù)里面非驮,只讀了這個句柄的第一行交汤,得到了請求的方法和url
void accept_request(void *arg)
{
    int client = (intptr_t)arg;
    char buf[1024];
    size_t numchars;
    char method[255];
    char url[255];
    char path[512];
    size_t i, j;
    struct stat st;
    int cgi = 0;      /* becomes true if server decides this is a CGI */
    char *query_string = NULL;

    //獲得請求的第一行,請求的第一行往往是 : GET / HTTP/1.1
    numchars = get_line(client, buf, sizeof(buf));
    printf("buf : %s",buf);

    //分析請求 
    i = 0; j = 0;
    while (!ISspace(buf[i]) && (i < sizeof(method) - 1))
    {
        method[i] = buf[i];
        i++;
    }
    j=i;
    method[i] = '\0';

    //server 支持get 和post 兩種方法劫笙,如果是其他的方法芙扎,就不支持了,返回狀態(tài)碼501填大,服務(wù)器不支持這個方法
    if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
    {
        unimplemented(client);
        return;
    }

    //對于是post的請求戒洼,把cgi(common gateway interface)的flag 設(shè)為1,表示這個需要cgi來處理
    if (strcasecmp(method, "POST") == 0)
        cgi = 1;

    //獲得請求的url
    i = 0;
    while (ISspace(buf[j]) && (j < numchars))
        j++;
    while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < numchars))
    {
        url[i] = buf[j];
        i++; j++;
    }
    url[i] = '\0';
    printf("url is %s \n",url);  //比如說允华,這個請求的url可能是  /index.html 圈浇,或則是 /index.html?id=100

    //如果是get方法,判斷這個get請求靴寂,是否是帶有參數(shù)的請求
    if (strcasecmp(method, "GET") == 0)
    {
        query_string = url;
        while ((*query_string != '?') && (*query_string != '\0'))
            query_string++;
        if (*query_string == '?')
        {
            cgi = 1;
            *query_string = '\0';
            query_string++;
        }
    }

    //sprintf()函數(shù):將格式化的數(shù)據(jù)寫入字符串
    sprintf(path, "htdocs%s", url);  //獲取請求文件路徑
    printf("path is :%s \n",path);
    //如果路徑是一個目錄磷蜀,那么就給這個路徑加上index.html ,表示默認(rèn)的請求
    if (path[strlen(path) - 1] == '/')
        strcat(path, "index.html");
    //根據(jù)路徑找文件,并獲取path文件信息保存到結(jié)構(gòu)體st中百炬,-1表示尋找失敗
    if (stat(path, &st) == -1) {
        //如果尋找失敗
        while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
            numchars = get_line(client, buf, sizeof(buf));
        not_found(client);
    }
    else
    {
        if ((st.st_mode & S_IFMT) == S_IFDIR)
            strcat(path, "/index.html");
        if ((st.st_mode & S_IXUSR) ||
                (st.st_mode & S_IXGRP) ||
                (st.st_mode & S_IXOTH)    )
            cgi = 1;
        //如果cgi==0褐隆,表示僅是一個get請求,沒有帶參數(shù)
        if (!cgi){
            printf("\n to execute server_file \n");
            serve_file(client, path);
        }
        else{
            //表示是post方法或者是帶有參數(shù)的get方法
            printf("\n to execute execute_cgi \n");
            execute_cgi(client, path, method, query_string);
        }
    }
    close(client);
}

execute_cgi 函數(shù)

//對于帶有參數(shù)的get請求和 post請求剖踊,這兩類并不能直接返回一個靜態(tài)的html文件庶弃,需要cgi
//cgi是common gateway interface的簡稱
//談一下我對cgi的理解,就是對于不能直接返回靜態(tài)頁面的請求德澈,這些請求一定是需要在服務(wù)器上面運(yùn)行一段代碼歇攻,然后返回一個結(jié)果
//具體一點的談:
//比如一個 get請求 /index?uid=100,它可能對應(yīng)的場景是返回id=100用戶的頁面,這顯然不是一個靜態(tài)的頁面梆造,需要動態(tài)的生成缴守,然后服務(wù)器把這個id=100的參數(shù)拿到,去執(zhí)行本地的一個 xxx.cgi 文件,
//執(zhí)行這個文件的時候斧散,參數(shù)是id=100供常,然后將執(zhí)行這個文件的輸出返回給瀏覽器  可以參考 : http://www.runoob.com/python/python-cgi.html
//注意:client是一個文件劇本摊聋,在accept_request函數(shù)里面鸡捐,只讀了第一行,在execute_cgi函數(shù)里面麻裁,把剩下的讀完
void execute_cgi(int client, const char *path,const char *method, const char *query_string)
{
    printf ("\n in function execute cgi ! \n");
    char buf[1024];
    int cgi_output[2];
    int cgi_input[2];
    pid_t pid;
    int status;
    int i;
    char c;
    int numchars = 1;
    int content_length = -1;

    buf[0] = 'A'; buf[1] = '\0';
    if (strcasecmp(method, "GET") == 0)
        while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
            numchars = get_line(client, buf, sizeof(buf));
    else if (strcasecmp(method, "POST") == 0) /*POST*/
    {
        numchars = get_line(client, buf, sizeof(buf));
        while ((numchars > 0) && strcmp("\n", buf))
        {
            buf[15] = '\0';
            if (strcasecmp(buf, "Content-Length:") == 0)
                content_length = atoi(&(buf[16]));
            numchars = get_line(client, buf, sizeof(buf));
            printf("buf : %s",buf);
        }
        if (content_length == -1) {
            bad_request(client);
            return;
        }
    }
    else/*HEAD or other*/
    {
    }


    if (pipe(cgi_output) < 0) {
        cannot_execute(client);
        return;
    }
    if (pipe(cgi_input) < 0) {
        cannot_execute(client);
        return;
    }

    if ( (pid = fork()) < 0 ) {
        cannot_execute(client);
        return;
    }
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    send(client, buf, strlen(buf), 0);
    if (pid == 0)  /* child: CGI script */
    {
        char meth_env[255];
        char query_env[255];
        char length_env[255];

        dup2(cgi_output[1], STDOUT);
        dup2(cgi_input[0], STDIN);
        close(cgi_output[0]);
        close(cgi_input[1]);
        sprintf(meth_env, "REQUEST_METHOD=%s", method);
        putenv(meth_env);
        if (strcasecmp(method, "GET") == 0) {
            sprintf(query_env, "QUERY_STRING=%s", query_string);
            printf("qery_env : %s  ",query_env);
            putenv(query_env);
        }
        else {   /* POST */
            sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
            putenv(length_env);
        }
        printf("\npath :  %s",path);
        //執(zhí)行外部腳本
        execl(path,path, NULL);
        exit(0);
    } else {    /*父進(jìn)程 */
        close(cgi_output[1]);
        close(cgi_input[0]);
        //注意:這段代碼的意識是箍镜,如果請求是post類型,post的請求是在正文里面有post的具體數(shù)據(jù)的 
        if (strcasecmp(method, "POST") == 0)
            for (i = 0; i < content_length; i++) {
                //在這里讀的就是post請求的具體參數(shù)煎源,父子進(jìn)程共享文件句柄色迂,然后這個socet的header部分已經(jīng)讀完了,在往下讀手销,就是post的正文了
                recv(client, &c, 1, 0);
                printf("c: %c \n",c);
                //將讀到的數(shù)據(jù)寫給子進(jìn)程
                write(cgi_input[1], &c, 1);
            }
        while (read(cgi_output[0], &c, 1) > 0)
            send(client, &c, 1, 0);

        close(cgi_output[0]);
        close(cgi_input[1]);
        waitpid(pid, &status, 0);
    }
}
others

假如不借鑒源碼的思路歇僧,自己去寫一個http server,那么下面的幾個問題锋拖,可能需要考慮如何去解決诈悍。

  1. 如何去處理并發(fā)的http請求?
    源碼給出的思路是兽埃,對每一個來的請求侥钳,創(chuàng)建一個線程出處理并發(fā)的請求。如果換成你柄错,你會怎么做舷夺?你的解決方案可以支持高并發(fā)嗎?

  2. 對于一個http請求售貌,如果從請求里面解析到關(guān)鍵的字段信息给猾,比如
    http method,是get颂跨,post敢伸,put,delete毫捣,還是head?
    url是什么详拙?
    如果是post類型的請求,post的參數(shù)是在http請求的正文里面的蔓同,那么怎么讀取出來他們饶辙?他們的長度是如何確定的?

  3. 對于帶有參數(shù)的get方法斑粱,和post方法弃揽,你的服務(wù)器如何去處理?

對于問題2,
這個需要了解http的格式矿微,http請求的格式痕慢,如下圖:

image.png

判斷http 請求header的每一行的標(biāo)志是 :\r\n
判斷http請求header和請求正文的標(biāo)志是:兩個\r\n (如上圖)
對于,一個post請求涌矢,請求的正文里面是post的請求數(shù)據(jù)掖举,header里面的content-length指明了post請求的數(shù)據(jù)的長度。

可以參考:http://www.reibang.com/p/f5a5db039737

對于問題3:
帶有參數(shù)的get請求娜庇,和post請求塔次,服務(wù)器沒有辦法簡單的返回一個靜態(tài)的文件,服務(wù)器需要在服務(wù)器端將相應(yīng)的頁面“計算”出來名秀,然后返回給瀏覽器励负。

假如,瀏覽器發(fā)送了一個請求 /index?id=100匕得,請求id=100的人的主頁
那么继榆,服務(wù)器需要“計算”出來id=100的這個人的主頁的的頁面。這個就需要cgi來幫忙了汁掠,cgi可以理解為在服務(wù)器端可以執(zhí)行的小腳本略吨。服務(wù)器收到這個請求之后,執(zhí)行index.cgi (這個文件是提前寫好了调塌,專門來處理這樣的請求) 晋南,服務(wù)器執(zhí)行index.cgi ,參數(shù)是id=100羔砾,然后“計算”出網(wǎng)頁的數(shù)據(jù)负间,返回給瀏覽器。

舉個例子姜凄,cgi的小腳本長什么樣子:


image.png

可以參考:http://www.runoob.com/python/python-cgi.html

這是我在看源碼的時候政溃,給自己提的幾個問題√恚看懂tinyhttp應(yīng)該是理解http server的第一步吧董虱,nginx正在前面等你,一起前進(jìn)吧申鱼,少年~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末愤诱,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子捐友,更是在濱河造成了極大的恐慌淫半,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件匣砖,死亡現(xiàn)場離奇詭異科吭,居然都是意外死亡昏滴,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門对人,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谣殊,“玉大人,你說我怎么就攤上這事牺弄∫黾福” “怎么了?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵猖闪,是天一觀的道長鲜棠。 經(jīng)常有香客問我肌厨,道長培慌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任柑爸,我火速辦了婚禮吵护,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘表鳍。我一直安慰自己馅而,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布譬圣。 她就那樣靜靜地躺著瓮恭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪厘熟。 梳的紋絲不亂的頭發(fā)上屯蹦,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天,我揣著相機(jī)與錄音绳姨,去河邊找鬼登澜。 笑死,一個胖子當(dāng)著我的面吹牛飘庄,可吹牛的內(nèi)容都是我干的脑蠕。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼跪削,長吁一口氣:“原來是場噩夢啊……” “哼谴仙!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起碾盐,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤晃跺,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后廓旬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體哼审,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡谐腰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了涩盾。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片十气。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖春霍,靈堂內(nèi)的尸體忽然破棺而出砸西,到底是詐尸還是另有隱情,我是刑警寧澤址儒,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布芹枷,位于F島的核電站,受9級特大地震影響莲趣,放射性物質(zhì)發(fā)生泄漏鸳慈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一喧伞、第九天 我趴在偏房一處隱蔽的房頂上張望走芋。 院中可真熱鬧,春花似錦潘鲫、人聲如沸翁逞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽挖函。三九已至,卻和暖如春浊竟,著一層夾襖步出監(jiān)牢的瞬間怨喘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工逐沙, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留哲思,地道東北人。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓吩案,卻偏偏與公主長得像棚赔,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子徘郭,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,619評論 2 354

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