實現自己的http server

jack_wong2010發表於2014-03-18

原文出自;http://www.cppblog.com/kevinlynx/archive/2008/07/30/57521.html


Write your own http server

author : Kevin Lynx

Why write your own?

    看這個問題的人證明你知道什麼是http server,世界上有很多各種規模的http server,為什麼要自己實現一個?其實沒什麼
理由。我自己問自己,感覺就是在自己娛樂自己,或者說只是練習下網路程式設計,或者是因為某日我看到某個庫宣稱自己附帶一個小
型的http server時,我不知道是什麼東西,於是就想自己去實現一個。

What's httpd ?

    httpd就是http daemon,這個是類unix系統上的名稱,也就是http server。httpd遵循HTTP協議,響應HTTP客戶端的request,
然後返回response。
    那麼,什麼是HTTP協議?最簡單的例子,就是你的瀏覽器與網頁伺服器之間使用的應用層協議。雖然官方文件說HTTP協議可以
建立在任何可靠傳輸的協議之上,但是就我們所見到的,HTTP還是建立在TCP之上的。
    httpd最簡單的response是返回靜態的HTML頁面。在這裡我們的目標也只是一個響應靜態網頁的httpd而已(也許你願意加入CGI
特性)。

More details about HTTP protocol

    在這裡有必要講解HTTP協議的更多細節,因為我們的httpd就是要去解析這個協議。
    關於HTTP協議的詳細文件,可以參看rfc2616。但事實上對於實現一個簡單的響應靜態網頁的httpd來說,完全沒必要讀這麼一
分冗長的文件。在這裡我推薦<HTTP Made Really Easy>,以下內容基本取自於本文件。

- HTTP協議結構
  HTTP協議無論是請求報文(request message)還是迴應報文(response message)都分為四部分:
  * 報文頭 (initial line )
  * 0個或多個header line
  * 空行(作為header lines的結束)
  * 可選body
  HTTP協議是基於行的協議,每一行以\r\n作為分隔符。報文頭通常表明報文的型別(例如請求型別),報文頭只佔一行;header line
  附帶一些特殊資訊,每一個header line佔一行,其格式為name:value,即以分號作為分隔;空行也就是一個\r\n;可選body通常
  包含資料,例如伺服器返回的某個靜態HTML檔案的內容。舉個例子,以下是一個很常見的請求報文,你可以截獲瀏覽器傳送的資料
  包而獲得:

    1  GET /index.html HTTP/1.1
    2  Accept-Language: zh-cn
    3  Accept-Encoding: gzip, deflate
    4  User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; MAXTHON 2.0)
    5  Host: localhost
    6  Connection: Keep-Alive
    7
  我為每一行都新增了行號,第1行就是initial line,2-6行是header lines,7行是一個header line的結束符,沒有顯示出來。
  以下是一個迴應報文:
    1  HTTP/1.1 200 OK
    2  Server: klhttpd/0.1.0
    3  Content-Type: text/html
    4  Content-Length: 67
    5
    6  <head><head><title>index.html</title></head><body>index.html</body>
  第6行就是可選的body,這裡是index.html這個檔案的內容。

- HTTP request method
  因為我們做的事伺服器端,所以我們重點對請求報文做說明。首先看initial line,該行包含幾個欄位,每個欄位用空格分開,例
  如以上的GET /index.html HTTP/1.1就可以分為三部分:GET、/index.html、HTTP/1.1。其中第一個欄位GET就是所謂的request
  method。它表明請求型別,HTTP有很多method,例如:GET、POST、HEAD等。

  就我們的目標而言,我們只需要實現對GET和HEAD做響應即可。

  GET是最普遍的method,表示請求一個資源。什麼是資源?諸如HTML網頁、圖片、聲音檔案等都是資源。順便提一句,HTTP協議
  中為每一個資源設定一個唯一的識別符號,就是所謂的URI(更寬泛的URL)。
  HEAD與GET一樣,不過它不請求資源內容,而是請求資源資訊,例如檔案長度等資訊。

- More detail 
  繼續說說initial line後面的內容:
  對應於GET和HEAD兩個method,緊接著的欄位就是資源名,其實從這裡可以看出,也就是檔名(相對於你伺服器的資源目錄),例
  如這裡的/index.html;最後一個欄位表明HTTP協議版本號。目前我們只需要支援HTTP1.1和1.0,沒有多大的技術差別。

  然後是header line。我們並不需要關注每一個header line。我只羅列有用的header line :
  - Host : 對於HTTP1.1而言,請求報文中必須包含此header,如果沒有包含,伺服器需要返回bad request錯誤資訊。
  - Date : 用於迴應報文,用於客戶端快取資料用。
  - Content-Type : 用於迴應報文,表示迴應資源的檔案型別,以MIME形式給出。什麼是MIME?它們都有自己的格式,例如:
    text/html, image/jpg, image/gif等。
  - Content-Length : 用於迴應報文,表示迴應資源的檔案長度。

body域很簡單,你只需要將一個檔案全部讀入記憶體,然後附加到迴應報文段後傳送即可,即使是二進位制資料。

- 迴應報文
  之前提到的一個迴應報文例子很典型,我們以其為例講解。首先是initial line,第一個欄位表明HTTP協議版本,可以直接以請求
  報文為準(即請求報文版本是多少這裡就是多少);第二個欄位是一個status code,也就是迴應狀態,相當於請求結果,請求結果
  被HTTP官方事先定義,例如200表示成功、404表示資源不存在等;最後一個欄位為status code的可讀字串,你隨便給吧。

  迴應報文中最好跟上Content-Type、Content-Length等header。

具體實現
    正式寫程式碼之前我希望你能明白HTTP協議的這種請求/迴應模式,即客戶端發出一個請求,然後伺服器端迴應該請求。然後繼續
這個過程(HTTP1.1是長連線模式,而HTTP1.0是短連線,當伺服器端返回第一個請求時,連線就斷開了)。
    這裡,我們無論客戶端,例如瀏覽器,發出什麼樣的請求,請求什麼資源,我們都回應相同的資料:

               

/* 阻塞地接受一個客戶端連線 */
        SOCKET con = accept( s, 0, 0 ); 
        /* recv request */
        char request[1024] = { 0 };
        ret = recv( con, request, sizeof( request ), 0 );
        printf( request );
        /* whatever we recv, we send 200 response */
        {
            char content[] = "<head><head><title>index.html</title></head><body>index.html</body>";
            char response[512];
            sprintf( response, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: %d\r\n\r\n%s", strlen( content ), content );
            ret = send( con, response, strlen( response ), 0 );
        }

        closesocket( con ); 

 

    程式以最簡單的阻塞模式執行,我們可以將重點放在協議的分析上。執行程式,在瀏覽器裡輸入http://localhost:8080/index.html
,然後就可以看到瀏覽器正常顯示content中描述的HTML檔案。假設程式在8080埠監聽。

   現在你基本上明白了整個工作過程,我們可以把程式碼寫得更全面一點,例如根據GET的URI來載入對應的檔案然後迴應給客戶端。
其實這個很簡單,只需要從initial line裡解析出(很一般的字串解析)URI欄位,然後載入對應的檔案即可。例如以下函式:

void http_response( SOCKET con, const char *request )
{
    /* get the method */
    char *token = strtok( request, " " );
    char *uri = strtok( 0, " " );
    char file[64];
    sprintf( file, ".%s", uri ); 

    {
        /* load the file content */
        FILE *fp = fopen( file, "rb" );
        if( fp == 0 )
        {
            /* response 404 status code */
            char response[] = "HTTP/1.1 404 NOT FOUND\r\n\r\n";
            send( con, response, strlen( response ), 0 );
        }

        else
        {
            /* response the resource */
            /* first, load the file */
            int file_size ;
            char *content;
            char response[1024];
            fseek( fp, 0, SEEK_END );
            file_size = ftell( fp );
            fseek( fp, 0, SEEK_SET );
            content = (char*)malloc( file_size + 1 );
            fread( content, file_size, 1, fp );
            content[file_size] = 0; 

            sprintf( response, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: %d\r\n\r\n%s", file_size, content );
            send( con, response, strlen( response ), 0 );
            free( content );
        }

    }

}
 



其他

    要將這個簡易的httpd做完善,我們還需要注意很多細節。包括:對不支援的method返回501錯誤;對於HTTP1.1要求有Host這個
header;為了支援客戶端cache,需要新增Date header;支援HEAD請求等。

    相關下載中我提供了一個完整的httpd library,純C的程式碼,在其上加上一層資源載入即可實現一個簡單的httpd。在這裡我將
對程式碼做簡要的說明:
    evbuffer.h/buffer.c : 取自libevent的buffer,用於快取資料;
    klhttp-internal.h/klhttp-internal.c :主要用於處理/解析HTTP請求,以及建立迴應報文;
    klhttp-netbase.h/klhttp-netbase.c :對socket api的一個簡要封裝,使用select模型;
    klhttp.h/klhttp.c :庫的最上層,應用層主要與該層互動,這一層主要集合internal和netbase。
    test_klhttp.c :一個測試例子。

相關下載:
    klhttpd
    文中相關程式碼

參考資料:

http://www.w3.org/Protocols/rfc2616/rfc2616.html
http://jmarshall.com/easy/http/
http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html

相關文章