http協議和web處理請求相關

參考:

    http://www.ietf.org/rfc/rfc2616.txt (http協議)

    https://en.wikipedia.org/wiki/Endianness(大端法和小端法)

    http://www.cnblogs.com/gaojing/archive/2012/02/04/2413626.html(url和uri)

    http://www.cnblogs.com/kissdodog/archive/2013/01/11/2856335.html(http報文)

    http://baike.baidu.com/item/通用網關接口  (cgi)

    http://www.showerlee.com/archives/807 (apache的prefork、work、event工作模式)


1.http(hypertext transfer protocol)協議

    http協議最早用來傳輸超文本(http/0.9版本),什麼是超文本呢,其實是用超鏈接的方法,將各種不同空間的文字信息組織在一起的網狀文本。http/1.0引入了MIME機制,使得非文本類信息也可以利用http協議傳輸。但是http/1.0對長連接、緩存機制、虛擬主機等的支持並不是很好,所以最後http/1.1解決了這些問題,也成爲我們現在最常用的http協議。


    超鏈接:簡單來講就是內容鏈接,一個頁面位置可以跳到其他地方

    html(hyper text marked language):我們知道http可以傳輸文件數據,但是我們又希望這些數據傳輸過來後按我們希望的樣子展示,於是就給這些文字加上標籤,通過這些標籤,客戶端(一般是瀏覽器)可以知道該以什麼樣的方式去展示頁面給我們。這些標籤與原始數據的結合就是html。

    MIME(Multipurpose Internet Mail Extensions):我們知道,每個人都有自己的不同點,計算的製造也是一樣。這就導致了計算機存儲數據的時候存在兩種不同的方法:大端法和小端法。 簡單來說,存儲“protocol”單詞的時候,一個從小到大存儲爲 pr、ot、oc、ol,另一個存儲爲ol、oc、ot、pr(ascii編碼,一個字母只需1字節表示),這就導致了程序讀取數據流,會因爲計算機硬件的不同而導致意外結果。解決這個問題的方法之一是不使用二進制流,而使用文本流,先把二進制流編碼爲文本流,再進行程序處理。因爲文本流具有易讀性等優點,且不會因爲硬件的不同而發生變化,所以很多程序對數據的處理都採用文本流。當然,它的缺點也很明顯,轉碼增加了額外開銷,降低了效率。 同樣地,圖片和視頻等的編碼和文本是不一樣的,因爲http/0.9協議只支持文本傳輸,所以不能傳輸圖片,而http/1.0 MIME機制引入則改變了這一現狀,MIME機制把圖片等格式轉碼爲對應的文本流並標識之,使之可以傳輸到遠端,遠端根據標識還原爲原來的格式並展示,所以我們看到在http協議頭部有Content-Type標識。


url和uri:

    uri(uniform resource identifier):統一資源標識符,用來唯一的標識一個資源,它是種抽象概念,只要是能唯一標識資源的符號即可。

    url(uniform resource locator):可以看做互聯網上對uri的一種實現,可以唯一某一資源,它的格式一般如下:

        protocol://host[:port]/path/to/file [ "?" query ]

            protocol:協議,比如http、ftp等

            host:主機,可以是ip也可以是fqdn

            port:對應的端口,可省略

            path/to/file:在主機上具體的位置,nginx中把這個當做uri

            ?query:url附帶的信息,比如查詢某個用戶等,可省略


http報文:

http協議的報文只有兩種:請求和響應報文。


    請求報文格式如下:

    <method> <request-uri> <http version>    //請求行

    <header>                                                 //消息報頭

                                                                    //空格行,必備,好像是報文頭部和主體區分用,待查明

    [Entity]                                                      //報文主體 put和post方法會用到

    method:表示請求的方法,常見如下:

        get:請求獲取request-uri所標識的資源

        post:在資源後面附加新的數據

        head:僅獲取響應報文頭部

        put:請求服務器存儲一個資源,用request-uri作爲其標識,和post的區別在於,post是修改文件內容,put是創建文件

        delete:請求刪除資源

        trace:測試用,請求服務器回送收到的請求信息

        connect:保留

        options:查詢服務器的性能,或者查詢與資源相關的選項和需求

    request-uri:同url

    http version:有0.9、 1.0 、1.1三個版本

    header:頭部信息,有很多種類頭部,緩存相關的,編碼相關的等等,這裏只是常見幾個

        host:主機名,很多人會疑惑,http被tcp/ip協議封裝,而tcp/ip已經包含目的ip了,爲什麼這裏還需要一個host頭部? 其實這個是爲了實現虛擬主機,正是因爲有了這個頭部,web纔可以利用這個頭部對不同的請求轉發給不同的虛擬主機。

        Cache-Control:緩存控制指令,常見的如下:

            no-cache:這個是很經常被誤解的緩存指令,並非不能使用緩存,而是使用緩存前必須跟服務器確認緩存沒有過期。

            no-store:這個纔是大家理解上的不使用緩存

            max-age:最大緩存時長,(這個可以覆蓋max-age頭部,有優先級,Pragram>Cache Control>Expires>其他

        Accept:可接受的類型

        Accept-Encoding:可接受的編碼

        Referer:這個頭部用來記錄請求是從哪個網頁跳轉過來的,經常利用這個字段來防盜鏈。

例子:

GET /5bd1bjqh_Q23odCf/static/wiseindex/js/superframe_0467623d.js HTTP/1.1

Host: gss0.bdstatic.com

Connection: keep-alive

User-Agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.76 Mobile Safari/537.36

Accept: */*

Referer: https://m.baidu.com/?from=844b&vit=fps

Accept-Encoding: gzip, deflate, sdch, br

Accept-Language: zh-CN,zh;q=0.8


響應報文格式如下:

    <http-version> <status-code> <reason-phrase>

    <header>


    <Entity>

    http-version:和請求報文一樣

    status-code:狀態碼,描述請求資源的狀態,分爲以下五類:

        1xx:信息類,不常用

        2xx:成功類狀態碼,比如200 (ok)

        3xx:重定向類狀態碼

        301:永久重定向,即從這個頁面完全轉移到另一個頁面

        302:臨時重定向,即只是臨時去跳去某個頁面而已,主體還在,對網絡蜘蛛有用

        304:no modify,表示沒修改過,可以使用本地的緩存

        4xx:客戶端類錯誤

        400:請求有語法錯誤

        401:unauthorized未授權

        403:forbidden 禁止,服務器收到請求,但拒絕提供服務

        404:not found沒有這個資源

        5xx:服務端類錯誤,比如502(網關錯誤)等

    reason-phrase:即對狀態碼的簡單說明,比如403的“forbidden”就是這個字段

    header:和客戶端的header有些相同,有些不一樣,但格式都一樣,具體參考rfc2616

    Entity:即報文的數據內容,沒啥好說的。


爲什麼使用緩存?

    每個url稱作一個資源或一個對象,我們知道,一個index.html頁面裏面可能包含很多個對象,而在最開始,每個請求對象都是獨立發起一個請求的(這樣是爲什麼我們ss -tn同一時間能看到多個相同ip的請求的原因),我們知道http是應用層協議,在tcp/ip之上,而tcp協議的連接需要“三次握手,四次斷開”,這就意味着如果一個頁面包含10個對象的話,用這種方法,我們訪問一個頁面就要30次握手,40次斷開。這顯然是不理想的。

    於是,http中引入了一種長連接機制,當客戶端和服務器建立tcp鏈接,並傳送好對象的時候,並不馬上斷開,而是等待下一個對象的請求和傳送。當然,這個並不是一個一個地發起請求,那樣達不到併發效果,因爲瀏覽器一般是多線程的,所以它是兩者的結合,利用算法先把請求對象合理分批,同時發起並建立長連接,這樣效率大大提高。當然,長連接也有它的缺點,當併發請求數很大的時候,因爲進程能打開的文件句柄有限,所以,長連接會導致鏈接釋放過慢,相比普通鏈接能處理的請求更少。

    頁面的請求是如此消耗資源,尤其是圖片、壓縮包等對象更是大大佔用了帶寬,而這些對象,都有很少更改的特點,所以,緩存就派上用場了。


    http/1.1中增加了很多頭部用以控制緩存。

    比如,我們明確知道我們將在2017年10月23日 10:10修改1.jpg,而在此之前都不會修改,那麼我們就可以讓服務器返回一個If-Modified-Since頭部,在此之前都可以使用緩存(客戶端請求對象時會收到一個date頭部,通過這個頭部和If-Modified-Since對比即可判斷是否該使用緩存),其他方式比如max-age等等。


public和private緩存:

    public緩存:簡單地說就是允許大家訪問的訪問,比如無關緊要的圖片等。

    private緩存:個人的隱私的緩存,比如個人cookie(可能包含用戶登陸信息)等


2.web的請求處理工作模式:

一個web處理請求的過程應該大致包括以下幾個步驟:

    1.接收請求

    2.分析請求

    3.讀取請求內容數據

    4.封裝請求內容數據爲響應報文

    5.發送響應報文

    

    這裏要注意到的一點是一個純粹意義上的web只能處理靜態內容,而類似於php腳本需要在訪問是動態執行的內容,要獲取它們的內容,則需要藉助額外的機制。web本身並不參與其中。這種機制的實現方法有多種,比如httpd就把php做成自己的子模塊,需要執行php相關的內容的時候則調用這個模塊。當然它也支持更爲流行的cgi。cgi是種接口,它可以把頁面相關的動態內容轉發給有能力處理它們的程序,並返回處理結果。

    還有一點是這裏的封裝請求內容數據,並不是指tcp/ip對http報文的封裝,而是因爲我們訪問的內容都是一些數據,我們需要給它們打上html標籤,這樣客戶端才知道我們希望給它展示爲什麼樣子。

    

    那麼,當請求到來的時候,web是怎麼工作的呢?

  最簡單的模型是單進程模型,一個進程順序處理完一個請求的所有步驟後,再處理下一個請求報文

wKiom1jbSjOyQG9DAAA7DkMJjUc468.png

    這種模型的好處的是程序邏輯十分簡單,但是缺點也很明顯,當有多個請求鏈接並發進來的時候,只有一個請求能得到響應,其他請求有可能因爲無法及時響應而超時斷開,尤其是使用長連接的時候,所以現在應該沒有人用這種。

    因此,出現了第二種模型。

wKioL1jbSzfDTy37AABKyy0PCgU001.png

  web使用一個主進程接受請求但是自己並不響應,而是創建一個子進程,交給子進程去響應(比如主進程只完成步驟1接受請求,子進程完成剩下步驟),這樣,當同時進來多個請求的時候,只要爲其都創建個子進程便可同時響應,解決了因等待太久而超時斷開的問題。

    但是,這種模型的缺點也很明顯,進程是個重量級的數據結構單位,這就意味着,大量的進程會佔用大量的系統資源,比如一個進程佔用2M內存,那麼1000個請求就要2G的內存空間,而且因爲單個cpu一次只能運行一個進程,爲了達到同時的效果,它進行進程上下文切換,這就意味着,多個進程光是進行上下文切換就要耗費大量的系統開銷。

    所以,這種模型只適合在併發不高的場景下工作,並且有它最適合的進程個數,一旦超過這個個數,這種模型的優勢會被系統資源消耗的劣勢所抵消,甚至一個請求都響應不了。apache的prefork就是類似這種模型,不同的是,它總是預留一些空閒進程出來,這比請求進來的時候臨時fork效率高得多。

    於是,有了第三種模型。

wKiom1jbTkGgL-YHAAA6pmdbHSA010.png

    這種模型使用了比進程更小的單位--線程去處理請求,因爲只有一個進程,而且線程是輕量級的單位,所以對內存的資源消耗大大降低,又因爲可以同時使用多個線程,所以也達到了併發的目的。又因爲線程在同一進程內,所以它們可以共享內存空間,當多個請求訪問相同的內容的時候,這是一個極大的優點,進程只需讀取一次數據,執行一次i/o操作,把數據緩存在內存空間中響應下一次相同請求內容的鏈接即可。大家知道i/o操作比內存操作慢上幾個數量級,所以這個會大大提高系統的吞吐能力,適合在高併發的情況下使用。

    但是,這種模型也有它的缺點。首先,多個線程使用相同的進程內存空間,這就意味着,當有線程對進程內存空間的內容進行修改的時候,它必須對其加鎖,用以防止多個線程同時修改帶來的數據崩潰。而且,進程需要管理線程,這使得進程變得複雜,此時進程對於線程類似cpu對於進程,進程必須知道哪個請求交給哪些線程,涉及到i/o操作還需要知道,哪些線程已經完成,可以發送響應報文,哪些報文尚未完成,因此,進程對線程的管理也有最優上限,一個進程根據應該場景不同,可以最有效地管理不同的線程數量。根據這個結合前面幾個模型優缺點,有了第四種模型:

wKioL1jbbobCZwPqAABGWevrKeI253.png

    這種模型下,主進程事先fork幾個空閒進程出來,當請求進來的時候,交給子進程,子進程生成線程進行管理,每個子進程只響應有限個請求,這樣,每個進程都能發揮它的最優性能,當併發量的時候,這種模型的好處便體現出來。當然,缺點也顯而易見,當訪問量很小簡單模型足以應付的時候,這種模型由於比較複雜,相對於簡單的模型,反而效率更低。


進程對線程的管理

   前面提到,進程必須知道線程的相關狀態才能對其進行管理。假設現在有100個線程在進程i/o操作,進程怎麼知道哪些處理好,哪些沒有呢?

      一種方法是定時對其輪詢,每隔一段時間把所有線程輪詢一遍。 顯而易見,這種方法雖然能滿足需求,但是每次輪詢勢必浪費大量時間。於是有了第二種方法。

     第二種方法是給每個線程設置一個標誌位,類似於磁盤文件系統管理中的位圖。每次有哪些線程i/o操作好了,就把標誌位置1,默認爲0.進程逐一輪詢,而是掃描這些標誌位。

     有人會好奇,這不是也要輪詢一遍嗎?怎麼效率更高了?  其實是因爲同樣是輪詢,但是掃描的單位大小變化了,第一種方法的輪詢,就好比一個班級裏,你一個個地問,xxx你作業做好了沒?  而第二種則是把每個人的名字寫在本子上,如果誰寫了作業就打個勾,想知道誰好了沒就只需要看這個本子即可。

     第二種方法也是http的work模型,然而,這種模型仍然解決不了一種問題: 因爲我們輪詢是定期執行的,這就會導致一種場景,當前面線程的標誌位被掃描過後,前面的線程卻剛好在此時完成了i/o操作,但是因爲輪詢只掃一次,所以它只能等待下一次輪詢週期的到來,於是催生了第三種方法。

    第三種方法不使用輪詢,而是改用通知機制,(進程允許線程進行中斷,這點是我猜的,沒驗證,當然整篇都是我個人理解,不權威),當線程完成i/o操作後,通知進程數據準備好了,進程收到後及時把數據發出去,這樣,大大提高了同時響應請求的能力,這也是apache的event模型

    

    最後,我要聲明,這些都是我瞎寫的j_0057.gif


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章