天天看點

Http/1.1 (RFC 2616)閱讀筆記 1.請求/響應鍊(Request and Response Chain) 2.消息和實體(Message and Entity) 3.頭字段的分類 4.chunked傳輸模式及如何确定消息長度 5.pipeline為何不能落地

我們每天都在跟http打交道,卻未必敢說對它了如指掌。最近花了點時間閱讀了這份十年前的協定,借此機會分享感受較深的幾點。

1.請求/響應鍊(Request and Response Chain)

如圖,一則請求在最終抵達我們的應用伺服器之前,可能會經過像A、B、C那樣層層的代理伺服器、緩存伺服器的中轉,請求和響應就像接力棒一樣在整個網絡中穿梭。這種結構給分布式緩存埋下了伏筆,也讓我意識到CDN不是後人的發明,而是前人精心的設計。雖然緩存相關的文法破壞了HTTP語義的透明,但其對性能深厚的影響迫使協定的制定者們不惜将其“浮出水面”。Caching in http是專門的一章,主要介紹了兩種模型,一是Expiration Model,二是Validation Model。前者即直接複用本地緩存的政策,徹底省去了網絡上來回的開銷;後者則在302等響應碼中得以展現,確定内容最新的同時省去了重複傳輸的開銷。

request chain ---------->
 UserAgent -----v----- A -----v----- B - - - - - - C - - - - - - Orignal Sever
      <--------- response chain
           

2.消息和實體(Message and Entity)

可能是因為我們的業務少有涉及原生的http檔案上傳,即使需要也大多出于性能和互動的考量轉而采用flash(細節被封裝),實體的概念幾乎無人問津。

下面是我在stackoverflow.com中試圖回答“What's an HTTP Entity”:

Here are 3 simple cases:

Case 1. You're uploading 3 files in a single request. Those 3 files are 3 entities. Each of them has its own Content-Type to indicate what kind of file it is.

Case 2. You're viewing a web page. Browser has downloaded an html file as the entity in the background. Since the page could be updated continuously, you may get a totally different entity later.

Case 3. You've got a 304 Not Modified. No entity has been transferred.

In a word, Entity is an optional payload inside an http message(either request or response), so it is a "part-whole" relation between Entity and Message.

簡單說來,消息是面向傳輸的,而實體是面向業務的。當我們鍵入url浏覽一張網頁,其完整的html檔案即作為實體被封裝在響應的消息中傳輸回來。由于消息是載體,請求響應鍊上沿途的每一個經手的裝置都要關心消息的各個屬性,然而終端使用者隻關心實體的内容。

此前,我一直以為http協定限制了每次隻能上傳/下載下傳單個檔案,理由是HTML5之前的file uploader()不能多選;此外點選“下載下傳”按鈕後即使一下蹦出多個“另存為”對話框,也會認定是js在起作用。

然而3.7.2 Multipart Types一節中提到,multipart的目的是在單一消息中封裝多個實體,是以除了Get和Head請求由于文法受限不含消息體,一次請求或響應中上傳/下載下傳多個檔案是協定本身所支援的。

來看一次post請求上傳多個檔案的例子:http://www.cnblogs.com/kaixuan/archive/2008/01/31/1060284.html 

再來看一次響應中下載下傳多個檔案的例子(注意不是所有浏覽器都支援,因為multipart在語義上是給MIME定義的電子郵件和http定義的post請求所用):http://stackoverflow.com/questions/1041542/how-to-download-multiple-files-with-one-http-request

3.頭字段的分類

頭字段(Header Fields,或簡稱Headers)有以下幾種分類方式:

  • 分類一)以前隻知道頭字段可以分成請求專用、響應專用和通用三大類。
  • 分類二)前面提到,概念上分離了消息和實體後,有些頭字段是消息專屬(如Transfer-Encoding決定了消息的傳輸方式),有些則是實體專用(如某些“Content-”開頭的,用于描述實體的類型、長度等)。且協定中建議消息頭字段統一排在實體頭字段之前。這樣一來,概念上我們就有了消息頭和實體頭,從HTTP傳輸的内容來看,它們之間沒有明顯的邊界,純粹靠語義區分。
  • 分類三)13.5.1一節又給出了另一種分類方法:End-to-end 和 Hop-by-hop Headers。如前述請求響應鍊圖中所示,A,B,C之間都由獨立的連接配接維持,消息在穿透每一跳的過程中,甚至可以采用不同的傳輸方式,是以諸如某些表述傳輸方式的頭字段是可以在跳與跳之間任意增删的。顧名思義,這些被稱為Hop-by-hop Headers,而那些全程伴随直到終點的即End-to-end Headers。

分類二說明了消息頭和實體頭是平行的關系,然而消息主體(message-body)和實體主體(entity-body)卻是包含關系:

message-body = Transfer-Encoding( Content-Encoding(entity-body) )

其中Transfer-Encoding是傳輸編碼方式,如chunked模式,即塊狀分割後傳輸;而Content-Encoding是内容編碼方式,如gzip壓縮實體。

可見,實體主體是毛,消息主體是皮,兩者受體和載體的關系可見一斑。

分類三則強調了某些頭字段可以在中間環節增減。

4.chunked傳輸模式及如何确定消息長度

前面提到的chunked傳輸模式,是HTTP/1.1的一大特性。消息得以拆成小塊,并逐塊傳輸。優點是相對減輕了消息發送者緩沖的壓力、允許其發送的内容在發送的同時動态産生,甚至長度不限。抛開相容性和細節,可以簡單将其消息結構大緻描述為:

chunk-1,chunk-2,chunk-3...final-chunk,trailer

其中每一塊chunk都辨別了本塊的長度;最後的trailer用于存放僅當内容徹底發送完後才能計算的頭字段,如長度、md5等資訊。

簡述chunked後,再來看HTTP消息長度是如何确定的(譯自4.4節):

傳輸長度transfer-length即消息主體的長度,且這是經過transfer-code傳輸編碼後的結果。當出現消息體時,傳輸長度由下列因素決定(序号小的優先)

1、前述不應出現消息體的情況( 1xx, 204和304響應碼或響應HEAD請求 ),都以整個頭字段之後的第一個空行終止消息,并忽略頭字段中所有的實體頭。

(譯注:不應出現消息體的情況。皮之不存毛将焉附,語義就不涉及實體,是以一并忽略實體頭)

2、若Transfer-Encoding存在且其值不為“identity”,則傳輸長度以“chunked”模式為準,除非消息通過關閉連接配接來終止。

(譯注:消息體分塊傳輸的情況。)

3、若 Content-Length存在,其值既表示實體長度也表示傳輸長度的位元組數。兩者不等的情況下不應該發送 Content-Length(例如已經應用了Transfer-Encoding時)。若一條消息同時包含了Transfer-Encoding和Content-Length,後者應當被忽略。

(譯注:消息體與實體主體相同,未經傳輸編碼轉換的情況。)

4、若消息采用的媒體類型(media type)為"multipart/byteranges",且傳輸長度未能以上述方式指明,那麼這種自分割的媒體類型已經定義了如何确定傳輸長度。對用戶端而言,發送這種格式前應該确認接收者有能力解析;對服務端而言,收到一個由HTTP 1.1用戶端發來的含有Range頭字段且指定了multiple byte-range 的消息,即說明該用戶端有能力解析針對該格式的響應。

Range頭可能被1.0的代理轉發,它對“multipart/byteranges”一無所知。伺服器必須按本節1、3、5條所述對消息進行分割。

5、靠服務端關閉連接配接來确定。(關閉連接配接不能用來表示請求主體的結束,因為關閉後服務端無法發送響應了)

為了和HTTP1.0的應用相容,所有附帶了消息主體的HTTP/1.1請求必須包含有效的Content-Length,除非能确定服務端是1.1相容的。若請求附帶消息體但未給出Content-Length,伺服器應該在無法推斷消息長度時響應400(bad request),或者411 (length required)表示伺服器期望收到有效的Content-Length。

所有HTTP1.1應用在接收實體時必須支援chunked作為傳輸編碼,以應對消息長度無法預知的場景。

消息不能既包含Content-Length頭,又指定非identity的傳輸編碼方式。上述情況下必須忽略Content-Length。(譯注:同上述第3點)

5.pipeline為何不能落地

8.1.2.2一節介紹了流水線技術(pipeline,有别于facebook的BigPipe,後者其實是chunked模式的應用層展現)。協定中提到,支援長連接配接的用戶端可以複用單個連接配接,不等響應傳回連續發出多個請求,由伺服器保證響應逐個傳回時的順序與請求一緻。這種方式顯然比每個請求占用一個連接配接來得高效,而我在實踐過程中,卻從未觀察到。

原來現實環境中,請求、響應鍊沿途不少的伺服器和代理不支援流水線,會把後續的請求吃掉;此外,由于要保證響應傳回的順序,會造成隊頭阻塞(Head-of-line blocking)的問題,即靠前的響應一旦阻塞,會耽誤後續的響應發送。由于上述兩個原因,當代浏覽器從未将流水線全面鋪開,而是普遍把并發連接配接數從協定規定的2提升到了6。

相比之下,SPDY協定采用了多路複用(multiplexing)技術,請求的發送類似于pipeline,但允許亂序傳回,且序列上的各個響應可以分塊後間歇交錯地傳回。參考連結