1. 程式人生 > >總結http伺服器框架

總結http伺服器框架

  HTTP又叫做超文字傳輸協議,現如今用的最多的版本是1.1版本。HTTP有如下的特點:

    支援客戶/伺服器模式(C/S或B/S)

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

    靈活:HTTP允許傳送人任意型別的資料物件。

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

    無狀態:HTTP是無狀態協議。無狀態是指協議對於事務處理沒有記憶能力。這次的請求和上次的請求之間是沒有關係的。缺少狀態意味著如果後續處理需要前面的一些資訊,則必須重傳,這樣可能導致每次連線傳送的資料量增大,但是當伺服器不需要前面的資訊時他的應答較快。

  我們平常使用的HTTP協議工作過程如下:

    一個HTTP操作叫做事物:

      1)首先客戶機與伺服器需要建立連線。

      2)建立連線後,客戶機發送一個請求給伺服器,請求方式的格式為:請求方法|統一資源識別符號(URL)|協議版本號,後面是MIME資訊包括請求修飾符、客戶機資訊和可能的內容。

      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”這種形式的,所以我們要找的關鍵符號就是“=”和“&”這樣我們就可以渠道引數進行運算了。