10 months ago

進日拜讀Jerry Qu大大一系列關於HTTP的文章,實例切入剖析HTTP設計的原理並用深入淺出的方式釐清觀念,十分佩服大大的實力與文筆之好,在拜讀下列幾篇文章後,我發現一個令我十分感興趣的議題,也就是HTTP如何定義一次完整的request/response
HTTP 协议中的 Transfer-Encoding
HTTP 协议中 Vary 的一些研究
从 Nginx 默认不压缩 HTTP/1.0 说起

在《从 Nginx 默认不压缩 HTTP/1.0 说起》一文提到:

HTTP 1.0 对于 TCP 持久连接上的 HTTP 报文,客户端需要一种机制来准确判断结束位置。而在 HTTP/1.0 中,这种机制只有 Content-Length;
HTTP/1.1 新增的 Transfer-Encoding: chunked 所对应的分块传输机制可以完美解决这类问题。

這不禁讓我開始思考幾個問題

  1. 預設瀏覽器XHR是發起HTTP 1.0還是1.1? 這部分到沒有找到文件說明,在XHR文件也沒看到,但如果是用CURL則可以指定HTTP發出的版本。
  2. NodeJS伺服器怎麼處理這兩種不同協定的細微差異?
    例如Content-Length的計算到底是怎樣?
    平日開發根本就沒有手動自己計算Content-Length或是其他header的處理,那框架有協助處理這件事嗎?
  3. 如果瀏覽器發起HTTP 1.0的協定,我返回HTTP 1.1的回應會產生什麼狀況呢?
  4. HTTP 1.1 ---Keep-alive到底是保持多久?--- 因為會有啞代理問題,如果要建立持久化連線,HTTP 1.0 需在Header Connection: Keep-Alive;但HTTP 1.1預設就是持久化連線(可透過Connection:Close關閉)

以下紀錄動手實驗並釐清問題的過程。

HTTP 1.0 片段RFC定義

首先看到定義HTTP 1.0的RFC No.1945,整份文件蠻少的共60頁,這裡記錄幾段蠻特別的段落

1.3 Overall Operation

HTTP通訊協定是基於 Client/Server的模式,Client會發起連結,並發送由(request方法、URI、協定版號、Request修改狀態描述、Client資訊和有可能還包含 body content)組成的Request;
Server則是會回應由(協定版號、成功或錯誤的狀態碼、Server狀態描述、內容狀態描述且有可能還包含 body content)
.... 中間一段描述連線狀態描述,略過 ....

在實際應用上,HTTP通常基於TCP/IP建立通訊,預設使用TCP Port 80,但這不代表不能使用其他協定或是綁定其他Port,只要協定能夠保證穩定連線與對照HTTP Request/Response的資料格式即可;
目前連線的建立是透過Client發起Request並由Server Response後關閉連線,但是Client/Server都必須針對意外斷線做處理,像是用戶操作/逾時斷線/程式崩潰等(譯:當作意料之中的Exception處理)
任何一方關閉連線,不論目前狀態為何視為此次Request中斷。

3.6.1 Canonicalization and Text Defaults

網路多媒體資源需表示為典範形式(canonical form),一般來說透過HTTP傳輸的Entity-Body在傳送階段之前必須被表示為合法的媒體典範形式,像是在Content-Encoding編碼階段前Entity-Body必須符合典範形式;

像是"text"的多媒體形式利用CRLF當作文字的斷行,然而在HTTP傳送Entity Body時會單獨使用CR或LF代表斷行;
所以HTTP的應用必須識別CR/LF/CRLF為合法的斷行;
此外如果字符集不是用八進位的13/10代表CR/LF的話,HTTP允許使用個別字符集定義的CR/LF當作斷行字元,但這樣的彈性只限於Entity Body,但斷行字元不能用來取代CRLF在HTTP的控制結構(如headers/multipart-boundaries)

如果"charset"沒有特別定義則與設為ISO-8859-1。

Request/Response

Request/Response又可分為完整請求/回應與簡單請求/回應,Simple-Request只能用於GET且不含headers,Simple-Request盡量少用因為這會讓server不知道該返回怎樣的mime格式;
Simple-Response則只含[Entity-Body]

完整請求的格式為
Request-Line  = Request-Line
  \*( General-Header        ; Section 4.3
  | Request-Header        ; Section 5.2
  | Entity-Header )       ; Section 7.1
  CRLF
  [ Entity-Body ] 
  
完整回應的格式為
Full-Response  = Status-Line
  \*( General-Header        ; Section 4.3
  | Response-Header       ; Section 6.2
  | Entity-Header )       ; Section 7.1
  CRLF
  [ Entity-Body ]          ; Section 7.2
7.2 Entity Body

Entity Body是由Request夾帶或是Response回應的主體,會根據Entity-Header的內容編碼(Content-Encoding決定壓縮格式),並以位元組表示

Entity-Body = *OCTET

Request可選擇夾帶Entity Body,但如果夾帶了就必須包含合法的Content-Lentgh
而Response則依據Request與狀態碼決定是否夾帶Entity Body,分成以下情況

  1. Request為HEAD一律不回傳Entity Body
  2. 狀態碼為1XX/204/304一律不回傳
  3. 其餘Response夾帶或回傳"Content-Length: 0"表示不夾帶
7.2.1 Type

Entity Body的型別是由header Content-Type、Content-Encoding所決定,公式為

entity-body := Content-Encoding( Content-Type( data ) )

Content-Type決定資料是屬於哪種多媒體格式MIME;而Content-Encoding則是指內容經過何種編碼轉換,通常是用於資料壓縮,預設是不會有任何轉換。

在HTTP 1.0如果有夾帶Entity Body則必須包含Content-Type;
但如果發生沒有Content-type情況下,像是Simple-Response等,則接收方就需要透過內容或是副檔名去猜測檔案格式;如果猜不出來則預設檔案格式為application/octet-stream

7.2.2 Length

當Entity Body存在時,Entity Body的長度計算有兩種方式:

  1. 如果header Content-Length存在,則以此值為準(單位為bytes)
  2. 直到server斷開連結

但必須注意,Request不可以用斷開連結(close connection)的方式決定Body的長度,因為這樣server就無法Response,所以HTTP 1.0下Request必須包含有效的Content-Length;
但是在Request有Body但沒有Content-Length情況下,如果Server不能識別或計算Body的長度,則必須返回400(Bad Request)

在HTTP 1.0這份文件中解釋了連線的建立與Request/Response的機制,但沒有看到支援Keep-Alive的說明;
接著翻1.1的文件,最後再一次做實驗跟查看程式碼。

HTTP 1.1

一開始是看RFC2616,但好險後來有查到此篇文章RFC2616 is Dead才發現2616被廢止了,改由RFC7230~7235定義HTTP 1.1,主要是有一些安全性上的考量才全面更新文件;

・7230 Message Syntax and Routing
・7231 Semantics and Content:語意和內容的定義,像Methods/跟內容相關的Headers/狀態碼等(略)
・7232 Conditional Requests:客戶端在某些時候透過預先請求判斷是否發送請求,像是透過標頭ETag/Last-Modified判斷資料是否過期等(略)
・7233 Range Requests:客戶端在發出請求時可以指定資源的位元組範圍,用於部分下載(略)
・7234 Caching :快取相關的定義(略)
・7235 Authentication:驗證相關(略)

3.3.1. Transfer-Encoding

為了傳輸上的安全考量,header透過定義Transfer-Encoding決定傳輸編碼的方式,Transfer-Encoding是hop-by-hop,也就是用於兩個通訊結點間而非資源本身,也就是說Client->...->Server間如果有Proxy可能會修改或去除此header;
相對的如果希望是end-to-end(Client->Server)的壓縮,還是要用Content-Encoding指名內文壓縮的格式。

每個chunk的表示法

 chunked-body   = *chunk
                  last-chunk
                  trailer-part
                  CRLF

 chunk          = chunk-size [ chunk-ext ] CRLF
                  chunk-data CRLF
 chunk-size     = 1*HEXDIG
 last-chunk     = 1*("0") [ chunk-ext ] CRLF

 chunk-data     = 1*OCTET ;

需用chunk-size指名每塊chunk的尺寸,如果要結束傳輸則最後一個chunk內文為"0 CRLF"

3.3.2. Content-Length

如果header沒有Transfer-Encoding,則用Content-Length表明需要傳送的body長度;
但如果有Transfer-Encoding則不能用Content-Length

6.3. Persistence

HTTP 1.1預設建立持久化連線,多個HTTP Request/Response可以復用同一個連線;
可以透過 close 選項表明此次Request/Response後就關閉連線。

總結

稍微整理一下,可以發現
HTTP 1.0 透過 Content-Length決定每次傳輸的Body尺寸,進而了解何時結束傳輸;
但也可以不指名Content-Length,透過關閉連結當作傳輸的結束。
HTTP 1.0一開始設計理念是為了少且大的資料傳輸,但是實際的網路應用是多且片段的資料傳輸,有些資料是動態且持續的生成,HTTP 1.0在傳輸上有許多限制與不彈性的地方;

在 1996完成HTTP 1.0(RFC 1945)標準化後,HTTP 1.1就在1999年提出了(RFC 2616),並於2014年完成修正(RFC7230~7235);

HTTP 1.1 加入了持久化連線(HTTP 1.0需透過keep-alive指定),並新增header “Transfer-Encoding:chunked”讓傳輸的大小更動態與彈性,可以將Body切成多個chunk分批傳輸。

但我們也可以看到說 HTTP 1.1的chunk沒有個ID指名屬於哪個Response以及順序性,因為HTTP 1.1每次只能依序處理一個Request/Response,這造成head-of-line blocking問題,也就是第一個請求很費時後續的請求也都會被卡住(在單一連線的情況下);
雖然有設計 Pipelining 可以在未收到現有的Response先發送下一個Request併行處理,但因為沒有被業界大量採用所以就略過不提。

HTTP2用新的chunk結構解決了這樣的問題,並加入了HPACK標頭壓縮機制與二進制傳輸編碼等改善。

在查資料的過程中,意外看到牛人的部落格 mnot’s blog ,作者是co-chair the IETF HTTP and QUIC Working Groups,所以部落格中分享很多起草規格與思考的脈絡,乾貨滿滿,讀起來很過癮但也很吃力OTZ

← Koa2 源碼解析 - 簡潔美麗的框架 使用JMeter與JMeter-ec2佈建遠端分佈式壓力測試 →
 
comments powered by Disqus