小型Http服務器

  HTTP又叫作超文本傳輸協議,現現在用的最多的版本是1.1版本。HTTP有以下的特色:html

    支持客戶/服務器模式(C/S或B/S)web

    簡單快速:基於請求和響應,請求只需傳送請求方法和請求路徑瀏覽器

    靈活:HTTP容許傳送人任意類型的數據對象。服務器

    無鏈接:這個無鏈接說的是應用層,應用層無鏈接,下層使用TCP依然是面向鏈接的,無鏈接的含義是限制在每一次鏈接只處理一個請求,服務器處理完客戶的請求之後,收到客戶應答,就斷開鏈接。markdown

    無狀態:HTTP是無狀態協議。無狀態是指協議對於事務處理沒有記憶能力。此次的請求和上次的請求之間是沒有關係的。缺乏狀態意味着若是後續處理須要前面的一些信息,則必須重傳,這樣可能致使每次鏈接傳送的數據量增大,可是當服務器不須要前面的信息時他的應答較快。網絡

  咱們日常使用的HTTP協議工做過程以下:多線程

    一個HTTP操做叫作事物:併發

      1)首先客戶機與服務器須要創建鏈接。框架

      2)創建鏈接後,客戶機發送一個請求給服務器,請求方式的格式爲:請求方法|統一資源標識符(URL)|協議版本號,後面是MIME信息包括請求修飾符、客戶機信息和可能的內容。socket

      3)服務器接到請求後,基於相應的響應信息、實體信息和可能的內容。

      4)客戶端接收服務器所返回的信息經過瀏覽器顯示在用戶的顯示屏上,而後客戶機和服務器斷開鏈接。

    若是在以上過程當中的某一步出現錯誤,那麼產生錯誤的信息將返回到客戶端,有顯示屏輸出。對於用戶來講,這些過程是由HTTP本身完成的,用戶只要點擊鼠標,等待信息顯示就能夠了。

  咱們所實現的HTTP也要可以實現這些基本的功能。

  本文的重點在於介紹HTTP服務器的框架結構,旨在瞭解HTTP服務器的流程,而後本身實現一個多線程的HTTP/1.0版本服務器,支持GET和POST方法。

  首先咱們來了解一下HTTP協議

    1.URL(統一資源定位符)

      它是一種特殊類型的URI,包含了用於查找某個資源的足夠信息。

      URL格式:

        http://host[":"port][abs_path]

      http表示經過http協議來定位網絡資源,host表示合法的主機域名或IP地址。port指定一個端口號,爲空則默認使用80端口。abs_path指定請求資源的路徑,若是URL中沒有給出abs_path,那麼瀏覽器會自動加上"/",表示web根目錄。

        如:http://baidu.com通過瀏覽器以後變成http://baidu.com/

      上面都是不帶參的URL,帶參數的URL以下:

        https://www.baidu.com/?wd=100&rsv_spt=1

      其中「?」表示參數的開始,每一個參數都是「name=value」的形式,每一個參數之間以「&」分隔。

    2.HTTP請求和響應格式

      

     請求報文是由請求行、請求報頭、空行和請求正文組成,響應報文是由響應行、響應報頭、空行和響應正文組成。

     請求方法:

      GET:請求獲取Request-URI所標識的資源

      POST:在Request-URI所標識的資源後附加新的數據

      HEAD:請求獲取Request-URI所標識的資源的響應消息報頭

      DELETE:請求服務器刪除Requet-URI做爲其標識

      . . . . . . .

    最經常使用的就是GET方法和POST方法了。

    請求路徑:表示的是請求資源的路徑,若是是GET方法的話,能夠帶有參數。他的值就是URL中的abs_path.若是是POST方法的話它的參數在消息正文中。

    空行其實是一種避免粘包的策略,咱們知道,第一行是請求行,從第二行開始一直到空行就是消息報頭了。

    狀態碼:

      狀態碼由三位數字組成,總共分爲5類:

      1xx:指示信息,表示請求已接受,繼續處理

      2xx:成功 表示請求被成功接收、理解、接受

      3xx:重定向 要完成請求必須進行更一步的操做

      4xx:客戶端錯誤 請求語法有錯誤或請求沒法實現

      5xx:服務器端錯誤 服務器未能實現合法的請求

    常見狀態碼:

      200 OK   //客戶端請求成功

      403 Forbidden  //服務器收到請求,可是拒絕提供服務

      404 Not Found  //請求資源不存在,也就是輸入了錯誤的URL

      500 Internal Server Error  //服務器發生了不可預期的錯誤

      503 Server Unavailable  //服務器當前不能處理客戶端的請求

 

    這裏咱們還要補充一個知識就是HTTP的長鏈接和短鏈接

      HTTP協議的長鏈接和短鏈接其實是TCP的長鏈接和短鏈接。

      長鏈接:HTTP/1.1開始使用長鏈接,用來保持鏈接的特性。使用長鏈接的HTTP協議會在響應頭加入一行代碼:Connection:keep-Alive,在使用長鏈接的狀況下,當網頁打開完成後,客戶端和服務器之間用於傳輸HTTP數據的TCP鏈接不會關閉,若是客戶端再次去訪問這個服務器上面的網頁,會繼續使用這一條已經創建的鏈接。Keep-Alive不會永久保持鏈接的,它會有一個保持時間,能夠再不一樣的服務器軟件上去設動這個時間。實現長鏈接須要服務器和客戶端都支持長鏈接。

      短鏈接:HTTP/1.0默認使用短鏈接,瀏覽器和服務器每進行一次HTTP操做,就創建一次鏈接,任務結束之後中斷鏈接。當客戶端瀏覽器再次訪問西苑的時候,就須要從新創建會話。

      如下是長短鏈接的操做:

        長鏈接: 創建鏈接——數據傳輸。。。(保持鏈接)。。。數據傳輸——關閉鏈接
        短鏈接: 創建鏈接——數據傳輸——關閉鏈接。。。創建鏈接——數據傳輸。。。

      HTTP協議的底層使用TCP協議,因此HTTP協議的長短鏈接本質上是TCP的長短鏈接。長鏈接能夠節省較多的TCP鏈接、釋放的操做,節省時間,對於頻繁請求資源的用戶來講,長鏈接最適合不過了。可是因爲有保活功能,當遇到大量的惡意鏈接時,服務器的壓力會愈來愈大。這時服務器會採起一些策略,關閉一些長時間沒有進行讀寫事件的鏈接。短鏈接對服務器來講管理比較簡單,只要是存在的鏈接都是有效的鏈接,不須要額外的控制手段,並且不會長時間的佔用資源。但若是客戶端請求頻繁的話,會在TCP創建和鏈接上浪費大量的時間。HTTP長短鏈接沒有什麼好壞優劣之分,只是使用的場景不一樣罷了。

     下面咱們就正式開始瞭解HTTP總體框架設計:

      

        http/1.0版本的服務器採用的是短鏈接。咱們要搭建的是多線程服務器而且使用短鏈接,因此每當創建一個鏈接以後,就建立一個線程去處理這個請求,並將這個線程設置成分離狀態,而後主線程繼續處於監聽狀態。當線程處理完這個請求以後,而後斷開鏈接。這樣一來一回就處理完一個請求。

        CGI模式與非CGI模式:

          當咱們判斷是GET請求時,而且URL中沒有參數的時候,就使用非CGI模式,非CGI模式比較簡單,首先咱們須要解析出請求路徑,判斷請求的是否是合法資源,若是是的話,咱們就返回這個資源。

          當時CGI模式處理請求的話,咱們須要fork一個子進程,對子進程exec替換CGI程序。在這過程當中,咱們使用pipe進行父子間的通訊。全部須要的參數在exec以前,咱們都將這些參數導出爲環境變量,這樣就算exec的話,子進程仍是可以經過環境變量獲取所需的參數。

         如何實現支持GET和POST方法的小型http服務器呢?

          GET方法:若是GET方法只是簡單的請求一份資源,而不傳遞參數的話則由服務器直接返回資源便可,若是GET方法的URL中帶有參數,則要是用CGI模式進行處理。

          POST方法:POST方法要是用CGI模式進行處理。POST的參數在消息正文中出現。(如上圖二中所示)

         因爲請求方法在http請求報文中的第一行,因此咱們須要讀取第一行而後判斷是那種方法,而且判斷是否是CGI模式。

      咱們的整個項目採用了B/S模式(瀏覽器/服務器模式),經過瀏覽器發送HTTP的GET和POST方法,而後服務器響應,最終經過html看到咱們最終顯示的效果。爲了支持併發,咱們採用了多線程結構。

      1.建立監聽套接字

        建立過程是socket-->bind-->listen

      2.進行accpet多線程的創建

        咱們使用accept接收客戶端的connect請求。這個過程其實是對backlog隊列的一個操做。在accept前,內核接收到connect請求首先把socket放入未完成隊列,而後accept的時候,須要把socket放入已完成隊列當中去,而後accept成功之後從已完成的隊列中取出。

        accept成功之後,咱們使用pthread_create建立線程,把socket託付給線程來進行操做。在線程處理的過程當中須要線程等待,爲了解決這個問題,咱們可使用線程分離,將線程做爲孤兒進程託管給1號進程,當執行完畢以後,由1號進程來進行資源的回收。

      3.線程處理

        在整個線程處理函數內部,咱們對HTTP的請求進行分析,經過對其中的路徑參數等信息進行處理。

        首先是對HTTP報文信息的處理,從這些中提取出有效的信息,咱們採起的讀取方式是按行讀取。對於HTTP方法的第一行進行讀取,這一行的三個字段是按照空格分開的,咱們利用這個特性,把HTTP請求的方法,資源路徑(URL)和HTTP版本信息提取出來。接下來咱們須要考慮處理的就是參數,HTTP請求常常會帶有一些參數,經過這些參數請求資源。GET方法的資源是在URL中,POST方法的資源是在消息正文當中。這樣咱們就能獲得資源了。

        在非cgi模式下,咱們能夠獲得資源路徑,這個資源路徑實際上是根目錄下的路徑,默認咱們去尋找根目錄下的主頁。因此咱們須要給資源加上index.html,而後咱們把整個index.html的信息發送給socket。咱們這裏採用的方式是sendfile的操做。sendfile主要是實現零拷貝發送文件,實現一個高效的數據傳輸,而且對其進行驗證。這樣socket接收到主頁信息,就能夠顯示出來網頁了,固然這個過程是按照HTTPPOST響應發送過去的。

        在cgi模式下咱們處理帶參的HTTP請求,咱們把這些參數都取出來,而後使得函數得到cgi參數,而後用獲取到的參數進行計算或者數據處理。

     具體框架如圖:
         

        在這裏咱們的處理方式就是對這兩組管道進行一下重定向,對於fork之後的子進程,咱們把管道重定向,利用dup2系統調用,而後達到的效果就是子進程最終能夠從stdin中獲得父進程給的信息,而父進程也就是服務器又能夠從socket獲得HTTP請求的內容。而後子進程數據計算之後把數據寫到stdout中,server從管道中取回數據,發送給socket,這樣socket端也就是瀏覽器那邊能夠顯示最後的結果。在這裏面重要的還有一個點就是HTTP的參數如何傳遞到cgi程序中,咱們使用的是環境變量的方式。cgi程序在子程序當中運行,能夠獲取到環境變量因此就能夠獲得所需的參數,下面是具體細節:

        GET cgi模式:GET方法的時候,這時CGI所須要的參數是放在URL中的,因此這個時候咱們就去在HTTP GET請求行的第二個內容資源路徑中進行字符串的處理,咱們找「?」,當找到之後,咱們讓指針指向這裏,叫作query_string,咱們把這個做爲環境變量傳給子進程就能夠了。對於GET的cgi模式,最重要的就是method和query_string.

        POST cgi模式:使用POST cgi模式時會有一個問題,就是咱們的參數是在正文當中,另外須要知道正文的字節數。這個時候POST消息報頭就起做用了,它在其中阻止了name:value形式的content_length:xxx這樣的內容,而後獲取到這個長度以後,咱們就能夠知道socket讀取多少長度的內容了,而後讀取完以後咱們就能夠得到參數,一樣是按照「?」和「&」形式組織的,咱們取出這個內容,而後進行數據操做。

        咱們須要說一下父進程後續操做,父進程處理的時候須要重定向管道,這樣纔好進行後續的操做,而後咱們進行查看方法,若是是POST方法,咱們須要把獲取到的HTTP請求的正文所有放入和cgi打交道的管道當中。這樣才能讓cgi獲取到正文信息。其餘狀況下咱們都須要從cgi返回到管道的結果當中進行獲取返回的信息,把這個信息發送給socket.最後,使用waitpid等待子進程。

      4.cgi的編寫方式

        cgi的編寫方式咱們能夠叫作cgi網關協議,咱們全部的cgi程序需均可以套用這一套來進行操做,咱們採用的傳遞參數方式是環境變量,其實還可使用管道來傳輸。而後咱們進行字符串處理,由於參數的組織方式是」?data1=100&data2=200」這種形式的,因此咱們要找的關鍵符號就是「=」和「&」這樣咱們就能夠渠道參數進行運算了。

相關文章
相關標籤/搜索