也談如何寫一個Webserver(-)

grassroot72 發表於 2021-05-03

    關於如何寫一個Webserver,很多大咖都發表過類似的文章.趁著這個五一假期,我也來湊個份子.

    我寫Webserver的原因,還得從如何將http協議傳送的訊息解析說起.當時,我只是想了解一下http的訊息解析過程,好能夠提高基於http協議的訊息處理效率,所以就在網上搜了一下,發現很多人都在用nodejs的http-parser,也許是智商上限封頂^_^!,我居然沒太看懂大神的程式碼邏輯.後來也考察過h2o這個專案的parser,無奈還是沒有能領悟大神的精神^_^!.

    怎麼辦...,掙扎了半天,最終決定硬著頭皮自己寫一個http訊息的parser吧.就醬,就有了後來我寫Maestro Webserver的故事.

    既然談到了http message的解析,那今天這第一篇隨筆就談這個東西吧.http協議的內容說起來歷史太久遠了,我不是歷史老師,網上很多講解都很棒,我就不多說了.此外,RFC2616, RFC7231等文件也明確的講解了協議的含義.不過還是應該吐槽一下RFC文件的晦澀難懂哈...

還是讓我引用一段相對清晰的關於http message的RFC講解吧

   HTTP messages consist of requests from client to server and responses
   from server to client.

       HTTP-message   = Request | Response     ; HTTP/1.1 messages

   Request (section 5) and Response (section 6) messages use the generic
   message format of RFC 822 [9] for transferring entities (the payload
   of the message). Both types of message consist of a start-line, zero
   or more header fields (also known as "headers"), an empty line (i.e.,
   a line with nothing preceding the CRLF) indicating the end of the
   header fields, and possibly a message-body.

        generic-message = start-line
                          *(message-header CRLF)
                          CRLF
                          [ message-body ]
        start-line      = Request-Line | Status-Line

 從這段文字中,我們可以知道不論是request還是response,http message分三段,即start-line,message headersmessage body.

那麼,在設計我的messge結構體時(對了,我是用C語言開發的),我會包含這三段內容.我並沒有把parser寫成獨立的單一函式,而是將他們分解成了一組能重複被呼叫的更小的函式.而從封裝的角度來說,我也沒有遵守儘量封裝資料結構體的原則.我的目的很簡單,那就是,簡單易懂,容易呼叫(這會不會被老師調打一頓:-).

 

還是看看定義的資料結構體吧.

typedef struct {
  int method;   /* GET/POST... */
  char *path;
  int ver_major;
  int ver_minor;
  int code;      /* status code */
  char *status;  /* status text */

  sllist_t *headers;

  int len_startline;
  int len_headers;

  unsigned char *body;    
  unsigned char *body_zipped;
  unsigned char *body_s;  /* point to the range start of the body */
  size_t len_body;
} httpmsg_t;

先不用看和body相關的部分,因為我會在後續如何寫Webserver中介紹相關的內容(涉及到body的壓縮,斷點續傳等等).

下面是相關的函式,

int msg_parse(sllist_t *headers,
              unsigned char **startline,
              unsigned char **body,
              size_t *len_body,
              const unsigned char *buf);

這個是對底層message進行解析的函式,再此之上,我用兩個函式封裝了它,分別用於解析http request和http response.

httpmsg_t *http_parse_req(const unsigned char *buf);
httpmsg_t *http_parse_rep(const unsigned char *buf);

 

我寫這些底層函式的原則是,儘量利用上一步的結果,不做重複的計算,比如,同一字串的長度不要多次通過strlen計算,希望這樣應該能提高(微不足道^_^!)的效能吧.

在上面的httpmsg_t結構體中,我用了單連結串列來管理http headers,因為headers的數量不是很多,單連結串列輪詢反而速度更快.

至於上述函式如何實現,感興趣朋友可以請訪問我的github專案,連結https://github.com/grassroot72/Maestro2.

歡迎和我探討..

 

我會在第二篇內容裡介紹socket和epoll在Webserver中的應用...