HTTP Transfer-Encoding介紹

參考資料:https://www.cnblogs.com/micro-chen/p/7183275.html

 

Transfer-Encoding

 

 

Transfer-Encoding,是一個 HTTP 頭部字段,字面意思是「傳輸編碼」。實際上,HTTP 協議中還有另外一個頭部與編碼有關:Content-Encoding(內容編碼)。Content-Encoding 通常用於對實體內容進行壓縮編碼,目的是優化傳輸,例如用 gzip 壓縮文本文件,能大幅減小體積。內容編碼通常是選擇性的,例如 jpg / png 這類文件一般不開啓,因爲圖片格式已經是高度壓縮過的,再壓一遍沒什麼效果不說還浪費 CPU。

 

而 Transfer-Encoding 則是用來改變報文格式,它不但不會減少實體內容傳輸大小,甚至還會使傳輸變大,那它的作用是什麼呢?本文接下來主要就是講這個。我們先記住一點,Content-Encoding 和 Transfer-Encoding 二者是相輔相成的,對於一個 HTTP 報文,很可能同時進行了內容編碼和傳輸編碼。

 

Persistent Connection

暫時把 Transfer-Encoding 放一邊,我們來看 HTTP 協議中另外一個重要概念:Persistent Connection(持久連接,通俗說法叫長連接)。我們知道,HTTP 是運行在傳輸層 TCP 連接協議之上的應用層協議,自然也有着跟 TCP 一樣的三次握手、慢啓動等特性,建立連接開銷較大,爲了儘可能的提高 HTTP 性能,使用持久連接就顯得尤爲重要了。爲此,HTTP 協議引入了相應的機制。

 

HTTP/1.0 的持久連接機制是後來才引入的,通過 Connection: keep-alive 這個頭部來實現,服務端和客戶端都可以使用它告訴對方在發送完數據之後不需要斷開 TCP 連接,以備後用。HTTP/1.1 則規定所有連接都默認是持久的,除非顯式地在頭部加上 Connection: close。所以實際上,HTTP/1.1 中 Connection 這個頭部字段已經沒有 keep-alive 這個取值了,但由於歷史原因,很多 Web Server 和瀏覽器,還是保留着給 HTTP/1.1 長連接發送 Connection: keep-alive 的習慣。

 

瀏覽器重用已經打開的空閒持久連接,可以避開緩慢的三次握手,還可以避免遇上 TCP 慢啓動的擁塞適應階段,聽起來十分美妙。

 

爲了深入研究持久連接的特性,我決定用 Node 寫一個最簡單的 Web Server 用於測試,Node 提供了 http 模塊用於快速創建 HTTP Web Server,但我需要更多的控制,所以用 net 模塊創建了一個 TCP Server:

 

require('net').createServer(function(sock) {

    sock.on('data', function(data) {

        sock.write('HTTP/1.1 200 OK\r\n');

        sock.write('\r\n');

        sock.write('hello world!');

        sock.destroy();

    });

}).listen(9090, '127.0.0.1');

 

啓動服務後,在瀏覽器裏訪問 127.0.0.1:9090,正確輸出了指定內容,一切正常。去掉 sock.destroy() 這一行,讓它變成持久連接,重啓服務後再訪問一下。這次的結果就有點奇怪了:遲遲看不到輸出,通過 Network 查看請求狀態,一直是 pending。

 

這是因爲,對於非持久連接,瀏覽器可以通過連接是否關閉來界定請求或響應實體的邊界;而對於持久連接,這種方法顯然不奏效。上例中,儘管我已經發送完所有數據,但瀏覽器並不知道這一點,它無法得知這個打開的連接上是否還會有新數據進來,只能傻傻地等了。

 

Content-Length

要解決上面這個問題,最容易想到的辦法就是計算實體長度,並通過頭部告訴對方。這就要用到 Content-Length 了,改造一下上面的例子:

 

require('net').createServer(function(sock) {

    sock.on('data', function(data) {

        sock.write('HTTP/1.1 200 OK\r\n');

        sock.write('Content-Length: 12\r\n');

        sock.write('\r\n');

        sock.write('hello world!');

    });

}).listen(9090, '127.0.0.1');

 

這次我們使用了另一個頭部字段:Content-Length。可以看到,這次發送完數據並沒有關閉 TCP 連接,但瀏覽器能正常輸出內容並結束請求,因爲瀏覽器可以通過 Content-Length 的長度信息,判斷出響應實體已結束。那如果 Content-Length 和實體實際長度不一致會怎樣?有興趣的同學可以自己試試,通常如果 Content-Length 比實際長度短,會造成內容被截斷;如果比實體內容長,會造成 pending。

 

由於 Content-Length 字段必須真實反映實體長度,但實際應用中,有些時候實體長度並沒那麼好獲得,例如實體來自於網絡文件,或者由動態語言生成。這時候要想準確獲取長度,只能在服務端開一個足夠大的 buffer,等內容全部生成好再計算Content-Length,然後纔將內容發送給客戶端。但這樣做一方面需要更大的內存開銷,另一方面也會讓客戶端等更久。

 

我們在做 WEB 性能優化時,有一個重要的指標叫 TTFB(Time To First Byte),它代表的是從客戶端發出請求到收到響應的第一個字節所花費的時間。大部分瀏覽器自帶的 Network 面板都可以看到這個指標,越短的 TTFB 意味着用戶可以越早看到頁面內容,體驗越好。可想而知,服務端爲了計算響應實體長度而緩存所有內容,跟更短的 TTFB 理念背道而馳。但在 HTTP 報文中,實體一定要在頭部之後,順序不能顛倒,爲此我們需要一個新的機制:不依賴頭部的長度信息,也能知道實體的邊界。

 

Transfer-Encoding: chunked

本文主角終於再次出現了,Transfer-Encoding 正是用來解決上面這個問題的。歷史上 Transfer-Encoding 可以有多種取值,爲此還引入了一個名爲 TE 的頭部用來協商採用何種傳輸編碼。但是最新的 HTTP 規範裏,只定義了一種傳輸編碼:分塊編碼(chunked)。

 

分塊編碼相當簡單,在頭部加入 Transfer-Encoding: chunked 之後,就代表這個報文采用了分塊編碼。這時,報文中的實體需要改爲用一系列分塊來傳輸。每個分塊包含十六進制的長度值和數據,長度值獨佔一行,長度不包括它結尾的 CRLF(\r\n),也不包括分塊數據結尾的 CRLF。最後一個分塊長度值必須爲 0,對應的分塊數據沒有內容,表示實體結束。按照這個格式改造下之前的代碼:

 

require('net').createServer(function(sock) {

    sock.on('data', function(data) {

        sock.write('HTTP/1.1 200 OK\r\n');

        sock.write('Transfer-Encoding: chunked\r\n');

        sock.write('\r\n');



        sock.write('b\r\n');

        sock.write('01234567890\r\n');



        sock.write('5\r\n');

        sock.write('12345\r\n');



        sock.write('0\r\n');

        sock.write('\r\n');

    });

}).listen(9090, '127.0.0.1');

 

上面這個例子中,我在響應頭中表明接下來的實體會採用分塊編碼,然後輸出了 11 字節的分塊,接着又輸出了 5 字節的分塊,最後用一個 0 長度的分塊表明數據已經傳完了。用瀏覽器訪問這個服務,可以得到正確結果。可以看到,通過這種簡單的分塊策略,很好的解決了前面提出的問題。

 

Transfer-Encoding: chunked 與 Content-Length

Transfer-Encoding: chunked 與 Content-Length 同爲頭部字段,它們不會同時出現在頭部中,當使用分塊傳輸時,頭部將出現 Transfer-Encoding: chunked,而不再包含Content-Length字段,即使強行設定該字段,也會被忽略。

 

在HTTP中,我們通常依賴 HttpCode/HttpStatus 來判斷一個 HTTP 請求是否成功,如:

HTTP: Status 200 – 成功,服務器成功返回網頁

HTTP: Status 304 – 成功,網頁未修改

HTTP: Status 404 – 失敗,請求的網頁不存在

HTTP: Status 503 – 失敗,服務不可用

… …

 

但開發人員有時候也會有令人意外的想象力。我們的一部分開發人員決定使用 Content-Length 來判斷 HTTP 請求是否成功,當 Content-Length 的值小於等於0或者爲162時,認爲請求失敗。

當 Content-Length 的值小於等於0時認爲http請求失敗還好理解,因爲開發人員錯誤地以爲 HTTP 響應頭中一定會包含 Content-Length 字段。

爲什麼當 Content-Length 的值爲162時,也認爲請求失敗呢。這是因爲我們的服務器的404頁面的長度恰好是162。驚不驚喜,意不意外!

 

以上開發的代碼長期以來並沒有出現什麼問題,直到有一天我在服務端 Nginx 開啓了chunked_transfer_encoding。此時,HTTP 使用了分塊傳輸模式,HTTP 響應頭中出現了 Transfer-Encoding: chunked,而不再包含 Content-Length,開發的代碼始終取到 Content-Length 的值爲-1,一直認爲下載失敗,導致客戶端無法正常運行。

當然,我只要稍微修改404頁面,也會導致開發的下載判斷出現問題。更何況,或許下載資源的長度剛好就是162呢?

 

gzip 與 Transfer-Encoding: chunked      

gzip on;

        gzip_min_length 1k;

        gzip_buffers 4 16k;

        #gzip_http_version 1.0;

        gzip_comp_level 2;

        gzip_types text/xml text/plain text/css text/js application/javascript application/json;

        gzip_vary on;

        gzip_disable "MSIE [1-6]\.";

 

當在 Nginx 的配置文件 nginx.conf 的location等位置配置以上內容時,Nginx 服務器將對指定的文件類型開啓壓縮 (gzip)以優化傳輸,減少傳輸量。

分塊傳輸可以將壓縮對象分爲多個部分,在這種情況下,資源整個進行壓縮,壓縮的輸出分塊傳輸。在壓縮的情形中,分塊傳輸有利於一邊進行壓縮一邊發送數據,而不是先完成壓縮過程,得知壓縮後數據的大小之後再進行傳輸,從而使得用戶能夠更快地接受到數據,TTFB 指標更好。

 

對於開啓了 gzip 的傳輸,報文的頭部將增加 Content-Encoding: gzip 來標記傳輸內容的編碼方式。同時,Nginx 服務器默認就會對壓縮內容進行分塊傳輸,而無須顯示開啓chunked_transfer_encoding。

 

Nginx 中如何關閉分塊傳輸呢,在 Nginx 配置文件 location 段中加一行“chunked_transfer_encoding off;”即可。

location / {

        chunked_transfer_encoding       off;

}

 

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